Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 180
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 180
0.00% covered (danger)
0.00%
0 / 12
2256
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onGetPreferences
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 onArticleParserOptions
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 onParserOutputPostCacheTransform
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
110
 onSidebarBeforeOutput
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
156
 onResourceLoaderGetConfigVars
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 onListDefinedTags
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onChangeTagsListActive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onChangeTagsAllowedAdd
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onConfirmEditTriggersCaptcha
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
90
 getFeedbackTitleUrl
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 shouldShowReportVisualBug
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace MediaWiki\Extension\ParserMigration;
4
5use MediaWiki\ChangeTags\Hook\ChangeTagsAllowedAddHook;
6use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
7use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
8use MediaWiki\Config\Config;
9use MediaWiki\Hook\ParserOutputPostCacheTransformHook;
10use MediaWiki\Hook\SidebarBeforeOutputHook;
11use MediaWiki\Html\Html;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Page\Article;
14use MediaWiki\Page\Hook\ArticleParserOptionsHook;
15use MediaWiki\Page\PageIdentity;
16use MediaWiki\Parser\ParserOptions;
17use MediaWiki\Parser\ParserOutput;
18use MediaWiki\Preferences\Hook\GetPreferencesHook;
19use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook;
20use MediaWiki\Skin\Skin;
21use MediaWiki\Title\Title;
22use MediaWiki\Title\TitleValue;
23use MediaWiki\User\Options\UserOptionsManager;
24use MediaWiki\User\User;
25use MediaWiki\WikiMap\WikiMap;
26
27class Hooks implements
28    ArticleParserOptionsHook,
29    GetPreferencesHook,
30    ParserOutputPostCacheTransformHook,
31    ResourceLoaderGetConfigVarsHook,
32    SidebarBeforeOutputHook,
33    ListDefinedTagsHook,
34    ChangeTagsListActiveHook,
35    ChangeTagsAllowedAddHook
36{
37
38    public function __construct(
39        private readonly Config $mainConfig,
40        private readonly UserOptionsManager $userOptionsManager,
41        private readonly Oracle $oracle,
42    ) {
43    }
44
45    /**
46     * @param User $user
47     * @param array &$defaultPreferences
48     * @return bool
49     */
50    public function onGetPreferences( $user, &$defaultPreferences ) {
51        $defaultPreferences['parsermigration-parsoid-readviews'] = [
52            'type' => 'select',
53            'label-message' => 'parsermigration-parsoid-readviews-selector-label',
54            'help-message' => 'parsermigration-parsoid-readviews-selector-help',
55            'section' => 'editing/developertools',
56            'options-messages' => [
57                'parsermigration-parsoid-readviews-always' => Oracle::USERPREF_ALWAYS,
58                'parsermigration-parsoid-readviews-default' => Oracle::USERPREF_DEFAULT,
59                'parsermigration-parsoid-readviews-never' => Oracle::USERPREF_NEVER,
60            ],
61        ];
62
63        $defaultPreferences['parsermigration'] = [
64            'type' => 'toggle',
65            'label-message' => 'parsermigration-pref-label',
66            'help-message' => 'parsermigration-pref-help',
67            'section' => 'editing/developertools'
68        ];
69
70        return true;
71    }
72
73    /**
74     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleParserOptions
75     * @param Article $article
76     * @param ParserOptions $popts
77     * @return bool|void
78     */
79    public function onArticleParserOptions(
80        Article $article, ParserOptions $popts
81    ) {
82        // T348257: Allow individual user to opt in to Parsoid read views as a
83        // user option in the ParserMigration section.
84        $context = $article->getContext();
85        if ( $this->oracle->shouldUseParsoid( $context->getUser(), $context->getRequest(), $article->getTitle() ) ) {
86            $popts->setUseParsoid();
87        }
88        return true;
89    }
90
91    /**
92     * This hook is called from ParserOutput::runOutputPipeline() to do
93     * post-cache transforms.
94     *
95     * @since 1.35
96     *
97     * @param ParserOutput $parserOutput
98     * @param string &$text Text being transformed, before core transformations are done
99     * @param array &$options Options array being used for the transformation
100     * @return void This hook must not abort, it must return no value
101     */
102    public function onParserOutputPostCacheTransform( $parserOutput, &$text,
103        &$options
104    ): void {
105        // Make "whether Parsoid was used" visible to client-side JS
106        $user = null;
107        if ( $parserOutput->getContentHolder()->isParsoidContent() ) {
108            $parserOutput->setJsConfigVar( 'parsermigration-parsoid', true );
109            // Add a user notice for named users
110            $named = false;
111            $userPref = Oracle::USERPREF_DEFAULT;
112            if ( $options['skin'] ?? null ) {
113                $user = $options['skin']->getUser();
114                $named = $user->isNamed();
115                $userPref = intval( $this->userOptionsManager->getOption(
116                    $user, 'parsermigration-parsoid-readviews'
117                ) );
118            }
119            if (
120                $this->mainConfig->get( 'ParserMigrationEnableUserNotice' ) &&
121                // Only display user notice for logged in ("named") users
122                $named
123            ) {
124                $parserOutput->setJsConfigVar(
125                    'parsermigration-notice-version',
126                    $this->mainConfig->get(
127                        'ParserMigrationUserNoticeVersion'
128                    )
129                );
130                $parserOutput->setJsConfigVar(
131                    'parsermigration-notice-days',
132                    $this->mainConfig->get(
133                        'ParserMigrationUserNoticeDays'
134                    )
135                );
136                $parserOutput->addModules( [ 'ext.parsermigration.notice' ] );
137            }
138
139            if (
140                $this->mainConfig->get( 'ParserMigrationEnableIndicator' ) &&
141                // Only display indicator for "opt in always" users.
142                $userPref === Oracle::USERPREF_ALWAYS
143            ) {
144                // Add an indicator using an ad-hoc Codex InfoChip
145                // Replace when T357324 blesses a CSS-only InfoChip
146                $parserOutput->addModuleStyles( [ 'ext.parsermigration.indicator' ] );
147                $parserOutput->setIndicator(
148                    'parsoid',
149                    $this->mainConfig->get( 'ParserMigrationCompactIndicator' ) ?
150                    Html::rawElement(
151                        'div',
152                        [
153                            'class' => 'mw-parsoid-icon notheme mw-no-invert',
154                            'title' => wfMessage( 'parsermigration-parsoid-chip-label' )->text(),
155                        ]
156                    ) :
157                    Html::rawElement(
158                        'div',
159                        [ 'class' => 'cdx-info-chip' ],
160                        Html::element(
161                            'span',
162                            [ 'class' => 'cdx-info-chip--text' ],
163                            wfMessage( 'parsermigration-parsoid-chip-label' )->text()
164                        )
165                    )
166                );
167            }
168
169            if ( $user === null || $this->shouldShowReportVisualBug( $user ) ) {
170                // Harmless to add this module if we don't know user.
171                $parserOutput->addModules( [ 'ext.parsermigration.reportbug.init' ] );
172            }
173
174        }
175    }
176
177    /**
178     * @param Skin $skin
179     * @param array &$sidebar Sidebar content
180     * @return void
181     */
182    public function onSidebarBeforeOutput( $skin, &$sidebar ): void {
183        $out = $skin->getOutput();
184        if ( !$out->isArticleRelated() ) {
185            // Only add sidebar links before article-related pages
186            return;
187        }
188
189        $user = $skin->getUser();
190        $title = $skin->getTitle();
191        $usingParsoid = $this->oracle->shouldUseParsoid( $user, $skin->getRequest(), $title );
192
193        $queryStringEnabled = $this->mainConfig->get(
194            'ParserMigrationEnableQueryString'
195        );
196        $reportVisualBugEnabled = $this->mainConfig->get(
197            'ParserMigrationEnableReportVisualBug'
198        );
199
200        $editToolPref = $this->userOptionsManager->getOption(
201            $user, 'parsermigration'
202        );
203        $userPref = intval( $this->userOptionsManager->getOption(
204            $user, 'parsermigration-parsoid-readviews'
205        ) );
206
207        $shouldShowToggle = false;
208        if ( $editToolPref && $queryStringEnabled ) {
209            $sidebar['TOOLBOX']['parsermigration-edit-tool'] = [
210                'href' => $title->getLocalURL( [
211                    'action' => 'parsermigration-edit',
212                ] ),
213                'text' => $skin->msg( 'parsermigration-toolbox-label' )->text(),
214            ];
215            $shouldShowToggle = true;
216        }
217        if ( $this->oracle->isParsoidDefaultFor( $title ) ) {
218            $shouldShowToggle = true;
219        }
220        if ( $userPref === Oracle::USERPREF_ALWAYS ) {
221            $shouldShowToggle = true;
222        }
223        if ( !$queryStringEnabled ) {
224            // On some wikis we don't want the user to be able to put
225            // Parsoid pages into the parser cache.
226            $shouldShowToggle = false;
227        }
228
229        if ( $shouldShowToggle ) {
230            $queryParams = $out->getRequest()->getQueryValues();
231            // Allow toggling 'useParsoid' from the current state
232            $queryParams[ 'useparsoid' ] = $usingParsoid ? '0' : '1';
233            // title is handled by getLocalURL, no need to pass it twice from a index.php?title= url
234            unset( $queryParams[ 'title' ] );
235            $sidebar[ 'TOOLBOX' ][ 'parsermigration-switch' ] = [
236                'href' => $title->getLocalURL( $queryParams ),
237                'text' => $skin->msg(
238                    $usingParsoid ?
239                    'parsermigration-use-legacy-parser-toolbox-label' :
240                    'parsermigration-use-parsoid-toolbox-label'
241                )->text(),
242            ];
243        }
244
245        if ( $usingParsoid && $this->shouldShowReportVisualBug( $user ) ) {
246            // Add "report visual bug" sidebar link
247            $href = $this->getFeedbackTitleUrl();
248            $sidebar[ 'TOOLBOX' ][ 'parsermigration-report-bug' ] = [
249                'html' => Html::rawElement( 'a', [
250                    // This will be overridden in JavaScript, but is a useful
251                    // fallback for no-javascript browsers and unnamed users
252                    'href' => $href,
253                    'title' => $skin->msg(
254                        'parsermigration-report-bug-toolbox-title'
255                    )->text(),
256                ], $skin->msg(
257                        'parsermigration-report-bug-toolbox-label'
258                )->parse() . '&nbsp;' . Html::rawElement( 'span', [
259                    'class' => "parsermigration-report-bug-icon",
260                ] ) ),
261                'id' => 'parsermigration-report-bug',
262                // These properties are for the mobile skins
263                'text' => $skin->msg( 'parsermigration-report-bug-toolbox-label' ),
264                'href' => $href,
265                'icon' => 'feedback',
266            ];
267        }
268    }
269
270    /**
271     * Adds extra variables to the global config
272     *
273     * @param array &$vars Global variables object
274     * @param string $skin
275     * @param Config $config
276     */
277    public function onResourceLoaderGetConfigVars( array &$vars, $skin, Config $config ): void {
278        if ( $this->mainConfig->get( 'ParserMigrationEnableReportVisualBug' ) ) {
279            $vars['wgParserMigrationConfig'] = [
280                'onlyLoggedIn' => $this->mainConfig->get( 'ParserMigrationEnableReportVisualBugOnlyLoggedIn' ),
281                'isMobile' => $this->oracle->showingMobileView(),
282                'feedbackApiUrl' => $this->mainConfig->get( 'ParserMigrationFeedbackAPIURL' ),
283                'feedbackTitle' => $this->mainConfig->get( 'ParserMigrationFeedbackTitle' ),
284                'iwp' => WikiMap::getCurrentWikiId(),
285            ];
286        }
287    }
288
289    /**
290     * A change tag for "report visual bug" reports.
291     * @inheritDoc
292     */
293    public function onListDefinedTags( &$tags ) {
294        $tags[] = 'parsermigration-visual-bug';
295        return true;
296    }
297
298    /**
299     * All tags are still active.
300     * @inheritDoc
301     */
302    public function onChangeTagsListActive( &$tags ) {
303        return $this->onListDefinedTags( $tags );
304    }
305
306    /**
307     * MessagePoster acts on behalf of the user, so the
308     * user (any user) needs to be allowed to add this tag.
309     * @inheritDoc
310     */
311    public function onChangeTagsAllowedAdd( &$allowedTags, $addTags, $user ) {
312        return $this->onListDefinedTags( $allowedTags );
313    }
314
315    /**
316     * Suppress captcha when posting to the "Report Visual Bug" feedback
317     * page.
318     * @param string $action Action user is performing, one of sendmail,
319     *  createaccount, badlogin, edit, create, addurl.
320     * @param PageIdentity|null $page
321     * @param bool &$result
322     * @return bool|void True or no return value to continue or false to abort
323     */
324    public static function onConfirmEditTriggersCaptcha(
325        string $action,
326        ?PageIdentity $page,
327        bool &$result
328    ) {
329        // If we don't have a target page, bail.
330        if ( $page === null ) {
331            return true;
332        }
333        // We're only going to intervene if this is an edit or create
334        // ('create' since this could be the first report posted)
335        // 'addurl' because our subject lines link to the page which is
336        // the subject of the report.
337        if ( !( $action === 'edit' || $action === 'create' || $action === 'addurl' ) ) {
338            return true;
339        }
340        $services = MediaWikiServices::getInstance();
341        $mainConfig = $services->getMainConfig();
342        // If the Report Visual Bug tool is not enabled, bail.
343        if ( !$mainConfig->get( 'ParserMigrationEnableReportVisualBug' ) ) {
344            return true;
345        }
346        $apiUrl = $mainConfig->get( 'ParserMigrationFeedbackAPIURL' );
347        // If a foreign API URL is set, we can't do anything to help.
348        if ( $apiUrl ) {
349            return true;
350        }
351        $title = $mainConfig->get( 'ParserMigrationFeedbackTitle' ) ?:
352            wfMessage( 'parsermigration-reportbug-feedback-title' )->plain();
353        $titleValue = $services->getTitleParser()->parseTitle( $title );
354        // If the page doesn't match our feedback page, do nothing.
355        // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
356        if ( !$titleValue->isSameLinkAs( TitleValue::castPageToLinkTarget( $page ) ) ) {
357            return true;
358        }
359        // Ok, this is our page!  Suppress the captcha.
360        $result = false;
361        return false;
362    }
363
364    private function getFeedbackTitleUrl(): string {
365        $apiURL = $this->mainConfig->get( 'ParserMigrationFeedbackAPIURL' );
366        $url = $this->mainConfig->get( 'ParserMigrationFeedbackTitleURL' );
367        if ( $apiURL && $url ) {
368            return $url;
369        }
370        $title = Title::newFromText(
371            $this->mainConfig->get( 'ParserMigrationFeedbackTitle' ) ?:
372            wfMessage( 'parsermigration-reportbug-feedback-title' )->plain()
373        );
374        return $title->getLinkURL();
375    }
376
377    protected function shouldShowReportVisualBug( User $user ): bool {
378        if ( !$this->mainConfig->get( 'ParserMigrationEnableReportVisualBug' ) ) {
379            return false;
380        }
381        if ( $user->isNamed() ) {
382            return true;
383        }
384        return !$this->mainConfig->get( 'ParserMigrationEnableReportVisualBugOnlyLoggedIn' );
385    }
386}