Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
4.36% covered (danger)
4.36%
14 / 321
3.33% covered (danger)
3.33%
1 / 30
CRAP
0.00% covered (danger)
0.00%
0 / 1
ProofreadPage
4.36% covered (danger)
4.36%
14 / 321
3.33% covered (danger)
3.33%
1 / 30
6256.46
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPageNamespaceId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIndexNamespaceId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPageAndIndexNamespace
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 onWgQueryPages
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onContentHandlerDefaultModelFor
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 onParserFirstCallInit
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 onGetLinkColours
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 onImageOpenShowImageInlineBefore
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 onOutputPageParserOutput
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 onEditFormPreloadText
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 onGetPreferences
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
20
 onCanonicalNamespaces
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 addPageNsNavigation
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
182
 addIndexLink
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 addIndexNsNavigation
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 addBookSourceNavigation
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 onSkinTemplateNavigation__Universal
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 onInfoAction
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 getLinkUrlForTitle
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onRegistration
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 onListDefinedTags
0.00% covered (danger)
0.00%
0 / 1
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
 addDefinedTags
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 onRecentChange_save
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 onMultiContentSave
76.47% covered (warning)
76.47%
13 / 17
0.00% covered (danger)
0.00%
0 / 1
6.47
 getQualityLevelClassesForTitle
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getQualityClassesForQualityLevel
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 onGetBetaFeaturePreferences
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 * @ingroup ProofreadPage
20 */
21
22namespace ProofreadPage;
23
24use ExtensionRegistry;
25use IContextSource;
26use ImagePage;
27use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
28use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
29use MediaWiki\CommentStore\CommentStoreComment;
30use MediaWiki\Config\Config;
31use MediaWiki\Hook\BeforePageDisplayHook;
32use MediaWiki\Hook\CanonicalNamespacesHook;
33use MediaWiki\Hook\EditFormPreloadTextHook;
34use MediaWiki\Hook\GetLinkColoursHook;
35use MediaWiki\Hook\InfoActionHook;
36use MediaWiki\Hook\OutputPageParserOutputHook;
37use MediaWiki\Hook\ParserFirstCallInitHook;
38use MediaWiki\Hook\RecentChange_saveHook;
39use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook;
40use MediaWiki\MediaWikiServices;
41use MediaWiki\Output\OutputPage;
42use MediaWiki\Page\Hook\ImageOpenShowImageInlineBeforeHook;
43use MediaWiki\Parser\ParserOutput;
44use MediaWiki\Preferences\Hook\GetPreferencesHook;
45use MediaWiki\Revision\Hook\ContentHandlerDefaultModelForHook;
46use MediaWiki\Revision\RenderedRevision;
47use MediaWiki\Revision\SlotRecord;
48use MediaWiki\SpecialPage\Hook\WgQueryPagesHook;
49use MediaWiki\Status\Status;
50use MediaWiki\Storage\Hook\MultiContentSaveHook;
51use MediaWiki\Title\Title;
52use MediaWiki\User\User;
53use MediaWiki\User\UserIdentity;
54use OutOfBoundsException;
55use Parser;
56use ProofreadPage\Index\IndexTemplateStyles;
57use ProofreadPage\Page\DatabasePageQualityLevelLookup;
58use ProofreadPage\Page\PageContent;
59use ProofreadPage\Page\PageContentBuilder;
60use ProofreadPage\Page\PageDisplayHandler;
61use ProofreadPage\Page\PageRevisionTagger;
62use ProofreadPage\Pagination\PageNotInPaginationException;
63use ProofreadPage\Parser\PagelistTagParser;
64use ProofreadPage\Parser\PagequalityTagParser;
65use ProofreadPage\Parser\PagesTagParser;
66use ProofreadPage\Parser\TranslusionPagesModifier;
67use RequestContext;
68use Skin;
69use SkinTemplate;
70
71/*
72 @todo :
73 - check uniqueness of the index page : when index is saved too
74*/
75
76class ProofreadPage implements
77    RecentChange_saveHook,
78    SkinTemplateNavigation__UniversalHook,
79    OutputPageParserOutputHook,
80    ParserFirstCallInitHook,
81    GetLinkColoursHook,
82    GetPreferencesHook,
83    BeforePageDisplayHook,
84    ImageOpenShowImageInlineBeforeHook,
85    WgQueryPagesHook,
86    CanonicalNamespacesHook,
87    ContentHandlerDefaultModelForHook,
88    EditFormPreloadTextHook,
89    MultiContentSaveHook,
90    InfoActionHook,
91    ListDefinedTagsHook,
92    ChangeTagsListActiveHook
93{
94
95    /** @var Config */
96    private $config;
97
98    /**
99     * @param Config $config
100     */
101    public function __construct( Config $config ) {
102        $this->config = $config;
103    }
104
105    /**
106     * @deprecated use Context::getPageNamespaceId
107     *
108     * Returns id of Page namespace.
109     *
110     * @return int
111     */
112    public static function getPageNamespaceId() {
113        return ProofreadPageInit::getNamespaceId( 'page' );
114    }
115
116    /**
117     * @deprecated use Context::getIndexNamespaceId
118     *
119     * Returns id of Index namespace.
120     *
121     * @return int
122     */
123    public static function getIndexNamespaceId() {
124        return ProofreadPageInit::getNamespaceId( 'index' );
125    }
126
127    /**
128     * @deprecated
129     * @return string[]
130     */
131    public static function getPageAndIndexNamespace() {
132        static $res;
133        if ( $res === null ) {
134            global $wgExtraNamespaces;
135            $res = [
136                preg_quote( $wgExtraNamespaces[self::getPageNamespaceId()], '/' ),
137                preg_quote( $wgExtraNamespaces[self::getIndexNamespaceId()], '/' ),
138            ];
139        }
140        return $res;
141    }
142
143    /**
144     * @see https://www.mediawiki.org/wiki/Manual:Hooks/wgQueryPages
145     *
146     * @param array[] &$queryPages
147     */
148    public function onWgQueryPages( &$queryPages ) {
149        $queryPages[] = [ 'SpecialProofreadPages', 'IndexPages' ];
150        $queryPages[] = [ 'SpecialPagesWithoutScans', 'PagesWithoutScans' ];
151    }
152
153    /**
154     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ContentHandlerDefaultModelFor
155     *
156     * @param Title $title the title page
157     * @param string &$model the content model for the page
158     * @return bool if we have to continue the research for a content handler
159     */
160    public function onContentHandlerDefaultModelFor( $title, &$model ) {
161        // Warning: do not use Context here because it assumes ContentHandler is already initialized
162        if ( $title->inNamespace( self::getPageNamespaceId() ) ) {
163            $model = CONTENT_MODEL_PROOFREAD_PAGE;
164            return false;
165        } elseif ( $title->inNamespace( self::getIndexNamespaceId() ) && !$title->isSubpage() ) {
166            $model = CONTENT_MODEL_PROOFREAD_INDEX;
167            return false;
168        } else {
169            return true;
170        }
171    }
172
173    /**
174     * Set up our custom parser hooks when initializing parser.
175     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ParserFirstCallInit
176     *
177     * @param Parser $parser
178     */
179    public function onParserFirstCallInit( $parser ) {
180        $parser->setHook( 'pagelist', static function ( $input, array $args, Parser $parser ) {
181            $context = Context::getDefaultContext( true );
182            $tagParser = new PagelistTagParser( $parser, $context );
183            return $tagParser->render( $args );
184        } );
185        $parser->setHook( 'pages', static function ( $input, array $args, Parser $parser ) {
186            $context = Context::getDefaultContext( true );
187            $tagParser = new PagesTagParser( $parser, $context );
188            return $tagParser->render( $args );
189        } );
190        $parser->setHook( 'pagequality', static function ( $input, array $args, Parser $parser ) {
191            $tagParser = new PagequalityTagParser();
192            return $tagParser->render( $args );
193        } );
194    }
195
196    /**
197     * Loads JS modules
198     * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageDisplay
199     *
200     * @param OutputPage $out
201     * @param Skin $skin
202     */
203    public function onBeforePageDisplay( $out, $skin ): void {
204        $title = $out->getTitle();
205
206        if ( $title->inNamespace( self::getIndexNamespaceId() ) ) {
207            $out->addModuleStyles( 'ext.proofreadpage.base' );
208        } elseif ( $title->inNamespace( self::getPageNamespaceId() ) ) {
209            $out->addModuleStyles( 'ext.proofreadpage.page.navigation' );
210        }
211    }
212
213    /**
214     * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetLinkColours
215     *
216     * @param string[] $linkcolour_ids Prefixed DB keys of the pages linked to, indexed by page_id
217     * @param string[] &$colours CSS classes, indexed by prefixed DB keys
218     * @param Title $title Title of the page being parsed, on which the links will be shown
219     */
220    public function onGetLinkColours( $linkcolour_ids, &$colours, $title ) {
221        $context = Context::getDefaultContext();
222        $inIndexNamespace = $title->inNamespace( $context->getIndexNamespaceId() );
223        $pageQualityLevelLookup = $context->getPageQualityLevelLookup();
224
225        $pageTitles = array_map( [ Title::class, 'newFromText' ], $linkcolour_ids );
226        $pageQualityLevelLookup->prefetchQualityLevelForTitles( $pageTitles );
227
228        /** @var Title|null $pageTitle */
229        foreach ( $pageTitles as $pageTitle ) {
230            if ( $pageTitle !== null && $pageTitle->inNamespace( $context->getPageNamespaceId() ) ) {
231                $pageLevel = $pageQualityLevelLookup->getQualityLevelForPageTitle( $pageTitle );
232                if ( $pageLevel !== null ) {
233                    $colours[$pageTitle->getPrefixedDBkey()] = self::getQualityClassesForQualityLevel(
234                            $pageLevel,
235                            $inIndexNamespace
236                        );
237                }
238            }
239        }
240    }
241
242    /**
243     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ImageOpenShowImageInlineBefore
244     *
245     * @param ImagePage $imgpage
246     * @param OutputPage $out
247     */
248    public function onImageOpenShowImageInlineBefore(
249        $imgpage, $out
250    ) {
251        $image = $imgpage->getPage()->getFile();
252        if ( !$image->isMultipage() ) {
253            return;
254        }
255
256        $name = $image->getTitle()->getText();
257        $title = Title::makeTitle( self::getIndexNamespaceId(), $name );
258        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
259        $link = $linkRenderer->makeKnownLink(
260            $title, $out->msg( 'proofreadpage_image_message' )->text()
261        );
262        $out->addHTML( $link );
263    }
264
265    /**
266     * @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageParserOutput
267     *
268     * @param OutputPage $outputPage
269     * @param ParserOutput $parserOutput
270     */
271    public function onOutputPageParserOutput(
272        $outputPage, $parserOutput
273    ): void {
274        $title = $outputPage->getTitle();
275        $bookNamespaces = $outputPage->getConfig()->get( 'ProofreadPageBookNamespaces' );
276
277        $outputPage->addJsConfigVars( 'prpProofreadPageBookNamespaces', $bookNamespaces );
278
279        if ( $title->inNamespaces( $bookNamespaces ) && !$title->isMainPage() ) {
280            $context = Context::getDefaultContext();
281            $modifier = new TranslusionPagesModifier(
282                $context->getPageQualityLevelLookup(),
283                $context->getIndexQualityStatsLookup(),
284                $context->getIndexForPageLookup(),
285                $context->getPageNamespaceId()
286            );
287            $modifier->modifyPage( $parserOutput, $outputPage );
288        }
289    }
290
291    /**
292     * Provides text for preload API
293     * @see https://www.mediawiki.org/wiki/Manual:Hooks/EditFormPreloadText
294     *
295     * @param string &$text
296     * @param Title $title
297     */
298    public function onEditFormPreloadText( &$text, $title ) {
299        if ( !$title->inNamespace( self::getPageNamespaceId() ) ) {
300            return;
301        }
302
303        $pageContentBuilder = new PageContentBuilder(
304            RequestContext::getMain(), Context::getDefaultContext()
305        );
306        $content = $pageContentBuilder->buildDefaultContentForPageTitle( $title );
307        $text = $content->serialize();
308    }
309
310    /**
311     * Add ProofreadPage preferences to the preferences menu
312     * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetPreferences
313     *
314     * @param User $user
315     * @param array[] &$preferences
316     */
317    public function onGetPreferences( $user, &$preferences ) {
318        $type = 'toggle';
319        // Hide the option from the preferences tab if WikiEditor is loaded
320        if ( ExtensionRegistry::getInstance()->isLoaded( 'WikiEditor' ) &&
321            MediaWikiServices::getInstance()->getUserOptionsLookup()
322                ->getBoolOption( $user, 'usebetatoolbar' )
323        ) {
324            $type = 'api';
325        }
326        // Show header and footer fields when editing in the Page namespace
327        $preferences['proofreadpage-showheaders'] = [
328            'type'           => $type,
329            'label-message'  => 'proofreadpage-preferences-showheaders-label',
330            'section'        => 'editing/proofread-pagenamespace',
331        ];
332
333        // Use horizontal layout when editing in the Page namespace
334        $preferences['proofreadpage-horizontal-layout'] = [
335            'type'           => $type,
336            'label-message'  => 'proofreadpage-preferences-horizontal-layout-label',
337            'section'        => 'editing/proofread-pagenamespace',
338        ];
339
340        if ( $this->config->get( 'ProofreadPageEnableEditInSequence' ) ) {
341            // Show dialog before saving the a Page using EditInSequence
342            $preferences['proofreadpage-show-dialog-before-every-save'] = [
343                'type'           => 'api',
344                'label-message'  => 'proofreadpage-preferences-show-dialog-before-every-save-label',
345                'section'        => 'editing/proofread-pagenamespace',
346            ];
347
348            // Preference denoting action to be performed
349            // after saving a page in EditInSequence,
350            $preferences['proofreadpage-after-save-action'] = [
351                'type'           => 'api',
352                'options-messages'        => [
353                    'prp-editinsequence-save-next-action-do-nothing' => 'do-nothing',
354                    'prp-editinsequence-save-next-action-go-to-next' => 'go-to-next',
355                    'prp-editinsequence-save-next-action-go-to-prev' => 'go-to-prev',
356                ],
357                'label-message'  => 'proofreadpage-preferences-after-save-action-label',
358                'section'        => 'editing/proofread-pagenamespace',
359            ];
360        }
361
362        // Page image viewer zoom factor
363        $preferences['proofreadpage-zoom-factor'] = [
364            'type'           => 'float',
365            'min'            => 1.0,
366            'max'            => 2.0,
367            'label-message'  => 'proofreadpage-preferences-zoom-factor-label',
368            'section'        => 'editing/proofread-pagenamespace',
369        ];
370
371        // Page image viewer animation time (higher is smoother)
372        $preferences['proofreadpage-animation-time'] = [
373            'type'           => 'float',
374            'min'            => 0,
375            'max'            => 2.0,
376            'label-message'  => 'proofreadpage-preferences-animation-time-label',
377            'section'        => 'editing/proofread-pagenamespace',
378        ];
379
380        // Mode selection for the new PagelistInputWidget
381        $preferences['proofreadpage-pagelist-use-visual-mode'] = [
382            'type'           => 'toggle',
383            'label-message'  => 'proofreadpage-preferences-pagelist-use-visual-mode',
384            'section'        => 'editing/advancedediting',
385        ];
386    }
387
388    /**
389     * @see https://www.mediawiki.org/wiki/Manual:Hooks/CanonicalNamespaces
390     *
391     * @param string[] &$namespaces
392     */
393    public function onCanonicalNamespaces( &$namespaces ) {
394        $pageNamespaceId = self::getPageNamespaceId();
395        $indexNamespaceId = self::getIndexNamespaceId();
396
397        $namespaces[$pageNamespaceId] = 'Page';
398        $namespaces[$pageNamespaceId + 1] = 'Page_talk';
399        $namespaces[$indexNamespaceId] = 'Index';
400        $namespaces[$indexNamespaceId + 1] = 'Index_talk';
401    }
402
403    /**
404     * Add the links to previous, next, index page and scan image to Page: pages.
405     * @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinTemplateNavigation
406     *
407     * @param Title $title the page title
408     * @param SkinTemplate $skin
409     * @param array[] &$links Structured navigation links
410     */
411    private static function addPageNsNavigation( Title $title, SkinTemplate $skin, array &$links ) {
412        $context = Context::getDefaultContext();
413        $pageDisplayHandler = new PageDisplayHandler( $context );
414
415        // Image link
416        $image = $pageDisplayHandler->getImageFullSize( $title );
417        if ( $image ) {
418            $links['namespaces']['proofreadPageScanLink'] = [
419                'class' => '',
420                'href' => $image->getUrl(),
421                'text' => wfMessage( 'proofreadpage_image' )->plain()
422            ];
423        }
424
425        $indexTitle = $context
426            ->getIndexForPageLookup()->getIndexForPageTitle( $title );
427
428        if ( EditInSequence::isEnabled( $skin ) ) {
429            $isLoaded = EditInSequence::shouldLoadEditInSequence( $skin );
430            $links['views']['proofreadPageEditInSequenceLink'] = [
431                'class' => $isLoaded ? 'selected' : '',
432                'href' => $title->getLocalURL( [
433                        'action' => 'edit',
434                        EditInSequence::URLPARAMNAME => 'true'
435                    ] ),
436                'text' => wfMessage( 'proofreadpage_edit_in_sequence' )->plain()
437            ];
438
439            if ( $isLoaded ) {
440                // Deselect the Edit link when EditInSequence is loaded
441                $links['views']['edit']['class'] = '';
442                self::addIndexLink( $skin, $indexTitle, $links );
443                return;
444            }
445        }
446
447        // Prev, Next and Index links
448        if ( $indexTitle !== null ) {
449            $pagination = $context
450                ->getPaginationFactory()->getPaginationForIndexTitle( $indexTitle );
451
452            $firstLinks = [];
453            try {
454                $pageNumber = $pagination->getPageNumber( $title );
455
456                try {
457                    $prevTitle  = $pagination->getPageTitle( $pageNumber - 1 );
458                    $prevText = wfMessage( 'proofreadpage_prevpage' )->plain();
459                    $prevUrl = self::getLinkUrlForTitle( $prevTitle );
460                    $firstLinks['proofreadPagePrevLink'] = [
461                        'class' => in_array( $skin->getSkinName(), [ 'vector', 'vector-2022' ], true ) ? 'icon' : '',
462                        'href' => $prevUrl,
463                        'text' => $prevText,
464                        'title' => $prevText
465                    ];
466                    $prevThumbnailLinkAttributes = $pageDisplayHandler->getImageHtmlLinkAttributes(
467                        $prevTitle, 'prefetch', 'prp-prev-image'
468                    );
469                    if ( $prevThumbnailLinkAttributes ) {
470                        $skin->getOutput()->addLink( $prevThumbnailLinkAttributes );
471                    }
472                } catch ( OutOfBoundsException $e ) {
473                    // if the previous page does not exist
474                }
475
476                try {
477                    $nextTitle  = $pagination->getPageTitle( $pageNumber + 1 );
478                    $nextText = wfMessage( 'proofreadpage_nextpage' )->plain();
479                    $nextUrl = self::getLinkUrlForTitle( $nextTitle );
480                    $firstLinks['proofreadPageNextLink'] = [
481                        'class' => in_array( $skin->getSkinName(), [ 'vector', 'vector-2022' ], true ) ? 'icon' : '',
482                        'href' => $nextUrl,
483                        'text' => $nextText,
484                        'title' => $nextText
485                    ];
486                    $nextThumbnailLinkAttributes = $pageDisplayHandler->getImageHtmlLinkAttributes(
487                        $nextTitle, 'prefetch', 'prp-next-image'
488                    );
489                    if ( $nextThumbnailLinkAttributes ) {
490                        $skin->getOutput()->addLink( $nextThumbnailLinkAttributes );
491                    }
492                } catch ( OutOfBoundsException $e ) {
493                    // if the next page does not exist
494                }
495            } catch ( PageNotInPaginationException $e ) {
496            }
497
498            // Prepend Prev, Next to namespaces tabs
499            $links['namespaces'] = array_merge( $firstLinks, $links['namespaces'] );
500
501            self::addIndexLink( $skin, $indexTitle, $links );
502        }
503    }
504
505    /**
506     * Add the link to the index page from Page: pages.
507     * @param SkinTemplate $skin
508     * @param ?Title $indexTitle
509     * @param array[] &$links Structured navigation links
510     */
511    private static function addIndexLink( SkinTemplate $skin, ?Title $indexTitle, array &$links ) {
512        if ( $indexTitle === null ) {
513            return;
514        }
515
516        $links['namespaces']['proofreadPageIndexLink'] = [
517            'class' => ( in_array( $skin->getSkinName(), [ 'vector', 'vector-2022' ], true ) ) ? 'icon' : '',
518            'href' => $indexTitle->getLinkURL(),
519            'text' => wfMessage( 'proofreadpage_index' )->plain(),
520            'title' => wfMessage( 'proofreadpage_index' )->plain()
521        ];
522    }
523
524    /**
525     * Add the link the style page (if any) on the Index: pages
526     * @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinTemplateNavigation
527     *
528     * @param Title $title the page title
529     * @param SkinTemplate $skin
530     * @param array[] &$links Structured navigation links
531     */
532    private static function addIndexNsNavigation( Title $title, SkinTemplate $skin, array &$links ) {
533        $indexTs = new IndexTemplateStyles( $title );
534
535        $stylesTitle = $indexTs->getTemplateStylesPage();
536
537        if ( $stylesTitle !== null ) {
538            // set up styles navigation
539
540            $stylesSelected = $title->equals( $stylesTitle );
541
542            // link to the styles page
543            $links['namespaces']['proofreadPageStylesLink'] = $skin->tabAction(
544                $stylesTitle, 'proofreadpage_styles', $stylesSelected, '', true );
545
546            if ( $stylesSelected ) {
547                // redirect the Index and Talk links to the root page
548                $rootIndex = $indexTs->getAssociatedIndexPage();
549                $links['namespaces']['index'] = $skin->tabAction(
550                    $rootIndex, 'index', false, '', true
551                );
552                $links['namespaces']['index_talk'] = $skin->tabAction(
553                    $rootIndex->getTalkPage(),
554                    'talk', false, '', true
555                );
556            }
557        }
558    }
559
560    /**
561     * Add the link the style page (if any) on the main namespace pages
562     * @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinTemplateNavigation
563     *
564     * @param Title $title the page title
565     * @param SkinTemplate $skin
566     * @param array[] &$links Structured navigation links
567     */
568    private static function addBookSourceNavigation( Title $title, SkinTemplate $skin, array &$links ) {
569        $outputPage = $skin->getOutput();
570        $indexTitleText = $outputPage->getProperty( 'prpSourceIndexPage' );
571        if ( $indexTitleText !== null ) {
572            $links['namespaces'] = array_slice( $links['namespaces'], 0, 1, true ) +
573                [
574                    'proofread-source' => [
575                        'title' => $outputPage->msg( 'proofreadpage_source_message' )->text(),
576                        'text' => $outputPage->msg( 'proofreadpage_source' )->text(),
577                        'href' => Title::newFromText( $indexTitleText )->getLocalUrl(),
578                    ]
579                ] +
580                array_slice( $links['namespaces'], 1, count( $links['namespaces'] ), true );
581        }
582    }
583
584    // phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
585
586    /**
587     * @inheritDoc
588     */
589    public function onSkinTemplateNavigation__Universal( $skin, &$links ): void {
590        // phpcs:enable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
591        $title = $skin->getTitle();
592        if ( $title === null ) {
593            return;
594        }
595
596        if ( $title->inNamespace( self::getPageNamespaceId() ) ) {
597            self::addPageNsNavigation( $title, $skin, $links );
598        } elseif ( $title->inNamespace( self::getIndexNamespaceId() ) ) {
599            self::addIndexNsNavigation( $title, $skin, $links );
600        } elseif ( $title->inNamespaces( $skin->getConfig()->get( 'ProofreadPageBookNamespaces' ) ) ) {
601            self::addBookSourceNavigation( $title, $skin, $links );
602        }
603    }
604
605    /**
606     * Add proofreading status to action=info
607     * @see https://www.mediawiki.org/wiki/Manual:Hooks/InfoAction
608     *
609     * @param IContextSource $context
610     * @param array[] &$pageInfo The page information
611     */
612    public function onInfoAction( $context, &$pageInfo ) {
613        $title = $context->getTitle();
614        if ( !$title || !$title->canExist() ) {
615            return;
616        }
617        if ( !$title->inNamespace( self::getPageNamespaceId() ) ) {
618            return;
619        }
620
621        $pageQualityLevelLookup = Context::getDefaultContext()->getPageQualityLevelLookup();
622        $pageQualityLevel = $pageQualityLevelLookup->getQualityLevelForPageTitle( $title );
623        if ( $pageQualityLevel !== null ) {
624            $pageInfo['header-basic'][] = [
625                wfMessage( 'proofreadpage-pageinfo-status' ),
626                wfMessage( "proofreadpage_quality{$pageQualityLevel}_category" ),
627            ];
628        }
629    }
630
631    /**
632     * Get URL for particular title
633     * @param Title $title
634     * @return string
635     */
636    protected static function getLinkUrlForTitle( Title $title ) {
637        if ( $title->exists() ) {
638            return $title->getLinkURL();
639        } else {
640            return $title->getLinkURL( 'action=edit&redlink=1' );
641        }
642    }
643
644    /**
645     * @see https://www.mediawiki.org/wiki/Manual:Extension_registration#Customizing_registration
646     */
647    public static function onRegistration() {
648        // L10n
649        include_once __DIR__ . '/../ProofreadPage.namespaces.php';
650
651        // Content handler
652        define( 'CONTENT_MODEL_PROOFREAD_PAGE', 'proofread-page' );
653        define( 'CONTENT_MODEL_PROOFREAD_INDEX', 'proofread-index' );
654    }
655
656    /**
657     * ListDefinedTags hook handler
658     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ListDefinedTags
659     *
660     * @param array &$tags The list of tags. Add your extension's tags to this array.
661     */
662    public function onListDefinedTags( &$tags ) {
663        $this->addDefinedTags( $tags );
664    }
665
666    /**
667     * ChangeTagsListActive hook handler
668     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ChangeTagsListActive
669     *
670     * @param array &$tags The list of tags. Add your extension's tags to this array.
671     */
672    public function onChangeTagsListActive( &$tags ) {
673        $this->addDefinedTags( $tags );
674    }
675
676    /**
677     * @param array &$tags
678     */
679    private function addDefinedTags( &$tags ) {
680        $tags[] = Tags::WITHOUT_TEXT_TAG;
681        $tags[] = Tags::NOT_PROOFREAD_TAG;
682        $tags[] = Tags::PROBLEMATIC_TAG;
683        $tags[] = Tags::PROOFREAD_TAG;
684        $tags[] = Tags::VALIDATED_TAG;
685        // Add a tag for edits made using EditInSequence
686        $tags[] = EditInSequence::TAGNAME;
687    }
688
689    // phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
690
691    /**
692     * @inheritDoc
693     */
694    public function onRecentChange_save( $rc ) {
695        // phpcs:enable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
696
697        $ns = $rc->getAttribute( 'rc_namespace' );
698
699        if ( $ns === self::getPageNamespaceId() ) {
700
701            $requestContext = RequestContext::getMain();
702
703            if ( EditInSequence::isEditInSequenceEdit( $requestContext ) ) {
704                $rc->addTags( [ EditInSequence::TAGNAME ] );
705            }
706
707            $useProofreadTags = $this->config->get( 'ProofreadPageUseStatusChangeTags' );
708
709            // not configured to add tags to revisions
710            if ( !$useProofreadTags ) {
711                return;
712            }
713
714            $tagger = new PageRevisionTagger();
715            $tags = $tagger->getTagsForChange( $rc );
716
717            if ( $tags ) {
718                $rc->addTags( $tags );
719            }
720        }
721    }
722
723    /**
724     * @see https://www.mediawiki.org/wiki/Manual:Hooks/MultiContentSave
725     *
726     * @param RenderedRevision $renderedRevision
727     * @param UserIdentity $user
728     * @param CommentStoreComment $summary
729     * @param int $flags
730     * @param Status $hookStatus
731     * @return bool|void
732     */
733    public function onMultiContentSave(
734        $renderedRevision,
735        $user,
736        $summary,
737        $flags,
738        $hookStatus
739    ) {
740        $revisionRecord = $renderedRevision->getRevision();
741        $content = $revisionRecord->getContent( SlotRecord::MAIN );
742        if ( !( $content instanceof PageContent ) ) {
743            // We just need to prepare this check for CONTENT_MODEL_PROOFREAD_PAGE (PageContent)
744            return;
745        }
746
747        if ( !( $content->isValid() ) ) {
748            $hookStatus->fatal( 'invalid-content-data' );
749            return;
750        }
751
752        $oldContent = PageContent::getContentForRevId( $revisionRecord->getParentId() );
753        if ( $oldContent->getModel() !== CONTENT_MODEL_PROOFREAD_PAGE ) {
754            // Let's convert it to Page: page content
755            $oldContent = $oldContent->convert( CONTENT_MODEL_PROOFREAD_PAGE );
756        }
757
758        if ( !( $oldContent instanceof PageContent ) ) {
759            $hookStatus->fatal( 'invalid-content-data' );
760            return;
761        }
762
763        $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
764        if ( !$oldContent->getLevel()->isChangeAllowed( $content->getLevel(), $permissionManager ) ) {
765            $hookStatus->fatal( 'proofreadpage_notallowedtext' );
766            return;
767        }
768    }
769
770    /**
771     * @param Title $pageTitle
772     * @param bool $inIndexNamespace
773     * @return string
774     */
775    public static function getQualityLevelClassesForTitle( Title $pageTitle, bool $inIndexNamespace ): string {
776        $dbLookup = new DatabasePageQualityLevelLookup( $pageTitle->getNamespace() );
777        $pageLevel = $dbLookup->getQualityLevelForPageTitle( $pageTitle );
778
779        return self::getQualityClassesForQualityLevel( $pageLevel, $inIndexNamespace );
780    }
781
782    /**
783     * @param int|null $pageLevel
784     * @param bool $inIndexNamespace
785     * @return string
786     */
787    public static function getQualityClassesForQualityLevel( ?int $pageLevel, bool $inIndexNamespace ): string {
788        $classes = "";
789        if ( $pageLevel !== null ) {
790            $classes = "prp-pagequality-{$pageLevel}";
791            if ( $inIndexNamespace ) {
792                $classes .= " quality{$pageLevel}";
793            }
794        }
795        return $classes;
796    }
797
798    /**
799     * @param User $user
800     * @param array &$betaPrefs
801     * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetBetaFeaturePreferences
802     */
803    public static function onGetBetaFeaturePreferences( User $user, array &$betaPrefs ) {
804        $extensionAssetsPath = MediaWikiServices::getInstance()
805            ->getMainConfig()
806            ->get( 'ExtensionAssetsPath' );
807        $betaPrefs[ EditInSequence::BETA_FEATURE_NAME ] = [
808            // The first two are message keys
809            'label-message' => 'prp-editinsequence-beta-label',
810            'desc-message' => 'prp-editinsequence-beta-description',
811            'screenshot' => [
812                'ltr' => "$extensionAssetsPath/ProofreadPage/modules/page/images/eis-ltr.svg",
813                'rtl' => "$extensionAssetsPath/ProofreadPage/modules/page/images/eis-rtl.svg",
814            ],
815            'info-link' => 'https://meta.wikimedia.org/wiki/Special:MyLanguage/Wikisource_EditInSequence',
816            'discussion-link' =>
817            'https://meta.wikimedia.org/wiki/Special:MyLanguage/Talk:Wikisource_EditInSequence',
818        ];
819    }
820
821}