Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 137 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
DoubleWiki | |
0.00% |
0 / 137 |
|
0.00% |
0 / 5 |
462 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
onOutputPageBeforeHTML | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
56 | |||
getMangledTextAndTranslation | |
0.00% |
0 / 53 |
|
0.00% |
0 / 1 |
110 | |||
matchColumns | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
2 | |||
onBeforePageDisplay | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | /* |
4 | * This program is free software; you can redistribute it and/or modify |
5 | * it under the terms of the GNU General Public License as published by |
6 | * the Free Software Foundation; either version 2 of the License, or |
7 | * (at your option) any later version. |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License along |
15 | * with this program; if not, write to the Free Software Foundation, Inc., |
16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | * http://www.gnu.org/copyleft/gpl.html |
18 | */ |
19 | |
20 | namespace MediaWiki\Extension\DoubleWiki; |
21 | |
22 | use Config; |
23 | use Language; |
24 | use MediaWiki\Hook\BeforePageDisplayHook; |
25 | use MediaWiki\Hook\OutputPageBeforeHTMLHook; |
26 | use MediaWiki\Html\Html; |
27 | use MediaWiki\Html\HtmlHelper; |
28 | use MediaWiki\Http\HttpRequestFactory; |
29 | use MediaWiki\Languages\LanguageFactory; |
30 | use MediaWiki\Languages\LanguageNameUtils; |
31 | use MediaWiki\Title\Title; |
32 | use OutputPage; |
33 | use Skin; |
34 | use WANObjectCache; |
35 | use Wikimedia\RemexHtml\Serializer\SerializerNode; |
36 | |
37 | class DoubleWiki implements OutputPageBeforeHTMLHook, BeforePageDisplayHook { |
38 | |
39 | private Config $mainConfig; |
40 | private Language $contentLanguage; |
41 | private LanguageFactory $languageFactory; |
42 | private LanguageNameUtils $languageNameUtils; |
43 | private HttpRequestFactory $httpRequestFactory; |
44 | private WANObjectCache $cache; |
45 | |
46 | /** Constructor. */ |
47 | public function __construct( |
48 | Config $mainConfig, |
49 | Language $contentLanguage, |
50 | LanguageFactory $languageFactory, |
51 | LanguageNameUtils $languageNameUtils, |
52 | HttpRequestFactory $httpRequestFactory, |
53 | WANObjectCache $cache |
54 | ) { |
55 | $this->mainConfig = $mainConfig; |
56 | $this->contentLanguage = $contentLanguage; |
57 | $this->languageFactory = $languageFactory; |
58 | $this->languageNameUtils = $languageNameUtils; |
59 | $this->httpRequestFactory = $httpRequestFactory; |
60 | $this->cache = $cache; |
61 | } |
62 | |
63 | /** |
64 | * OutputPageBeforeHTML hook handler. Transform $text into |
65 | * a bilingual version if `match` query parameter is provided. |
66 | * @link https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageBeforeHTML |
67 | * |
68 | * @param OutputPage $out OutputPage object |
69 | * @param string &$text HTML to mangle |
70 | */ |
71 | public function onOutputPageBeforeHTML( $out, &$text ): bool { |
72 | $matchCode = $out->getRequest()->getText( 'match' ); |
73 | if ( $matchCode === '' ) { |
74 | return true; |
75 | } |
76 | |
77 | $fname = __METHOD__; |
78 | |
79 | foreach ( $out->getLanguageLinks() as $iwLinkText ) { |
80 | $iwt = Title::newFromText( $iwLinkText ); |
81 | if ( !$iwt || $iwt->getInterwiki() !== $matchCode ) { |
82 | continue; |
83 | } |
84 | |
85 | $newText = $this->cache->getWithSetCallback( |
86 | $this->cache->makeKey( |
87 | 'doublewiki-bilingual-pagetext', |
88 | $out->getLanguage()->getCode(), |
89 | $iwt->getPrefixedDbKey() |
90 | ), |
91 | $this->mainConfig->get( 'DoubleWikiCacheTime' ), |
92 | // @TODO: maybe integrate with WikiPage::purgeInterwikiCheckKey() somehow? |
93 | function ( $oldValue ) use ( $iwt, $out, $matchCode, $text, $fname ) { |
94 | $foreignUrl = $iwt->getCanonicalURL(); |
95 | $currentUrl = $out->getTitle()->getLocalURL(); |
96 | |
97 | // TODO: Consider getting Last-Modified header and use $cache->daptiveTTL() |
98 | $translation = $this->httpRequestFactory |
99 | ->get( wfAppendQuery( $foreignUrl, [ 'action' => 'render' ] ), [], $fname ); |
100 | |
101 | if ( $translation === null ) { |
102 | // not cached |
103 | return false; |
104 | } |
105 | |
106 | [ $text, $translation ] = $this->getMangledTextAndTranslation( |
107 | $text, |
108 | $translation, |
109 | $matchCode |
110 | ); |
111 | |
112 | return $this->matchColumns( |
113 | $text, |
114 | $currentUrl, |
115 | $this->contentLanguage, |
116 | $translation, |
117 | $foreignUrl, |
118 | $this->languageFactory->getLanguage( $matchCode ) |
119 | ); |
120 | } |
121 | ); |
122 | |
123 | if ( $newText !== false ) { |
124 | $text = $newText; |
125 | $out->addModuleStyles( 'ext.doubleWiki' ); |
126 | } |
127 | |
128 | break; |
129 | } |
130 | |
131 | return true; |
132 | } |
133 | |
134 | /** |
135 | * @return string[] (new text, new translation) |
136 | */ |
137 | private function getMangledTextAndTranslation( string $text, string $translation, string $matchLangCode ): array { |
138 | // add prefixes to internal links, in order to prevent duplicates |
139 | $translation = HtmlHelper::modifyElements( |
140 | $translation, |
141 | static function ( SerializerNode $n ): bool { |
142 | return $n->name === 'a' && isset( $n->attrs['href'] ) && str_starts_with( $n->attrs['href'], '#' ); |
143 | }, |
144 | static function ( SerializerNode $n ): SerializerNode { |
145 | $n->attrs['href'] = '#l_' . substr( $n->attrs['href'], 1 ); |
146 | return $n; |
147 | } |
148 | ); |
149 | $translation = HtmlHelper::modifyElements( |
150 | $translation, |
151 | static function ( SerializerNode $n ): bool { |
152 | return $n->name === 'li' && isset( $n->attrs['id'] ); |
153 | }, |
154 | static function ( SerializerNode $n ): SerializerNode { |
155 | $n->attrs['id'] = 'l_' . $n->attrs['id']; |
156 | return $n; |
157 | } |
158 | ); |
159 | |
160 | $text = HtmlHelper::modifyElements( |
161 | $text, |
162 | static function ( SerializerNode $n ): bool { |
163 | return $n->name === 'a' && isset( $n->attrs['href'] ) && str_starts_with( $n->attrs['href'], '#' ); |
164 | }, |
165 | static function ( SerializerNode $n ): SerializerNode { |
166 | $n->attrs['href'] = '#r_' . substr( $n->attrs['href'], 1 ); |
167 | return $n; |
168 | } |
169 | ); |
170 | $text = HtmlHelper::modifyElements( |
171 | $text, |
172 | static function ( SerializerNode $n ): bool { |
173 | return $n->name === 'li' && isset( $n->attrs['id'] ); |
174 | }, |
175 | static function ( SerializerNode $n ): SerializerNode { |
176 | $n->attrs['id'] = 'r_' . $n->attrs['id']; |
177 | return $n; |
178 | } |
179 | ); |
180 | |
181 | // add ?match= to local links of the local wiki |
182 | $text = HtmlHelper::modifyElements( |
183 | $text, |
184 | static function ( SerializerNode $n ): bool { |
185 | return $n->name === 'a' && isset( $n->attrs['href'] ) |
186 | && str_starts_with( $n->attrs['href'], '/' ) |
187 | && !str_contains( $n->attrs['href'], '?' ); |
188 | }, |
189 | static function ( SerializerNode $n ) use ( $matchLangCode ): SerializerNode { |
190 | $n->attrs['href'] = wfAppendQuery( $n->attrs['href'], [ 'match' => $matchLangCode ] ); |
191 | return $n; |
192 | } |
193 | ); |
194 | |
195 | return [ $text, $translation ]; |
196 | } |
197 | |
198 | /** |
199 | * Format the text as a two-column table |
200 | */ |
201 | private function matchColumns( |
202 | string $left_text, string $left_url, Language $left_lang, |
203 | string $right_text, string $right_url, Language $right_lang |
204 | ): string { |
205 | $left_langcode = $left_lang->getHtmlCode(); |
206 | $left_langdir = $left_lang->getDir(); |
207 | $right_langcode = $right_lang->getHtmlCode(); |
208 | $right_langdir = $right_lang->getDir(); |
209 | $left_title = $this->languageNameUtils->getLanguageName( $left_lang->getCode() ); |
210 | $right_title = $this->languageNameUtils->getLanguageName( $right_lang->getCode() ); |
211 | |
212 | return Html::rawElement( 'table', [ 'id' => 'doubleWikiTable' ], |
213 | Html::rawElement( 'thead', [], |
214 | Html::rawElement( 'tr', [], |
215 | Html::rawElement( 'td', [ 'lang' => $left_langcode ], |
216 | Html::element( 'a', [ 'href' => $left_url ], |
217 | $left_title |
218 | ) |
219 | ) . |
220 | Html::rawElement( 'td', [ 'lang' => $right_langcode ], |
221 | Html::element( 'a', [ 'href' => $right_url, 'class' => 'extiw' ], |
222 | $right_title |
223 | ) |
224 | ) |
225 | ) |
226 | ) . |
227 | Html::rawElement( 'tr', [], |
228 | // phpcs:ignore Generic.Files.LineLength.TooLong |
229 | Html::rawElement( 'td', [ 'lang' => $left_langcode, 'dir' => $left_langdir, 'class' => "mw-content-$left_langdir" ], |
230 | Html::rawElement( 'div', [], |
231 | $left_text |
232 | ) |
233 | ) . |
234 | // phpcs:ignore Generic.Files.LineLength.TooLong |
235 | Html::rawElement( 'td', [ 'lang' => $right_langcode, 'dir' => $right_langdir, 'class' => "mw-content-$right_langdir" ], |
236 | Html::rawElement( 'div', [], |
237 | $right_text |
238 | ) |
239 | ) |
240 | ) |
241 | ); |
242 | } |
243 | |
244 | /** |
245 | * BeforePageDisplay hook handler |
246 | * @link https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageDisplay |
247 | * |
248 | * @param OutputPage $out OutputPage object |
249 | * @param Skin $skin The skin in use |
250 | */ |
251 | public function onBeforePageDisplay( $out, $skin ): void { |
252 | if ( $out->getRequest()->getText( 'match' ) !== '' ) { |
253 | $out->setRobotPolicy( 'noindex,nofollow' ); |
254 | } |
255 | } |
256 | } |