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