src/Diagnostics/FuzzyTranslationsMaintenanceScript.php
<?php
declare( strict_types = 1 );
namespace MediaWiki\Extension\Translate\Diagnostics;
use ContentHandler;
use IDBAccessObject;
use MediaWiki\CommentStore\CommentStoreComment;
use MediaWiki\Extension\Translate\SystemUsers\FuzzyBot;
use MediaWiki\Extension\Translate\Utilities\BaseMaintenanceScript;
use MediaWiki\Extension\Translate\Utilities\Utilities;
use MediaWiki\MediaWikiServices;
use MediaWiki\Page\WikiPageFactory;
use MediaWiki\Revision\RevisionStore;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Title\Title;
use MediaWiki\User\ActorNormalization;
use Wikimedia\Rdbms\ILoadBalancer;
use Wikimedia\Rdbms\IResultWrapper;
/**
* @since 2022.01
* @license GPL-2.0-or-later
* @author Niklas Laxström
*/
class FuzzyTranslationsMaintenanceScript extends BaseMaintenanceScript {
private ActorNormalization $actorNormalization;
private RevisionStore $revisionStore;
private ILoadBalancer $DBLoadBalancer;
private WikiPageFactory $wikiPageFactory;
public function __construct() {
parent::__construct();
$this->addDescription( 'Fuzzy bot command line script.' );
$this->addArg(
'arg',
'Title pattern or username if user option is provided.'
);
$this->addOption(
'really',
'(optional) Really fuzzy, no dry-run'
);
$this->addOption(
'skiplanguages',
'(optional) Skip some languages (comma separated)',
self::OPTIONAL,
self::HAS_ARG
);
$this->addOption(
'comment',
'(optional) Comment for updating',
self::OPTIONAL,
self::HAS_ARG
);
$this->addOption(
'user',
'(optional) Fuzzy the translations made by user given as an argument.',
self::OPTIONAL,
self::NO_ARG
);
$this->requireExtension( 'Translate' );
}
private function initServices(): void {
$mwServices = MediaWikiServices::getInstance();
$this->actorNormalization = $mwServices->getActorNormalization();
$this->revisionStore = $mwServices->getRevisionStore();
$this->DBLoadBalancer = $mwServices->getDBLoadBalancer();
$this->wikiPageFactory = $mwServices->getWikiPageFactory();
}
public function execute(): void {
$this->initServices();
$skipLanguages = [];
if ( $this->hasOption( 'skiplanguages' ) ) {
$skipLanguages = array_map(
'trim',
explode( ',', $this->getOption( 'skiplanguages' ) )
);
}
if ( $this->hasOption( 'user' ) ) {
$pages = $this->getPagesForUser( $this->getArg( 0 ), $skipLanguages );
} else {
$pages = $this->getPagesForPattern( $this->getArg( 0 ), $skipLanguages );
}
$dryrun = !$this->hasOption( 'really' );
$comment = $this->getOption( 'comment' );
$this->fuzzyTranslations( $pages, $dryrun, $comment );
}
private function fuzzyTranslations( array $pages, bool $dryrun, ?string $comment ): void {
$count = count( $pages );
$this->output( "Found $count pages to update.", 'pagecount' );
foreach ( $pages as [ $title, $text ] ) {
$this->updateMessage( $title, TRANSLATE_FUZZY . $text, $dryrun, $comment );
}
}
/**
* Gets the message contents from database rows.
* @param IResultWrapper $rows
* @return array containing page titles and the text content of the page
*/
private function getMessageContentsFromRows( IResultWrapper $rows ): array {
$messagesContents = [];
$slots = $this->revisionStore->getContentBlobsForBatch( $rows, [ SlotRecord::MAIN ] )->getValue();
foreach ( $rows as $row ) {
$title = Title::makeTitle( $row->page_namespace, $row->page_title );
if ( isset( $slots[$row->rev_id] ) ) {
$text = $slots[$row->rev_id][SlotRecord::MAIN]->blob_data;
} else {
$content = $this->revisionStore
->newRevisionFromRow( $row, IDBAccessObject::READ_NORMAL, $title )
->getContent( SlotRecord::MAIN );
$text = Utilities::getTextFromTextContent( $content );
}
$messagesContents[] = [ $title, $text ];
}
return $messagesContents;
}
/** Searches pages that match given patterns */
private function getPagesForPattern( string $pattern, array $skipLanguages = [] ): array {
$dbr = $this->DBLoadBalancer->getMaintenanceConnectionRef( DB_REPLICA );
$conds = [
'page_latest=rev_id',
];
$title = Title::newFromText( $pattern );
if ( $title->inNamespace( NS_MAIN ) ) {
$namespace = $this->getConfig()->get( 'TranslateMessageNamespaces' );
} else {
$namespace = $title->getNamespace();
}
$conds['page_namespace'] = $namespace;
$conds[] = 'page_title' . $dbr->buildLike( $title->getDBkey(), $dbr->anyString() );
if ( count( $skipLanguages ) ) {
$skiplist = $dbr->makeList( $skipLanguages );
$conds[] = "substring_index(page_title, '/', -1) NOT IN ($skiplist)";
}
$rows = $this->revisionStore->newSelectQueryBuilder( $dbr )
->joinPage()
->where( $conds )
->caller( __METHOD__ )
->fetchResultSet();
return $this->getMessageContentsFromRows( $rows );
}
private function getPagesForUser( string $userName, array $skipLanguages = [] ): array {
$dbr = $this->DBLoadBalancer->getMaintenanceConnectionRef( DB_REPLICA );
$actorId = $this->actorNormalization->findActorIdByName( $userName, $dbr );
if ( $actorId === null ) {
return [];
}
$conds = [
'page_latest=rev_id',
'rev_actor' => $actorId,
'page_namespace' => $this->getConfig()->get( 'TranslateMessageNamespaces' ),
'page_title' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() ),
];
if ( count( $skipLanguages ) ) {
$skiplist = $dbr->makeList( $skipLanguages );
$conds[] = "substring_index(page_title, '/', -1) NOT IN ($skiplist)";
}
$rows = $this->revisionStore->newSelectQueryBuilder( $dbr )
->joinPage()
->joinUser()
->where( $conds )
->caller( __METHOD__ )
->fetchResultSet();
return $this->getMessageContentsFromRows( $rows );
}
/**
* Does the actual edit if possible.
* @param Title $title
* @param string $text
* @param bool $dryrun Whether to really do it or just show what would be done.
* @param string|null $comment Edit summary.
*/
private function updateMessage( Title $title, string $text, bool $dryrun, ?string $comment = null ) {
$this->output( "Updating {$title->getPrefixedText()}... ", $title );
$documentationLanguageCode = $this->getConfig()->get( 'TranslateDocumentationLanguageCode' );
$items = explode( '/', $title->getText(), 2 );
if ( isset( $items[1] ) && $items[1] === $documentationLanguageCode ) {
$this->output( 'IGNORED!', $title );
return;
}
if ( $dryrun ) {
$this->output( 'DRY RUN!', $title );
return;
}
$wikiPage = $this->wikiPageFactory->newFromTitle( $title );
$summary = CommentStoreComment::newUnsavedComment( $comment ?? 'Marking as fuzzy' );
$content = ContentHandler::makeContent( $text, $title );
$updater = $wikiPage->newPageUpdater( FuzzyBot::getUser() );
$updater
->setContent( SlotRecord::MAIN, $content )
->saveRevision( $summary, EDIT_FORCE_BOT | EDIT_UPDATE );
$status = $updater->getStatus();
$this->output( $status->isOK() ? 'OK' : 'FAILED', $title );
}
}