Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
43.00% |
43 / 100 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
PageRenderingHandler | |
43.00% |
43 / 100 |
|
0.00% |
0 / 4 |
173.19 | |
0.00% |
0 / 1 |
onSkinTemplateNavigation__Universal | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
72 | |||
onHtmlPageLinkRendererEnd | |
84.31% |
43 / 51 |
|
0.00% |
0 / 1 |
19.25 | |||
onWebRequestPathInfoRouter | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
onBeforePageDisplay | |
0.00% |
0 / 5 |
|
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 | |
11 | namespace MediaWiki\Extension\WikiLambda\HookHandler; |
12 | |
13 | use HtmlArmor; |
14 | use MediaWiki\Context\RequestContext; |
15 | use MediaWiki\Extension\WikiLambda\Registry\ZLangRegistry; |
16 | use MediaWiki\Extension\WikiLambda\WikiLambdaServices; |
17 | use MediaWiki\Extension\WikiLambda\ZObjectContent; |
18 | use MediaWiki\Extension\WikiLambda\ZObjectUtils; |
19 | use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook; |
20 | use MediaWiki\Hook\WebRequestPathInfoRouterHook; |
21 | use MediaWiki\Linker\Hook\HtmlPageLinkRendererEndHook; |
22 | use MediaWiki\Linker\LinkRenderer; |
23 | use MediaWiki\Linker\LinkTarget; |
24 | use MediaWiki\MediaWikiServices; |
25 | use MediaWiki\Output\Hook\BeforePageDisplayHook; |
26 | use MediaWiki\Output\OutputPage; |
27 | use MediaWiki\Title\Title; |
28 | use Skin; |
29 | |
30 | class 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 | } |