Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.47% covered (warning)
76.47%
13 / 17
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
LessVarFileModule
76.47% covered (warning)
76.47%
13 / 17
83.33% covered (warning)
83.33%
5 / 6
11.30
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getMessages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 pluckFromMessageBlob
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getMessageBlob
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 wrapAndEscapeMessage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLessVars
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20namespace MediaWiki\ResourceLoader;
21
22use Wikimedia\Minify\CSSMin;
23
24// Per https://phabricator.wikimedia.org/T241091
25// phpcs:disable MediaWiki.Commenting.FunctionAnnotations.UnrecognizedAnnotation
26
27/**
28 * Module augmented with context-specific LESS variables.
29 *
30 * @ingroup ResourceLoader
31 * @since 1.32
32 */
33class LessVarFileModule extends FileModule {
34    /** @var array */
35    protected $lessVariables = [];
36
37    /**
38     * @inheritDoc
39     */
40    public function __construct(
41        array $options = [],
42        $localBasePath = null,
43        $remoteBasePath = null
44    ) {
45        if ( isset( $options['lessMessages'] ) ) {
46            $this->lessVariables = $options['lessMessages'];
47        }
48        parent::__construct( $options, $localBasePath, $remoteBasePath );
49    }
50
51    /**
52     * @inheritDoc
53     */
54    public function getMessages() {
55        // Overload so MessageBlobStore can detect updates to messages and purge as needed.
56        return array_merge( $this->messages, $this->lessVariables );
57    }
58
59    /**
60     * Return a subset of messages from a JSON string representation.
61     *
62     * @param string|null $blob JSON, or null if module has no declared messages
63     * @param string[] $allowed
64     * @return array
65     */
66    private function pluckFromMessageBlob( $blob, array $allowed ): array {
67        $data = $blob ? json_decode( $blob, true ) : [];
68        // Keep only the messages intended for LESS export
69        // (opposite of getMessages essentially).
70        return array_intersect_key( $data, array_fill_keys( $allowed, true ) );
71    }
72
73    /**
74     * @inheritDoc
75     */
76    protected function getMessageBlob( Context $context ) {
77        $blob = parent::getMessageBlob( $context );
78        if ( !$blob ) {
79            // If module has no blob, preserve null to avoid needless WAN cache allocation
80            // client output for modules without messages.
81            return $blob;
82        }
83        return json_encode( (object)$this->pluckFromMessageBlob( $blob, $this->messages ) );
84    }
85
86    // phpcs:disable MediaWiki.Commenting.DocComment.SpacingDocTag, Squiz.WhiteSpace.FunctionSpacing.Before
87    /**
88     * Escape and wrap a message value as literal string for LESS.
89     *
90     * This mostly lets CSSMin escape it and wrap it, but also escape single quotes
91     * for compatibility with LESS's feature of variable interpolation into other strings.
92     * This is relatively rare for most use of LESS, but for messages it is quite common.
93     *
94     * Example:
95     *
96     * @code
97     *     @x: "foo's";
98     *     .eg { content: 'Value is @{x}'; }
99     * @endcode
100     *
101     * Produces output: `.eg { content: 'Value is foo's'; }`.
102     * (Tested in less.php 1.8.1, and Less.js 2.7)
103     *
104     * @param string $msg
105     * @return string wrapped LESS variable value
106     */
107    private static function wrapAndEscapeMessage( $msg ) {
108        return str_replace( "'", "\'", CSSMin::serializeStringValue( $msg ) );
109    }
110
111    // phpcs:enable MediaWiki.Commenting.DocComment.SpacingDocTag, Squiz.WhiteSpace.FunctionSpacing.Before
112
113    /**
114     * Get language-specific LESS variables for this module.
115     *
116     * @param Context $context
117     * @return array LESS variables
118     */
119    protected function getLessVars( Context $context ) {
120        $vars = parent::getLessVars( $context );
121
122        $blob = parent::getMessageBlob( $context );
123        $messages = $this->pluckFromMessageBlob( $blob, $this->lessVariables );
124
125        // It is important that we iterate the declared list from $this->lessVariables,
126        // and not $messages since in the case of undefined messages, the key is
127        // omitted entirely from the blob. This emits a log warning for developers,
128        // but we must still carry on and produce a valid LESS variable declaration,
129        // to avoid a LESS syntax error (T267785).
130        foreach ( $this->lessVariables as $msgKey ) {
131            $vars['msg-' . $msgKey] = self::wrapAndEscapeMessage( $messages[$msgKey] ?? "{$msgKey}" );
132        }
133
134        return $vars;
135    }
136}