diff options
Diffstat (limited to 'www/wiki/extensions/Translate/webservices/TranslationWebService.php')
-rw-r--r-- | www/wiki/extensions/Translate/webservices/TranslationWebService.php | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/www/wiki/extensions/Translate/webservices/TranslationWebService.php b/www/wiki/extensions/Translate/webservices/TranslationWebService.php new file mode 100644 index 00000000..a72be868 --- /dev/null +++ b/www/wiki/extensions/Translate/webservices/TranslationWebService.php @@ -0,0 +1,352 @@ +<?php +/** + * Contains code related to web service support. + * + * @file + * @author Niklas Laxström + * @license GPL-2.0-or-later + */ + +use MediaWiki\Logger\LoggerFactory; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; + +/** + * Multipurpose class: + * - 1) Interface for web services. + * - 2) Source text picking logic. + * - 3) Factory class. + * - 4) Service failure tracking and suspending. + * @since 2013-01-01 + * @defgroup TranslationWebService Translation Web Services + */ +abstract class TranslationWebService implements LoggerAwareInterface { + /* Public api */ + + /** + * Get a webservice handler. + * + * @see $wgTranslateTranslationServices + * @param string $name Name of the service. + * @param array $config + * @return TranslationWebService|null + */ + public static function factory( $name, $config ) { + $handlers = [ + 'microsoft' => 'MicrosoftWebService', + 'apertium' => 'ApertiumWebService', + 'yandex' => 'YandexWebService', + 'remote-ttmserver' => 'RemoteTTMServerWebService', + 'cxserver' => 'CxserverWebService', + 'restbase' => 'RESTBaseWebService', + 'caighdean' => 'CaighdeanWebService', + ]; + + if ( !isset( $config['timeout'] ) ) { + $config['timeout'] = 3; + } + + // Alter local ttmserver instance to appear as remote + // to take advantage of the query aggregator. But only + // if they are public. + if ( + isset( $config['class'] ) && + $config['class'] === 'ElasticSearchTTMServer' && + isset( $config['public'] ) && + $config['public'] === true + ) { + $config['type'] = 'remote-ttmserver'; + $config['service'] = $name; + $config['url'] = wfExpandUrl( wfScript( 'api' ), PROTO_CANONICAL ); + } + + if ( isset( $handlers[$config['type']] ) ) { + $class = $handlers[$config['type']]; + + $obj = new $class( $name, $config ); + $obj->setLogger( LoggerFactory::getInstance( 'translationservices' ) ); + return $obj; + } + + return null; + } + + /** + * Gets the name of this service, for example to display it for the user. + * + * @return string Plain text name for this service. + * @since 2014.02 + */ + public function getName() { + return $this->service; + } + + /** + * Get queries for this service. Queries from multiple services can be + * collected and run asynchronously with QueryAggregator. + * + * @param string $text Source text + * @param string $from Source language + * @param string $to Target language + * @return TranslationQuery[] + * @since 2015.12 + * @throws TranslationWebServiceConfigurationException + */ + public function getQueries( $text, $from, $to ) { + $from = $this->mapCode( $from ); + $to = $this->mapCode( $to ); + + try { + return [ $this->getQuery( $text, $from, $to ) ]; + } catch ( TranslationWebServiceException $e ) { + $this->reportTranslationServiceFailure( $e->getMessage() ); + return []; + } catch ( TranslationWebServiceInvalidInputException $e ) { + // Not much we can do about this, just ignore. + return []; + } + } + + /** + * Get the web service specific response returned by QueryAggregator. + * + * @param TranslationQueryResponse $response + * @return mixed|null Returns null on error. + * @since 2015.12 + */ + public function getResultData( TranslationQueryResponse $response ) { + if ( $response->getStatusCode() !== 200 ) { + $this->reportTranslationServiceFailure( + 'STATUS: ' . $response->getStatusMessage() . "\n" . + 'BODY: ' . $response->getBody() + ); + return null; + } + + try { + return $this->parseResponse( $response ); + } catch ( TranslationWebServiceException $e ) { + $this->reportTranslationServiceFailure( $e->getMessage() ); + return null; + } + } + + /** + * Returns the type of this web service. + * @see TranslationAid::getTypes + * @return string + */ + abstract public function getType(); + + /* Service api */ + + /** + * Map a MediaWiki (almost standard) language code to the code used by the + * translation service. + * + * @param string $code MediaWiki language code. + * @return string Translation service language code. + */ + abstract protected function mapCode( $code ); + + /** + * Get the list of supported language pairs for the web service. The codes + * should be the ones used by the service. Caching is handled by the public + * getSupportedLanguagePairs. + * + * @return array $list[source language][target language] = true + * @throws TranslationWebServiceException + * @throws TranslationWebServiceConfigurationException + */ + abstract protected function doPairs(); + + /** + * Get the query. See getQueries for the public method. + * + * @param string $text Text to translate. + * @param string $from Language code of the text, as used by the service. + * @param string $to Language code of the translation, as used by the service. + * @return TranslationQuery + * @since 2015.02 + * @throws TranslationWebServiceException + * @throws TranslationWebServiceConfigurationException + * @throws TranslationWebServiceInvalidInputException + */ + abstract protected function getQuery( $text, $from, $to ); + + /** + * Get the response. See getResultData for the public method. + * + * @param TranslationQueryResponse $response + * @return string + * @since 2015.02 + * @throws TranslationWebServiceException + */ + abstract protected function parseResponse( TranslationQueryResponse $response ); + + /* Default implementation */ + + /** + * @var string Name of this webservice. + */ + protected $service; + + /** + * @var array + */ + protected $config; + + /** + * @param string $service Name of the webservice + * @param array $config + */ + protected function __construct( $service, $config ) { + $this->service = $service; + $this->config = $config; + } + + /** + * Test whether given language pair is supported by the service. + * + * @param string $from Source language + * @param string $to Target language + * @return bool + * @since 2015.12 + * @throws TranslationWebServiceConfigurationException + */ + public function isSupportedLanguagePair( $from, $to ) { + $pairs = $this->getSupportedLanguagePairs(); + $from = $this->mapCode( $from ); + $to = $this->mapCode( $to ); + + return isset( $pairs[$from][$to] ); + } + + /** + * @see self::doPairs + * @return array + * @throws TranslationWebServiceConfigurationException + */ + protected function getSupportedLanguagePairs() { + $key = wfMemcKey( 'translate-tmsug-pairs-' . $this->service ); + $pairs = wfGetCache( CACHE_ANYTHING )->get( $key ); + if ( !is_array( $pairs ) ) { + try { + $pairs = $this->doPairs(); + } catch ( Exception $e ) { + $this->reportTranslationServiceFailure( $e->getMessage() ); + return []; + } + // Cache the result for a day + wfGetCache( CACHE_ANYTHING )->set( $key, $pairs, 60 * 60 * 24 ); + } + + return $pairs; + } + + /** + * Some mangling that tries to keep some parts of the message unmangled + * by the translation service. Most of them support either class=notranslate + * or translate=no. + * @param string $text + * @return string + */ + protected function wrapUntranslatable( $text ) { + $text = str_replace( "\n", '!N!', $text ); + $pattern = '~%[^% ]+%|\$\d|{VAR:[^}]+}|{?{(PLURAL|GRAMMAR|GENDER):[^|]+\||%(\d\$)?[sd]~'; + $wrap = '<span class="notranslate" translate="no">\0</span>'; + return preg_replace( $pattern, $wrap, $text ); + } + + /** + * Undo the hopyfully untouched mangling done by wrapUntranslatable. + * @param string $text + * @return string + */ + protected function unwrapUntranslatable( $text ) { + $text = str_replace( '!N!', "\n", $text ); + $pattern = '~<span class="notranslate" translate="no">(.*?)</span>~'; + return preg_replace( $pattern, '\1', $text ); + } + + /* Failure handling and suspending */ + + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; + } + + /** + * @var int How many failures during failure period need to happen to + * consider the service being temporarily off-line. + */ + protected $serviceFailureCount = 5; + + /** + * @var int How long after the last detected failure we clear the status and + * try again. + */ + protected $serviceFailurePeriod = 900; + + /** + * Checks whether the service has exceeded failure count + * @return bool + */ + public function checkTranslationServiceFailure() { + $service = $this->service; + $key = wfMemcKey( "translate-service-$service" ); + $value = wfGetCache( CACHE_ANYTHING )->get( $key ); + if ( !is_string( $value ) ) { + return false; + } + list( $count, $failed ) = explode( '|', $value, 2 ); + + if ( $failed + ( 2 * $this->serviceFailurePeriod ) < wfTimestamp() ) { + if ( $count >= $this->serviceFailureCount ) { + $this->logger->warning( "Translation service $service (was) restored" ); + } + wfGetCache( CACHE_ANYTHING )->delete( $key ); + + return false; + } elseif ( $failed + $this->serviceFailurePeriod < wfTimestamp() ) { + /* We are in suspicious mode and one failure is enough to update + * failed timestamp. If the service works however, let's use it. + * Previous failures are forgotten after another failure period + * has passed */ + return false; + } + + // Check the failure count against the limit + return $count >= $this->serviceFailureCount; + } + + /** + * Increases the failure count for this service + * @param string $msg + */ + protected function reportTranslationServiceFailure( $msg ) { + $service = $this->service; + $this->logger->warning( "Translation service $service problem: $msg" ); + + $key = wfMemcKey( "translate-service-$service" ); + $value = wfGetCache( CACHE_ANYTHING )->get( $key ); + if ( !is_string( $value ) ) { + $count = 0; + } else { + list( $count, ) = explode( '|', $value, 2 ); + } + + $count++; + $failed = wfTimestamp(); + wfGetCache( CACHE_ANYTHING )->set( + $key, + "$count|$failed", + $this->serviceFailurePeriod * 5 + ); + + if ( $count === $this->serviceFailureCount ) { + $this->logger->error( "Translation service $service suspended" ); + } elseif ( $count > $this->serviceFailureCount ) { + $this->logger->warning( "Translation service $service still suspended" ); + } + } +} |