Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 137
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
DoubleWiki
0.00% covered (danger)
0.00%
0 / 137
0.00% covered (danger)
0.00%
0 / 5
462
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 onOutputPageBeforeHTML
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
56
 getMangledTextAndTranslation
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
110
 matchColumns
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
2
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
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
20namespace MediaWiki\Extension\DoubleWiki;
21
22use Config;
23use Language;
24use MediaWiki\Hook\BeforePageDisplayHook;
25use MediaWiki\Hook\OutputPageBeforeHTMLHook;
26use MediaWiki\Html\Html;
27use MediaWiki\Html\HtmlHelper;
28use MediaWiki\Http\HttpRequestFactory;
29use MediaWiki\Languages\LanguageFactory;
30use MediaWiki\Languages\LanguageNameUtils;
31use MediaWiki\Title\Title;
32use OutputPage;
33use Skin;
34use WANObjectCache;
35use Wikimedia\RemexHtml\Serializer\SerializerNode;
36
37class 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}