Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.21% covered (warning)
78.21%
61 / 78
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
78.21% covered (warning)
78.21%
61 / 78
37.50% covered (danger)
37.50%
3 / 8
44.27
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 shouldLoadCodeMirror
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
12
 conflictingGadgetsEnabled
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 onEditPage__showEditForm_initial
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
5.01
 onEditPage__showReadOnlyForm_initial
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 shouldUseV6
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 onResourceLoaderGetConfigVars
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onGetPreferences
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace MediaWiki\Extension\CodeMirror;
4
5use ExtensionRegistry;
6use InvalidArgumentException;
7use MediaWiki\Config\Config;
8use MediaWiki\EditPage\EditPage;
9use MediaWiki\Extension\Gadgets\GadgetRepo;
10use MediaWiki\Hook\EditPage__showEditForm_initialHook;
11use MediaWiki\Hook\EditPage__showReadOnlyForm_initialHook;
12use MediaWiki\Output\OutputPage;
13use MediaWiki\Preferences\Hook\GetPreferencesHook;
14use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook;
15use MediaWiki\User\Options\UserOptionsLookup;
16use MediaWiki\User\User;
17
18/**
19 * @phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
20 */
21class Hooks implements
22    EditPage__showEditForm_initialHook,
23    EditPage__showReadOnlyForm_initialHook,
24    ResourceLoaderGetConfigVarsHook,
25    GetPreferencesHook
26{
27
28    private UserOptionsLookup $userOptionsLookup;
29    private array $conflictingGadgets;
30    private bool $useV6;
31    private bool $isSupportedRtlWiki;
32    private ?GadgetRepo $gadgetRepo;
33
34    /**
35     * @param UserOptionsLookup $userOptionsLookup
36     * @param Config $config
37     * @param GadgetRepo|null $gadgetRepo
38     */
39    public function __construct(
40        UserOptionsLookup $userOptionsLookup,
41        Config $config,
42        ?GadgetRepo $gadgetRepo
43    ) {
44        $this->userOptionsLookup = $userOptionsLookup;
45        $this->useV6 = $config->get( 'CodeMirrorV6' );
46        $this->conflictingGadgets = $config->get( 'CodeMirrorConflictingGadgets' );
47        $this->isSupportedRtlWiki = $config->get( 'CodeMirrorRTL' );
48        $this->gadgetRepo = $gadgetRepo;
49    }
50
51    /**
52     * Checks if CodeMirror for textarea wikitext editor should be loaded on this page or not.
53     *
54     * @param OutputPage $out
55     * @param ExtensionRegistry|null $extensionRegistry Overridden in tests.
56     * @return bool
57     */
58    public function shouldLoadCodeMirror( OutputPage $out, ?ExtensionRegistry $extensionRegistry = null ): bool {
59        // Disable CodeMirror when CodeEditor is active on this page
60        // Depends on ext.codeEditor being added by \MediaWiki\EditPage\EditPage::showEditForm:initial
61        if ( in_array( 'ext.codeEditor', $out->getModules(), true ) ) {
62            return false;
63        }
64
65        $shouldUseV6 = $this->shouldUseV6( $out );
66        $useCodeMirror = $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usecodemirror' );
67        $useWikiEditor = $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usebetatoolbar' );
68        // Disable CodeMirror 5 when the WikiEditor toolbar is not enabled in preferences.
69        if ( !$shouldUseV6 && !$useWikiEditor ) {
70            return false;
71        }
72        // In CodeMirror 6, either WikiEditor or the 'usecodemirror' preference must be enabled.
73        if ( $shouldUseV6 && !$useWikiEditor && !$useCodeMirror ) {
74            return false;
75        }
76
77        $extensionRegistry = $extensionRegistry ?: ExtensionRegistry::getInstance();
78        $contentModels = $extensionRegistry->getAttribute( 'CodeMirrorContentModels' );
79        $isRTL = $out->getTitle()->getPageLanguage()->isRTL();
80        // Disable CodeMirror if we're on an edit page with a conflicting gadget. See T178348.
81        return !$this->conflictingGadgetsEnabled( $extensionRegistry, $out->getUser() ) &&
82            // CodeMirror 5 on textarea wikitext editors doesn't support RTL (T170001)
83            ( !$isRTL || ( $this->shouldUseV6( $out ) && $this->isSupportedRtlWiki ) ) &&
84            // Limit to supported content models that use wikitext.
85            // See https://www.mediawiki.org/wiki/Content_handlers#Extension_content_handlers
86            in_array( $out->getTitle()->getContentModel(), $contentModels );
87    }
88
89    /**
90     * @param ExtensionRegistry $extensionRegistry
91     * @param User $user
92     * @return bool
93     */
94    private function conflictingGadgetsEnabled( ExtensionRegistry $extensionRegistry, User $user ): bool {
95        if ( !$extensionRegistry->isLoaded( 'Gadgets' ) || !$this->gadgetRepo ) {
96            return false;
97        }
98        $conflictingGadgets = array_intersect( $this->conflictingGadgets, $this->gadgetRepo->getGadgetIds() );
99        foreach ( $conflictingGadgets as $conflictingGadget ) {
100            try {
101                if ( $this->gadgetRepo->getGadget( $conflictingGadget )->isEnabled( $user ) ) {
102                    return true;
103                }
104            } catch ( InvalidArgumentException $e ) {
105                // Safeguard for an invalid gadget ID; treat as gadget not enabled.
106                continue;
107            }
108        }
109        return false;
110    }
111
112    /**
113     * Load CodeMirror if necessary.
114     *
115     * @see https://www.mediawiki.org/wiki/Manual:Hooks/EditPage::showEditForm:initial
116     *
117     * @param EditPage $editor
118     * @param OutputPage $out
119     */
120    public function onEditPage__showEditForm_initial( $editor, $out ): void {
121        if ( !$this->shouldLoadCodeMirror( $out ) ) {
122            return;
123        }
124
125        $useCodeMirror = $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usecodemirror' );
126        $useWikiEditor = $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usebetatoolbar' );
127
128        if ( $this->shouldUseV6( $out ) ) {
129            $out->addModules( $useWikiEditor ?
130                'ext.CodeMirror.v6.WikiEditor.init' :
131                'ext.CodeMirror.v6.init'
132            );
133        } else {
134            $out->addModules( 'ext.CodeMirror.WikiEditor' );
135
136            if ( $useCodeMirror ) {
137                // These modules are predelivered for performance when needed
138                // keep these modules in sync with ext.CodeMirror.js
139                $out->addModules( [ 'ext.CodeMirror.lib', 'ext.CodeMirror.mode.mediawiki' ] );
140            }
141        }
142    }
143
144    /**
145     * Load CodeMirror 6 on read-only pages.
146     *
147     * @param EditPage $editor
148     * @param OutputPage $out
149     */
150    public function onEditPage__showReadOnlyForm_initial( $editor, $out ): void {
151        if ( $this->shouldUseV6( $out ) && $this->shouldLoadCodeMirror( $out ) ) {
152            $useWikiEditor = $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usebetatoolbar' );
153            $out->addModules( $useWikiEditor ?
154                'ext.CodeMirror.v6.WikiEditor.init' :
155                'ext.CodeMirror.v6.init'
156            );
157        }
158    }
159
160    /**
161     * @param OutputPage $out
162     * @return bool
163     * @todo Remove check for cm6enable flag after migration is complete
164     */
165    private function shouldUseV6( OutputPage $out ): bool {
166        return $this->useV6 || $out->getRequest()->getRawVal( 'cm6enable' );
167    }
168
169    /**
170     * Hook handler for enabling bracket matching.
171     *
172     * TODO: Remove after migration to CodeMirror 6 is complete.
173     *
174     * @param array &$vars Array of variables to be added into the output of the startup module
175     * @param string $skin
176     * @param Config $config
177     * @return void This hook must not abort, it must return no value
178     */
179    public function onResourceLoaderGetConfigVars( array &$vars, $skin, Config $config ): void {
180        $vars['wgCodeMirrorLineNumberingNamespaces'] = $config->get( 'CodeMirrorLineNumberingNamespaces' );
181    }
182
183    /**
184     * GetPreferences hook handler
185     *
186     * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetPreferences
187     *
188     * @param User $user
189     * @param array &$defaultPreferences
190     * @return bool|void True or no return value to continue or false to abort
191     */
192    public function onGetPreferences( $user, &$defaultPreferences ) {
193        if ( !$this->useV6 ) {
194            $defaultPreferences['usecodemirror'] = [
195                'type' => 'api',
196            ];
197
198            // The following messages are generated upstream by the 'section' value
199            // * prefs-accessibility
200            $defaultPreferences['usecodemirror-colorblind'] = [
201                'type' => 'toggle',
202                'label-message' => 'codemirror-prefs-colorblind',
203                'help-message' => 'codemirror-prefs-colorblind-help',
204                'section' => 'editing/accessibility',
205            ];
206            return;
207        }
208
209        // Show message with a link to the Help page under "Syntax highlighting".
210        // The following messages are generated upstream by the 'section' value:
211        // * prefs-syntax-highlighting
212        $defaultPreferences['usecodemirror-summary'] = [
213            'type' => 'info',
214            'default' => wfMessage( 'codemirror-prefs-summary' )->parse(),
215            'raw' => true,
216            'section' => 'editing/syntax-highlighting'
217        ];
218
219        // CodeMirror is disabled by default for all users. It can enabled for everyone
220        // by default by adding '$wgDefaultUserOptions['usecodemirror'] = 1;' into LocalSettings.php
221        $defaultPreferences['usecodemirror'] = [
222            'type' => 'toggle',
223            'label-message' => 'codemirror-prefs-enable',
224            'section' => 'editing/syntax-highlighting',
225        ];
226
227        $defaultPreferences['usecodemirror-colorblind'] = [
228            'type' => 'toggle',
229            'label-message' => 'codemirror-prefs-colorblind',
230            'section' => 'editing/syntax-highlighting',
231            'disable-if' => [ '!==', 'usecodemirror', '1' ]
232        ];
233    }
234}