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