summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Translate/api
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/Translate/api
first commit
Diffstat (limited to 'www/wiki/extensions/Translate/api')
-rw-r--r--www/wiki/extensions/Translate/api/ApiAggregateGroups.php238
-rw-r--r--www/wiki/extensions/Translate/api/ApiGroupReview.php153
-rw-r--r--www/wiki/extensions/Translate/api/ApiQueryLanguageStats.php61
-rw-r--r--www/wiki/extensions/Translate/api/ApiQueryMessageCollection.php279
-rw-r--r--www/wiki/extensions/Translate/api/ApiQueryMessageGroupStats.php63
-rw-r--r--www/wiki/extensions/Translate/api/ApiQueryMessageGroups.php322
-rw-r--r--www/wiki/extensions/Translate/api/ApiQueryMessageTranslations.php135
-rw-r--r--www/wiki/extensions/Translate/api/ApiQueryTranslationAids.php132
-rw-r--r--www/wiki/extensions/Translate/api/ApiSearchTranslations.php131
-rw-r--r--www/wiki/extensions/Translate/api/ApiStatsQuery.php95
-rw-r--r--www/wiki/extensions/Translate/api/ApiTTMServer.php96
-rw-r--r--www/wiki/extensions/Translate/api/ApiTranslateSandbox.php213
-rw-r--r--www/wiki/extensions/Translate/api/ApiTranslationCheck.php78
-rw-r--r--www/wiki/extensions/Translate/api/ApiTranslationReview.php166
-rw-r--r--www/wiki/extensions/Translate/api/ApiTranslationStash.php135
15 files changed, 2297 insertions, 0 deletions
diff --git a/www/wiki/extensions/Translate/api/ApiAggregateGroups.php b/www/wiki/extensions/Translate/api/ApiAggregateGroups.php
new file mode 100644
index 00000000..a8a259df
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiAggregateGroups.php
@@ -0,0 +1,238 @@
+<?php
+/**
+ * API module for managing aggregate message groups
+ * @file
+ * @author Santhosh Thottingal
+ * @author Niklas Laxström
+ * @copyright Copyright © 2012-2013, Santhosh Thottingal
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * API module for managing aggregate message groups
+ * Only supports aggregate message groups defined inside the wiki.
+ * Aggregate message group defined in YAML configuration cannot be altered.
+ *
+ * @ingroup API TranslateAPI
+ */
+class ApiAggregateGroups extends ApiBase {
+ protected static $right = 'translate-manage';
+
+ public function execute() {
+ $this->checkUserRightsAny( self::$right );
+
+ $params = $this->extractRequestParams();
+ $action = $params['do'];
+ $output = [];
+ if ( $action === 'associate' || $action === 'dissociate' ) {
+ // Group is mandatory only for these two actions
+ if ( !isset( $params['group'] ) ) {
+ $this->dieWithError( [ 'apierror-missingparam', 'group' ] );
+ }
+ if ( !isset( $params['aggregategroup'] ) ) {
+ $this->dieWithError( [ 'apierror-missingparam', 'aggregategroup' ] );
+ }
+ $aggregateGroup = $params['aggregategroup'];
+ $subgroups = TranslateMetadata::getSubgroups( $aggregateGroup );
+ if ( !$subgroups ) {
+ // For newly created groups the subgroups value might be empty,
+ // but check that.
+ if ( TranslateMetadata::get( $aggregateGroup, 'name' ) === false ) {
+ $this->dieWithError( 'apierror-translate-invalidaggregategroup', 'invalidaggregategroup' );
+ }
+ $subgroups = [];
+ }
+
+ $subgroupId = $params['group'];
+ $group = MessageGroups::getGroup( $subgroupId );
+
+ // Add or remove from the list
+ if ( $action === 'associate' ) {
+ if ( !$group instanceof WikiPageMessageGroup ) {
+ $this->dieWithError( 'apierror-translate-invalidgroup', 'invalidgroup' );
+ }
+
+ $subgroups[] = $subgroupId;
+ $subgroups = array_unique( $subgroups );
+ } elseif ( $action === 'dissociate' ) {
+ // Allow removal of non-existing groups
+ $subgroups = array_flip( $subgroups );
+ unset( $subgroups[$subgroupId] );
+ $subgroups = array_flip( $subgroups );
+ }
+
+ TranslateMetadata::setSubgroups( $aggregateGroup, $subgroups );
+
+ $logParams = [
+ 'aggregategroup' => TranslateMetadata::get( $aggregateGroup, 'name' ),
+ 'aggregategroup-id' => $aggregateGroup,
+ ];
+
+ /* Note that to allow removing no longer existing groups from
+ * aggregate message groups, the message group object $group
+ * might not always be available. In this case we need to fake
+ * some title. */
+ $title = $group ?
+ $group->getTitle() :
+ Title::newFromText( "Special:Translate/$subgroupId" );
+
+ $entry = new ManualLogEntry( 'pagetranslation', $action );
+ $entry->setPerformer( $this->getUser() );
+ $entry->setTarget( $title );
+ // @todo
+ // $entry->setComment( $comment );
+ $entry->setParameters( $logParams );
+
+ $logid = $entry->insert();
+ $entry->publish( $logid );
+ } elseif ( $action === 'remove' ) {
+ if ( !isset( $params['aggregategroup'] ) ) {
+ $this->dieWithError( [ 'apierror-missingparam', 'aggregategroup' ] );
+ }
+ TranslateMetadata::deleteGroup( $params['aggregategroup'] );
+ // @todo Logging
+
+ } elseif ( $action === 'add' ) {
+ if ( !isset( $params['groupname'] ) ) {
+ $this->dieWithError( [ 'apierror-missingparam', 'groupname' ] );
+ }
+ $name = trim( $params['groupname'] );
+ if ( strlen( $name ) === 0 ) {
+ $this->dieWithError(
+ 'apierror-translate-invalidaggregategroupname', 'invalidaggregategroupname'
+ );
+ }
+
+ if ( !isset( $params['groupdescription'] ) ) {
+ $this->dieWithError( [ 'apierror-missingparam', 'groupdescription' ] );
+ }
+ $desc = trim( $params['groupdescription'] );
+
+ $aggregateGroupId = self::generateAggregateGroupId( $name );
+
+ // Throw error if group already exists
+ $nameExists = MessageGroups::labelExists( $name );
+ if ( $nameExists ) {
+ $this->dieWithError( 'apierror-translate-duplicateaggregategroup', 'duplicateaggregategroup' );
+ }
+
+ // ID already exists- Generate a new ID by adding a number to it.
+ $idExists = MessageGroups::getGroup( $aggregateGroupId );
+ if ( $idExists ) {
+ $i = 1;
+ while ( $idExists ) {
+ $tempId = $aggregateGroupId . '-' . $i;
+ $idExists = MessageGroups::getGroup( $tempId );
+ $i++;
+ }
+ $aggregateGroupId = $tempId;
+ }
+
+ TranslateMetadata::set( $aggregateGroupId, 'name', $name );
+ TranslateMetadata::set( $aggregateGroupId, 'description', $desc );
+ TranslateMetadata::setSubgroups( $aggregateGroupId, [] );
+
+ // Once new aggregate group added, we need to show all the pages that can be added to that.
+ $output['groups'] = self::getAllPages();
+ $output['aggregategroupId'] = $aggregateGroupId;
+ // @todo Logging
+ } elseif ( $action === 'update' ) {
+ if ( !isset( $params['groupname'] ) ) {
+ $this->dieWithError( [ 'apierror-missingparam', 'groupname' ] );
+ }
+ $name = trim( $params['groupname'] );
+ if ( strlen( $name ) === 0 ) {
+ $this->dieWithError(
+ 'apierror-translate-invalidaggregategroupname', 'invalidaggregategroupname'
+ );
+ }
+ $desc = trim( $params['groupdescription'] );
+ $aggregateGroupId = $params['aggregategroup'];
+
+ $oldName = TranslateMetadata::get( $aggregateGroupId, 'name' );
+ $oldDesc = TranslateMetadata::get( $aggregateGroupId, 'description' );
+
+ // Error if the label exists already
+ $exists = MessageGroups::labelExists( $name );
+ if ( $exists && $oldName !== $name ) {
+ $this->dieWithError( 'apierror-translate-duplicateaggregategroup', 'duplicateaggregategroup' );
+ }
+
+ if ( $oldName === $name && $oldDesc === $desc ) {
+ $this->dieWithError( 'apierror-translate-invalidupdate', 'invalidupdate' );
+ }
+ TranslateMetadata::set( $aggregateGroupId, 'name', $name );
+ TranslateMetadata::set( $aggregateGroupId, 'description', $desc );
+ }
+
+ // If we got this far, nothing has failed
+ $output['result'] = 'ok';
+ $this->getResult()->addValue( null, $this->getModuleName(), $output );
+ // Cache needs to be cleared after any changes to groups
+ MessageGroups::singleton()->recache();
+ MessageIndexRebuildJob::newJob()->insertIntoJobQueue();
+ }
+
+ protected function generateAggregateGroupId( $aggregateGroupName, $prefix = 'agg-' ) {
+ // The database field has maximum limit of 200 bytes
+ if ( strlen( $aggregateGroupName ) + strlen( $prefix ) >= 200 ) {
+ return $prefix . substr( sha1( $aggregateGroupName ), 0, 5 );
+ } else {
+ $pattern = '/[\x00-\x1f\x23\x27\x2c\x2e\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/i';
+ return $prefix . preg_replace( $pattern, '_', $aggregateGroupName );
+ }
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function needsToken() {
+ return 'csrf';
+ }
+
+ public function getAllowedParams() {
+ return [
+ 'do' => [
+ ApiBase::PARAM_TYPE => [ 'associate', 'dissociate', 'remove', 'add', 'update' ],
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'aggregategroup' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ],
+ 'group' => [
+ // Not providing list of values, to allow dissociation of unknown groups
+ ApiBase::PARAM_TYPE => 'string',
+ ],
+ 'groupname' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ],
+ 'groupdescription' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ],
+ 'token' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ ];
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=aggregategroups&do=associate&group=groupId&aggregategroup=aggregateGroupId'
+ => 'apihelp-aggregategroups-example-1',
+ ];
+ }
+
+ public static function getAllPages() {
+ $groups = MessageGroups::getAllGroups();
+ $pages = [];
+ foreach ( $groups as $group ) {
+ if ( $group instanceof WikiPageMessageGroup ) {
+ $pages[$group->getId()] = $group->getTitle()->getPrefixedText();
+ }
+ }
+
+ return $pages;
+ }
+}
diff --git a/www/wiki/extensions/Translate/api/ApiGroupReview.php b/www/wiki/extensions/Translate/api/ApiGroupReview.php
new file mode 100644
index 00000000..67583854
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiGroupReview.php
@@ -0,0 +1,153 @@
+<?php
+/**
+ * API module for switching workflow states for message groups
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * API module for switching workflow states for message groups
+ *
+ * @ingroup API TranslateAPI
+ */
+class ApiGroupReview extends ApiBase {
+ protected static $right = 'translate-groupreview';
+
+ public function execute() {
+ $user = $this->getUser();
+ $requestParams = $this->extractRequestParams();
+
+ $group = MessageGroups::getGroup( $requestParams['group'] );
+ $code = $requestParams['language'];
+
+ if ( !$group || MessageGroups::isDynamic( $group ) ) {
+ $this->dieWithError( [ 'apierror-missingparam', 'group' ] );
+ }
+ $stateConfig = $group->getMessageGroupStates()->getStates();
+ if ( !$stateConfig ) {
+ $this->dieWithError( 'apierror-translate-groupreviewdisabled', 'disabled' );
+ }
+
+ $this->checkUserRightsAny( self::$right );
+
+ if ( $user->isBlocked() ) {
+ $this->dieBlocked( $user->getBlock() );
+ }
+
+ $requestParams = $this->extractRequestParams();
+
+ $languages = Language::fetchLanguageNames();
+ if ( !isset( $languages[$code] ) ) {
+ $this->dieWithError( [ 'apierror-missingparam', 'language' ] );
+ }
+
+ $targetState = $requestParams['state'];
+ if ( !isset( $stateConfig[$targetState] ) ) {
+ $this->dieWithError( 'apierror-translate-invalidstate', 'invalidstate' );
+ }
+
+ if ( is_array( $stateConfig[$targetState] )
+ && isset( $stateConfig[$targetState]['right'] )
+ ) {
+ $this->checkUserRightsAny( $stateConfig[$targetState]['right'] );
+ }
+
+ self::changeState( $group, $code, $targetState, $user );
+
+ $output = [ 'review' => [
+ 'group' => $group->getId(),
+ 'language' => $code,
+ 'state' => $targetState,
+ ] ];
+
+ $this->getResult()->addValue( null, $this->getModuleName(), $output );
+ }
+
+ public static function getState( MessageGroup $group, $code ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $table = 'translate_groupreviews';
+
+ $field = 'tgr_state';
+ $conds = [
+ 'tgr_group' => $group->getId(),
+ 'tgr_lang' => $code
+ ];
+
+ return $dbw->selectField( $table, $field, $conds, __METHOD__ );
+ }
+
+ public static function changeState( MessageGroup $group, $code, $newState, User $user ) {
+ $currentState = self::getState( $group, $code );
+ if ( $currentState === $newState ) {
+ return false;
+ }
+
+ $table = 'translate_groupreviews';
+ $index = [ 'tgr_group', 'tgr_language' ];
+ $row = [
+ 'tgr_group' => $group->getId(),
+ 'tgr_lang' => $code,
+ 'tgr_state' => $newState,
+ ];
+
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->replace( $table, [ $index ], $row, __METHOD__ );
+
+ $entry = new ManualLogEntry( 'translationreview', 'group' );
+ $entry->setPerformer( $user );
+ $entry->setTarget( SpecialPage::getTitleFor( 'Translate', $group->getId() ) );
+ // @todo
+ // $entry->setComment( $comment );
+ $entry->setParameters( [
+ '4::language' => $code,
+ '5::group-label' => $group->getLabel(),
+ '6::old-state' => $currentState,
+ '7::new-state' => $newState,
+ ] );
+
+ $logid = $entry->insert();
+ $entry->publish( $logid );
+
+ Hooks::run( 'TranslateEventMessageGroupStateChange',
+ [ $group, $code, $currentState, $newState ] );
+
+ return true;
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function needsToken() {
+ return 'csrf';
+ }
+
+ public function getAllowedParams() {
+ return [
+ 'group' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'language' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_DFLT => 'en',
+ ],
+ 'state' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'token' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ ];
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=groupreview&group=page-Example&language=de&state=ready&token=foo'
+ => 'apihelp-groupreview-example-1',
+ ];
+ }
+}
diff --git a/www/wiki/extensions/Translate/api/ApiQueryLanguageStats.php b/www/wiki/extensions/Translate/api/ApiQueryLanguageStats.php
new file mode 100644
index 00000000..a25cda21
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiQueryLanguageStats.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Api module for language group stats.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * Api module for querying language stats.
+ *
+ * @ingroup API TranslateAPI
+ * @since 2012-11-30
+ */
+class ApiQueryLanguageStats extends ApiStatsQuery {
+ public function __construct( $query, $moduleName ) {
+ parent::__construct( $query, $moduleName, 'ls' );
+ }
+
+ /// Overwritten from ApiStatsQuery
+ protected function validateTargetParamater( array $params ) {
+ $all = TranslateUtils::getLanguageNames( null );
+ $requested = $params[ 'language' ];
+
+ if ( !isset( $all[ $requested ] ) ) {
+ $this->dieWithError( [ 'apierror-translate-invalidlanguage' ] );
+ }
+
+ return $requested;
+ }
+
+ /// Overwritten from ApiStatsQuery
+ protected function loadStatistics( $target, $flags = 0 ) {
+ return MessageGroupStats::forLanguage( $target, $flags );
+ }
+
+ protected function makeItem( $item, $stats ) {
+ $data = parent::makeItem( $item, $stats );
+ $data['group'] = $item;
+
+ return $data;
+ }
+
+ public function getAllowedParams() {
+ $params = parent::getAllowedParams();
+ $params['language'] = [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ];
+
+ return $params;
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=query&meta=languagestats&lslanguage=fi'
+ => 'apihelp-query+languagestats-example-1',
+ ];
+ }
+}
diff --git a/www/wiki/extensions/Translate/api/ApiQueryMessageCollection.php b/www/wiki/extensions/Translate/api/ApiQueryMessageCollection.php
new file mode 100644
index 00000000..f9353d62
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiQueryMessageCollection.php
@@ -0,0 +1,279 @@
+<?php
+/**
+ * Api module for querying MessageCollection.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * Api module for querying MessageCollection.
+ *
+ * @ingroup API TranslateAPI
+ */
+class ApiQueryMessageCollection extends ApiQueryGeneratorBase {
+
+ public function __construct( $query, $moduleName ) {
+ parent::__construct( $query, $moduleName, 'mc' );
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function getCacheMode( $params ) {
+ return 'public';
+ }
+
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
+ }
+
+ private function validateLanguageCode( $code ) {
+ if ( !Language::isValidBuiltInCode( $code ) ) {
+ $this->dieWithError( [ 'apierror-translate-invalidlanguage' ] );
+ }
+ }
+
+ private function run( ApiPageSet $resultPageSet = null ) {
+ global $wgTranslateBlacklist;
+
+ $params = $this->extractRequestParams();
+
+ $group = MessageGroups::getGroup( $params['group'] );
+ if ( !$group ) {
+ $this->dieWithError( [ 'apierror-missingparam', 'mcgroup' ] );
+ }
+
+ $languageCode = $params[ 'language' ];
+ $this->validateLanguageCode( $languageCode );
+ if ( $group->getSourceLanguage() === $languageCode ) {
+ $name = Language::fetchLanguageName( $languageCode, $this->getLanguage()->getCode() );
+ $this->addWarning( [ 'apiwarn-translate-language-disabled-source', wfEscapeWikiText( $name ) ] );
+ }
+ $languages = $group->getTranslatableLanguages();
+ if ( $languages !== null ) {
+ if ( !isset( $languages[ $languageCode ] ) ) {
+ $name = Language::fetchLanguageName( $languageCode, $this->getLanguage()->getCode() );
+ $this->dieWithError( [ 'apierror-translate-language-disabled', $name ] );
+ }
+ } else {
+ $checks = [
+ $group->getId(),
+ strtok( $group->getId(), '-' ),
+ '*'
+ ];
+
+ foreach ( $checks as $check ) {
+ if ( isset( $wgTranslateBlacklist[ $check ][ $languageCode ] ) ) {
+ $name = Language::fetchLanguageName( $languageCode, $this->getLanguage()->getCode() );
+ $reason = $wgTranslateBlacklist[ $check ][ $languageCode ];
+ $this->dieWithError( [ 'apierror-translate-language-disabled-reason', $name, $reason ] );
+ }
+ }
+ }
+
+ if ( MessageGroups::isDynamic( $group ) ) {
+ /**
+ * @var RecentMessageGroup $group
+ */
+ $group->setLanguage( $params['language'] );
+ }
+
+ $messages = $group->initCollection( $params['language'] );
+
+ foreach ( $params['filter'] as $filter ) {
+ $value = null;
+ if ( strpos( $filter, ':' ) !== false ) {
+ list( $filter, $value ) = explode( ':', $filter, 2 );
+ }
+ /* The filtering params here are swapped wrt MessageCollection.
+ * There (fuzzy) means do not show fuzzy, which is the same as !fuzzy
+ * here and fuzzy here means (fuzzy, false) there. */
+ try {
+ if ( $filter[0] === '!' ) {
+ $messages->filter( substr( $filter, 1 ), true, $value );
+ } else {
+ $messages->filter( $filter, false, $value );
+ }
+ } catch ( MWException $e ) {
+ $this->dieWithError(
+ [ 'apierror-translate-invalidfilter', wfEscapeWikiText( $e->getMessage() ) ],
+ 'invalidfilter'
+ );
+ }
+ }
+
+ $resultSize = count( $messages );
+ $offsets = $messages->slice( $params['offset'], $params['limit'] );
+ $batchSize = count( $messages );
+ list( /*$backwardsOffset*/, $forwardsOffset, $startOffset ) = $offsets;
+
+ $result = $this->getResult();
+ $result->addValue(
+ [ 'query', 'metadata' ],
+ 'state',
+ self::getWorkflowState( $group->getId(), $params['language'] )
+ );
+
+ $result->addValue( [ 'query', 'metadata' ], 'resultsize', $resultSize );
+ $result->addValue(
+ [ 'query', 'metadata' ],
+ 'remaining',
+ $resultSize - $startOffset - $batchSize
+ );
+
+ $messages->loadTranslations();
+
+ $pages = [];
+
+ if ( $forwardsOffset !== false ) {
+ $this->setContinueEnumParameter( 'offset', $forwardsOffset );
+ }
+
+ $props = array_flip( $params['prop'] );
+
+ /** @var Title $title */
+ foreach ( $messages->keys() as $mkey => $title ) {
+ if ( is_null( $resultPageSet ) ) {
+ $data = $this->extractMessageData( $result, $props, $messages[$mkey] );
+ $data['title'] = $title->getPrefixedText();
+ $handle = new MessageHandle( $title );
+
+ if ( $handle->isValid() ) {
+ $data['primaryGroup'] = $handle->getGroup()->getId();
+ }
+
+ $result->addValue( [ 'query', $this->getModuleName() ], null, $data );
+ } else {
+ $pages[] = $title;
+ }
+ }
+
+ if ( is_null( $resultPageSet ) ) {
+ $result->addIndexedTagName(
+ [ 'query', $this->getModuleName() ],
+ 'message'
+ );
+ } else {
+ $resultPageSet->populateFromTitles( $pages );
+ }
+ }
+
+ /**
+ * @param ApiResult $result
+ * @param array $props
+ * @param ThinMessage $message
+ * @return array
+ */
+ public function extractMessageData( $result, $props, $message ) {
+ $data['key'] = $message->key();
+
+ if ( isset( $props['definition'] ) ) {
+ $data['definition'] = $message->definition();
+ }
+ if ( isset( $props['translation'] ) ) {
+ // Remove !!FUZZY!! from translation if present.
+ $translation = $message->translation();
+ if ( $translation !== null ) {
+ $translation = str_replace( TRANSLATE_FUZZY, '', $translation );
+ }
+ $data['translation'] = $translation;
+ }
+ if ( isset( $props['tags'] ) ) {
+ $data['tags'] = $message->getTags();
+ $result->setIndexedTagName( $data['tags'], 'tag' );
+ }
+ // BC
+ if ( isset( $props['revision'] ) ) {
+ $data['revision'] = $message->getProperty( 'revision' );
+ }
+ if ( isset( $props['properties'] ) ) {
+ foreach ( $message->getPropertyNames() as $prop ) {
+ $data['properties'][$prop] = $message->getProperty( $prop );
+ ApiResult::setIndexedTagNameRecursive( $data['properties'], 'val' );
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Get the current workflow state for the message group for the given language
+ *
+ * @param string $groupId Group id.
+ * @param string $language Language tag.
+ * @return string|bool State id or false.
+ */
+ protected static function getWorkflowState( $groupId, $language ) {
+ $dbr = wfGetDB( DB_REPLICA );
+
+ return $dbr->selectField(
+ 'translate_groupreviews',
+ 'tgr_state',
+ [
+ 'tgr_group' => $groupId,
+ 'tgr_lang' => $language
+ ],
+ __METHOD__
+ );
+ }
+
+ public function getAllowedParams() {
+ return [
+ 'group' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'language' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_DFLT => 'en',
+ ],
+ 'limit' => [
+ ApiBase::PARAM_DFLT => 500,
+ ApiBase::PARAM_TYPE => 'limit',
+ ApiBase::PARAM_MIN => 1,
+ ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG2,
+ ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2,
+ ],
+ 'offset' => [
+ ApiBase::PARAM_DFLT => '',
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
+ ],
+ 'filter' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_DFLT => '!optional|!ignored',
+ ApiBase::PARAM_ISMULTI => true,
+ ],
+ 'prop' => [
+ ApiBase::PARAM_TYPE => [
+ 'definition',
+ 'translation',
+ 'tags',
+ 'revision',
+ 'properties'
+ ],
+ ApiBase::PARAM_DFLT => 'definition|translation',
+ ApiBase::PARAM_ISMULTI => true,
+ ApiBase::PARAM_HELP_MSG =>
+ [ 'apihelp-query+messagecollection-param-prop', '!!FUZZY!!' ],
+ ],
+ ];
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=query&meta=siteinfo&siprop=languages'
+ => 'apihelp-query+messagecollection-example-1',
+ 'action=query&list=messagecollection&mcgroup=page-Example'
+ => 'apihelp-query+messagecollection-example-2',
+ 'action=query&list=messagecollection&mcgroup=page-Example&mclanguage=fi&' .
+ 'mcprop=definition|translation|tags&mcfilter=optional'
+ => 'apihelp-query+messagecollection-example-3',
+ 'action=query&generator=messagecollection&gmcgroup=page-Example&gmclanguage=nl&prop=revisions'
+ => 'apihelp-query+messagecollection-example-4',
+ ];
+ }
+}
diff --git a/www/wiki/extensions/Translate/api/ApiQueryMessageGroupStats.php b/www/wiki/extensions/Translate/api/ApiQueryMessageGroupStats.php
new file mode 100644
index 00000000..3f9ea6c4
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiQueryMessageGroupStats.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * Api module for querying message group stats.
+ *
+ * @file
+ * @author Tim Gerundt
+ * @author Niklas Laxström
+ * @copyright Copyright © 2012-2013, Tim Gerundt
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * Api module for querying message group stats.
+ *
+ * @ingroup API TranslateAPI
+ */
+class ApiQueryMessageGroupStats extends ApiStatsQuery {
+ public function __construct( $query, $moduleName ) {
+ parent::__construct( $query, $moduleName, 'mgs' );
+ }
+
+ /// Overwritten from ApiStatsQuery
+ protected function validateTargetParamater( array $params ) {
+ $group = MessageGroups::getGroup( $params['group'] );
+ if ( !$group ) {
+ $this->dieWithError( [ 'apierror-missingparam', 'mgsgroup' ] );
+ } elseif ( MessageGroups::isDynamic( $group ) ) {
+ $this->dieWithError( 'apierror-translate-nodynamicgroups', 'invalidparam' );
+ }
+
+ return $group->getId();
+ }
+
+ /// Overwritten from ApiStatsQuery
+ protected function loadStatistics( $target, $flags = 0 ) {
+ return MessageGroupStats::forGroup( $target, $flags );
+ }
+
+ protected function makeItem( $item, $stats ) {
+ $data = parent::makeItem( $item, $stats );
+ $data['code'] = $item; // For BC
+ $data['language'] = $item;
+
+ return $data;
+ }
+
+ public function getAllowedParams() {
+ $params = parent::getAllowedParams();
+ $params['group'] = [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ];
+
+ return $params;
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=query&meta=messagegroupstats&mgsgroup=page-Example'
+ => 'apihelp-query+messagegroupstats-example-1',
+ ];
+ }
+}
diff --git a/www/wiki/extensions/Translate/api/ApiQueryMessageGroups.php b/www/wiki/extensions/Translate/api/ApiQueryMessageGroups.php
new file mode 100644
index 00000000..6996bb05
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiQueryMessageGroups.php
@@ -0,0 +1,322 @@
+<?php
+/**
+ * Api module for querying MessageGroups.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @author Harry Burt
+ * @copyright Copyright © 2012-2013, Harry Burt
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * Api module for querying MessageGroups.
+ *
+ * @ingroup API TranslateAPI
+ */
+class ApiQueryMessageGroups extends ApiQueryBase {
+
+ public function __construct( $query, $moduleName ) {
+ parent::__construct( $query, $moduleName, 'mg' );
+ }
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+ $filter = $params['filter'];
+
+ $groups = [];
+
+ // Parameter root as all for all pages subgroups
+ if ( $params['root'] === 'all' ) {
+ $allGroups = MessageGroups::getAllGroups();
+ foreach ( $allGroups as $id => $group ) {
+ if ( $group instanceof WikiPageMessageGroup ) {
+ $groups[$id] = $group;
+ }
+ }
+ TranslateMetadata::preloadGroups( array_keys( $groups ) );
+ } elseif ( $params['format'] === 'flat' ) {
+ if ( $params['root'] !== '' ) {
+ $group = MessageGroups::getGroup( $params['root'] );
+ if ( $group ) {
+ $groups[$params['root']] = $group;
+ }
+ } else {
+ $groups = MessageGroups::getAllGroups();
+ // Not sorted by default, so do it now
+ // Work around php bug: https://bugs.php.net/bug.php?id=50688
+ Wikimedia\suppressWarnings();
+ usort( $groups, [ 'MessageGroups', 'groupLabelSort' ] );
+ Wikimedia\restoreWarnings();
+ }
+ TranslateMetadata::preloadGroups( array_keys( $groups ) );
+ } elseif ( $params['root'] !== '' ) {
+ // format=tree from now on, as it is the only other valid option
+ $group = MessageGroups::getGroup( $params['root'] );
+ if ( $group instanceof AggregateMessageGroup ) {
+ $childIds = [];
+ $groups = MessageGroups::subGroups( $group, $childIds );
+ // The parent group is the first, ignore it
+ array_shift( $groups );
+ TranslateMetadata::preloadGroups( $childIds );
+ }
+ } else {
+ $groups = MessageGroups::getGroupStructure();
+ TranslateMetadata::preloadGroups( array_keys( MessageGroups::getAllGroups() ) );
+ }
+
+ if ( $params['root'] === '' ) {
+ $dynamicGroups = [];
+ foreach ( array_keys( MessageGroups::getDynamicGroups() ) as $id ) {
+ $dynamicGroups[$id] = MessageGroups::getGroup( $id );
+ }
+ // Have dynamic groups appear first in the list
+ $groups = $dynamicGroups + $groups;
+ }
+
+ // Do not list the sandbox group. The code that knows it
+ // exists can access it directly.
+ if ( isset( $groups['!sandbox'] ) ) {
+ unset( $groups['!sandbox'] );
+ }
+
+ $props = array_flip( $params['prop'] );
+
+ $result = $this->getResult();
+ $matcher = new StringMatcher( '', $filter );
+ /**
+ * @var MessageGroup $mixed
+ */
+ foreach ( $groups as $mixed ) {
+ if ( $filter !== [] && !$matcher->match( $mixed->getId() ) ) {
+ continue;
+ }
+
+ $a = $this->formatGroup( $mixed, $props );
+
+ $result->setIndexedTagName( $a, 'group' );
+
+ // @todo Add a continue?
+ $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $a );
+ if ( !$fit ) {
+ // Even if we're not going to give a continue, no point carrying on
+ // if the result is full
+ break;
+ }
+ }
+
+ $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'group' );
+ }
+
+ /**
+ * @param array|MessageGroup $mixed
+ * @param array $props List of props as the array keys
+ * @param int $depth
+ * @return array
+ */
+ protected function formatGroup( $mixed, $props, $depth = 0 ) {
+ $params = $this->extractRequestParams();
+ $context = $this->getContext();
+
+ // Default
+ $g = $mixed;
+ $subgroups = [];
+
+ // Format = tree and has subgroups
+ if ( is_array( $mixed ) ) {
+ $g = array_shift( $mixed );
+ $subgroups = $mixed;
+ }
+
+ $a = [];
+
+ $groupId = $g->getId();
+
+ if ( isset( $props['id'] ) ) {
+ $a['id'] = $groupId;
+ }
+
+ if ( isset( $props['label'] ) ) {
+ $a['label'] = $g->getLabel( $context );
+ }
+
+ if ( isset( $props['description'] ) ) {
+ $a['description'] = $g->getDescription( $context );
+ }
+
+ if ( isset( $props['class'] ) ) {
+ $a['class'] = get_class( $g );
+ }
+
+ if ( isset( $props['namespace'] ) ) {
+ $a['namespace'] = $g->getNamespace();
+ }
+
+ if ( isset( $props['exists'] ) ) {
+ $a['exists'] = $g->exists();
+ }
+
+ if ( isset( $props['icon'] ) ) {
+ $formats = TranslateUtils::getIcon( $g, $params['iconsize'] );
+ if ( $formats ) {
+ $a['icon'] = $formats;
+ }
+ }
+
+ if ( isset( $props['priority'] ) ) {
+ $priority = MessageGroups::getPriority( $g );
+ $a['priority'] = $priority ?: 'default';
+ }
+
+ if ( isset( $props['prioritylangs'] ) ) {
+ $prioritylangs = TranslateMetadata::get( $groupId, 'prioritylangs' );
+ $a['prioritylangs'] = $prioritylangs ? explode( ',', $prioritylangs ) : false;
+ }
+
+ if ( isset( $props['priorityforce'] ) ) {
+ $a['priorityforce'] = ( TranslateMetadata::get( $groupId, 'priorityforce' ) === 'on' );
+ }
+
+ if ( isset( $props['workflowstates'] ) ) {
+ $a['workflowstates'] = $this->getWorkflowStates( $g );
+ }
+
+ Hooks::run(
+ 'TranslateProcessAPIMessageGroupsProperties',
+ [ &$a, $props, $params, $g ]
+ );
+
+ // Depth only applies to tree format
+ if ( $depth >= $params['depth'] && $params['format'] === 'tree' ) {
+ $a['groupcount'] = count( $subgroups );
+
+ // Prevent going further down in the three
+ return $a;
+ }
+
+ // Always empty array for flat format, only sometimes for tree format
+ if ( $subgroups !== [] ) {
+ foreach ( $subgroups as $sg ) {
+ $a['groups'][] = $this->formatGroup( $sg, $props );
+ }
+ $result = $this->getResult();
+ $result->setIndexedTagName( $a['groups'], 'group' );
+ }
+
+ return $a;
+ }
+
+ /**
+ * Get the workflow states applicable to the given message group
+ *
+ * @param MessageGroup $group
+ * @return bool|array Associative array with states as key and localized state
+ * labels as values
+ */
+ protected function getWorkflowStates( MessageGroup $group ) {
+ if ( MessageGroups::isDynamic( $group ) ) {
+ return false;
+ }
+
+ $stateConfig = $group->getMessageGroupStates()->getStates();
+
+ if ( !is_array( $stateConfig ) || $stateConfig === [] ) {
+ return false;
+ }
+
+ $user = $this->getUser();
+
+ foreach ( $stateConfig as $state => $config ) {
+ if ( is_array( $config ) ) {
+ // Check if user is allowed to change states generally
+ $allowed = $user->isAllowed( 'translate-groupreview' );
+ // Check further restrictions
+ if ( $allowed && isset( $config['right'] ) ) {
+ $allowed = $user->isAllowed( $config['right'] );
+ }
+
+ if ( $allowed ) {
+ $stateConfig[$state]['canchange'] = 1;
+ }
+
+ $stateConfig[$state]['name'] =
+ $this->msg( "translate-workflow-state-$state" )->text();
+ }
+ }
+
+ return $stateConfig;
+ }
+
+ public function getAllowedParams() {
+ $allowedParams = [
+ 'depth' => [
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_DFLT => 100,
+ ],
+ 'filter' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_DFLT => '',
+ ApiBase::PARAM_ISMULTI => true,
+ ],
+ 'format' => [
+ ApiBase::PARAM_TYPE => [ 'flat', 'tree' ],
+ ApiBase::PARAM_DFLT => 'flat',
+ ],
+ 'iconsize' => [
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_DFLT => 64,
+ ],
+ 'prop' => [
+ ApiBase::PARAM_TYPE => array_keys( self::getPropertyList() ),
+ ApiBase::PARAM_DFLT => 'id|label|description|class|exists',
+ ApiBase::PARAM_ISMULTI => true,
+ ],
+ 'root' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_DFLT => '',
+ ],
+ ];
+ Hooks::run( 'TranslateGetAPIMessageGroupsParameterList', [ &$allowedParams ] );
+
+ return $allowedParams;
+ }
+
+ /**
+ * Returns array of key value pairs of properties and their descriptions
+ *
+ * @return array
+ */
+ protected static function getPropertyList() {
+ $properties = [
+ 'id' => ' id - Include id of the group',
+ 'label' => ' label - Include label of the group',
+ 'description' => ' description - Include description of the group',
+ 'class' => ' class - Include class name of the group',
+ 'namespace' =>
+ ' namespace - Include namespace of the group. Not all groups belong ' .
+ 'to a single namespace.',
+ 'exists' =>
+ ' exists - Include self-calculated existence property of the group',
+ 'icon' => ' icon - Include urls to icon of the group',
+ 'priority' => ' priority - Include priority status like discouraged',
+ 'prioritylangs' =>
+ ' prioritylangs - Include preferred languages. If not set, this returns false',
+ 'priorityforce' =>
+ ' priorityforce - Include priority status - is the priority languages ' .
+ 'setting forced',
+ 'workflowstates' =>
+ ' workflowstates - Include the workflow states for the message group',
+ ];
+
+ Hooks::run( 'TranslateGetAPIMessageGroupsPropertyDescs', [ &$properties ] );
+
+ return $properties;
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=query&meta=messagegroups'
+ => 'apihelp-query+messagegroups-example-1',
+ ];
+ }
+}
diff --git a/www/wiki/extensions/Translate/api/ApiQueryMessageTranslations.php b/www/wiki/extensions/Translate/api/ApiQueryMessageTranslations.php
new file mode 100644
index 00000000..13eed8b6
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiQueryMessageTranslations.php
@@ -0,0 +1,135 @@
+<?php
+/**
+ * Api module for querying message translations.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * Api module for querying message translations.
+ *
+ * @ingroup API TranslateAPI
+ */
+class ApiQueryMessageTranslations extends ApiQueryBase {
+ public function __construct( $query, $moduleName ) {
+ parent::__construct( $query, $moduleName, 'mt' );
+ }
+
+ public function getCacheMode( $params ) {
+ return 'public';
+ }
+
+ /**
+ * Returns all translations of a given message.
+ * @param MessageHandle $handle Language code is ignored.
+ * @return array[]
+ * @since 2012-12-18
+ */
+ public static function getTranslations( MessageHandle $handle ) {
+ $namespace = $handle->getTitle()->getNamespace();
+ $base = $handle->getKey();
+
+ $dbr = wfGetDB( DB_REPLICA );
+
+ $res = $dbr->select( 'page',
+ [ 'page_namespace', 'page_title' ],
+ [
+ 'page_namespace' => $namespace,
+ 'page_title ' . $dbr->buildLike( "$base/", $dbr->anyString() ),
+ ],
+ __METHOD__,
+ [
+ 'ORDER BY' => 'page_title',
+ 'USE INDEX' => 'name_title',
+ ]
+ );
+
+ $titles = [];
+ foreach ( $res as $row ) {
+ $titles[] = $row->page_title;
+ }
+
+ if ( $titles === [] ) {
+ return [];
+ }
+
+ $pageInfo = TranslateUtils::getContents( $titles, $namespace );
+
+ return $pageInfo;
+ }
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ $title = Title::newFromText( $params['title'] );
+ if ( !$title ) {
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] );
+ }
+
+ $handle = new MessageHandle( $title );
+ if ( !$handle->isValid() ) {
+ $this->dieWithError( 'apierror-translate-nomessagefortitle', 'nomessagefortitle' );
+ }
+
+ $namespace = $title->getNamespace();
+ $pageInfo = self::getTranslations( $handle );
+
+ $result = $this->getResult();
+ $count = 0;
+
+ foreach ( $pageInfo as $key => $info ) {
+ if ( ++$count <= $params['offset'] ) {
+ continue;
+ }
+
+ $tTitle = Title::makeTitle( $namespace, $key );
+ $tHandle = new MessageHandle( $tTitle );
+
+ $data = [
+ 'title' => $tTitle->getPrefixedText(),
+ 'language' => $tHandle->getCode(),
+ 'lasttranslator' => $info[1],
+ ];
+
+ $fuzzy = MessageHandle::hasFuzzyString( $info[0] ) || $tHandle->isFuzzy();
+
+ if ( $fuzzy ) {
+ $data['fuzzy'] = 'fuzzy';
+ }
+
+ $translation = str_replace( TRANSLATE_FUZZY, '', $info[0] );
+ ApiResult::setContentValue( $data, 'translation', $translation );
+
+ $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $data );
+ if ( !$fit ) {
+ $this->setContinueEnumParameter( 'offset', $count );
+ break;
+ }
+ }
+
+ $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'message' );
+ }
+
+ public function getAllowedParams() {
+ return [
+ 'title' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'offset' => [
+ ApiBase::PARAM_DFLT => 0,
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
+ ],
+ ];
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=query&meta=messagetranslations&mttitle=MediaWiki:January'
+ => 'apihelp-query+messagetranslations-example-1',
+ ];
+ }
+}
diff --git a/www/wiki/extensions/Translate/api/ApiQueryTranslationAids.php b/www/wiki/extensions/Translate/api/ApiQueryTranslationAids.php
new file mode 100644
index 00000000..67560df1
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiQueryTranslationAids.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * Api module for querying message aids.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * Api module for querying message aids.
+ *
+ * @ingroup API TranslateAPI
+ */
+class ApiTranslationAids extends ApiBase {
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ $title = Title::newFromText( $params['title'] );
+ if ( !$title ) {
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] );
+ }
+
+ $handle = new MessageHandle( $title );
+ if ( !$handle->isValid() ) {
+ $this->dieWithError( 'apierror-translate-nomessagefortitle', 'nomessagefortitle' );
+ }
+
+ if ( (string)$params['group'] !== '' ) {
+ $group = MessageGroups::getGroup( $params['group'] );
+ } else {
+ $group = $handle->getGroup();
+ }
+
+ if ( !$group ) {
+ $this->dieWithError( 'apierror-translate-invalidgroup', 'invalidgroup' );
+ }
+
+ $data = [];
+ $times = [];
+
+ $props = $params['prop'];
+ $aggregator = new QueryAggregator();
+
+ // Figure out the intersection of supported and requested aids
+ $types = $group->getTranslationAids();
+ $props = array_intersect( $props, array_keys( $types ) );
+
+ $result = $this->getResult();
+
+ // Create list of aids, populate web services queries
+ $aids = [];
+
+ $dataProvider = new TranslationAidDataProvider( $handle );
+ foreach ( $props as $type ) {
+ // Do not proceed if translation aid is not supported for this message group
+ if ( !isset( $types[$type] ) ) {
+ $types[$type] = 'UnsupportedTranslationAid';
+ }
+
+ $class = $types[$type];
+ $obj = new $class( $group, $handle, $this, $dataProvider );
+
+ if ( $obj instanceof QueryAggregatorAware ) {
+ $obj->setQueryAggregator( $aggregator );
+ try {
+ $obj->populateQueries();
+ } catch ( TranslationHelperException $e ) {
+ $data[$type] = [ 'error' => $e->getMessage() ];
+ // Prevent processing this aids and thus overwriting our error
+ continue;
+ }
+ }
+
+ $aids[$type] = $obj;
+ }
+
+ // Execute all web service queries asynchronously to save time
+ $start = microtime( true );
+ $aggregator->run();
+ $times['query_aggregator'] = round( microtime( true ) - $start, 3 );
+
+ // Construct the result data structure
+ foreach ( $aids as $type => $obj ) {
+ $start = microtime( true );
+
+ try {
+ $aid = $obj->getData();
+ } catch ( TranslationHelperException $e ) {
+ $aid = [ 'error' => $e->getMessage() ];
+ }
+
+ if ( isset( $aid['**'] ) ) {
+ $result->setIndexedTagName( $aid, $aid['**'] );
+ unset( $aid['**'] );
+ }
+
+ $data[$type] = $aid;
+ $times[$type] = round( microtime( true ) - $start, 3 );
+ }
+
+ $result->addValue( null, 'helpers', $data );
+ $result->addValue( null, 'times', $times );
+ }
+
+ public function getAllowedParams() {
+ $props = array_keys( TranslationAid::getTypes() );
+ Hooks::run( 'TranslateTranslationAids', [ &$props ] );
+
+ return [
+ 'title' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'group' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ],
+ 'prop' => [
+ ApiBase::PARAM_DFLT => implode( '|', $props ),
+ ApiBase::PARAM_TYPE => $props,
+ ApiBase::PARAM_ISMULTI => true,
+ ],
+ ];
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=translationaids&title=MediaWiki:January/fi'
+ => 'apihelp-translationaids-example-1',
+ ];
+ }
+}
diff --git a/www/wiki/extensions/Translate/api/ApiSearchTranslations.php b/www/wiki/extensions/Translate/api/ApiSearchTranslations.php
new file mode 100644
index 00000000..d2787a0c
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiSearchTranslations.php
@@ -0,0 +1,131 @@
+<?php
+/**
+ * API module for search translations
+ * @since 2015.07
+ * @license GPL-2.0-or-later
+ */
+class ApiSearchTranslations extends ApiBase {
+ public function execute() {
+ global $wgTranslateTranslationServices;
+
+ if ( !$this->getAvailableTranslationServices() ) {
+ $this->dieWithError( 'apierror-translate-notranslationservices' );
+ }
+
+ $params = $this->extractRequestParams();
+
+ $config = $wgTranslateTranslationServices[$params['service']];
+ /** @var SearchableTTMServer $server */
+ $server = TTMServer::factory( $config );
+
+ $result = $this->getResult();
+
+ if ( $params['filter'] !== '' ) {
+ $translationSearch = new CrossLanguageTranslationSearchQuery( $params, $server );
+ $documents = $translationSearch->getDocuments();
+ $total = $translationSearch->getTotalHits();
+ } else {
+ $searchResults = $server->search(
+ $params['query'],
+ $params,
+ [ '', '' ]
+ );
+ $documents = $server->getDocuments( $searchResults );
+ $total = $server->getTotalHits( $searchResults );
+ }
+ $result->addValue( [ 'search', 'metadata' ], 'total', $total );
+ $result->addValue( 'search', 'translations', $documents );
+ }
+
+ protected function getAvailableTranslationServices() {
+ global $wgTranslateTranslationServices;
+
+ $good = [];
+ foreach ( $wgTranslateTranslationServices as $id => $config ) {
+ if ( TTMServer::factory( $config ) instanceof SearchableTTMServer ) {
+ $good[] = $id;
+ }
+ }
+
+ return $good;
+ }
+
+ protected function getAllowedFilters() {
+ return [
+ '',
+ 'translated',
+ 'fuzzy',
+ 'untranslated'
+ ];
+ }
+
+ public function getAllowedParams() {
+ global $wgLanguageCode,
+ $wgTranslateTranslationDefaultService;
+ $available = $this->getAvailableTranslationServices();
+
+ $filters = $this->getAllowedFilters();
+
+ $ret = [
+ 'service' => [
+ ApiBase::PARAM_TYPE => $available,
+ ],
+ 'query' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'sourcelanguage' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_DFLT => $wgLanguageCode,
+ ],
+ 'language' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_DFLT => '',
+ ],
+ 'group' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_DFLT => '',
+ ],
+ 'filter' => [
+ ApiBase::PARAM_TYPE => $filters,
+ ApiBase::PARAM_DFLT => '',
+ ],
+ 'match' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_DFLT => '',
+ ],
+ 'case' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_DFLT => '0',
+ ],
+ 'offset' => [
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_DFLT => 0,
+ ],
+ 'limit' => [
+ ApiBase::PARAM_DFLT => 25,
+ ApiBase::PARAM_TYPE => 'limit',
+ ApiBase::PARAM_MIN => 1,
+ ApiBase::PARAM_MAX => ApiBase::LIMIT_SML1,
+ ApiBase::PARAM_MAX2 => ApiBase::LIMIT_SML2
+ ],
+ ];
+
+ if ( $available ) {
+ // Don't add this if no services are available, it makes
+ // ApiStructureTest unhappy
+ $ret['service'][ApiBase::PARAM_DFLT] = $wgTranslateTranslationDefaultService;
+ }
+
+ return $ret;
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=searchtranslations&language=fr&query=aide'
+ => 'apihelp-searchtranslations-example-1',
+ 'action=searchtranslations&language=fr&query=edit&filter=untranslated'
+ => 'apihelp-searchtranslations-example-2',
+ ];
+ }
+}
diff --git a/www/wiki/extensions/Translate/api/ApiStatsQuery.php b/www/wiki/extensions/Translate/api/ApiStatsQuery.php
new file mode 100644
index 00000000..51244e0e
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiStatsQuery.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * A base module for querying message group related stats.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * A base module for querying message group related stats.
+ *
+ * @ingroup API TranslateAPI
+ * @since 2012-11-30
+ */
+abstract class ApiStatsQuery extends ApiQueryBase {
+ public function getCacheMode( $params ) {
+ return 'public';
+ }
+
+ /**
+ * Implement this to implement input validation and return the name of the target that
+ * is then given to loadStats.
+ * @param array $params
+ * @return string
+ */
+ abstract protected function validateTargetParamater( array $params );
+
+ /**
+ * Implement this to load stats.
+ * @param string $target
+ * @param int $flags See MessageGroupStats for possible flags
+ * @return array[]
+ */
+ abstract protected function loadStatistics( $target, $flags = 0 );
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ $target = $this->validateTargetParamater( $params );
+ $cache = $this->loadStatistics( $target, MessageGroupStats::FLAG_CACHE_ONLY );
+
+ $result = $this->getResult();
+ $incomplete = false;
+
+ foreach ( $cache as $item => $stats ) {
+ if ( $item < $params['offset'] ) {
+ continue;
+ }
+
+ if ( $stats[MessageGroupStats::TOTAL] === null ) {
+ $incomplete = true;
+ $this->setContinueEnumParameter( 'offset', $item );
+ break;
+ }
+
+ $data = $this->makeItem( $item, $stats );
+ $result->addValue( [ 'query', $this->getModuleName() ], null, $data );
+ }
+
+ $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'stats' );
+
+ if ( $incomplete ) {
+ DeferredUpdates::addCallableUpdate( function () use ( $target ) {
+ $this->loadStatistics( $target );
+ } );
+ }
+ }
+
+ protected function makeItem( $item, $stats ) {
+ return [
+ 'total' => $stats[MessageGroupStats::TOTAL],
+ 'translated' => $stats[MessageGroupStats::TRANSLATED],
+ 'fuzzy' => $stats[MessageGroupStats::FUZZY],
+ 'proofread' => $stats[MessageGroupStats::PROOFREAD],
+ ];
+ }
+
+ public function getAllowedParams() {
+ return [
+ 'offset' => [
+ ApiBase::PARAM_DFLT => '0',
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
+ ],
+ 'timelimit' => [
+ ApiBase::PARAM_DFLT => 8,
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_MAX => 10,
+ ApiBase::PARAM_MIN => 0,
+ ApiBase::PARAM_DEPRECATED => true, // Since 2018.10
+ ],
+ ];
+ }
+}
diff --git a/www/wiki/extensions/Translate/api/ApiTTMServer.php b/www/wiki/extensions/Translate/api/ApiTTMServer.php
new file mode 100644
index 00000000..bedc0270
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiTTMServer.php
@@ -0,0 +1,96 @@
+<?php
+/**
+ * API module for TTMServer
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * API module for TTMServer
+ *
+ * @ingroup API TranslateAPI TTMServer
+ * @since 2012-01-26
+ */
+class ApiTTMServer extends ApiBase {
+
+ public function execute() {
+ global $wgTranslateTranslationServices;
+
+ if ( !$this->getAvailableTranslationServices() ) {
+ $this->dieWithError( 'apierror-translate-notranslationservices' );
+ }
+
+ $params = $this->extractRequestParams();
+
+ $config = $wgTranslateTranslationServices[$params['service']];
+ $server = TTMServer::factory( $config );
+
+ $suggestions = $server->query(
+ $params['sourcelanguage'],
+ $params['targetlanguage'],
+ $params['text']
+ );
+
+ $result = $this->getResult();
+ foreach ( $suggestions as $sug ) {
+ $sug['location'] = $server->expandLocation( $sug );
+ unset( $sug['wiki'] );
+ $result->addValue( $this->getModuleName(), null, $sug );
+ }
+
+ $result->addIndexedTagName( $this->getModuleName(), 'suggestion' );
+ }
+
+ protected function getAvailableTranslationServices() {
+ global $wgTranslateTranslationServices;
+
+ $good = [];
+ foreach ( $wgTranslateTranslationServices as $id => $config ) {
+ if ( isset( $config['public'] ) && $config['public'] === true ) {
+ $good[] = $id;
+ }
+ }
+
+ return $good;
+ }
+
+ public function getAllowedParams() {
+ global $wgTranslateTranslationDefaultService;
+ $available = $this->getAvailableTranslationServices();
+
+ $ret = [
+ 'service' => [
+ ApiBase::PARAM_TYPE => $available,
+ ],
+ 'sourcelanguage' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'targetlanguage' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'text' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ ];
+
+ if ( $available ) {
+ // Don't add this if no services are available, it makes
+ // ApiStructureTest unhappy
+ $ret['service'][ApiBase::PARAM_DFLT] = $wgTranslateTranslationDefaultService;
+ }
+
+ return $ret;
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=ttmserver&sourcelanguage=en&targetlanguage=fi&text=Help'
+ => 'apihelp-ttmserver-example-1',
+ ];
+ }
+}
diff --git a/www/wiki/extensions/Translate/api/ApiTranslateSandbox.php b/www/wiki/extensions/Translate/api/ApiTranslateSandbox.php
new file mode 100644
index 00000000..3aed7170
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiTranslateSandbox.php
@@ -0,0 +1,213 @@
+<?php
+/**
+ * WebAPI for the sandbox feature of Translate.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * WebAPI for the sandbox feature of Translate.
+ * @ingroup API TranslateAPI
+ */
+class ApiTranslateSandbox extends ApiBase {
+ public function execute() {
+ global $wgTranslateUseSandbox;
+ if ( !$wgTranslateUseSandbox ) {
+ $this->dieWithError( 'apierror-translate-sandboxdisabled', 'sandboxdisabled' );
+ }
+
+ $params = $this->extractRequestParams();
+ switch ( $params['do'] ) {
+ case 'create':
+ $this->doCreate();
+ break;
+ case 'delete':
+ $this->doDelete();
+ break;
+ case 'promote':
+ $this->doPromote();
+ break;
+ case 'remind':
+ $this->doRemind();
+ break;
+ }
+ }
+
+ protected function doCreate() {
+ $params = $this->extractRequestParams();
+
+ // Do validations
+ foreach ( explode( '|', 'username|password|email' ) as $field ) {
+ if ( !isset( $params[$field] ) ) {
+ $this->dieWithError( [ 'apierror-missingparam', $field ], 'missingparam' );
+ }
+ }
+
+ $username = $params['username'];
+ if ( User::getCanonicalName( $username, 'creatable' ) === false ) {
+ $this->dieWithError( 'noname', 'invalidusername' );
+ }
+
+ $user = User::newFromName( $username );
+ if ( $user->getId() !== 0 ) {
+ $this->dieWithError( 'userexists', 'nonfreeusername' );
+ }
+
+ $password = $params['password'];
+ if ( !$user->isValidPassword( $password ) ) {
+ $this->dieWithError( 'apierror-translate-sandbox-invalidpassword', 'invalidpassword' );
+ }
+
+ $email = $params['email'];
+ if ( !Sanitizer::validateEmail( $email ) ) {
+ $this->dieWithError( 'invalidemailaddress', 'invalidemail' );
+ }
+
+ $user = TranslateSandbox::addUser( $username, $email, $password );
+ $output = [ 'user' => [
+ 'name' => $user->getName(),
+ 'id' => $user->getId(),
+ ] ];
+
+ $user->setOption( 'language', $this->getContext()->getLanguage()->getCode() );
+ $user->saveSettings();
+
+ $this->getResult()->addValue( null, $this->getModuleName(), $output );
+ }
+
+ protected function doDelete() {
+ $this->checkUserRightsAny( 'translate-sandboxmanage' );
+
+ $params = $this->extractRequestParams();
+
+ foreach ( $params['userid'] as $user ) {
+ $user = User::newFromId( $user );
+ $userpage = $user->getUserPage();
+
+ TranslateSandbox::sendEmail( $this->getUser(), $user, 'rejection' );
+
+ try {
+ TranslateSandbox::deleteUser( $user );
+ } catch ( MWException $e ) {
+ $this->dieWithError(
+ [ 'apierror-translate-sandbox-invalidparam', wfEscapeWikiText( $e->getMessage() ) ],
+ 'invalidparam'
+ );
+ }
+
+ $logEntry = new ManualLogEntry( 'translatorsandbox', 'rejected' );
+ $logEntry->setPerformer( $this->getUser() );
+ $logEntry->setTarget( $userpage );
+ $logid = $logEntry->insert();
+ $logEntry->publish( $logid );
+ }
+ }
+
+ protected function doPromote() {
+ $this->checkUserRightsAny( 'translate-sandboxmanage' );
+
+ $params = $this->extractRequestParams();
+
+ foreach ( $params['userid'] as $user ) {
+ $user = User::newFromId( $user );
+
+ try {
+ TranslateSandbox::promoteUser( $user );
+ } catch ( MWException $e ) {
+ $this->dieWithError(
+ [ 'apierror-translate-sandbox-invalidparam', wfEscapeWikiText( $e->getMessage() ) ],
+ 'invalidparam'
+ );
+ }
+
+ TranslateSandbox::sendEmail( $this->getUser(), $user, 'promotion' );
+
+ $logEntry = new ManualLogEntry( 'translatorsandbox', 'promoted' );
+ $logEntry->setPerformer( $this->getUser() );
+ $logEntry->setTarget( $user->getUserPage() );
+ $logEntry->setParameters( [
+ '4::userid' => $user->getId(),
+ ] );
+ $logid = $logEntry->insert();
+ $logEntry->publish( $logid );
+
+ $this->createUserPage( $user );
+ }
+ }
+
+ protected function doRemind() {
+ $params = $this->extractRequestParams();
+
+ foreach ( $params['userid'] as $user ) {
+ $user = User::newFromId( $user );
+
+ try {
+ TranslateSandbox::sendEmail( $this->getUser(), $user, 'reminder' );
+ } catch ( MWException $e ) {
+ $this->dieWithError(
+ [ 'apierror-translate-sandbox-invalidparam', wfEscapeWikiText( $e->getMessage() ) ],
+ 'invalidparam'
+ );
+ }
+ }
+ }
+
+ /**
+ * Create a user page for a user with a babel template based on the signup
+ * preferences.
+ *
+ * @param User $user
+ * @return Status|bool False when a user page already existed, or the Status
+ * of the user page creation from WikiPage::doEditContent().
+ */
+ protected function createUserPage( User $user ) {
+ $userpage = $user->getUserPage();
+
+ if ( $userpage->exists() ) {
+ return false;
+ }
+
+ $languagePrefs = FormatJson::decode( $user->getOption( 'translate-sandbox' ) );
+ $languages = implode( '|', $languagePrefs->languages );
+ $babeltext = "{{#babel:$languages}}";
+ $summary = $this->msg( 'tsb-create-user-page' )->inContentLanguage()->text();
+
+ $page = WikiPage::factory( $userpage );
+ $content = ContentHandler::makeContent( $babeltext, $userpage );
+
+ $editResult = $page->doEditContent( $content, $summary, EDIT_NEW, false, $user );
+
+ return $editResult;
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function needsToken() {
+ return 'csrf';
+ }
+
+ public function getAllowedParams() {
+ return [
+ 'do' => [
+ ApiBase::PARAM_TYPE => [ 'create', 'delete', 'promote', 'remind' ],
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'userid' => [
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_DFLT => 0,
+ ApiBase::PARAM_ISMULTI => true,
+ ],
+ 'token' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'username' => [ ApiBase::PARAM_TYPE => 'string' ],
+ 'password' => [ ApiBase::PARAM_TYPE => 'string' ],
+ 'email' => [ ApiBase::PARAM_TYPE => 'string' ],
+ ];
+ }
+}
diff --git a/www/wiki/extensions/Translate/api/ApiTranslationCheck.php b/www/wiki/extensions/Translate/api/ApiTranslationCheck.php
new file mode 100644
index 00000000..b4921b4b
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiTranslationCheck.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * @since 2017.10
+ * @license GPL-2.0-or-later
+ */
+class ApiTranslationCheck extends ApiBase {
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ $title = Title::newFromText( $params[ 'title' ] );
+ if ( !$title ) {
+ $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] );
+ }
+ $handle = new MessageHandle( $title );
+ $translation = $params[ 'translation' ];
+
+ $checkResults = $this->getWarnings( $handle, $translation );
+
+ $warnings = [];
+ foreach ( $checkResults as $item ) {
+ $key = array_shift( $item );
+ $msg = $this->getContext()->msg( $key, $item )->parse();
+ $this->getResult()->addValue( 'warnings', null, $msg );
+ }
+ }
+
+ public function getWarnings( MessageHandle $handle, $translation ) {
+ if ( $translation === '' ) {
+ return [];
+ }
+
+ if ( $handle->isDoc() || !$handle->isValid() ) {
+ return [];
+ }
+
+ $checker = $handle->getGroup()->getChecker();
+ if ( !$checker ) {
+ return [];
+ }
+
+ $definition = $this->getDefinition( $handle );
+ $message = new FatMessage( $handle->getKey(), $definition );
+ $message->setTranslation( $translation );
+
+ $checks = $checker->checkMessage( $message, $handle->getCode() );
+ if ( $checks === [] ) {
+ return [];
+ }
+
+ return $checks;
+ }
+
+ private function getDefinition( MessageHandle $handle ) {
+ $group = $handle->getGroup();
+ if ( method_exists( $group, 'getMessageContent' ) ) {
+ return $group->getMessageContent( $handle );
+ } else {
+ return $group->getMessage( $handle->getKey(), $group->getSourceLanguage() );
+ }
+ }
+
+ public function getAllowedParams() {
+ return [
+ 'title' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'translation' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ ];
+ }
+
+ public function isInternal() {
+ return true;
+ }
+}
diff --git a/www/wiki/extensions/Translate/api/ApiTranslationReview.php b/www/wiki/extensions/Translate/api/ApiTranslationReview.php
new file mode 100644
index 00000000..d8fb8045
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiTranslationReview.php
@@ -0,0 +1,166 @@
+<?php
+/**
+ * API module for marking translations as reviewed
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * API module for marking translations as reviewed
+ *
+ * @ingroup API TranslateAPI
+ */
+class ApiTranslationReview extends ApiBase {
+ protected static $right = 'translate-messagereview';
+
+ public function execute() {
+ $this->checkUserRightsAny( self::$right );
+
+ $params = $this->extractRequestParams();
+
+ $revision = Revision::newFromId( $params['revision'] );
+ if ( !$revision ) {
+ $this->dieWithError( [ 'apierror-nosuchrevid', $params['revision'] ], 'invalidrevision' );
+ }
+
+ $error = self::getReviewBlockers( $this->getUser(), $revision );
+ switch ( $error ) {
+ case '':
+ // Everything is okay
+ break;
+ case 'permissiondenied':
+ $this->dieWithError( 'apierror-permissiondenied-generic', 'permissiondenied' );
+ break; // Unreachable, but throws off code analyzer.
+ case 'blocked':
+ $this->dieBlocked( $this->getUser()->getBlock() );
+ break; // Unreachable, but throws off code analyzer.
+ case 'unknownmessage':
+ $this->dieWithError( 'apierror-translate-unknownmessage', $error );
+ break; // Unreachable, but throws off code analyzer.
+ case 'owntranslation':
+ $this->dieWithError( 'apierror-translate-owntranslation', $error );
+ break; // Unreachable, but throws off code analyzer.
+ case 'fuzzymessage':
+ $this->dieWithError( 'apierror-translate-fuzzymessage', $error );
+ break; // Unreachable, but throws off code analyzer.
+ default:
+ $this->dieWithError( [ 'apierror-unknownerror', $error ], $error );
+ }
+
+ $ok = self::doReview( $this->getUser(), $revision );
+ if ( !$ok ) {
+ $this->addWarning( 'apiwarn-translate-alreadyreviewedbyyou' );
+ }
+
+ $output = [ 'review' => [
+ 'title' => $revision->getTitle()->getPrefixedText(),
+ 'pageid' => $revision->getPage(),
+ 'revision' => $revision->getId()
+ ] ];
+
+ $this->getResult()->addValue( null, $this->getModuleName(), $output );
+ }
+
+ /**
+ * Executes the real stuff. No checks done!
+ * @param User $user
+ * @param Revision $revision
+ * @param null|string $comment
+ * @return bool whether the action was recorded.
+ */
+ public static function doReview( User $user, Revision $revision, $comment = null ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $table = 'translate_reviews';
+ $row = [
+ 'trr_user' => $user->getId(),
+ 'trr_page' => $revision->getPage(),
+ 'trr_revision' => $revision->getId(),
+ ];
+ $options = [ 'IGNORE' ];
+ $dbw->insert( $table, $row, __METHOD__, $options );
+
+ if ( !$dbw->affectedRows() ) {
+ return false;
+ }
+
+ $title = $revision->getTitle();
+
+ $entry = new ManualLogEntry( 'translationreview', 'message' );
+ $entry->setPerformer( $user );
+ $entry->setTarget( $title );
+ $entry->setComment( $comment );
+ $entry->setParameters( [
+ '4::revision' => $revision->getId(),
+ ] );
+
+ $logid = $entry->insert();
+ $entry->publish( $logid );
+
+ $handle = new MessageHandle( $title );
+ Hooks::run( 'TranslateEventTranslationReview', [ $handle ] );
+
+ return true;
+ }
+
+ /**
+ * Validates review action by checking permissions and other things.
+ * @param User $user
+ * @param Revision $revision
+ * @return string Error key or empty string if review is allowed.
+ * @since 2012-09-24
+ */
+ public static function getReviewBlockers( User $user, Revision $revision ) {
+ if ( !$user->isAllowed( self::$right ) ) {
+ return 'permissiondenied';
+ }
+
+ if ( $user->isBlocked() ) {
+ return 'blocked';
+ }
+
+ $title = $revision->getTitle();
+ $handle = new MessageHandle( $title );
+ if ( !$handle->isValid() ) {
+ return 'unknownmessage';
+ }
+
+ if ( $revision->getUser() === $user->getId() ) {
+ return 'owntranslation';
+ }
+
+ if ( $handle->isFuzzy() ) {
+ return 'fuzzymessage';
+ }
+
+ return '';
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function needsToken() {
+ return 'csrf';
+ }
+
+ public function getAllowedParams() {
+ return [
+ 'revision' => [
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'token' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ ];
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=translationreview&revision=1&token=foo'
+ => 'apihelp-translationreview-example-1',
+ ];
+ }
+}
diff --git a/www/wiki/extensions/Translate/api/ApiTranslationStash.php b/www/wiki/extensions/Translate/api/ApiTranslationStash.php
new file mode 100644
index 00000000..dad11719
--- /dev/null
+++ b/www/wiki/extensions/Translate/api/ApiTranslationStash.php
@@ -0,0 +1,135 @@
+<?php
+/**
+ * WebAPI module for stashing translations.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * WebAPI module for storing translations for users who are in a sandbox.
+ * Access is controlled by hooks in TranslateSandbox class.
+ * @since 2013.06
+ */
+class ApiTranslationStash extends ApiBase {
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ // The user we are operating on, not necessarly the user making the request
+ $user = $this->getUser();
+
+ if ( isset( $params['username'] ) ) {
+ if ( $this->getUser()->isAllowed( 'translate-sandboxmanage' ) ) {
+ $user = User::newFromName( $params['username'] );
+ if ( !$user ) {
+ $this->dieWithError( [ 'apierror-badparameter', 'username' ], 'invalidparam' );
+ }
+ } else {
+ $this->dieWithError( [ 'apierror-badparameter', 'username' ], 'invalidparam' );
+ }
+ }
+
+ $stash = new TranslationStashStorage( wfGetDB( DB_MASTER ) );
+ $action = $params['subaction'];
+
+ if ( $action === 'add' ) {
+ if ( !isset( $params['title'] ) ) {
+ $this->dieWithError( [ 'apierror-missingparam', 'title' ] );
+ }
+ if ( !isset( $params['translation'] ) ) {
+ $this->dieWithError( [ 'apierror-missingparam', 'translation' ] );
+ }
+
+ // @todo: Return value of Title::newFromText not checked
+ $translation = new StashedTranslation(
+ $user,
+ Title::newFromText( $params['title'] ),
+ $params['translation'],
+ FormatJson::decode( $params['metadata'], true )
+ );
+ $stash->addTranslation( $translation );
+ }
+
+ if ( $action === 'query' ) {
+ $output['translations'] = [];
+
+ $translations = $stash->getTranslations( $user );
+ foreach ( $translations as $translation ) {
+ $output['translations'][] = $this->formatTranslation( $translation );
+ }
+ }
+
+ // If we got this far, nothing has failed
+ $output['result'] = 'ok';
+ $this->getResult()->addValue( null, $this->getModuleName(), $output );
+ }
+
+ protected function formatTranslation( StashedTranslation $translation ) {
+ $title = $translation->getTitle();
+ $handle = new MessageHandle( $title );
+
+ // Prepare for the worst
+ $definition = '';
+ $comparison = '';
+ if ( $handle->isValid() ) {
+ $groupId = MessageIndex::getPrimaryGroupId( $handle );
+ $group = MessageGroups::getGroup( $groupId );
+
+ $key = $handle->getKey();
+
+ $definition = $group->getMessage( $key, $group->getSourceLanguage() );
+ $comparison = $group->getMessage( $key, $handle->getCode() );
+ }
+
+ return [
+ 'title' => $title->getPrefixedText(),
+ 'definition' => $definition,
+ 'translation' => $translation->getValue(),
+ 'comparison' => $comparison,
+ 'metadata' => $translation->getMetadata(),
+ ];
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function needsToken() {
+ return 'csrf';
+ }
+
+ public function getAllowedParams() {
+ return [
+ 'subaction' => [
+ ApiBase::PARAM_TYPE => [ 'add', 'query' ],
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'title' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ],
+ 'translation' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ],
+ 'metadata' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ],
+ 'token' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'username' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ],
+ ];
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=translationstash&subaction=add&title=MediaWiki:Jan/fi&translation=tammikuu&metadata={}'
+ => 'apihelp-translationstash-example-1',
+ 'action=translationstash&subaction=query'
+ => 'apihelp-translationstash-example-2',
+ ];
+ }
+}