Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
67.86% covered (warning)
67.86%
38 / 56
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
OOUIImageModule
67.86% covered (warning)
67.86%
38 / 56
0.00% covered (danger)
0.00%
0 / 3
28.76
0.00% covered (danger)
0.00%
0 / 1
 loadFromDefinition
68.97% covered (warning)
68.97%
20 / 29
0.00% covered (danger)
0.00%
0 / 1
14.62
 loadOOUIDefinition
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 readJSONFile
60.00% covered (warning)
60.00%
12 / 20
0.00% covered (danger)
0.00%
0 / 1
6.60
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 */
20
21namespace MediaWiki\ResourceLoader;
22
23use LogicException;
24
25/**
26 * Loads the module definition from JSON files in the format that OOUI uses, converting it to the
27 * format we use. (Previously known as secret special sauce.)
28 *
29 * @since 1.26
30 */
31class OOUIImageModule extends ImageModule {
32    use OOUIModule;
33
34    protected function loadFromDefinition() {
35        if ( $this->definition === null ) {
36            // Do nothing if definition was already processed
37            return;
38        }
39
40        $themes = self::getSkinThemeMap();
41
42        // For backwards-compatibility, allow missing 'themeImages'
43        $module = $this->definition['themeImages'] ?? '';
44
45        $definition = [];
46        foreach ( $themes as $skin => $theme ) {
47            $data = $this->loadOOUIDefinition( $theme, $module );
48
49            if ( !$data ) {
50                // If there's no file for this module of this theme, that's okay, it will just use the defaults
51                continue;
52            }
53
54            // Convert into a definition compatible with the parent vanilla ImageModule
55            foreach ( $data as $key => $value ) {
56                switch ( $key ) {
57                    // Images and color variants are defined per-theme, here converted to per-skin
58                    case 'images':
59                    case 'variants':
60                        $definition[$key][$skin] = $value;
61                        break;
62
63                    // Other options must be identical for each theme (or only defined in the default one)
64                    default:
65                        if ( !isset( $definition[$key] ) ) {
66                            $definition[$key] = $value;
67                        } elseif ( $definition[$key] !== $value ) {
68                            throw new LogicException(
69                                "Mismatched OOUI theme images definition: " .
70                                    "key '$key' of theme '$theme' for module '$module" .
71                                    "does not match other themes"
72                            );
73                        }
74                        break;
75                }
76            }
77        }
78
79        // Extra selectors to allow using the same icons for old-style MediaWiki UI code
80        if ( str_starts_with( $module, 'icons' ) ) {
81            $definition['selectorWithoutVariant'] = '.oo-ui-icon-{name}, .mw-ui-icon-{name}:before';
82            $definition['selectorWithVariant'] = '.oo-ui-image-{variant}.oo-ui-icon-{name}, ' .
83                '.mw-ui-icon-{name}-{variant}:before';
84        }
85
86        // Fields from module definition silently override keys from JSON files
87        $this->definition += $definition;
88
89        parent::loadFromDefinition();
90    }
91
92    /**
93     * Load the module definition from the JSON file(s) for the given theme and module.
94     *
95     * @since 1.34
96     * @param string $theme
97     * @param string $module
98     * @return array|false
99     * @suppress PhanTypeArraySuspiciousNullable
100     */
101    protected function loadOOUIDefinition( $theme, $module ) {
102        // Find the path to the JSON file which contains the actual image definitions for this theme
103        if ( $module ) {
104            $dataPath = $this->getThemeImagesPath( $theme, $module );
105        } else {
106            // Backwards-compatibility for things that probably shouldn't have used this class...
107            $dataPath =
108                $this->definition['rootPath'] . '/' .
109                strtolower( $theme ) . '/' .
110                $this->definition['name'] . '.json';
111        }
112
113        return $this->readJSONFile( $dataPath );
114    }
115
116    /**
117     * Read JSON from a file, and transform all paths in it to be relative to the module's base path.
118     *
119     * @since 1.34
120     * @param string $dataPath Path relative to the module's base bath
121     * @return array|false
122     */
123    protected function readJSONFile( $dataPath ) {
124        $localDataPath = $this->getLocalPath( $dataPath );
125
126        if ( !file_exists( $localDataPath ) ) {
127            return false;
128        }
129
130        $data = json_decode( file_get_contents( $localDataPath ), true );
131
132        // Expand the paths to images (since they are relative to the JSON file that defines them, not
133        // our base directory)
134        $fixPath = static function ( &$path ) use ( $dataPath ) {
135            if ( $dataPath instanceof FilePath ) {
136                $path = new FilePath(
137                    dirname( $dataPath->getPath() ) . '/' . $path,
138                    $dataPath->getLocalBasePath(),
139                    $dataPath->getRemoteBasePath()
140                );
141            } else {
142                $path = dirname( $dataPath ) . '/' . $path;
143            }
144        };
145        // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
146        array_walk( $data['images'], static function ( &$value ) use ( $fixPath ) {
147            if ( is_string( $value['file'] ) ) {
148                $fixPath( $value['file'] );
149            } elseif ( is_array( $value['file'] ) ) {
150                array_walk_recursive( $value['file'], $fixPath );
151            }
152        } );
153
154        return $data;
155    }
156}