Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
43.00% covered (danger)
43.00%
43 / 100
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageRenderingHandler
43.00% covered (danger)
43.00%
43 / 100
0.00% covered (danger)
0.00%
0 / 4
173.19
0.00% covered (danger)
0.00%
0 / 1
 onSkinTemplateNavigation__Universal
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
72
 onHtmlPageLinkRendererEnd
84.31% covered (warning)
84.31%
43 / 51
0.00% covered (danger)
0.00%
0 / 1
19.25
 onWebRequestPathInfoRouter
0.00% covered (danger)
0.00%
0 / 4
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
2
1<?php
2/**
3 * WikiLambda handler for hooks which alter page rendering
4 *
5 * @file
6 * @ingroup Extensions
7 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
8 * @license MIT
9 */
10
11namespace MediaWiki\Extension\WikiLambda\HookHandler;
12
13use HtmlArmor;
14use MediaWiki\Context\RequestContext;
15use MediaWiki\Extension\WikiLambda\Registry\ZLangRegistry;
16use MediaWiki\Extension\WikiLambda\WikiLambdaServices;
17use MediaWiki\Extension\WikiLambda\ZObjectContent;
18use MediaWiki\Extension\WikiLambda\ZObjectUtils;
19use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook;
20use MediaWiki\Hook\WebRequestPathInfoRouterHook;
21use MediaWiki\Linker\Hook\HtmlPageLinkRendererEndHook;
22use MediaWiki\Linker\LinkRenderer;
23use MediaWiki\Linker\LinkTarget;
24use MediaWiki\MediaWikiServices;
25use MediaWiki\Output\Hook\BeforePageDisplayHook;
26use MediaWiki\Output\OutputPage;
27use MediaWiki\Title\Title;
28use Skin;
29
30class PageRenderingHandler implements
31    HtmlPageLinkRendererEndHook,
32    SkinTemplateNavigation__UniversalHook,
33    WebRequestPathInfoRouterHook,
34    BeforePageDisplayHook
35{
36
37    // phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
38
39    /**
40     * @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinTemplateNavigation::Universal
41     *
42     * @inheritDoc
43     */
44    public function onSkinTemplateNavigation__Universal( $skinTemplate, &$links ): void {
45        $targetTitle = $skinTemplate->getRelevantTitle();
46
47        // For any page: Add a language control, for users to navigate to another language.
48        // TODO (T362235): This only works for browsers with Javascript. The button is invisible
49        // until the ext.wikilambda.languageselector module creates the Vue component to replace
50        // it; instead, render this Codex component properly server-side somehow.
51        $ourButton = [ 'wikifunctions-language' => [
52            'button' => true,
53            'id' => 'ext-wikilambda-pagelanguagebutton',
54            'text' => '',
55            'active' => false,
56            'link-class' => [ 'wikifunctions-trigger' ],
57            'href' => '#'
58        ] ];
59        $links['user-interface-preferences'] = $ourButton + $links['user-interface-preferences'];
60
61        if ( !$targetTitle->hasContentModel( CONTENT_MODEL_ZOBJECT ) ) {
62            // Nothing to do, exit.
63            return;
64        }
65
66        // Don't show a "View source" link, it's meaningless for our content type
67        unset( $links['views']['viewsource'] );
68
69        // Don't show a "Variants" selector even though we're futzing with lang, we have our own control
70        $links['variants'] = [];
71
72        // Work out our ZID
73        $zid = $targetTitle->getText();
74        // Default language if not specified in the URL
75        $lang = 'en';
76
77        // Special handling if we're on our special view page
78        $title = $skinTemplate->getTitle();
79        if ( $title->isSpecial( 'ViewObject' ) ) {
80            preg_match( "/^([^\/]+)\/([^\/]+)\/(.*)$/", $title->getText(), $matches );
81            if ( $matches ) {
82                // We're on Special:ViewObject with the ZID and language set in the URL, so use them
83                $lang = $matches['2'];
84                $zid = $matches['3'];
85            }
86        }
87
88        // Allow the user to over-ride the content language if explicitly requested
89        $lang = $skinTemplate->getRequest()->getRawVal( 'uselang' ) ?? $lang;
90
91        // Add "selected" class to read tab, if we're viewing the page
92        if (
93            $skinTemplate->getContext()->getActionName() === 'view'
94        ) {
95            $links['views']['view']['class'] = 'selected';
96        }
97
98        // Rewrite history link to have ?uselang in it
99        $links['views']['history']['href'] = '/wiki/' . $zid . '?action=history&uselang=' . $lang;
100        // Rewrite history link to have ?uselang in it, but only if it exists (e.g. not for logged-out users)
101        if ( array_key_exists( 'edit', $links['views'] ) ) {
102            $links['views']['edit']['href'] = '/wiki/' . $zid . '?action=edit&uselang=' . $lang;
103            // If editing old revision, we want the edit button to route us to the oldid
104            $oldid = $skinTemplate->getRequest()->getRawVal( 'oldid' );
105            if ( $oldid ) {
106                $links['views']['edit']['href'] .= '&oldid=' . $oldid;
107            }
108        }
109
110        // Rewrite the 'main' namespace link to the Special page
111        // We have to set under 'namespaces' and 'associated-pages' due to a migration.
112        $contentCanonicalHref = '/view/' . $lang . '/' . $zid;
113        $links['namespaces']['main']['href'] = $contentCanonicalHref;
114        $links['associated-pages']['main']['href'] = $contentCanonicalHref;
115
116        // Re-write the 'view' link as well
117        $links['views']['view']['href'] = $contentCanonicalHref;
118
119        // Rewrite the 'talk' namespace link to have ?uselang in it
120        // Again, we have to set it twice
121        if ( strpos( $links['namespaces']['talk']['href'] ?? '', '?' ) ) {
122            // @phan-suppress-next-next-line PhanTypeArraySuspiciousNull, PhanTypeInvalidDimOffset
123            // @phan-suppress-next-line PhanTypeSuspiciousStringExpression
124            $talkRewrittenHref = $links['namespaces']['talk']['href'] . '&uselang=' . $lang;
125        } else {
126            $talkRewrittenHref = '/wiki/Talk:' . $zid . '?uselang=' . $lang;
127        }
128
129        $links['namespaces']['talk']['href'] = $talkRewrittenHref;
130        $links['associated-pages']['talk']['href'] = $talkRewrittenHref;
131    }
132
133    // phpcs:enable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
134
135    /**
136     * @see https://www.mediawiki.org/wiki/Manual:Hooks/HtmlPageLinkRendererEnd
137     *
138     * @param LinkRenderer $linkRenderer
139     * @param LinkTarget $linkTarget
140     * @param bool $isKnown
141     * @param string|HtmlArmor &$text
142     * @param string[] &$attribs
143     * @param string &$ret
144     * @return bool|void
145     */
146    public function onHtmlPageLinkRendererEnd(
147        $linkRenderer, $linkTarget, $isKnown, &$text, &$attribs, &$ret
148    ) {
149        // Convert the slimline LinkTarget into a full-fat Title so we can ask deeper questions
150        $targetTitle = Title::newFromLinkTarget( $linkTarget );
151        $zid = $targetTitle->getBaseText();
152
153        // Do nothing if the target isn't one of ours
154        if (
155            !$targetTitle->inNamespace( NS_MAIN )
156            || !$targetTitle->hasContentModel( CONTENT_MODEL_ZOBJECT )
157            || !ZObjectUtils::isValidZObjectReference( $zid )
158        ) {
159            return;
160        }
161
162        $context = RequestContext::getMain();
163        $currentPageContentLanguageCode = $context->getLanguage()->getCode();
164        // (T357702) If we don't know the language code, fall back to Z1002/'en'
165        $langRegistry = ZLangRegistry::singleton();
166        if ( !$langRegistry->isLanguageKnownGivenCode( $currentPageContentLanguageCode ) ) {
167            $currentPageContentLanguageCode = 'en';
168        }
169
170        // Re-write our path to include the content language ($attribs['href']) where appropriate
171        // HACK: Our hook doesn't tell us properly that the target is an action, so we have to pull it from the href
172        // … we could get $query from using HtmlPageLinkRendererBefore, but then we don't have access to the href
173        $queryPos = strpos( $attribs['href'], '?' );
174        $query = $queryPos ? wfCgiToArray( substr( $attribs['href'], $queryPos + 1 ) ) : [];
175
176        $action = $query['action'] ?? 'view';
177        if ( $action !== 'view' ) {
178            $attribs['href'] = '/wiki/' . $zid . '?action=' . $action . '&uselang='
179                . $currentPageContentLanguageCode . '&';
180        } elseif ( !isset( $query[ 'diff'] ) && !isset( $query['oldid'] ) ) {
181            $attribs['href'] = '/view/' . $currentPageContentLanguageCode . '/' . $zid . '?';
182        } else {
183            $attribs['href'] = '/wiki/' . $zid . '?uselang=' . $currentPageContentLanguageCode . '&';
184        }
185
186        unset( $query['action'] );
187        unset( $query['title'] );
188
189        foreach ( $query as $key => $value ) {
190            $attribs['href'] .= $key . '=' . $value . '&';
191        }
192        $attribs['href'] = substr( $attribs['href'], 0, -1 );
193
194        // **After this point, the only changes we're making are to the label ($text)**
195
196        // (T342212) Wrap our ZID in an LTR-enforced <span> so it works OK in RTL environments
197        $bidiWrappedZid = '<span dir="ltr">' . $zid . '</span>';
198
199        // Special handling for unknown (red) links; we want to add the wrapped ZID but don't want to try to fetch
200        // the label, which will fail
201        if ( !$isKnown && $text === $zid ) {
202            $text = new HtmlArmor( $bidiWrappedZid );
203            return;
204        }
205
206        // We don't re-write the label if the label is already set (e.g. for "prev" and "cur" and revision links on
207        // history pages, or inline links like [[Z1|this]]); we do however continue for
208        if ( $text !== null && $targetTitle->getFullText() !== HtmlArmor::getHtml( $text ) ) {
209            return;
210        }
211
212        // Rather than (rather expensively) fetching the whole object from the ZObjectStore, see if the labels are in
213        // the labels table already, which is very much faster:
214        $zObjectStore = WikiLambdaServices::getZObjectStore();
215        $label = $zObjectStore->fetchZObjectLabel(
216            $zid,
217            $currentPageContentLanguageCode,
218            true
219        );
220
221        // Just in case the database has no entry (e.g. the table is a millisecond behind or so), load the full object.
222        if ( $label === null ) {
223            $targetZObject = $zObjectStore->fetchZObjectByTitle( $targetTitle );
224            // Do nothing if somehow after all that it's not loadable
225            if ( !$targetZObject || !( $targetZObject instanceof ZObjectContent ) || !$targetZObject->isValid() ) {
226                return;
227            }
228
229            // At this point, we know they're linking to a ZObject page, so show a label, falling back
230            // to English even if that's not in the language's fall-back chain.
231            $label = $targetZObject->getLabels()
232                ->buildStringForLanguage( $context->getLanguage() )
233                ->fallbackWithEnglish()
234                ->placeholderForTitle()
235                ->getString() ?? '';
236        }
237
238        // Finally, set the label of the link to the *un*escaped user-supplied label, see
239        // https://www.mediawiki.org/wiki/Manual:Hooks/HtmlPageLinkRendererEnd
240        //
241        // &$text: the contents that the <a> tag should have; either a *plain, unescaped string* or a HtmlArmor object.
242        //
243        $text = new HtmlArmor(
244            htmlspecialchars( $label )
245                . $context->msg( 'word-separator' )->escaped()
246                . $context->msg( 'parentheses', [ $bidiWrappedZid ] )
247        );
248    }
249
250    /**
251     * @inheritDoc
252     */
253    public function onWebRequestPathInfoRouter( $router ) {
254        $router->addStrict(
255            '/view/$2/$1',
256            [ 'title' => 'Special:ViewObject/$2/$1' ]
257        );
258    }
259
260    /**
261     * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageDisplay
262     *
263     * @param OutputPage $out
264     * @param Skin $skin
265     * @return void
266     */
267    public function onBeforePageDisplay( $out, $skin ): void {
268        // Save language name in global variables, needed for language selector module
269        $languageNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils();
270        $userLang = $out->getLanguage();
271        $userLangName = $languageNameUtils->getLanguageName( $userLang->getCode() );
272        $out->addJsConfigVars( 'wgUserLanguageName', $userLangName );
273
274        // Add language selector module to all pages
275        $out->addModules( 'ext.wikilambda.languageselector' );
276    }
277}