Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialViewObject
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 6
306
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
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
 execute
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
156
 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 InvalidArgumentException;
15use MediaWiki\Extension\WikiLambda\ZObjectContentHandler;
16use MediaWiki\Extension\WikiLambda\ZObjectEditingPageTrait;
17use MediaWiki\Extension\WikiLambda\ZObjectStore;
18use MediaWiki\Extension\WikiLambda\ZObjectUtils;
19use MediaWiki\Html\Html;
20use MediaWiki\MediaWikiServices;
21use MediaWiki\Output\OutputPage;
22use MediaWiki\SpecialPage\SpecialPage;
23use MediaWiki\Title\Title;
24use ParserOptions;
25use RuntimeException;
26
27class SpecialViewObject extends SpecialPage {
28    use ZObjectEditingPageTrait;
29
30    private ZObjectStore $zObjectStore;
31
32    /**
33     * @param ZObjectStore $zObjectStore
34     */
35    public function __construct( ZObjectStore $zObjectStore ) {
36        parent::__construct( 'ViewObject', 'view', false );
37        $this->zObjectStore = $zObjectStore;
38    }
39
40    /**
41     * @inheritDoc
42     */
43    protected function getGroupName() {
44        // Triggers use of message specialpages-group-wikilambda
45        return 'wikilambda';
46    }
47
48    /**
49     * @inheritDoc
50     */
51    public function getDescription() {
52        return $this->msg( 'wikilambda-special-viewobject' );
53    }
54
55    /**
56     * @inheritDoc
57     */
58    public function execute( $subPage ) {
59        // TODO (T362246): Dependency-inject
60        $services = MediaWikiServices::getInstance();
61
62        $outputPage = $this->getOutput();
63
64        // Make sure things don't think the page is wikitext, so that e.g. VisualEditor
65        // doesn't try to instantiate its tabs
66        $outputPage->getTitle()->setContentModel( CONTENT_MODEL_ZOBJECT );
67
68        // If there's no sub-page, just exit.
69        if ( !$subPage || !is_string( $subPage ) ) {
70            $this->redirectToMain( $outputPage );
71            return;
72        }
73
74        $subPageSplit = [];
75        if ( !preg_match( '/([^\/]+)\/(Z\d+)/', $subPage, $subPageSplit ) ) {
76            // Fallback to 'en' if request doesn't specify a language.
77            $targetLanguage = 'en';
78            $targetPageName = $subPage;
79        } else {
80            $targetLanguage = $subPageSplit[1];
81            $targetPageName = $subPageSplit[2];
82        }
83
84        // Allow the user to over-ride the content language if explicitly requested
85        $targetLanguage = $this->getRequest()->getRawVal( 'uselang' ) ?? $targetLanguage;
86
87        $targetTitle = Title::newFromText( $targetPageName, NS_MAIN );
88
89        if (
90            // If the given page isn't a Title
91            !( $targetTitle instanceof Title ) || !$targetTitle->exists()
92            // … or somehow it's not for a valid ZObject
93            || !ZObjectUtils::isValidId( $targetPageName )
94        ) {
95            $this->redirectToMain( $outputPage );
96            return;
97        }
98
99        // (T345457) Tell OutputPage that our content is article-related, so we get Special:WhatLinksHere etc.
100        $outputPage->setArticleFlag( true );
101        // Tell the skin what content specifically we're related to, so edit/history links etc. work.
102        $this->getSkin()->setRelevantTitle( $targetTitle );
103
104        // (T345453) Have the standard copyright stuff show up.
105        $this->getContext()->getOutput()->setCopyright( true );
106
107        $this->setHeaders();
108
109        try {
110            $targetLanguageObject = $services->getLanguageFactory()->getLanguage( $targetLanguage );
111        } catch ( InvalidArgumentException $e ) {
112            // (T343006) Supplied language is invalid; probably a user-error, so just exit.
113            $this->redirectToMain( $outputPage );
114            return;
115        }
116
117        // Set the page language for our own purposes.
118        $this->getContext()->setLanguage( $targetLanguageObject );
119
120        $outputPage->addModules( [ 'ext.wikilambda.edit', 'mediawiki.special' ] );
121
122        $targetContent = $this->zObjectStore->fetchZObjectByTitle( $targetTitle );
123
124        if ( !$targetContent ) {
125            $this->redirectToMain( $outputPage );
126        }
127
128        // Request that we render the content in the given target language.
129        $parserOptions = ParserOptions::newFromUserAndLang( $this->getUser(), $targetLanguageObject );
130
131        $contentRenderer = $services->getContentRenderer();
132
133        $parserOutput = $contentRenderer->getParserOutput(
134            $targetContent,
135            $targetTitle,
136            null,
137            $parserOptions
138        );
139
140        // Add the header to the parserOutput
141        $header = ZObjectContentHandler::createZObjectViewHeader( $targetContent, $targetTitle, $targetLanguageObject );
142        $outputPage->setPageTitle( $header );
143
144        $outputPage->addParserOutput( $parserOutput );
145
146        // Add all the see-other links to versions of this page in each of the known languages.
147        $languages = $this->zObjectStore->fetchAllZLanguageObjects();
148        foreach ( $languages as $zid => $bcpcode ) {
149            if ( $bcpcode === $targetLanguage ) {
150                continue;
151            }
152            // Add each item individually to help phan understand the taint better, even though it's slower
153            $outputPage->addHeadItem(
154                'link-alternate-language-' . strtolower( $bcpcode ),
155                Html::element(
156                    'link',
157                    [
158                        'rel' => 'alternate',
159                        'hreflang' => $bcpcode,
160                        'href' => "/view/$bcpcode/$targetPageName",
161                    ]
162                )
163            );
164        }
165
166        // (T355546) Over-ride the canonical URL to the /view/ form.
167        $urlUtils = $services->getUrlUtils();
168        $viewURL = $urlUtils->expand( "/view/$targetLanguage/$targetPageName" );
169        // $viewURL can be null 'if no valid URL can be constructed', which shouldn't ever happen.
170        if ( $viewURL === null ) {
171            throw new RuntimeException( 'No valid URL could be constructed for the canonical path' );
172        }
173        $outputPage->setCanonicalUrl( $viewURL );
174
175        // TODO (T362241): Make this help page.
176        $this->addHelpLink( 'Help:Wikifunctions/Viewing Objects' );
177
178        $this->generateZObjectPayload( $outputPage, $this->getContext(), [
179            'createNewPage' => false,
180            'zId' => $targetPageName,
181            'viewmode' => true,
182        ] );
183    }
184
185    /**
186     * Redirect the user to the Main Page, as their request isn't valid / answerable.
187     *
188     * TODO (T343652): Actually tell the user why they ended up somewhere they might not want?
189     *
190     * @param OutputPage $outputPage
191     */
192    private function redirectToMain( OutputPage $outputPage ) {
193        // We use inContentLanguage() to get it in English, rather than redirecting to non-existent pages
194        // like https://www.wikifunctions.org/wiki/Strona_g%C5%82%C3%B3wna if the user's language is pl.
195        $mainPageUrl = '/wiki/' . $outputPage->msg( 'Mainpage' )->inContentLanguage()->text();
196        $outputPage->redirect( $mainPageUrl, 303 );
197    }
198
199    /**
200     * (T355441) Unlike regular Special pages, we actively want search engines to
201     * index our content and follow our links.
202     *
203     * @inheritDoc
204     */
205    protected function getRobotPolicy() {
206        return 'index,follow';
207    }
208}