Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.91% |
70 / 77 |
|
55.56% |
5 / 9 |
CRAP | |
0.00% |
0 / 1 |
MediaWikiHooksHelper | |
90.91% |
70 / 77 |
|
55.56% |
5 / 9 |
35.92 | |
0.00% |
0 / 1 |
getInstance | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
clearCache | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
registerHook | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
loadExtensionJson | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
5.03 | |||
readJsonFile | |
83.33% |
20 / 24 |
|
0.00% |
0 / 1 |
13.78 | |||
getHookSubscribers | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isSpecialHookSubscriber | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
getMwParserClassFQSEN | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
getPPFrameClassFQSEN | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
3.01 |
1 | <?php |
2 | |
3 | namespace SecurityCheckPlugin; |
4 | |
5 | use Phan\CodeBase; |
6 | use Phan\Config; |
7 | use Phan\Language\FQSEN\FullyQualifiedClassName; |
8 | use Phan\Language\FQSEN\FullyQualifiedFunctionLikeName; |
9 | use Phan\Language\FQSEN\FullyQualifiedFunctionName; |
10 | use Phan\Language\FQSEN\FullyQualifiedMethodName; |
11 | |
12 | /** |
13 | * This program is free software; you can redistribute it and/or modify |
14 | * it under the terms of the GNU General Public License as published by |
15 | * the Free Software Foundation; either version 2 of the License, or |
16 | * (at your option) any later version. |
17 | * |
18 | * This program is distributed in the hope that it will be useful, |
19 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
20 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
21 | * GNU General Public License for more details. |
22 | * |
23 | * You should have received a copy of the GNU General Public License along |
24 | * with this program; if not, write to the Free Software Foundation, Inc., |
25 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
26 | */ |
27 | |
28 | class MediaWikiHooksHelper { |
29 | /** |
30 | * @var bool Whether extension.json/skin.json was already loaded |
31 | */ |
32 | private $extensionJsonLoaded = false; |
33 | |
34 | /** |
35 | * @var FullyQualifiedFunctionLikeName[][] A mapping from hook names to FQSEN that implement it |
36 | * @phan-var array<string,FullyQualifiedFunctionLikeName[]> |
37 | */ |
38 | private $hookSubscribers = []; |
39 | |
40 | private ?FullyQualifiedClassName $parserFQSEN = null; |
41 | private ?FullyQualifiedClassName $ppFrameFQSEN = null; |
42 | |
43 | /** @var self|null */ |
44 | private static $instance; |
45 | |
46 | /** |
47 | * @return self |
48 | */ |
49 | public static function getInstance(): self { |
50 | if ( !self::$instance ) { |
51 | self::$instance = new self; |
52 | } |
53 | return self::$instance; |
54 | } |
55 | |
56 | /** |
57 | * Clear the extension.json cache, for testing purpose |
58 | * |
59 | * @suppress PhanUnreferencedPublicMethod |
60 | */ |
61 | public function clearCache(): void { |
62 | $this->extensionJsonLoaded = false; |
63 | } |
64 | |
65 | /** |
66 | * Add a hook implementation to our list. |
67 | * |
68 | * This also handles parser hooks which aren't normal hooks. |
69 | * Non-normal hooks start their name with a "!" |
70 | * |
71 | * @param string $hookName Name of hook |
72 | * @param FullyQualifiedFunctionLikeName $fqsen The implementing method |
73 | * @return bool true if already registered, false otherwise |
74 | */ |
75 | public function registerHook( string $hookName, FullyQualifiedFunctionLikeName $fqsen ): bool { |
76 | if ( !isset( $this->hookSubscribers[$hookName] ) ) { |
77 | $this->hookSubscribers[$hookName] = []; |
78 | } |
79 | if ( in_array( $fqsen, $this->hookSubscribers[$hookName], true ) ) { |
80 | return true; |
81 | } |
82 | $this->hookSubscribers[$hookName][] = $fqsen; |
83 | return false; |
84 | } |
85 | |
86 | /** |
87 | * Register hooks from extension.json/skin.json |
88 | * |
89 | * Assumes extension.json/skin.json is in project root directory |
90 | * unless SECURITY_CHECK_EXT_PATH is set |
91 | */ |
92 | protected function loadExtensionJson(): void { |
93 | if ( $this->extensionJsonLoaded ) { |
94 | return; |
95 | } |
96 | foreach ( [ 'extension.json', 'skin.json' ] as $filename ) { |
97 | $envPath = getenv( 'SECURITY_CHECK_EXT_PATH' ); |
98 | if ( $envPath ) { |
99 | $jsonPath = $envPath . '/' . $filename; |
100 | } else { |
101 | $jsonPath = Config::projectPath( $filename ); |
102 | } |
103 | if ( file_exists( $jsonPath ) ) { |
104 | $this->readJsonFile( $jsonPath ); |
105 | } |
106 | } |
107 | $this->extensionJsonLoaded = true; |
108 | } |
109 | |
110 | /** |
111 | * @param string $jsonPath |
112 | */ |
113 | private function readJsonFile( string $jsonPath ): void { |
114 | $json = json_decode( file_get_contents( $jsonPath ), true ); |
115 | if ( !is_array( $json ) || !isset( $json['Hooks'] ) || !is_array( $json['Hooks'] ) ) { |
116 | return; |
117 | } |
118 | $namedHandlers = []; |
119 | foreach ( $json['HookHandlers'] ?? [] as $name => $handler ) { |
120 | // TODO: This key is not unique if more than one extension is being analyzed. Is that wanted, though? |
121 | $namedHandlers[$name] = $handler; |
122 | } |
123 | |
124 | foreach ( $json['Hooks'] as $hookName => $cbList ) { |
125 | if ( isset( $cbList["handler"] ) ) { |
126 | $cbList = $cbList["handler"]; |
127 | } |
128 | if ( is_string( $cbList ) ) { |
129 | $cbList = [ $cbList ]; |
130 | } |
131 | |
132 | foreach ( $cbList as $cb ) { |
133 | if ( isset( $namedHandlers[$cb] ) ) { |
134 | // TODO ObjectFactory not fully handled here. Would deserve some code in a general-purpose |
135 | // MediaWiki plugin, see T275742. |
136 | if ( isset( $namedHandlers[$cb]['class'] ) ) { |
137 | // Like core's HookContainer::run |
138 | $normalizedHookName = ucfirst( strtr( $hookName, ':-', '__' ) ); |
139 | $callbackString = $namedHandlers[$cb]['class'] . "::on$normalizedHookName"; |
140 | } elseif ( isset( $namedHandlers[$cb]['factory'] ) ) { |
141 | // TODO: We'd need a CodeBase to retrieve the factory method and check its return value |
142 | continue; |
143 | } else { |
144 | // @phan-suppress-previous-line PhanPluginDuplicateIfStatements |
145 | continue; |
146 | } |
147 | $callback = FullyQualifiedMethodName::fromFullyQualifiedString( $callbackString ); |
148 | } elseif ( strpos( $cb, '::' ) === false ) { |
149 | $callback = FullyQualifiedFunctionName::fromFullyQualifiedString( $cb ); |
150 | } else { |
151 | $callback = FullyQualifiedMethodName::fromFullyQualifiedString( $cb ); |
152 | } |
153 | $this->registerHook( $hookName, $callback ); |
154 | } |
155 | } |
156 | } |
157 | |
158 | /** |
159 | * Get a list of subscribers for hook |
160 | * |
161 | * @param string $hookName Hook in question. Hooks starting with ! are special. |
162 | * @return FullyQualifiedFunctionLikeName[] |
163 | */ |
164 | public function getHookSubscribers( string $hookName ): array { |
165 | $this->loadExtensionJson(); |
166 | return $this->hookSubscribers[$hookName] ?? []; |
167 | } |
168 | |
169 | /** |
170 | * Is a particular function implementing a special hook. |
171 | * |
172 | * @note This assumes that any given func will only implement |
173 | * one hook |
174 | * @param FullyQualifiedFunctionLikeName $fqsen The function to check |
175 | * @return string|null The hook it is implementing or null if no hook |
176 | */ |
177 | public function isSpecialHookSubscriber( FullyQualifiedFunctionLikeName $fqsen ): ?string { |
178 | $this->loadExtensionJson(); |
179 | $specialHooks = [ |
180 | '!ParserFunctionHook', |
181 | '!ParserHook' |
182 | ]; |
183 | |
184 | // @todo This is probably not the most efficient thing. |
185 | foreach ( $specialHooks as $hook ) { |
186 | if ( !isset( $this->hookSubscribers[$hook] ) ) { |
187 | continue; |
188 | } |
189 | if ( in_array( $fqsen, $this->hookSubscribers[$hook], true ) ) { |
190 | return $hook; |
191 | } |
192 | } |
193 | return null; |
194 | } |
195 | |
196 | public function getMwParserClassFQSEN( CodeBase $codeBase ): FullyQualifiedClassName { |
197 | if ( !$this->parserFQSEN ) { |
198 | $namespacedFQSEN = FullyQualifiedClassName::fromFullyQualifiedString( |
199 | '\\MediaWiki\\Parser\\Parser' |
200 | ); |
201 | if ( $codeBase->hasClassWithFQSEN( $namespacedFQSEN ) ) { |
202 | $this->parserFQSEN = $namespacedFQSEN; |
203 | } else { |
204 | $this->parserFQSEN = FullyQualifiedClassName::fromFullyQualifiedString( |
205 | '\\Parser' |
206 | ); |
207 | } |
208 | } |
209 | return $this->parserFQSEN; |
210 | } |
211 | |
212 | public function getPPFrameClassFQSEN( CodeBase $codeBase ): FullyQualifiedClassName { |
213 | if ( !$this->ppFrameFQSEN ) { |
214 | $namespacedFQSEN = FullyQualifiedClassName::fromFullyQualifiedString( |
215 | '\\MediaWiki\\Parser\\PPFrame' |
216 | ); |
217 | if ( $codeBase->hasClassWithFQSEN( $namespacedFQSEN ) ) { |
218 | $this->ppFrameFQSEN = $namespacedFQSEN; |
219 | } else { |
220 | $this->ppFrameFQSEN = FullyQualifiedClassName::fromFullyQualifiedString( |
221 | '\\PPFrame' |
222 | ); |
223 | } |
224 | } |
225 | return $this->ppFrameFQSEN; |
226 | } |
227 | } |