summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Translate/tag/SpecialPageTranslation.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/Translate/tag/SpecialPageTranslation.php')
-rw-r--r--www/wiki/extensions/Translate/tag/SpecialPageTranslation.php993
1 files changed, 993 insertions, 0 deletions
diff --git a/www/wiki/extensions/Translate/tag/SpecialPageTranslation.php b/www/wiki/extensions/Translate/tag/SpecialPageTranslation.php
new file mode 100644
index 00000000..377d6c91
--- /dev/null
+++ b/www/wiki/extensions/Translate/tag/SpecialPageTranslation.php
@@ -0,0 +1,993 @@
+<?php
+/**
+ * Contains logic for special page Special:ImportTranslations.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @author Siebrand Mazeland
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * A special page for marking revisions of pages for translation.
+ *
+ * This page is the main tool for translation administrators in the wiki.
+ * It will list all pages in their various states and provides actions
+ * that are suitable for given translatable page.
+ *
+ * @ingroup SpecialPage PageTranslation
+ */
+class SpecialPageTranslation extends SpecialPage {
+ public function __construct() {
+ parent::__construct( 'PageTranslation' );
+ }
+
+ public function doesWrites() {
+ return true;
+ }
+
+ protected function getGroupName() {
+ return 'pagetools';
+ }
+
+ public function execute( $parameters ) {
+ $this->setHeaders();
+
+ $user = $this->getUser();
+ $request = $this->getRequest();
+
+ $target = $request->getText( 'target', $parameters );
+ $revision = $request->getInt( 'revision', 0 );
+ $action = $request->getVal( 'do' );
+ $out = $this->getOutput();
+ $out->addModules( 'ext.translate.special.pagetranslation' );
+ $out->addHelpLink( 'Help:Extension:Translate/Page_translation_example' );
+
+ if ( $target === '' ) {
+ $this->listPages();
+
+ return;
+ }
+
+ // Anything else than listing the pages need permissions
+ if ( !$user->isAllowed( 'pagetranslation' ) ) {
+ throw new PermissionsError( 'pagetranslation' );
+ }
+
+ $title = Title::newFromText( $target );
+ if ( !$title ) {
+ $out->addWikiMsg( 'tpt-badtitle' );
+
+ return;
+ } elseif ( !$title->exists() ) {
+ $out->addWikiMsg( 'tpt-nosuchpage', $title->getPrefixedText() );
+
+ return;
+ }
+
+ // Check token for all POST actions here
+ if ( $request->wasPosted() && !$user->matchEditToken( $request->getText( 'token' ) ) ) {
+ throw new PermissionsError( 'pagetranslation' );
+ }
+
+ if ( $action === 'mark' ) {
+ // Has separate form
+ $this->onActionMark( $title, $revision );
+
+ return;
+ }
+
+ // On GET requests, show form which has token
+ if ( !$request->wasPosted() ) {
+ if ( $action === 'unlink' ) {
+ $this->showUnlinkConfirmation( $title, $target );
+ } else {
+ $params = [
+ 'do' => $action,
+ 'target' => $title->getPrefixedText(),
+ 'revision' => $revision
+ ];
+ $this->showGenericConfirmation( $params );
+ }
+
+ return;
+ }
+
+ if ( $action === 'discourage' || $action === 'encourage' ) {
+ $id = TranslatablePage::getMessageGroupIdFromTitle( $title );
+ $current = MessageGroups::getPriority( $id );
+
+ if ( $action === 'encourage' ) {
+ $new = '';
+ } else {
+ $new = 'discouraged';
+ }
+
+ if ( $new !== $current ) {
+ MessageGroups::setPriority( $id, $new );
+ $entry = new ManualLogEntry( 'pagetranslation', $action );
+ $entry->setPerformer( $user );
+ $entry->setTarget( $title );
+ $logid = $entry->insert();
+ $entry->publish( $logid );
+ }
+
+ $this->listPages();
+
+ $group = MessageGroups::getGroup( $id );
+ $parents = MessageGroups::getSharedGroups( $group );
+ MessageGroupStats::clearGroup( $parents );
+
+ return;
+ }
+
+ if ( $action === 'unlink' ) {
+ $page = TranslatablePage::newFromTitle( $title );
+ $content = ContentHandler::makeContent(
+ self::getStrippedSourcePageText( $page->getParse() ),
+ $title
+ );
+
+ $status = WikiPage::factory( $title )->doEditContent(
+ $content,
+ $this->msg( 'tpt-unlink-summary' )->inContentLanguage()->text(),
+ EDIT_FORCE_BOT | EDIT_UPDATE
+ );
+
+ if ( !$status->isOK() ) {
+ $out->wrapWikiMsg(
+ '<div class="errorbox">$1</div>',
+ [ 'tpt-edit-failed', $status->getWikiText() ]
+ );
+
+ return;
+ }
+
+ $page = TranslatablePage::newFromTitle( $title );
+ $this->unmarkPage( $page, $user );
+ $out->wrapWikiMsg(
+ '<div class="successbox">$1</div>',
+ [ 'tpt-unmarked', $title->getPrefixedText() ]
+ );
+ $this->listPages();
+
+ return;
+ }
+
+ if ( $action === 'unmark' ) {
+ $page = TranslatablePage::newFromTitle( $title );
+ $this->unmarkPage( $page, $user );
+ $out->wrapWikiMsg(
+ '<div class="successbox">$1</div>',
+ [ 'tpt-unmarked', $title->getPrefixedText() ]
+ );
+ $this->listPages();
+
+ return;
+ }
+ }
+
+ protected function onActionMark( Title $title, $revision ) {
+ $request = $this->getRequest();
+ $out = $this->getOutput();
+
+ $out->addModuleStyles( 'ext.translate.special.pagetranslation.styles' );
+
+ if ( $revision === 0 ) {
+ // Get the latest revision
+ $revision = (int)$title->getLatestRevID();
+ }
+
+ $page = TranslatablePage::newFromRevision( $title, $revision );
+ if ( !$page instanceof TranslatablePage ) {
+ $out->wrapWikiMsg(
+ '<div class="errorbox">$1</div>',
+ [ 'tpt-notsuitable', $title->getPrefixedText(), $revision ]
+ );
+
+ return;
+ }
+
+ if ( $revision !== (int)$title->getLatestRevID() ) {
+ // We do want to notify the reviewer if the underlying page changes during review
+ $target = $title->getFullURL( [ 'oldid' => $revision ] );
+ $link = "<span class='plainlinks'>[$target $revision]</span>";
+ $out->wrapWikiMsg(
+ '<div class="warningbox">$1</div>',
+ [ 'tpt-oldrevision', $title->getPrefixedText(), $link ]
+ );
+ $this->listPages();
+
+ return;
+ }
+
+ $lastRev = $page->getMarkedTag();
+ $firstMark = $lastRev === false;
+ if ( !$firstMark && $lastRev === $revision ) {
+ $out->wrapWikiMsg(
+ '<div class="warningbox">$1</div>',
+ [ 'tpt-already-marked' ]
+ );
+ $this->listPages();
+
+ return;
+ }
+
+ // This will modify the sections to include name property
+ $error = false;
+ $sections = $this->checkInput( $page, $error );
+
+ // Non-fatal error which prevents saving
+ if ( $error === false && $request->wasPosted() ) {
+ // Check if user wants to translate title
+ // If not, remove it from the list of sections
+ if ( !$request->getCheck( 'translatetitle' ) ) {
+ $sections = array_filter( $sections, function ( $s ) {
+ return $s->id !== 'Page display title';
+ } );
+ }
+
+ $err = $this->markForTranslation( $page, $sections );
+
+ if ( $err ) {
+ call_user_func_array( [ $out, 'addWikiMsg' ], $err );
+ } else {
+ $this->showSuccess( $page, $firstMark );
+ $this->listPages();
+ }
+
+ return;
+ }
+
+ $this->showPage( $page, $sections );
+ }
+
+ /**
+ * Displays success message and other instructions after a page has been marked for translation.
+ * @param TranslatablePage $page
+ * @param bool $firstMark true if it is the first time the page is being marked for translation.
+ */
+ public function showSuccess( TranslatablePage $page, $firstMark = false ) {
+ $titleText = $page->getTitle()->getPrefixedText();
+ $num = $this->getLanguage()->formatNum( $page->getParse()->countSections() );
+ $link = SpecialPage::getTitleFor( 'Translate' )->getFullURL( [
+ 'group' => $page->getMessageGroupId(),
+ 'action' => 'page',
+ 'filter' => '',
+ ] );
+
+ $this->getOutput()->wrapWikiMsg(
+ '<div class="successbox">$1</div>',
+ [ 'tpt-saveok', $titleText, $num, $link ]
+ );
+
+ // If the page is being marked for translation for the first time
+ // add a link to Special:PageMigration.
+ if ( $firstMark ) {
+ $this->getOutput()->addWikiMsg( 'tpt-saveok-first' );
+ }
+
+ // If TranslationNotifications is installed, and the user can notify
+ // translators, add a convenience link.
+ if ( method_exists( 'SpecialNotifyTranslators', 'execute' ) &&
+ $this->getUser()->isAllowed( SpecialNotifyTranslators::$right )
+ ) {
+ $link = SpecialPage::getTitleFor( 'NotifyTranslators' )->getFullURL(
+ [ 'tpage' => $page->getTitle()->getArticleID() ] );
+ $this->getOutput()->addWikiMsg( 'tpt-offer-notify', $link );
+ }
+ }
+
+ protected function showGenericConfirmation( array $params ) {
+ $formParams = [
+ 'method' => 'post',
+ 'action' => $this->getPageTitle()->getFullURL(),
+ ];
+
+ $params['title'] = $this->getPageTitle()->getPrefixedText();
+ $params['token'] = $this->getUser()->getEditToken();
+
+ $hidden = '';
+ foreach ( $params as $key => $value ) {
+ $hidden .= Html::hidden( $key, $value );
+ }
+
+ $this->getOutput()->addHTML(
+ Html::openElement( 'form', $formParams ) .
+ $hidden .
+ $this->msg( 'tpt-generic-confirm' )->parseAsBlock() .
+ Xml::submitButton(
+ $this->msg( 'tpt-generic-button' )->text(),
+ [ 'class' => 'mw-ui-button mw-ui-progressive' ]
+ ) .
+ Html::closeElement( 'form' )
+ );
+ }
+
+ protected function showUnlinkConfirmation( Title $target ) {
+ $formParams = [
+ 'method' => 'post',
+ 'action' => $this->getPageTitle()->getFullURL(),
+ ];
+
+ $this->getOutput()->addHTML(
+ Html::openElement( 'form', $formParams ) .
+ Html::hidden( 'do', 'unlink' ) .
+ Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
+ Html::hidden( 'target', $target->getPrefixedText() ) .
+ Html::hidden( 'token', $this->getUser()->getEditToken() ) .
+ $this->msg( 'tpt-unlink-confirm', $target->getPrefixedText() )->parseAsBlock() .
+ Xml::submitButton(
+ $this->msg( 'tpt-unlink-button' )->text(),
+ [ 'class' => 'mw-ui-button mw-ui-destructive' ]
+ ) .
+ Html::closeElement( 'form' )
+ );
+ }
+
+ protected function unmarkPage( TranslatablePage $page, $user ) {
+ $page->unmarkTranslatablePage();
+ $page->getTitle()->invalidateCache();
+
+ $entry = new ManualLogEntry( 'pagetranslation', 'unmark' );
+ $entry->setPerformer( $user );
+ $entry->setTarget( $page->getTitle() );
+ $logid = $entry->insert();
+ $entry->publish( $logid );
+ }
+
+ public function loadPagesFromDB() {
+ $dbr = TranslateUtils::getSafeReadDB();
+ $tables = [ 'page', 'revtag' ];
+ $vars = [
+ 'page_id',
+ 'page_title',
+ 'page_namespace',
+ 'page_latest',
+ 'MAX(rt_revision) AS rt_revision',
+ 'rt_type'
+ ];
+ $conds = [
+ 'page_id=rt_page',
+ 'rt_type' => [ RevTag::getType( 'tp:mark' ), RevTag::getType( 'tp:tag' ) ],
+ ];
+ $options = [
+ 'ORDER BY' => 'page_namespace, page_title',
+ 'GROUP BY' => 'page_id, rt_type',
+ ];
+ $res = $dbr->select( $tables, $vars, $conds, __METHOD__, $options );
+
+ return $res;
+ }
+
+ protected function buildPageArray( /*db result*/$res ) {
+ $pages = [];
+ foreach ( $res as $r ) {
+ // We have multiple rows for same page, because of different tags
+ if ( !isset( $pages[$r->page_id] ) ) {
+ $pages[$r->page_id] = [];
+ $title = Title::newFromRow( $r );
+ $pages[$r->page_id]['title'] = $title;
+ $pages[$r->page_id]['latest'] = (int)$title->getLatestRevID();
+ }
+
+ $tag = RevTag::typeToTag( $r->rt_type );
+ $pages[$r->page_id][$tag] = (int)$r->rt_revision;
+ }
+
+ return $pages;
+ }
+
+ /**
+ * @param array $in
+ * @return array
+ */
+ protected function classifyPages( array $in ) {
+ $out = [
+ 'proposed' => [],
+ 'active' => [],
+ 'broken' => [],
+ 'discouraged' => [],
+ ];
+
+ foreach ( $in as $index => $page ) {
+ if ( !isset( $page['tp:mark'] ) ) {
+ // Never marked, check that the latest version is ready
+ if ( $page['tp:tag'] === $page['latest'] ) {
+ $out['proposed'][$index] = $page;
+ } // Otherwise ignore such pages
+ } elseif ( $page['tp:tag'] === $page['latest'] ) {
+ // Marked and latest version if fine
+ $out['active'][$index] = $page;
+ } else {
+ // Marked but latest version if not fine
+ $out['broken'][$index] = $page;
+ }
+ }
+
+ // broken and proposed take preference over discouraged status
+ foreach ( $out['active'] as $index => $page ) {
+ $id = TranslatablePage::getMessageGroupIdFromTitle( $page['title'] );
+ $group = MessageGroups::getGroup( $id );
+ if ( MessageGroups::getPriority( $group ) === 'discouraged' ) {
+ $out['discouraged'][$index] = $page;
+ unset( $out['active'][$index] );
+ }
+ }
+
+ return $out;
+ }
+
+ public function listPages() {
+ $out = $this->getOutput();
+
+ $res = $this->loadPagesFromDB();
+ $allPages = $this->buildPageArray( $res );
+ if ( !count( $allPages ) ) {
+ $out->addWikiMsg( 'tpt-list-nopages' );
+
+ return;
+ }
+
+ $lb = new LinkBatch();
+ $lb->setCaller( __METHOD__ );
+ foreach ( $allPages as $page ) {
+ $lb->addObj( $page['title'] );
+ }
+ $lb->execute();
+
+ $types = $this->classifyPages( $allPages );
+
+ $pages = $types['proposed'];
+ if ( count( $pages ) ) {
+ $out->wrapWikiMsg( '== $1 ==', 'tpt-new-pages-title' );
+ $out->addWikiMsg( 'tpt-new-pages', count( $pages ) );
+ $out->addHTML( '<ol>' );
+ foreach ( $pages as $page ) {
+ $link = Linker::link( $page['title'] );
+ $acts = $this->actionLinks( $page, 'proposed' );
+ $out->addHTML( "<li>$link $acts</li>" );
+ }
+ $out->addHTML( '</ol>' );
+ }
+
+ $pages = $types['active'];
+ if ( count( $pages ) ) {
+ $out->wrapWikiMsg( '== $1 ==', 'tpt-old-pages-title' );
+ $out->addWikiMsg( 'tpt-old-pages', count( $pages ) );
+ $out->addHTML( '<ol>' );
+ foreach ( $pages as $page ) {
+ $link = Linker::link( $page['title'] );
+ if ( $page['tp:mark'] !== $page['tp:tag'] ) {
+ $link = "<strong>$link</strong>";
+ }
+
+ $acts = $this->actionLinks( $page, 'active' );
+ $out->addHTML( "<li>$link $acts</li>" );
+ }
+ $out->addHTML( '</ol>' );
+ }
+
+ $pages = $types['broken'];
+ if ( count( $pages ) ) {
+ $out->wrapWikiMsg( '== $1 ==', 'tpt-other-pages-title' );
+ $out->addWikiMsg( 'tpt-other-pages', count( $pages ) );
+ $out->addHTML( '<ol>' );
+ foreach ( $pages as $page ) {
+ $link = Linker::link( $page['title'] );
+ $acts = $this->actionLinks( $page, 'broken' );
+ $out->addHTML( "<li>$link $acts</li>" );
+ }
+ $out->addHTML( '</ol>' );
+ }
+
+ $pages = $types['discouraged'];
+ if ( count( $pages ) ) {
+ $out->wrapWikiMsg( '== $1 ==', 'tpt-discouraged-pages-title' );
+ $out->addWikiMsg( 'tpt-discouraged-pages', count( $pages ) );
+ $out->addHTML( '<ol>' );
+ foreach ( $pages as $page ) {
+ $link = Linker::link( $page['title'] );
+ if ( $page['tp:mark'] !== $page['tp:tag'] ) {
+ $link = "<strong>$link</strong>";
+ }
+
+ $acts = $this->actionLinks( $page, 'discouraged' );
+ $out->addHTML( "<li>$link $acts</li>" );
+ }
+ $out->addHTML( '</ol>' );
+ }
+ }
+
+ /**
+ * @param array $page
+ * @param string $type
+ * @return string
+ */
+ protected function actionLinks( array $page, $type ) {
+ $actions = [];
+ /**
+ * @var Title $title
+ */
+ $title = $page['title'];
+ $user = $this->getUser();
+
+ // Class to allow one-click POSTs
+ $js = [ 'class' => 'mw-translate-jspost' ];
+
+ if ( $user->isAllowed( 'pagetranslation' ) ) {
+ $pending = $type === 'active' && $page['latest'] !== $page['tp:mark'];
+ if ( $type === 'proposed' || $pending ) {
+ $actions[] = Linker::linkKnown(
+ $this->getPageTitle(),
+ $this->msg( 'tpt-rev-mark' )->escaped(),
+ [ 'title' => $this->msg( 'tpt-rev-mark-tooltip' )->text() ],
+ [
+ 'do' => 'mark',
+ 'target' => $title->getPrefixedText(),
+ 'revision' => $title->getLatestRevID(),
+ ]
+ );
+ }
+
+ if ( $type === 'active' ) {
+ $actions[] = Linker::linkKnown(
+ $this->getPageTitle(),
+ $this->msg( 'tpt-rev-discourage' )->escaped(),
+ [ 'title' => $this->msg( 'tpt-rev-discourage-tooltip' )->text() ] + $js,
+ [
+ 'do' => 'discourage',
+ 'target' => $title->getPrefixedText(),
+ 'revision' => -1,
+ ]
+ );
+ } elseif ( $type === 'discouraged' ) {
+ $actions[] = Linker::linkKnown(
+ $this->getPageTitle(),
+ $this->msg( 'tpt-rev-encourage' )->escaped(),
+ [ 'title' => $this->msg( 'tpt-rev-encourage-tooltip' )->text() ] + $js,
+ [
+ 'do' => 'encourage',
+ 'target' => $title->getPrefixedText(),
+ 'revision' => -1,
+ ]
+ );
+ }
+
+ if ( $type !== 'proposed' ) {
+ $actions[] = Linker::linkKnown(
+ $this->getPageTitle(),
+ $this->msg( 'tpt-rev-unmark' )->escaped(),
+ [ 'title' => $this->msg( 'tpt-rev-unmark-tooltip' )->text() ],
+ [
+ 'do' => $type === 'broken' ? 'unmark' : 'unlink',
+ 'target' => $title->getPrefixedText(),
+ 'revision' => -1,
+ ]
+ );
+ }
+ }
+
+ if ( !count( $actions ) ) {
+ return '';
+ }
+
+ $flattened = $this->getLanguage()->semicolonList( $actions );
+
+ return Html::rawElement(
+ 'span',
+ [ 'class' => 'mw-tpt-actions' ],
+ $this->msg( 'parentheses' )->rawParams( $flattened )->escaped()
+ );
+ }
+
+ /**
+ * @param TranslatablePage $page
+ * @param bool &$error
+ * @return TPSection[] The array has string keys.
+ */
+ public function checkInput( TranslatablePage $page, &$error ) {
+ $usedNames = [];
+ $highest = (int)TranslateMetadata::get( $page->getMessageGroupId(), 'maxid' );
+ $parse = $page->getParse();
+ $sections = $parse->getSectionsForSave( $highest );
+
+ foreach ( $sections as $s ) {
+ // We need to do checks for both new and existing sections.
+ // Someone might have tampered with the page source adding
+ // duplicate or invalid markers.
+ if ( isset( $usedNames[$s->id] ) ) {
+ $this->getOutput()->addWikiMsg( 'tpt-duplicate', $s->id );
+ $error = true;
+ }
+ $usedNames[$s->id] = true;
+ $s->name = $s->id;
+ }
+
+ return $sections;
+ }
+
+ /**
+ * Displays the sections and changes for the user to review
+ * @param TranslatablePage $page
+ * @param TPSection[] $sections
+ */
+ public function showPage( TranslatablePage $page, array $sections ) {
+ $out = $this->getOutput();
+
+ $out->setSubtitle( Linker::link( $page->getTitle() ) );
+
+ $out->addWikiMsg( 'tpt-showpage-intro' );
+
+ $formParams = [
+ 'method' => 'post',
+ 'action' => $this->getPageTitle()->getFullURL(),
+ 'class' => 'mw-tpt-sp-markform',
+ ];
+
+ $out->addHTML(
+ Xml::openElement( 'form', $formParams ) .
+ Html::hidden( 'do', 'mark' ) .
+ Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
+ Html::hidden( 'revision', $page->getRevision() ) .
+ Html::hidden( 'target', $page->getTitle()->getPrefixedText() ) .
+ Html::hidden( 'token', $this->getUser()->getEditToken() )
+ );
+
+ $out->wrapWikiMsg( '==$1==', 'tpt-sections-oldnew' );
+
+ $diffOld = $this->msg( 'tpt-diff-old' )->escaped();
+ $diffNew = $this->msg( 'tpt-diff-new' )->escaped();
+ $hasChanges = false;
+
+ // Check whether page title was previously marked for translation.
+ // If the page is marked for translation the first time, default to checked.
+ $defaultChecked = $page->hasPageDisplayTitle();
+
+ $sourceLanguage = Language::factory( $page->getSourceLanguageCode() );
+
+ foreach ( $sections as $s ) {
+ if ( $s->name === 'Page display title' ) {
+ // Set section type as new if title previously unchecked
+ $s->type = $defaultChecked ? $s->type : 'new';
+
+ // Checkbox for page title optional translation
+ $this->getOutput()->addHTML( Xml::checkLabel(
+ $this->msg( 'tpt-translate-title' )->text(),
+ 'translatetitle',
+ 'mw-translate-title',
+ $defaultChecked
+ ) );
+ }
+
+ if ( $s->type === 'new' ) {
+ $hasChanges = true;
+ $name = $this->msg( 'tpt-section-new', $s->name )->escaped();
+ } else {
+ $name = $this->msg( 'tpt-section', $s->name )->escaped();
+ }
+
+ if ( $s->type === 'changed' ) {
+ $hasChanges = true;
+ $diff = new DifferenceEngine;
+ $diff->setTextLanguage( $sourceLanguage );
+ $diff->setReducedLineNumbers();
+
+ $oldContent = ContentHandler::makeContent( $s->getOldText(), $diff->getTitle() );
+ $newContent = ContentHandler::makeContent( $s->getText(), $diff->getTitle() );
+
+ $diff->setContent( $oldContent, $newContent );
+
+ $text = $diff->getDiff( $diffOld, $diffNew );
+ $diffOld = $diffNew = null;
+ $diff->showDiffStyle();
+
+ $id = "tpt-sect-{$s->id}-action-nofuzzy";
+ $checkLabel = Xml::checkLabel(
+ $this->msg( 'tpt-action-nofuzzy' )->text(),
+ $id,
+ $id,
+ false
+ );
+ $text = $checkLabel . $text;
+ } else {
+ $text = TranslateUtils::convertWhiteSpaceToHTML( $s->getText() );
+ }
+
+ # For changed text, the language is set by $diff->setTextLanguage()
+ $lang = $s->type === 'changed' ? null : $sourceLanguage;
+ $out->addHTML( MessageWebImporter::makeSectionElement(
+ $name,
+ $s->type,
+ $text,
+ $lang
+ ) );
+ }
+
+ $deletedSections = $page->getParse()->getDeletedSections();
+ if ( count( $deletedSections ) ) {
+ $hasChanges = true;
+ $out->wrapWikiMsg( '==$1==', 'tpt-sections-deleted' );
+
+ /**
+ * @var TPSection $s
+ */
+ foreach ( $deletedSections as $s ) {
+ $name = $this->msg( 'tpt-section-deleted', $s->id )->escaped();
+ $text = TranslateUtils::convertWhiteSpaceToHTML( $s->getText() );
+ $out->addHTML( MessageWebImporter::makeSectionElement(
+ $name,
+ $s->type,
+ $text,
+ $sourceLanguage
+ ) );
+ }
+ }
+
+ // Display template changes if applicable
+ if ( $page->getMarkedTag() !== false ) {
+ $hasChanges = true;
+ $newTemplate = $page->getParse()->getTemplatePretty();
+ $oldPage = TranslatablePage::newFromRevision(
+ $page->getTitle(),
+ $page->getMarkedTag()
+ );
+ $oldTemplate = $oldPage->getParse()->getTemplatePretty();
+
+ if ( $oldTemplate !== $newTemplate ) {
+ $out->wrapWikiMsg( '==$1==', 'tpt-sections-template' );
+
+ $diff = new DifferenceEngine;
+ $diff->setTextLanguage( $sourceLanguage );
+
+ $oldContent = ContentHandler::makeContent( $oldTemplate, $diff->getTitle() );
+ $newContent = ContentHandler::makeContent( $newTemplate, $diff->getTitle() );
+
+ $diff->setContent( $oldContent, $newContent );
+
+ $text = $diff->getDiff(
+ $this->msg( 'tpt-diff-old' )->escaped(),
+ $this->msg( 'tpt-diff-new' )->escaped()
+ );
+ $diff->showDiffStyle();
+ $diff->setReducedLineNumbers();
+
+ $contentParams = [ 'class' => 'mw-tpt-sp-content' ];
+ $out->addHTML( Xml::tags( 'div', $contentParams, $text ) );
+ }
+ }
+
+ if ( !$hasChanges ) {
+ $out->wrapWikiMsg( '<div class="successbox">$1</div>', 'tpt-mark-nochanges' );
+ }
+
+ $this->priorityLanguagesForm( $page );
+
+ $out->addHTML(
+ Xml::submitButton( $this->msg( 'tpt-submit' )->text() ) .
+ Xml::closeElement( 'form' )
+ );
+ }
+
+ /**
+ * @param TranslatablePage $page
+ */
+ protected function priorityLanguagesForm( TranslatablePage $page ) {
+ global $wgContLang;
+
+ $groupId = $page->getMessageGroupId();
+ $this->getOutput()->wrapWikiMsg( '==$1==', 'tpt-sections-prioritylangs' );
+
+ $langSelector = Xml::languageSelector(
+ $wgContLang->getCode(),
+ false,
+ $this->getLanguage()->getCode()
+ );
+
+ $hLangs = Xml::inputLabelSep(
+ $this->msg( 'tpt-select-prioritylangs' )->text(),
+ 'prioritylangs', // name
+ 'tpt-prioritylangs', // id
+ 50,
+ TranslateMetadata::get( $groupId, 'prioritylangs' )
+ );
+
+ $hForce = Xml::checkLabel(
+ $this->msg( 'tpt-select-prioritylangs-force' )->text(),
+ 'forcelimit', // name
+ 'tpt-priority-forcelimit', // id
+ TranslateMetadata::get( $groupId, 'priorityforce' ) === 'on'
+ );
+
+ $hReason = Xml::inputLabelSep(
+ $this->msg( 'tpt-select-prioritylangs-reason' )->text(),
+ 'priorityreason', // name
+ 'tpt-priority-reason', // id
+ 50, // size
+ TranslateMetadata::get( $groupId, 'priorityreason' )
+ );
+
+ $this->getOutput()->addHTML(
+ '<table>' .
+ '<tr>' .
+ "<td class='mw-label'>$hLangs[0]</td>" .
+ "<td class='mw-input'>$hLangs[1]$langSelector[1]</td>" .
+ '</tr>' .
+ "<tr><td></td><td class='mw-inout'>$hForce</td></tr>" .
+ '<tr>' .
+ "<td class='mw-label'>$hReason[0]</td>" .
+ "<td class='mw-input'>$hReason[1]</td>" .
+ '</tr>' .
+ '</table>'
+ );
+ }
+
+ /**
+ * This function does the heavy duty of marking a page.
+ * - Updates the source page with section markers.
+ * - Updates translate_sections table
+ * - Updates revtags table
+ * - Setups renderjobs to update the translation pages
+ * - Invalidates caches
+ * @param TranslatablePage $page
+ * @param TPSection[] $sections
+ * @return array|bool
+ */
+ public function markForTranslation( TranslatablePage $page, array $sections ) {
+ // Add the section markers to the source page
+ $wikiPage = WikiPage::factory( $page->getTitle() );
+ $content = ContentHandler::makeContent(
+ $page->getParse()->getSourcePageText(),
+ $page->getTitle()
+ );
+
+ $status = $wikiPage->doEditContent(
+ $content,
+ $this->msg( 'tpt-mark-summary' )->inContentLanguage()->text(),
+ EDIT_FORCE_BOT | EDIT_UPDATE
+ );
+
+ if ( !$status->isOK() ) {
+ return [ 'tpt-edit-failed', $status->getWikiText() ];
+ }
+
+ $newrevision = $status->value['revision'];
+
+ // In theory it is either null or Revision object,
+ // never revision object with null id, but who knows
+ if ( $newrevision instanceof Revision ) {
+ $newrevision = $newrevision->getId();
+ }
+
+ if ( $newrevision === null ) {
+ // Probably a no-change edit, so no new revision was assigned.
+ // Get the latest revision manually
+ $newrevision = $page->getTitle()->getLatestRevID();
+ }
+
+ $inserts = [];
+ $changed = [];
+ $maxid = (int)TranslateMetadata::get( $page->getMessageGroupId(), 'maxid' );
+
+ $pageId = $page->getTitle()->getArticleID();
+ /**
+ * @var TPSection $s
+ */
+ foreach ( array_values( $sections ) as $index => $s ) {
+ $maxid = max( $maxid, (int)$s->name );
+ $changed[] = $s->name;
+
+ if ( $this->getRequest()->getCheck( "tpt-sect-{$s->id}-action-nofuzzy" ) ) {
+ // TranslationsUpdateJob will only fuzzy when type is changed
+ $s->type = 'old';
+ }
+
+ $inserts[] = [
+ 'trs_page' => $pageId,
+ 'trs_key' => $s->name,
+ 'trs_text' => $s->getText(),
+ 'trs_order' => $index
+ ];
+ }
+
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->delete(
+ 'translate_sections',
+ [ 'trs_page' => $page->getTitle()->getArticleID() ],
+ __METHOD__
+ );
+ $dbw->insert( 'translate_sections', $inserts, __METHOD__ );
+ TranslateMetadata::set( $page->getMessageGroupId(), 'maxid', $maxid );
+
+ $page->addMarkedTag( $newrevision );
+ MessageGroups::singleton()->recache();
+
+ $job = TranslationsUpdateJob::newFromPage( $page, $sections );
+ JobQueueGroup::singleton()->push( $job );
+
+ // Logging
+ $this->handlePriorityLanguages( $this->getRequest(), $page );
+
+ $entry = new ManualLogEntry( 'pagetranslation', 'mark' );
+ $entry->setPerformer( $this->getUser() );
+ $entry->setTarget( $page->getTitle() );
+ $entry->setParameters( [
+ 'revision' => $newrevision,
+ 'changed' => count( $changed ),
+ ] );
+ $logid = $entry->insert();
+ $entry->publish( $logid );
+
+ // Clear more caches
+ $page->getTitle()->invalidateCache();
+
+ return false;
+ }
+
+ /**
+ * @param WebRequest $request
+ * @param TranslatablePage $page
+ */
+ protected function handlePriorityLanguages( WebRequest $request, TranslatablePage $page ) {
+ // new priority languages
+ $npLangs = rtrim( trim( $request->getVal( 'prioritylangs' ) ), ',' );
+ $npForce = $request->getCheck( 'forcelimit' ) ? 'on' : 'off';
+ $npReason = trim( $request->getText( 'priorityreason' ) );
+
+ // Normalize
+ $npLangs = array_map( 'trim', explode( ',', $npLangs ) );
+ $npLangs = array_unique( $npLangs );
+ // Remove invalid language codes.
+ $languages = Language::fetchLanguageNames();
+ foreach ( $npLangs as $index => $language ) {
+ if ( !array_key_exists( $language, $languages ) ) {
+ unset( $npLangs[$index] );
+ }
+ }
+ $npLangs = implode( ',', $npLangs );
+ if ( $npLangs === '' ) {
+ $npLangs = false;
+ $npForce = false;
+ $npReason = false;
+ }
+
+ $groupId = $page->getMessageGroupId();
+ // old priority languages
+ $opLangs = TranslateMetadata::get( $groupId, 'prioritylangs' );
+ $opForce = TranslateMetadata::get( $groupId, 'priorityforce' );
+ $opReason = TranslateMetadata::get( $groupId, 'priorityreason' );
+
+ TranslateMetadata::set( $groupId, 'prioritylangs', $npLangs );
+ TranslateMetadata::set( $groupId, 'priorityforce', $npForce );
+ TranslateMetadata::set( $groupId, 'priorityreason', $npReason );
+
+ if ( $opLangs !== $npLangs || $opForce !== $npForce || $opReason !== $npReason ) {
+ $params = [
+ 'languages' => $npLangs,
+ 'force' => $npForce,
+ 'reason' => $npReason,
+ ];
+
+ $entry = new ManualLogEntry( 'pagetranslation', 'prioritylanguages' );
+ $entry->setPerformer( $this->getUser() );
+ $entry->setTarget( $page->getTitle() );
+ $entry->setParameters( $params );
+ $entry->setComment( $npReason );
+ $logid = $entry->insert();
+ $entry->publish( $logid );
+ }
+ }
+
+ /**
+ * Returns the source page without any translation markup.
+ *
+ * @param TPParse $parse
+ * @return string
+ * @since 2014.09
+ */
+ public static function getStrippedSourcePageText( TPParse $parse ) {
+ $text = $parse->getTranslationPageText( [] );
+ $text = preg_replace( '~<languages\s*/>\n?~s', '', $text );
+ return $text;
+ }
+}