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