Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.91% covered (success)
90.91%
70 / 77
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialViewAbstract
90.91% covered (success)
90.91%
70 / 77
71.43% covered (warning)
71.43%
5 / 7
23.40
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
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
90.62% covered (success)
90.62%
58 / 64
0.00% covered (danger)
0.00%
0 / 1
16.21
 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\Html\Html;
17use MediaWiki\Language\LanguageFactory;
18use MediaWiki\Language\LanguageNameUtils;
19use MediaWiki\MediaWikiServices;
20use MediaWiki\Output\OutputPage;
21use MediaWiki\Page\Article;
22use MediaWiki\Parser\ParserOptions;
23use MediaWiki\SpecialPage\UnlistedSpecialPage;
24use MediaWiki\Title\Title;
25use MediaWiki\User\User;
26use MediaWiki\Utils\UrlUtils;
27
28class SpecialViewAbstract extends UnlistedSpecialPage {
29    private ContentRenderer $contentRenderer;
30    private LanguageFactory $languageFactory;
31    private LanguageNameUtils $languageNameUtils;
32    private UrlUtils $urlUtils;
33
34    public function __construct(
35        ContentRenderer $contentRenderer,
36        LanguageFactory $languageFactory,
37        LanguageNameUtils $languageNameUtils,
38        UrlUtils $urlUtils,
39    ) {
40        parent::__construct( 'ViewAbstract', 'read' );
41        $this->contentRenderer = $contentRenderer;
42        $this->languageFactory = $languageFactory;
43        $this->languageNameUtils = $languageNameUtils;
44        $this->urlUtils = $urlUtils;
45    }
46
47    /**
48     * @inheritDoc
49     */
50    protected function getGroupName() {
51        // Triggers use of message specialpages-group-wikilambda
52        return 'abstractwiki';
53    }
54
55    /**
56     * @inheritDoc
57     */
58    public function getDescription() {
59        return $this->msg( 'wikilambda-abstract-special-view' );
60    }
61
62    /**
63     * @inheritDoc
64     *
65     * @param User $user
66     * @return bool
67     */
68    public function userCanExecute( User $user ) {
69        // No usage allowed if not abstract mode
70        if ( !$this->getConfig()->get( 'WikiLambdaEnableAbstractMode' ) ) {
71            return false;
72        }
73        return parent::userCanExecute( $user );
74    }
75
76    /**
77     * @inheritDoc
78     *
79     * @throws ConfigException
80     */
81    public function execute( $subPage ) {
82        if ( !$this->userCanExecute( $this->getUser() ) ) {
83            $this->displayRestrictionError();
84        }
85
86        $request = $this->getRequest();
87        $output = $this->getOutput();
88
89        // If abstract not enabled, go back to Main
90        if ( !$this->getConfig()->get( 'WikiLambdaEnableAbstractMode' ) ) {
91            $this->redirectToMain( $output );
92            return;
93        }
94
95        // Force Special:ViewAbstract page to behave as view, even when action=edit
96        if ( $request->getVal( 'action' ) === 'edit' ) {
97            $request->setVal( 'action', 'view' );
98        }
99
100        // Make sure the correct content model is set, so that e.g. VisualEditor
101        // doesn't try to instantiate its tabs
102        $output->getTitle()->setContentModel( CONTENT_MODEL_ABSTRACT );
103
104        // If there's no subpage, just exit.
105        if ( !$subPage || !is_string( $subPage ) ) {
106            $this->redirectToMain( $output );
107            return;
108        }
109
110        $subPageSplit = [];
111        if ( !preg_match( '~^([^/]+)/(.+)$~', $subPage, $subPageSplit ) ) {
112            // Fallback to 'en' if request doesn't specify a language.
113            $targetLanguage = 'en';
114            $targetPageName = $subPage;
115        } else {
116            $targetLanguage = $subPageSplit[1];
117            $targetPageName = $subPageSplit[2];
118        }
119
120        $targetTitle = Title::newFromText( $targetPageName );
121
122        // If the given page doesn't exist, exit
123        if ( !( $targetTitle instanceof Title ) || !$targetTitle->exists() ) {
124            $this->redirectToMain( $output );
125            return;
126        }
127
128        // Allow the user to over-ride the content language if explicitly requested
129        $targetLanguage = $request->getRawVal( 'uselang' ) ?? $targetLanguage;
130
131        // (T343006) If supplied language is invalid; probably a user-error, so just exit.
132        // * isValidCode checks for code wellformedness -- $this->languageNameUtils->isValidCode( $targetLanguage
133        // * isKnownLanguageTag checks for code existing in registered language codes (and extraLanguageNames)
134        if ( !$this->languageNameUtils->isKnownLanguageTag( $targetLanguage ) ) {
135            $this->redirectToMain( $output );
136            return;
137        }
138
139        // Set the page language for our own purposes.
140        $targetLanguageObject = $this->languageFactory->getLanguage( $targetLanguage );
141        $this->getContext()->setLanguage( $targetLanguageObject );
142
143        // Tell the skin what content specifically we're related to, so edit/history links etc. work.
144        $this->getSkin()->setRelevantTitle( $targetTitle );
145
146        // (T343594) Set the title of the page to the target title, so Recent Changes Link works
147        $output->setTitle( $targetTitle );
148
149        // If this is a redirect from Create page, announce it somehow
150        // FIXME better text
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        $output->setPageTitle( $targetTitle->getPrefixedText() );
187
188        // Runs AbstractWikiContentHandler::fillParserOutput
189        $parserOptions = ParserOptions::newFromUserAndLang( $this->getUser(), $targetLanguageObject );
190        $parserOutput = $this->contentRenderer->getParserOutput(
191            $targetContent,
192            $targetTitle,
193            null,
194            $parserOptions
195        );
196        $output->addParserOutput( $parserOutput, $parserOptions );
197
198        // (T355546) Over-ride the canonical URL to the /view/ form.
199        $viewURL = $this->urlUtils->expand( "/view/$targetLanguage/$targetPageName" );
200        // $viewURL can be null 'if no valid URL can be constructed', which shouldn't ever happen.
201        if ( $viewURL === null ) {
202            throw new ConfigException( 'No valid URL could be constructed for the canonical path' );
203        }
204        $output->setCanonicalUrl( $viewURL );
205
206        // (T345457) Tell OutputPage that our content is article-related, so we get Special:WhatLinksHere etc.
207        // (T343594) The Special:WhatLinksHere weren't shown on view/en/ZXXXX pages,
208        // but they were on wiki/ZXXXX pages. Setting the flag here (lower in code) fixes it.
209        $output->setArticleFlag( true );
210        $this->addHelpLink( 'Abstract_Wikipedia:About' );
211    }
212
213    /**
214     * Redirect the user to the Main Page, as their request isn't valid / answerable.
215     *
216     * TODO (T343652): Actually tell the user why they ended up somewhere they might not want?
217     *
218     * @param OutputPage $output
219     */
220    private function redirectToMain( OutputPage $output ) {
221        // We use inContentLanguage() to get it in English, rather than redirecting to non-existent pages
222        // like https://www.wikifunctions.org/wiki/Strona_g%C5%82%C3%B3wna if the user's language is pl.
223        $mainPageUrl = '/wiki/' . $output->msg( 'Mainpage' )->inContentLanguage()->text();
224        $output->redirect( $mainPageUrl, 303 );
225    }
226
227    /**
228     * (T355441) Unlike regular Special pages, we actively want search engines to
229     * index our content and follow our links.
230     *
231     * @inheritDoc
232     */
233    protected function getRobotPolicy() {
234        return 'index,follow';
235    }
236}