diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/Translate/api |
first commit
Diffstat (limited to 'www/wiki/extensions/Translate/api')
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', + ]; + } +} |