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    protected $lessVariables = [];
35
36    /**
37     * @inheritDoc
38     */
39    public function __construct(
40        array $options = [],
41        $localBasePath = null,
42        $remoteBasePath = null
43    ) {
44        if ( isset( $options['lessMessages'] ) ) {
45            $this->lessVariables = $options['lessMessages'];
46        }
47        parent::__construct( $options, $localBasePath, $remoteBasePath );
48    }
49
50    /**
51     * @inheritDoc
52     */
53    public function getMessages() {
54        // Overload so MessageBlobStore can detect updates to messages and purge as needed.
55        return array_merge( $this->messages, $this->lessVariables );
56    }
57
58    /**
59     * Return a subset of messages from a JSON string representation.
60     *
61     * @param string|null $blob JSON, or null if module has no declared messages
62     * @param string[] $allowed
63     * @return array
64     */
65    private function pluckFromMessageBlob( $blob, array $allowed ): array {
66        $data = $blob ? json_decode( $blob, true ) : [];
67        // Keep only the messages intended for LESS export
68        // (opposite of getMessages essentially).
69        return array_intersect_key( $data, array_fill_keys( $allowed, true ) );
70    }
71
72    /**
73     * @inheritDoc
74     */
75    protected function getMessageBlob( Context $context ) {
76        $blob = parent::getMessageBlob( $context );
77        if ( !$blob ) {
78            // If module has no blob, preserve null to avoid needless WAN cache allocation
79            // client output for modules without messages.
80            return $blob;
81        }
82        return json_encode( (object)$this->pluckFromMessageBlob( $blob, $this->messages ) );
83    }
84
85    // phpcs:disable MediaWiki.Commenting.DocComment.SpacingDocTag, Squiz.WhiteSpace.FunctionSpacing.Before
86    /**
87     * Escape and wrap a message value as literal string for LESS.
88     *
89     * This mostly lets CSSMin escape it and wrap it, but also escape single quotes
90     * for compatibility with LESS's feature of variable interpolation into other strings.
91     * This is relatively rare for most use of LESS, but for messages it is quite common.
92     *
93     * Example:
94     *
95     * @code
96     *     @x: "foo's";
97     *     .eg { content: 'Value is @{x}'; }
98     * @endcode
99     *
100     * Produces output: `.eg { content: 'Value is foo's'; }`.
101     * (Tested in less.php 1.8.1, and Less.js 2.7)
102     *
103     * @param string $msg
104     * @return string wrapped LESS variable value
105     */
106    private static function wrapAndEscapeMessage( $msg ) {
107        return str_replace( "'", "\'", CSSMin::serializeStringValue( $msg ) );
108    }
109
110    // phpcs:enable MediaWiki.Commenting.DocComment.SpacingDocTag, Squiz.WhiteSpace.FunctionSpacing.Before
111
112    /**
113     * Get language-specific LESS variables for this module.
114     *
115     * @param Context $context
116     * @return array LESS variables
117     */
118    protected function getLessVars( Context $context ) {
119        $vars = parent::getLessVars( $context );
120
121        $blob = parent::getMessageBlob( $context );
122        $messages = $this->pluckFromMessageBlob( $blob, $this->lessVariables );
123
124        // It is important that we iterate the declared list from $this->lessVariables,
125        // and not $messages since in the case of undefined messages, the key is
126        // omitted entirely from the blob. This emits a log warning for developers,
127        // but we must still carry on and produce a valid LESS variable declaration,
128        // to avoid a LESS syntax error (T267785).
129        foreach ( $this->lessVariables as $msgKey ) {
130            $vars['msg-' . $msgKey] = self::wrapAndEscapeMessage( $messages[$msgKey] ?? "{$msgKey}" );
131        }
132
133        return $vars;
134    }
135}