Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.05% covered (success)
92.05%
81 / 88
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialViewAbstract
92.05% covered (success)
92.05%
81 / 88
75.00% covered (warning)
75.00%
6 / 8
24.29
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
 getRestriction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescription
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 userCanExecute
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 execute
92.31% covered (success)
92.31%
72 / 78
0.00% covered (danger)
0.00%
0 / 1
16.12
 redirectToMain
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRobotPolicy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * WikiLambda Special:ViewAbstract page
5 *
6 * @file
7 * @ingroup Extensions
8 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
9 * @license MIT
10 */
11
12namespace MediaWiki\Extension\WikiLambda\Special;
13
14use MediaWiki\Config\ConfigException;
15use MediaWiki\Content\Renderer\ContentRenderer;
16use MediaWiki\Extension\WikiLambda\PageTitle\PageTitleBuilder;
17use MediaWiki\Extension\WikiLambda\WikidataEntityLookup;
18use MediaWiki\Extension\WikiLambda\WikiLambdaServices;
19use MediaWiki\Html\Html;
20use MediaWiki\Language\LanguageFactory;
21use MediaWiki\Language\LanguageNameUtils;
22use MediaWiki\MainConfigNames;
23use MediaWiki\MediaWikiServices;
24use MediaWiki\Output\OutputPage;
25use MediaWiki\Page\Article;
26use MediaWiki\Parser\ParserOptions;
27use MediaWiki\SpecialPage\UnlistedSpecialPage;
28use MediaWiki\Title\Title;
29use MediaWiki\User\User;
30use MediaWiki\Utils\UrlUtils;
31
32class SpecialViewAbstract extends UnlistedSpecialPage {
33    public function __construct(
34        private readonly ContentRenderer $contentRenderer,
35        private readonly LanguageFactory $languageFactory,
36        private readonly LanguageNameUtils $languageNameUtils,
37        private readonly UrlUtils $urlUtils,
38        private readonly WikidataEntityLookup $entityLookup
39    ) {
40        parent::__construct( 'ViewAbstract' );
41    }
42
43    /** @inheritDoc */
44    public function getRestriction(): string {
45        return 'read';
46    }
47
48    /**
49     * @inheritDoc
50     */
51    protected function getGroupName() {
52        // Triggers use of message specialpages-group-wikilambda
53        return 'abstractwiki';
54    }
55
56    /**
57     * @inheritDoc
58     */
59    public function getDescription() {
60        return $this->msg( 'wikilambda-abstract-special-view' );
61    }
62
63    /**
64     * @inheritDoc
65     *
66     * @param User $user
67     * @return bool
68     */
69    public function userCanExecute( User $user ) {
70        // No usage allowed if not abstract mode
71        if ( !WikiLambdaServices::getMode()->isAbstract() ) {
72            return false;
73        }
74        return parent::userCanExecute( $user );
75    }
76
77    /**
78     * @inheritDoc
79     *
80     * @throws ConfigException
81     */
82    public function execute( $subPage ) {
83        if ( !$this->userCanExecute( $this->getUser() ) ) {
84            $this->displayRestrictionError();
85        }
86
87        $request = $this->getRequest();
88        $output = $this->getOutput();
89
90        // If abstract not enabled, go back to Main
91        if ( !WikiLambdaServices::getMode()->isAbstract() ) {
92            $this->redirectToMain( $output );
93            return;
94        }
95
96        // Force Special:ViewAbstract page to behave as view, even when action=edit
97        if ( $request->getVal( 'action' ) === 'edit' ) {
98            $request->setVal( 'action', 'view' );
99        }
100
101        // Make sure the correct content model is set, so that e.g. VisualEditor
102        // doesn't try to instantiate its tabs
103        $output->getTitle()->setContentModel( CONTENT_MODEL_ABSTRACT );
104
105        // If there's no subpage, just exit.
106        if ( !$subPage || !is_string( $subPage ) ) {
107            $this->redirectToMain( $output );
108            return;
109        }
110
111        $subPageSplit = [];
112        if ( !preg_match( '~^([^/]+)/(.+)$~', $subPage, $subPageSplit ) ) {
113            // Fallback to 'en' if request doesn't specify a language.
114            $targetLanguage = 'en';
115            $targetPageName = $subPage;
116        } else {
117            $targetLanguage = $subPageSplit[1];
118            $targetPageName = $subPageSplit[2];
119        }
120
121        $targetTitle = Title::newFromText( $targetPageName );
122
123        // If the given page doesn't exist, exit
124        if ( !( $targetTitle instanceof Title ) || !$targetTitle->exists() ) {
125            $this->redirectToMain( $output );
126            return;
127        }
128
129        // Allow the user to over-ride the content language if explicitly requested
130        $targetLanguage = $request->getRawVal( 'uselang' ) ?? $targetLanguage;
131
132        // (T343006) If supplied language is invalid; probably a user-error, so just exit.
133        // * isValidCode checks for code wellformedness -- $this->languageNameUtils->isValidCode( $targetLanguage
134        // * isKnownLanguageTag checks for code existing in registered language codes (and extraLanguageNames)
135        if ( !$this->languageNameUtils->isKnownLanguageTag( $targetLanguage ) ) {
136            $this->redirectToMain( $output );
137            return;
138        }
139
140        // Set the page language for our own purposes.
141        $targetLanguageObject = $this->languageFactory->getLanguage( $targetLanguage );
142        $this->getContext()->setLanguage( $targetLanguageObject );
143
144        // Tell the skin what content specifically we're related to, so edit/history links etc. work.
145        $this->getSkin()->setRelevantTitle( $targetTitle );
146
147        // (T343594) Set the title of the page to the target title, so Recent Changes Link works
148        $output->setTitle( $targetTitle );
149
150        // If this is a redirect from Create page, announce it somehow
151        if ( $request->getInt( 'created' ) ) {
152            $output->addSubtitle(
153                Html::noticeBox( $this->msg( 'wikilambda-abstract-special-create-existing-redirected' )->parse() )
154            );
155        }
156
157        // (T343594) Set the revision ID to the requested one or the latest, so the Permanent Link works
158        $latestRevId = $output->getTitle()->getLatestRevID();
159        $targetRevisionId = $this->getRequest()->getInt( 'oldid' ) ?: $latestRevId;
160
161        // FIXME inject revision store
162        $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
163        $targetRevision = $revisionStore->getRevisionById( $targetRevisionId );
164        $targetContent = $targetRevision ? $targetRevision->getMainContentRaw() : null;
165
166        // If content does not exist for the requested revision ID, send to Main
167        if ( !$targetContent ) {
168            $this->redirectToMain( $output );
169            return;
170        }
171
172        $output->setRevisionId( $targetRevisionId );
173
174        // (T364318) Add the revision navigation bar if seeing an oldid
175        if ( $targetRevisionId !== $latestRevId ) {
176            $article = Article::newFromTitle( $targetTitle, $this->getContext() );
177            $article->setOldSubtitle( $targetRevisionId );
178        }
179
180        $this->setHeaders();
181
182        // (T345453) Have the standard copyright stuff show up.
183        $output->setCopyright( true );
184
185        // Set page title to the object being viewed.
186        $qid = $targetTitle->getText();
187        $langCode = $targetLanguageObject->getCode();
188        $label = $this->entityLookup->resolveAbstractLabel( $qid, $langCode );
189
190        // Rich HTML for the H1 display: fall back to the QID as the title text when no
191        // Wikibase label is available (the QID chip still renders alongside it).
192        $output->setPageTitle(
193            PageTitleBuilder::createAbstractViewPageTitle(
194                $label ?? $targetTitle->getPrefixedText(),
195                $langCode,
196                $targetLanguageObject->getDir(),
197                $qid,
198            )
199        );
200        // Plain-text override for the browser <title> tag: "Label (QID)" or just "QID",
201        // plus the " - {{SITENAME}}" suffix, matching the /wiki/, edit and history views.
202        $output->setHTMLTitle(
203            PageTitleBuilder::createAbstractViewPageHtmlTitle( $label, $qid, $langCode )
204        );
205
206        // Runs AbstractWikiContentHandler::fillParserOutput
207        $parserOptions = ParserOptions::newFromUserAndLang( $this->getUser(), $targetLanguageObject );
208        $parserOutput = $this->contentRenderer->getParserOutput(
209            $targetContent,
210            $targetTitle,
211            null,
212            $parserOptions
213        );
214        $output->addParserOutput( $parserOutput, $parserOptions );
215
216        // (T355546) Over-ride the canonical URL to the /view/ form.
217        $viewURL = $this->urlUtils->expand( "/view/$targetLanguage/$targetPageName" );
218        // $viewURL can be null 'if no valid URL can be constructed', which shouldn't ever happen.
219        if ( $viewURL === null ) {
220            throw new ConfigException( 'No valid URL could be constructed for the canonical path' );
221        }
222        $output->setCanonicalUrl( $viewURL );
223
224        // Allow anonymous /view/ responses to be edge-cached, rather than recomputed per request like
225        // a normal Special page. Re-rendering embeds the abstract source and per-language label into the
226        // page, so each /view/<lang>/<page> URL is its own cache entry; ViewUrlCacheHandler purges
227        // them on edit/delete. OutputPage::sendCacheControl() keeps logged-in (session-bearing) responses
228        // private regardless, so only anonymous reads are cached.
229        $output->setCdnMaxage( $this->getConfig()->get( MainConfigNames::CdnMaxAge ) );
230
231        // (T345457) Tell OutputPage that our content is article-related, so we get Special:WhatLinksHere etc.
232        // (T343594) The Special:WhatLinksHere weren't shown on view/en/ZXXXX pages,
233        // but they were on wiki/ZXXXX pages. Setting the flag here (lower in code) fixes it.
234        $output->setArticleFlag( true );
235        $this->addHelpLink( 'Abstract_Wikipedia:About' );
236    }
237
238    /**
239     * Redirect the user to the Main Page, as their request isn't valid / answerable.
240     *
241     * TODO (T343652): Actually tell the user why they ended up somewhere they might not want?
242     *
243     * @param OutputPage $output
244     */
245    private function redirectToMain( OutputPage $output ) {
246        $mainPageUrl = Title::newMainPage( $output )->getFullURL();
247        $output->redirect( $mainPageUrl, 303 );
248    }
249
250    /**
251     * (T355441) Unlike regular Special pages, we actively want search engines to
252     * index our content and follow our links.
253     *
254     * @inheritDoc
255     */
256    protected function getRobotPolicy() {
257        return 'index,follow';
258    }
259}