Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialViewObject
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 7
462
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescription
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 userCanExecute
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 execute
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 1
210
 redirectToMain
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getRobotPolicy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * WikiLambda Special:ViewObject 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\ZObjectEditingPageTrait;
17use MediaWiki\Extension\WikiLambda\ZObjectRepoUtils;
18use MediaWiki\Extension\WikiLambda\ZObjectStore;
19use MediaWiki\Extension\WikiLambda\ZObjectUtils;
20use MediaWiki\Html\Html;
21use MediaWiki\Language\LanguageFactory;
22use MediaWiki\Output\OutputPage;
23use MediaWiki\Parser\ParserOptions;
24use MediaWiki\SpecialPage\UnlistedSpecialPage;
25use MediaWiki\Title\Title;
26use MediaWiki\User\User;
27use MediaWiki\Utils\UrlUtils;
28
29class SpecialViewObject extends UnlistedSpecialPage {
30    use ZObjectEditingPageTrait;
31
32    private ContentRenderer $contentRenderer;
33    private LanguageFactory $languageFactory;
34    private UrlUtils $urlUtils;
35
36    public function __construct(
37        ContentRenderer $contentRenderer,
38        LanguageFactory $languageFactory,
39        UrlUtils $urlUtils,
40        private readonly ZObjectStore $zObjectStore
41    ) {
42        parent::__construct( 'ViewObject', 'read' );
43        $this->contentRenderer = $contentRenderer;
44        $this->languageFactory = $languageFactory;
45        $this->urlUtils = $urlUtils;
46    }
47
48    /**
49     * @inheritDoc
50     */
51    protected function getGroupName() {
52        // Triggers use of message specialpages-group-wikilambda
53        return 'wikilambda';
54    }
55
56    /**
57     * @inheritDoc
58     */
59    public function getDescription() {
60        return $this->msg( 'wikilambda-special-viewobject' );
61    }
62
63    /**
64     * @inheritDoc
65     *
66     * @param User $user
67     * @return bool
68     */
69    public function userCanExecute( User $user ) {
70        if ( !$this->getConfig()->get( 'WikiLambdaEnableRepoMode' ) ) {
71            // No usage allowed on client-mode wikis.
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        $outputPage = $this->getOutput();
88
89        if ( !$this->getConfig()->get( 'WikiLambdaEnableRepoMode' ) ) {
90            // No usage allowed on client-mode wikis.
91            $this->redirectToMain( $outputPage );
92            return;
93        }
94
95        // Make sure things don't think the page is wikitext, so that e.g. VisualEditor
96        // doesn't try to instantiate its tabs
97        $outputPage->getTitle()->setContentModel( CONTENT_MODEL_ZOBJECT );
98
99        // If there's no subpage, just exit.
100        if ( !$subPage || !is_string( $subPage ) ) {
101            $this->redirectToMain( $outputPage );
102            return;
103        }
104
105        $subPageSplit = [];
106        if ( !preg_match( '/([^\/]+)\/(Z\d+)/', $subPage, $subPageSplit ) ) {
107            // Fallback to 'en' if request doesn't specify a language.
108            $targetLanguage = 'en';
109            $targetPageName = $subPage;
110        } else {
111            $targetLanguage = $subPageSplit[1];
112            $targetPageName = $subPageSplit[2];
113        }
114
115        // Allow the user to over-ride the content language if explicitly requested
116        $targetLanguage = $this->getRequest()->getRawVal( 'uselang' ) ?? $targetLanguage;
117
118        $targetTitle = Title::newFromText( $targetPageName, NS_MAIN );
119
120        if (
121            // If the given page isn't a Title
122            !( $targetTitle instanceof Title ) || !$targetTitle->exists()
123            // … or somehow it's not for a valid ZObject
124            || !ZObjectUtils::isValidZObjectReference( $targetPageName )
125        ) {
126            $this->redirectToMain( $outputPage );
127            return;
128        }
129
130        // Tell the skin what content specifically we're related to, so edit/history links etc. work.
131        $this->getSkin()->setRelevantTitle( $targetTitle );
132        // (T343594) Set the title of the page to the target title, so Recent Changes Link works
133        $outputPage->setTitle( $targetTitle );
134
135        /**
136         * (T343594) Set the revision ID to the requested one or the latest, so the Permanent Link works
137         *
138         * TODO (T364318): add the revision navigation bar to the page.
139         */
140        $latestRevId = $outputPage->getTitle()->getLatestRevID();
141        $targetRevision = $this->getRequest()->getInt( 'oldid' ) ?: $latestRevId;
142        $outputPage->setRevisionId( $targetRevision );
143
144        // (T345453) Have the standard copyright stuff show up.
145        $outputPage->setCopyright( true );
146
147        $this->setHeaders();
148
149        // Turn the selected language code or ZID reference into a Language
150        $targetLanguageObject = ZObjectRepoUtils::getLanguageFromString( $targetLanguage );
151
152        // Set the page language for our own purposes.
153        $this->getContext()->setLanguage( $targetLanguageObject );
154
155        $outputPage->addModules( [ 'ext.wikilambda.app', 'mediawiki.special' ] );
156
157        $targetContent = $this->zObjectStore->fetchZObjectByTitle( $targetTitle );
158        if ( !$targetContent ) {
159            $this->redirectToMain( $outputPage );
160        }
161
162        // Request that we render the content in the given target language.
163        $parserOptions = ParserOptions::newFromUserAndLang( $this->getUser(), $targetLanguageObject );
164        $parserOutput = $this->contentRenderer->getParserOutput(
165            $targetContent,
166            $targetTitle,
167            null,
168            $parserOptions
169        );
170
171        $outputPage->addParserOutput( $parserOutput, $parserOptions );
172
173        // Add all the see-other links to versions of this page in each of the known languages.
174        $languages = $this->zObjectStore->fetchAllZLanguageCodes();
175        foreach ( $languages as $bcpcode ) {
176            if ( $bcpcode === $targetLanguage ) {
177                continue;
178            }
179            // Add each item individually to help phan understand the taint better, even though it's slower
180            $outputPage->addHeadItem(
181                'link-alternate-language-' . strtolower( $bcpcode ),
182                Html::element(
183                    'link',
184                    [
185                        'rel' => 'alternate',
186                        'hreflang' => $bcpcode,
187                        'href' => "/view/$bcpcode/$targetPageName",
188                    ]
189                )
190            );
191        }
192
193        // (T355546) Over-ride the canonical URL to the /view/ form.
194        $viewURL = $this->urlUtils->expand( "/view/$targetLanguage/$targetPageName" );
195        // $viewURL can be null 'if no valid URL can be constructed', which shouldn't ever happen.
196        if ( $viewURL === null ) {
197            throw new ConfigException( 'No valid URL could be constructed for the canonical path' );
198        }
199        $outputPage->setCanonicalUrl( $viewURL );
200
201        // (T345457) Tell OutputPage that our content is article-related, so we get Special:WhatLinksHere etc.
202        // (T343594) The Special:WhatLinksHere weren't shown on view/en/ZXXXX pages,
203        // but they were on wiki/ZXXXX pages. Setting the flag here (lower in code) fixes it.
204        $outputPage->setArticleFlag( true );
205        $this->addHelpLink( 'Help:Wikifunctions/Viewing Objects' );
206
207        $this->generateZObjectPayload( $this->getContext(), $outputPage, [
208            'createNewPage' => false,
209            'zId' => $targetPageName,
210            'viewmode' => true,
211        ] );
212    }
213
214    /**
215     * Redirect the user to the Main Page, as their request isn't valid / answerable.
216     *
217     * TODO (T343652): Actually tell the user why they ended up somewhere they might not want?
218     *
219     * @param OutputPage $outputPage
220     */
221    private function redirectToMain( OutputPage $outputPage ) {
222        // We use inContentLanguage() to get it in English, rather than redirecting to non-existent pages
223        // like https://www.wikifunctions.org/wiki/Strona_g%C5%82%C3%B3wna if the user's language is pl.
224        $mainPageUrl = '/wiki/' . $outputPage->msg( 'Mainpage' )->inContentLanguage()->text();
225        $outputPage->redirect( $mainPageUrl, 303 );
226    }
227
228    /**
229     * (T355441) Unlike regular Special pages, we actively want search engines to
230     * index our content and follow our links.
231     *
232     * @inheritDoc
233     */
234    protected function getRobotPolicy() {
235        return 'index,follow';
236    }
237}