Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 149
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialPageLanguage
0.00% covered (danger)
0.00%
0 / 148
0.00% covered (danger)
0.00%
0 / 13
1056
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 preHtml
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getFormFields
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
56
 postHtml
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getDisplayFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 alterForm
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onSubmit
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
 changePageLanguage
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
90
 onSuccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 showLogFragment
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 prefixSearchSubpages
0.00% covered (danger)
0.00%
0 / 1
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
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Specials;
22
23use LogEventsList;
24use LogPage;
25use ManualLogEntry;
26use MediaWiki\Api\ApiMessage;
27use MediaWiki\Content\IContentHandlerFactory;
28use MediaWiki\Context\IContextSource;
29use MediaWiki\HTMLForm\HTMLForm;
30use MediaWiki\Language\RawMessage;
31use MediaWiki\Languages\LanguageNameUtils;
32use MediaWiki\MainConfigNames;
33use MediaWiki\MediaWikiServices;
34use MediaWiki\Permissions\PermissionStatus;
35use MediaWiki\SpecialPage\FormSpecialPage;
36use MediaWiki\Status\Status;
37use MediaWiki\Title\MalformedTitleException;
38use MediaWiki\Title\Title;
39use MediaWiki\Xml\Xml;
40use SearchEngineFactory;
41use Wikimedia\Rdbms\IConnectionProvider;
42use Wikimedia\Rdbms\IDatabase;
43
44/**
45 * Special page for changing the content language of a page
46 *
47 * @ingroup SpecialPage
48 * @author Kunal Grover
49 * @since 1.24
50 */
51class SpecialPageLanguage extends FormSpecialPage {
52    /**
53     * @var string URL to go to if language change successful
54     */
55    private $goToUrl;
56
57    private IContentHandlerFactory $contentHandlerFactory;
58    private LanguageNameUtils $languageNameUtils;
59    private IConnectionProvider $dbProvider;
60    private SearchEngineFactory $searchEngineFactory;
61
62    /**
63     * @param IContentHandlerFactory $contentHandlerFactory
64     * @param LanguageNameUtils $languageNameUtils
65     * @param IConnectionProvider $dbProvider
66     * @param SearchEngineFactory $searchEngineFactory
67     */
68    public function __construct(
69        IContentHandlerFactory $contentHandlerFactory,
70        LanguageNameUtils $languageNameUtils,
71        IConnectionProvider $dbProvider,
72        SearchEngineFactory $searchEngineFactory
73    ) {
74        parent::__construct( 'PageLanguage', 'pagelang' );
75        $this->contentHandlerFactory = $contentHandlerFactory;
76        $this->languageNameUtils = $languageNameUtils;
77        $this->dbProvider = $dbProvider;
78        $this->searchEngineFactory = $searchEngineFactory;
79    }
80
81    public function doesWrites() {
82        return true;
83    }
84
85    protected function preHtml() {
86        $this->getOutput()->addModules( 'mediawiki.misc-authed-ooui' );
87        return parent::preHtml();
88    }
89
90    protected function getFormFields() {
91        // Get default from the subpage of Special page
92        $defaultName = $this->par;
93        $title = $defaultName ? Title::newFromText( $defaultName ) : null;
94        if ( $title ) {
95            $defaultPageLanguage = $this->contentHandlerFactory->getContentHandler( $title->getContentModel() )
96                ->getPageLanguage( $title );
97
98            $hasCustomLanguageSet = !$defaultPageLanguage->equals( $title->getPageLanguage() );
99        } else {
100            $hasCustomLanguageSet = false;
101        }
102
103        $page = [];
104        $page['pagename'] = [
105            'type' => 'title',
106            'label-message' => 'pagelang-name',
107            'default' => $title ? $title->getPrefixedText() : $defaultName,
108            'autofocus' => $defaultName === null,
109            'exists' => true,
110        ];
111
112        // Options for whether to use the default language or select language
113        $selectoptions = [
114            (string)$this->msg( 'pagelang-use-default' )->escaped() => 1,
115            (string)$this->msg( 'pagelang-select-lang' )->escaped() => 2,
116        ];
117        $page['selectoptions'] = [
118            'id' => 'mw-pl-options',
119            'type' => 'radio',
120            'options' => $selectoptions,
121            'default' => $hasCustomLanguageSet ? 2 : 1
122        ];
123
124        // Building a language selector
125        $userLang = $this->getLanguage()->getCode();
126        $languages = $this->languageNameUtils->getLanguageNames( $userLang, LanguageNameUtils::SUPPORTED );
127        $options = [];
128        foreach ( $languages as $code => $name ) {
129            $options["$code - $name"] = $code;
130        }
131
132        $page['language'] = [
133            'id' => 'mw-pl-languageselector',
134            'cssclass' => 'mw-languageselector',
135            'type' => 'select',
136            'options' => $options,
137            'label-message' => 'pagelang-language',
138            'default' => $title ?
139                $title->getPageLanguage()->getCode() :
140                $this->getConfig()->get( MainConfigNames::LanguageCode ),
141        ];
142
143        // Allow user to enter a comment explaining the change
144        $page['reason'] = [
145            'type' => 'text',
146            'label-message' => 'pagelang-reason'
147        ];
148
149        return $page;
150    }
151
152    protected function postHtml() {
153        if ( $this->par ) {
154            return $this->showLogFragment( $this->par );
155        }
156        return '';
157    }
158
159    protected function getDisplayFormat() {
160        return 'ooui';
161    }
162
163    public function alterForm( HTMLForm $form ) {
164        $this->getHookRunner()->onLanguageSelector( $this->getOutput(), 'mw-languageselector' );
165        $form->setSubmitTextMsg( 'pagelang-submit' );
166    }
167
168    /**
169     * @param array $data
170     * @return Status
171     */
172    public function onSubmit( array $data ) {
173        $pageName = $data['pagename'];
174
175        // Check if user wants to use default language
176        if ( $data['selectoptions'] == 1 ) {
177            $newLanguage = 'default';
178        } else {
179            $newLanguage = $data['language'];
180        }
181
182        try {
183            $title = Title::newFromTextThrow( $pageName );
184        } catch ( MalformedTitleException $ex ) {
185            return Status::newFatal( $ex->getMessageObject() );
186        }
187
188        // Check permissions and make sure the user has permission to edit the page
189        $status = PermissionStatus::newEmpty();
190        if ( !$this->getAuthority()->authorizeWrite( 'edit', $title, $status ) ) {
191            $wikitext = $this->getOutput()->formatPermissionStatus( $status );
192            // Hack to get our wikitext parsed
193            return Status::newFatal( new RawMessage( '$1', [ $wikitext ] ) );
194        }
195
196        // Url to redirect to after the operation
197        $this->goToUrl = $title->getFullUrlForRedirect(
198            $title->isRedirect() ? [ 'redirect' => 'no' ] : []
199        );
200
201        return self::changePageLanguage(
202            $this->getContext(),
203            $title,
204            $newLanguage,
205            $data['reason'] ?? '',
206            [],
207            $this->dbProvider->getPrimaryDatabase()
208        );
209    }
210
211    /**
212     * @since 1.36 Added $dbw parameter
213     *
214     * @param IContextSource $context
215     * @param Title $title
216     * @param string $newLanguage Language code
217     * @param string $reason Reason for the change
218     * @param string[] $tags Change tags to apply to the log entry
219     * @param IDatabase|null $dbw
220     * @return Status
221     */
222    public static function changePageLanguage( IContextSource $context, Title $title,
223        $newLanguage, $reason = "", array $tags = [], ?IDatabase $dbw = null ) {
224        // Get the default language for the wiki
225        $defLang = $context->getConfig()->get( MainConfigNames::LanguageCode );
226
227        $pageId = $title->getArticleID();
228
229        // Check if article exists
230        if ( !$pageId ) {
231            return Status::newFatal(
232                'pagelang-nonexistent-page',
233                wfEscapeWikiText( $title->getPrefixedText() )
234            );
235        }
236
237        // Load the page language from DB
238        $dbw ??= MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
239        $oldLanguage = $dbw->newSelectQueryBuilder()
240            ->select( 'page_lang' )
241            ->from( 'page' )
242            ->where( [ 'page_id' => $pageId ] )
243            ->caller( __METHOD__ )->fetchField();
244
245        // Check if user wants to use the default language
246        if ( $newLanguage === 'default' ) {
247            $newLanguage = null;
248        }
249
250        // No change in language
251        if ( $newLanguage === $oldLanguage ) {
252            // Check if old language does not exist
253            if ( !$oldLanguage ) {
254                return Status::newFatal( ApiMessage::create(
255                    [
256                        'pagelang-unchanged-language-default',
257                        wfEscapeWikiText( $title->getPrefixedText() )
258                    ],
259                    'pagelang-unchanged-language'
260                ) );
261            }
262            return Status::newFatal(
263                'pagelang-unchanged-language',
264                wfEscapeWikiText( $title->getPrefixedText() ),
265                $oldLanguage
266            );
267        }
268
269        // Hardcoded [def] if the language is set to null
270        $logOld = $oldLanguage ?: $defLang . '[def]';
271        $logNew = $newLanguage ?: $defLang . '[def]';
272
273        // Writing new page language to database
274        $dbw->newUpdateQueryBuilder()
275            ->update( 'page' )
276            ->set( [ 'page_lang' => $newLanguage ] )
277            ->where( [
278                'page_id' => $pageId,
279                'page_lang' => $oldLanguage,
280            ] )
281            ->caller( __METHOD__ )->execute();
282
283        if ( !$dbw->affectedRows() ) {
284            return Status::newFatal( 'pagelang-db-failed' );
285        }
286
287        // Logging change of language
288        $logParams = [
289            '4::oldlanguage' => $logOld,
290            '5::newlanguage' => $logNew
291        ];
292        $entry = new ManualLogEntry( 'pagelang', 'pagelang' );
293        $entry->setPerformer( $context->getUser() );
294        $entry->setTarget( $title );
295        $entry->setParameters( $logParams );
296        $entry->setComment( is_string( $reason ) ? $reason : "" );
297        $entry->addTags( $tags );
298
299        $logid = $entry->insert();
300        $entry->publish( $logid );
301
302        // Force re-render so that language-based content (parser functions etc.) gets updated
303        $title->invalidateCache();
304
305        return Status::newGood( (object)[
306            'oldLanguage' => $logOld,
307            'newLanguage' => $logNew,
308            'logId' => $logid,
309        ] );
310    }
311
312    public function onSuccess() {
313        // Success causes a redirect
314        $this->getOutput()->redirect( $this->goToUrl );
315    }
316
317    private function showLogFragment( $title ) {
318        $moveLogPage = new LogPage( 'pagelang' );
319        $out1 = Xml::element( 'h2', null, $moveLogPage->getName()->text() );
320        $out2 = '';
321        LogEventsList::showLogExtract( $out2, 'pagelang', $title );
322        return $out1 . $out2;
323    }
324
325    /**
326     * Return an array of subpages beginning with $search that this special page will accept.
327     *
328     * @param string $search Prefix to search for
329     * @param int $limit Maximum number of results to return (usually 10)
330     * @param int $offset Number of results to skip (usually 0)
331     * @return string[] Matching subpages
332     */
333    public function prefixSearchSubpages( $search, $limit, $offset ) {
334        return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );
335    }
336
337    protected function getGroupName() {
338        return 'pagetools';
339    }
340}
341
342/**
343 * Retain the old class name for backwards compatibility.
344 * @deprecated since 1.41
345 */
346class_alias( SpecialPageLanguage::class, 'SpecialPageLanguage' );