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