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