Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
59.84% |
76 / 127 |
|
41.67% |
5 / 12 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
59.84% |
76 / 127 |
|
41.67% |
5 / 12 |
219.44 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
shouldLoadCodeMirror | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
16 | |||
conflictingGadgetsEnabled | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
42 | |||
onEditPage__showEditForm_initial | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
loadCodeMirrorOnEditPage | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
30 | |||
onEditPage__showReadOnlyForm_initial | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
onUploadForm_initial | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
shouldUseV6 | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
12 | |||
isBetaFeatureEnabled | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
onResourceLoaderGetConfigVars | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onGetPreferences | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
3 | |||
onGetBetaFeaturePreferences | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\CodeMirror; |
4 | |
5 | use InvalidArgumentException; |
6 | use MediaWiki\Config\Config; |
7 | use MediaWiki\EditPage\EditPage; |
8 | use MediaWiki\Extension\BetaFeatures\BetaFeatures; |
9 | use MediaWiki\Extension\Gadgets\GadgetRepo; |
10 | use MediaWiki\Hook\EditPage__showEditForm_initialHook; |
11 | use MediaWiki\Hook\EditPage__showReadOnlyForm_initialHook; |
12 | use MediaWiki\Hook\UploadForm_initialHook; |
13 | use MediaWiki\Output\OutputPage; |
14 | use MediaWiki\Preferences\Hook\GetPreferencesHook; |
15 | use MediaWiki\Registration\ExtensionRegistry; |
16 | use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook; |
17 | use Mediawiki\Specials\SpecialUpload; |
18 | use MediaWiki\User\Options\UserOptionsLookup; |
19 | use MediaWiki\User\User; |
20 | |
21 | /** |
22 | * @phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName |
23 | */ |
24 | class Hooks implements |
25 | EditPage__showEditForm_initialHook, |
26 | EditPage__showReadOnlyForm_initialHook, |
27 | UploadForm_initialHook, |
28 | ResourceLoaderGetConfigVarsHook, |
29 | GetPreferencesHook |
30 | { |
31 | |
32 | private UserOptionsLookup $userOptionsLookup; |
33 | private array $conflictingGadgets; |
34 | private bool $useV6; |
35 | private ?GadgetRepo $gadgetRepo; |
36 | private string $extensionAssetsPath; |
37 | private bool $debugMode; |
38 | private bool $readOnly = false; |
39 | |
40 | /** |
41 | * @param UserOptionsLookup $userOptionsLookup |
42 | * @param Config $config |
43 | * @param GadgetRepo|null $gadgetRepo |
44 | */ |
45 | public function __construct( |
46 | UserOptionsLookup $userOptionsLookup, |
47 | Config $config, |
48 | ?GadgetRepo $gadgetRepo |
49 | ) { |
50 | $this->userOptionsLookup = $userOptionsLookup; |
51 | $this->useV6 = $config->get( 'CodeMirrorV6' ); |
52 | $this->conflictingGadgets = $config->get( 'CodeMirrorConflictingGadgets' ); |
53 | $this->gadgetRepo = $gadgetRepo; |
54 | $this->extensionAssetsPath = $config->get( 'ExtensionAssetsPath' ); |
55 | $this->debugMode = $config->get( 'ShowExceptionDetails' ); |
56 | } |
57 | |
58 | /** |
59 | * Checks if any CodeMirror modules should be loaded on this page or not. |
60 | * Ultimately ::loadCodeMirrorOnEditPage() decides which module(s) get loaded. |
61 | * |
62 | * @param OutputPage $out |
63 | * @param ExtensionRegistry|null $extensionRegistry Overridden in tests. |
64 | * @param bool $supportWikiEditor |
65 | * @return bool |
66 | */ |
67 | public function shouldLoadCodeMirror( |
68 | OutputPage $out, |
69 | ?ExtensionRegistry $extensionRegistry = null, |
70 | bool $supportWikiEditor = true |
71 | ): bool { |
72 | // Disable CodeMirror when CodeEditor is active on this page. |
73 | // Depends on ext.codeEditor being added by \MediaWiki\EditPage\EditPage::showEditForm:initial |
74 | if ( in_array( 'ext.codeEditor', $out->getModules(), true ) ) { |
75 | return false; |
76 | } |
77 | |
78 | $shouldUseV6 = $this->shouldUseV6( $out ); |
79 | $useCodeMirror = $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usecodemirror' ); |
80 | $useWikiEditor = $supportWikiEditor && |
81 | $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usebetatoolbar' ); |
82 | // Disable CodeMirror 5 when the WikiEditor toolbar is not enabled in preferences. |
83 | if ( !$shouldUseV6 && !$useWikiEditor ) { |
84 | return false; |
85 | } |
86 | // In CodeMirror 6, either WikiEditor or the 'usecodemirror' preference must be enabled. |
87 | if ( $shouldUseV6 && !$useWikiEditor && !$useCodeMirror ) { |
88 | return false; |
89 | } |
90 | |
91 | $extensionRegistry ??= ExtensionRegistry::getInstance(); |
92 | // Keys are content models, values are the corresponding CodeMirror modes. |
93 | $contentModels = $extensionRegistry->getAttribute( 'CodeMirrorContentModels' ); |
94 | $contentModel = $out->getTitle()->getContentModel(); |
95 | // b/c: CodeMirrorContentModels extension attribute used to be a flat string array. |
96 | $isSupportedContentModel = $contentModel && ( |
97 | isset( $contentModels[ $contentModel ] ) || |
98 | in_array( $contentModel, $contentModels, true ) |
99 | ); |
100 | $isRTL = $out->getTitle()->getPageLanguage()->isRTL(); |
101 | // Disable CodeMirror if we're on an edit page with a conflicting gadget (T178348) |
102 | return !$this->conflictingGadgetsEnabled( $extensionRegistry, $out->getUser() ) && |
103 | // CodeMirror 5 on any textarea doesn't support RTL (T170001) |
104 | ( !$isRTL || $shouldUseV6 ) && |
105 | // Limit to supported content models. CM5 only supports wikitext. |
106 | // See https://www.mediawiki.org/wiki/Content_handlers#Extension_content_handlers |
107 | ( |
108 | ( $shouldUseV6 && $isSupportedContentModel ) || |
109 | ( !$shouldUseV6 && $contentModel === CONTENT_MODEL_WIKITEXT ) |
110 | ); |
111 | } |
112 | |
113 | /** |
114 | * @param ExtensionRegistry $extensionRegistry |
115 | * @param User $user |
116 | * @return bool |
117 | */ |
118 | private function conflictingGadgetsEnabled( ExtensionRegistry $extensionRegistry, User $user ): bool { |
119 | if ( !$extensionRegistry->isLoaded( 'Gadgets' ) || !$this->gadgetRepo ) { |
120 | return false; |
121 | } |
122 | $conflictingGadgets = array_intersect( $this->conflictingGadgets, $this->gadgetRepo->getGadgetIds() ); |
123 | foreach ( $conflictingGadgets as $conflictingGadget ) { |
124 | try { |
125 | if ( $this->gadgetRepo->getGadget( $conflictingGadget )->isEnabled( $user ) ) { |
126 | return true; |
127 | } |
128 | } catch ( InvalidArgumentException $e ) { |
129 | // Safeguard for an invalid gadget ID; treat as gadget not enabled. |
130 | continue; |
131 | } |
132 | } |
133 | return false; |
134 | } |
135 | |
136 | /** |
137 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/EditPage::showEditForm:initial |
138 | * |
139 | * @param EditPage $editor |
140 | * @param OutputPage $out |
141 | */ |
142 | public function onEditPage__showEditForm_initial( $editor, $out ): void { |
143 | if ( !$this->shouldLoadCodeMirror( $out ) ) { |
144 | return; |
145 | } |
146 | |
147 | $useCodeMirror = $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usecodemirror' ); |
148 | $useWikiEditor = $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usebetatoolbar' ); |
149 | |
150 | if ( $this->shouldUseV6( $out ) ) { |
151 | // Pre-deliver modules for faster loading. |
152 | $this->loadCodeMirrorOnEditPage( $out ); |
153 | } elseif ( $useWikiEditor ) { |
154 | // Legacy CM5 |
155 | |
156 | // ext.CodeMirror.WikiEditor adds the toggle button to the toolbar. |
157 | $out->addModules( 'ext.CodeMirror.WikiEditor' ); |
158 | |
159 | if ( $useCodeMirror ) { |
160 | // These modules are predelivered for performance when needed |
161 | // keep these modules in sync with ext.CodeMirror.js |
162 | $out->addModules( [ 'ext.CodeMirror.lib', 'ext.CodeMirror.mode.mediawiki' ] ); |
163 | } |
164 | } |
165 | } |
166 | |
167 | /** |
168 | * Set client-side JS variables and pre-deliver modules for optimal performance. |
169 | * `cmRLModules` is a list of modules that will be lazy-loaded by the client, and, |
170 | * if the 'usecodemirror' preference is enabled, pre-delivered by ResourceLoader. |
171 | * |
172 | * @param OutputPage $out |
173 | * @param bool $supportWikiEditor |
174 | */ |
175 | private function loadCodeMirrorOnEditPage( OutputPage $out, bool $supportWikiEditor = true ): void { |
176 | $useCodeMirror = $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usecodemirror' ); |
177 | $useWikiEditor = $supportWikiEditor && |
178 | $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usebetatoolbar' ); |
179 | $modules = [ |
180 | 'ext.CodeMirror.v6', |
181 | ...( $useWikiEditor ? [ 'ext.CodeMirror.v6.WikiEditor' ] : [] ), |
182 | 'ext.CodeMirror.v6.lib', |
183 | 'ext.CodeMirror.v6.init', |
184 | 'ext.CodeMirror.v6.mode.mediawiki' |
185 | ]; |
186 | |
187 | if ( $useCodeMirror ) { |
188 | // Pre-deliver modules if we know we're going to need them. |
189 | $out->addModules( $modules ); |
190 | } elseif ( $useWikiEditor ) { |
191 | // Load only the init module, which will add the toolbar button |
192 | // and lazy-load the rest of the modules via the cmRLModules config variable. |
193 | $out->addModules( 'ext.CodeMirror.v6.init' ); |
194 | } |
195 | |
196 | $out->addJsConfigVars( [ |
197 | 'cmRLModules' => $modules, |
198 | 'cmReadOnly' => $this->readOnly, |
199 | 'cmDebug' => $this->debugMode |
200 | ] ); |
201 | } |
202 | |
203 | /** |
204 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/EditPage::showReadOnlyForm:initial |
205 | * |
206 | * @param EditPage $editor |
207 | * @param OutputPage $out |
208 | */ |
209 | public function onEditPage__showReadOnlyForm_initial( $editor, $out ): void { |
210 | if ( $this->shouldUseV6( $out ) && $this->shouldLoadCodeMirror( $out ) ) { |
211 | $this->readOnly = true; |
212 | $this->loadCodeMirrorOnEditPage( $out ); |
213 | } |
214 | } |
215 | |
216 | /** |
217 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/UploadForm:initial |
218 | * |
219 | * @param SpecialUpload $upload |
220 | */ |
221 | public function onUploadForm_initial( $upload ): void { |
222 | if ( $upload->mForReUpload ) { |
223 | return; |
224 | } |
225 | $out = $upload->getOutput(); |
226 | if ( $this->shouldUseV6( $out ) && $this->shouldLoadCodeMirror( $out, null, false ) ) { |
227 | $this->loadCodeMirrorOnEditPage( $out, false ); |
228 | } |
229 | } |
230 | |
231 | /** |
232 | * @param OutputPage $out |
233 | * @return bool |
234 | * @todo Remove check for cm6enable flag after migration is complete |
235 | */ |
236 | private function shouldUseV6( OutputPage $out ): bool { |
237 | return $this->useV6 || $out->getRequest()->getBool( 'cm6enable' ) || |
238 | $this->isBetaFeatureEnabled( $out->getUser() ); |
239 | } |
240 | |
241 | /** |
242 | * @param User $user |
243 | * @return bool |
244 | */ |
245 | private function isBetaFeatureEnabled( User $user ): bool { |
246 | return ExtensionRegistry::getInstance()->isLoaded( 'BetaFeatures' ) && |
247 | BetaFeatures::isFeatureEnabled( $user, 'codemirror-beta-feature-enable' ); |
248 | } |
249 | |
250 | /** |
251 | * Hook handler for enabling bracket matching. |
252 | * |
253 | * TODO: Remove after migration to CodeMirror 6 is complete. |
254 | * |
255 | * @param array &$vars Array of variables to be added into the output of the startup module |
256 | * @param string $skin |
257 | * @param Config $config |
258 | * @return void This hook must not abort, it must return no value |
259 | */ |
260 | public function onResourceLoaderGetConfigVars( array &$vars, $skin, Config $config ): void { |
261 | $vars['wgCodeMirrorLineNumberingNamespaces'] = $config->get( 'CodeMirrorLineNumberingNamespaces' ); |
262 | } |
263 | |
264 | /** |
265 | * GetPreferences hook handler |
266 | * |
267 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetPreferences |
268 | * |
269 | * @param User $user |
270 | * @param array &$defaultPreferences |
271 | * @return bool|void True or no return value to continue or false to abort |
272 | */ |
273 | public function onGetPreferences( $user, &$defaultPreferences ) { |
274 | if ( !$this->useV6 && !$this->isBetaFeatureEnabled( $user ) ) { |
275 | $defaultPreferences['usecodemirror'] = [ |
276 | 'type' => 'api', |
277 | ]; |
278 | |
279 | // The following messages are generated upstream by the 'section' value |
280 | // * prefs-accessibility |
281 | $defaultPreferences['usecodemirror-colorblind'] = [ |
282 | 'type' => 'toggle', |
283 | 'label-message' => 'codemirror-prefs-colorblind', |
284 | 'help-message' => 'codemirror-prefs-colorblind-help', |
285 | 'section' => 'editing/accessibility', |
286 | ]; |
287 | return; |
288 | } |
289 | |
290 | // Show message with a link to the Help page under "Syntax highlighting". |
291 | // The following messages are generated upstream by the 'section' value: |
292 | // * prefs-syntax-highlighting |
293 | $defaultPreferences['usecodemirror-summary'] = [ |
294 | 'type' => 'info', |
295 | 'default' => wfMessage( 'codemirror-prefs-summary' )->parse(), |
296 | 'raw' => true, |
297 | 'section' => 'editing/syntax-highlighting' |
298 | ]; |
299 | |
300 | // CodeMirror is disabled by default for all users. It can enabled for everyone |
301 | // by default by adding '$wgDefaultUserOptions['usecodemirror'] = 1;' into LocalSettings.php |
302 | $defaultPreferences['usecodemirror'] = [ |
303 | 'type' => 'toggle', |
304 | 'label-message' => 'codemirror-prefs-enable', |
305 | 'section' => 'editing/syntax-highlighting', |
306 | ]; |
307 | |
308 | $defaultPreferences['usecodemirror-colorblind'] = [ |
309 | 'type' => 'toggle', |
310 | 'label-message' => 'codemirror-v6-prefs-colorblind', |
311 | 'section' => 'editing/syntax-highlighting', |
312 | 'disable-if' => [ '!==', 'usecodemirror', '1' ] |
313 | ]; |
314 | |
315 | $defaultPreferences['codemirror-preferences'] = [ |
316 | 'type' => 'api', |
317 | ]; |
318 | } |
319 | |
320 | /** |
321 | * GetBetaFeaturePreferences hook handler |
322 | * |
323 | * @param User $user |
324 | * @param array &$betaPrefs |
325 | */ |
326 | public function onGetBetaFeaturePreferences( User $user, array &$betaPrefs ): void { |
327 | if ( $this->useV6 ) { |
328 | return; |
329 | } |
330 | $betaPrefs[ 'codemirror-beta-feature-enable' ] = [ |
331 | 'label-message' => 'codemirror-beta-feature-title', |
332 | 'desc-message' => 'codemirror-beta-feature-description', |
333 | 'screenshot' => [ |
334 | 'ltr' => $this->extensionAssetsPath . '/CodeMirror/resources/images/codemirror.beta-feature-ltr.svg', |
335 | 'rtl' => $this->extensionAssetsPath . '/CodeMirror/resources/images/codemirror.beta-feature-rtl.svg' |
336 | ], |
337 | 'info-link' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Extension:CodeMirror', |
338 | 'discussion-link' => 'https://www.mediawiki.org/wiki/Help_talk:Extension:CodeMirror' |
339 | ]; |
340 | } |
341 | } |