Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 77 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
MediaWikiHooksHelper | |
0.00% |
0 / 77 |
|
0.00% |
0 / 9 |
1260 | |
0.00% |
0 / 1 |
getInstance | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
clearCache | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
registerHook | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
loadExtensionJson | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
readJsonFile | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
182 | |||
getHookSubscribers | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
isSpecialHookSubscriber | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
getMwParserClassFQSEN | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getPPFrameClassFQSEN | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 |
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 | public static function getInstance(): self { |
47 | if ( !self::$instance ) { |
48 | self::$instance = new self; |
49 | } |
50 | return self::$instance; |
51 | } |
52 | |
53 | /** |
54 | * Clear the extension.json cache, for testing purpose |
55 | * |
56 | * @suppress PhanUnreferencedPublicMethod |
57 | */ |
58 | public function clearCache(): void { |
59 | $this->extensionJsonLoaded = false; |
60 | } |
61 | |
62 | /** |
63 | * Add a hook implementation to our list. |
64 | * |
65 | * This also handles parser hooks which aren't normal hooks. |
66 | * Non-normal hooks start their name with a "!" |
67 | * |
68 | * @param string $hookName Name of hook |
69 | * @param FullyQualifiedFunctionLikeName $fqsen The implementing method |
70 | * @return bool true if already registered, false otherwise |
71 | */ |
72 | public function registerHook( string $hookName, FullyQualifiedFunctionLikeName $fqsen ): bool { |
73 | if ( !isset( $this->hookSubscribers[$hookName] ) ) { |
74 | $this->hookSubscribers[$hookName] = []; |
75 | } |
76 | if ( in_array( $fqsen, $this->hookSubscribers[$hookName], true ) ) { |
77 | return true; |
78 | } |
79 | $this->hookSubscribers[$hookName][] = $fqsen; |
80 | return false; |
81 | } |
82 | |
83 | /** |
84 | * Register hooks from extension.json/skin.json |
85 | * |
86 | * Assumes extension.json/skin.json is in project root directory |
87 | * unless SECURITY_CHECK_EXT_PATH is set |
88 | */ |
89 | protected function loadExtensionJson(): void { |
90 | if ( $this->extensionJsonLoaded ) { |
91 | return; |
92 | } |
93 | foreach ( [ 'extension.json', 'skin.json' ] as $filename ) { |
94 | $envPath = getenv( 'SECURITY_CHECK_EXT_PATH' ); |
95 | if ( $envPath ) { |
96 | $jsonPath = $envPath . '/' . $filename; |
97 | } else { |
98 | $jsonPath = Config::projectPath( $filename ); |
99 | } |
100 | if ( file_exists( $jsonPath ) ) { |
101 | $this->readJsonFile( $jsonPath ); |
102 | } |
103 | } |
104 | $this->extensionJsonLoaded = true; |
105 | } |
106 | |
107 | private function readJsonFile( string $jsonPath ): void { |
108 | $json = json_decode( file_get_contents( $jsonPath ), true ); |
109 | if ( !is_array( $json ) || !isset( $json['Hooks'] ) || !is_array( $json['Hooks'] ) ) { |
110 | return; |
111 | } |
112 | $namedHandlers = []; |
113 | foreach ( $json['HookHandlers'] ?? [] as $name => $handler ) { |
114 | // TODO: This key is not unique if more than one extension is being analyzed. Is that wanted, though? |
115 | $namedHandlers[$name] = $handler; |
116 | } |
117 | |
118 | foreach ( $json['Hooks'] as $hookName => $cbList ) { |
119 | if ( isset( $cbList["handler"] ) ) { |
120 | $cbList = $cbList["handler"]; |
121 | } |
122 | if ( is_string( $cbList ) ) { |
123 | $cbList = [ $cbList ]; |
124 | } |
125 | |
126 | foreach ( $cbList as $cb ) { |
127 | if ( isset( $namedHandlers[$cb] ) ) { |
128 | // TODO ObjectFactory not fully handled here. Would deserve some code in a general-purpose |
129 | // MediaWiki plugin, see T275742. |
130 | if ( isset( $namedHandlers[$cb]['class'] ) ) { |
131 | // Like core's HookContainer::run |
132 | $normalizedHookName = ucfirst( strtr( $hookName, ':-', '__' ) ); |
133 | $callbackString = $namedHandlers[$cb]['class'] . "::on$normalizedHookName"; |
134 | } elseif ( isset( $namedHandlers[$cb]['factory'] ) ) { |
135 | // TODO: We'd need a CodeBase to retrieve the factory method and check its return value |
136 | continue; |
137 | } else { |
138 | // @phan-suppress-previous-line PhanPluginDuplicateIfStatements |
139 | continue; |
140 | } |
141 | $callback = FullyQualifiedMethodName::fromFullyQualifiedString( $callbackString ); |
142 | } elseif ( strpos( $cb, '::' ) === false ) { |
143 | $callback = FullyQualifiedFunctionName::fromFullyQualifiedString( $cb ); |
144 | } else { |
145 | $callback = FullyQualifiedMethodName::fromFullyQualifiedString( $cb ); |
146 | } |
147 | $this->registerHook( $hookName, $callback ); |
148 | } |
149 | } |
150 | } |
151 | |
152 | /** |
153 | * Get a list of subscribers for hook |
154 | * |
155 | * @param string $hookName Hook in question. Hooks starting with ! are special. |
156 | * @return FullyQualifiedFunctionLikeName[] |
157 | */ |
158 | public function getHookSubscribers( string $hookName ): array { |
159 | $this->loadExtensionJson(); |
160 | return $this->hookSubscribers[$hookName] ?? []; |
161 | } |
162 | |
163 | /** |
164 | * Is a particular function implementing a special hook. |
165 | * |
166 | * @note This assumes that any given func will only implement |
167 | * one hook |
168 | * @param FullyQualifiedFunctionLikeName $fqsen The function to check |
169 | * @return string|null The hook it is implementing or null if no hook |
170 | */ |
171 | public function isSpecialHookSubscriber( FullyQualifiedFunctionLikeName $fqsen ): ?string { |
172 | $this->loadExtensionJson(); |
173 | $specialHooks = [ |
174 | '!ParserFunctionHook', |
175 | '!ParserHook' |
176 | ]; |
177 | |
178 | // @todo This is probably not the most efficient thing. |
179 | foreach ( $specialHooks as $hook ) { |
180 | if ( !isset( $this->hookSubscribers[$hook] ) ) { |
181 | continue; |
182 | } |
183 | if ( in_array( $fqsen, $this->hookSubscribers[$hook], true ) ) { |
184 | return $hook; |
185 | } |
186 | } |
187 | return null; |
188 | } |
189 | |
190 | public function getMwParserClassFQSEN( CodeBase $codeBase ): FullyQualifiedClassName { |
191 | if ( !$this->parserFQSEN ) { |
192 | $namespacedFQSEN = FullyQualifiedClassName::fromFullyQualifiedString( |
193 | '\\MediaWiki\\Parser\\Parser' |
194 | ); |
195 | if ( $codeBase->hasClassWithFQSEN( $namespacedFQSEN ) ) { |
196 | $this->parserFQSEN = $namespacedFQSEN; |
197 | } else { |
198 | $this->parserFQSEN = FullyQualifiedClassName::fromFullyQualifiedString( |
199 | '\\Parser' |
200 | ); |
201 | } |
202 | } |
203 | return $this->parserFQSEN; |
204 | } |
205 | |
206 | public function getPPFrameClassFQSEN( CodeBase $codeBase ): FullyQualifiedClassName { |
207 | if ( !$this->ppFrameFQSEN ) { |
208 | $namespacedFQSEN = FullyQualifiedClassName::fromFullyQualifiedString( |
209 | '\\MediaWiki\\Parser\\PPFrame' |
210 | ); |
211 | if ( $codeBase->hasClassWithFQSEN( $namespacedFQSEN ) ) { |
212 | $this->ppFrameFQSEN = $namespacedFQSEN; |
213 | } else { |
214 | $this->ppFrameFQSEN = FullyQualifiedClassName::fromFullyQualifiedString( |
215 | '\\PPFrame' |
216 | ); |
217 | } |
218 | } |
219 | return $this->ppFrameFQSEN; |
220 | } |
221 | } |