Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
TTMServerAid
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 9
1056
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 populateQueries
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getData
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
42
 formatWebSuggestions
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
20
 formatInternalSuggestions
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getInternalServices
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getWebServices
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getQueryableServices
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryableServicesUncached
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\TranslatorInterface\Aid;
5
6use Exception;
7use MediaWiki\Context\IContextSource;
8use MediaWiki\Extension\Translate\MessageLoading\MessageHandle;
9use MediaWiki\Extension\Translate\Services;
10use MediaWiki\Extension\Translate\TranslatorInterface\TranslationHelperException;
11use MediaWiki\Extension\Translate\TtmServer\ReadableTtmServer;
12use MediaWiki\Extension\Translate\TtmServer\TtmServer;
13use MediaWiki\Extension\Translate\TtmServer\TtmServerFactory;
14use MediaWiki\Extension\Translate\Utilities\Utilities;
15use MediaWiki\Extension\Translate\WebService\RemoteTTMServerWebService;
16use MediaWiki\Extension\Translate\WebService\TranslationWebService;
17use MediaWiki\MediaWikiServices;
18use MediaWiki\Title\Title;
19use MessageGroup;
20
21/**
22 * Translation aid that provides suggestion from translation memory.
23 * @ingroup TranslationAids
24 * @author Niklas Laxström
25 * @license GPL-2.0-or-later
26 * @since 2013-01-01 | 2015.02 extends QueryAggregatorAwareTranslationAid
27 */
28class TTMServerAid extends QueryAggregatorAwareTranslationAid {
29    /** @var array[]|null */
30    private ?array $services;
31    private TtmServerFactory $ttmServerFactory;
32
33    public function __construct(
34        MessageGroup $group,
35        MessageHandle $handle,
36        IContextSource $context,
37        TranslationAidDataProvider $dataProvider
38    ) {
39        parent::__construct( $group, $handle, $context, $dataProvider );
40        $this->ttmServerFactory = Services::getInstance()->getTtmServerFactory();
41    }
42
43    public function populateQueries(): void {
44        $text = $this->dataProvider->getDefinition();
45        $from = $this->group->getSourceLanguage();
46        $to = $this->handle->getCode();
47
48        if ( trim( $text ) === '' ) {
49            return;
50        }
51
52        foreach ( $this->getWebServices() as $service ) {
53            $this->storeQuery( $service, $from, $to, $text );
54        }
55    }
56
57    public function getData(): array {
58        $text = $this->dataProvider->getDefinition();
59        if ( trim( $text ) === '' ) {
60            return [];
61        }
62
63        $suggestions = [];
64        $from = $this->group->getSourceLanguage();
65        $to = $this->handle->getCode();
66
67        foreach ( $this->getInternalServices() as $name => $service ) {
68            try {
69                $queryData = $service->query( $from, $to, $text );
70            } catch ( TranslationHelperException $e ) {
71                throw $e;
72            } catch ( Exception $e ) {
73                // Not ideal to catch all exceptions
74                continue;
75            }
76
77            $serviceSuggestion = $this->formatInternalSuggestions( $queryData, $service, $name, $from );
78            $suggestions = array_merge( $suggestions, $serviceSuggestion );
79        }
80
81        // Results from web services
82        foreach ( $this->getQueryData() as $queryData ) {
83            $serviceSuggestion = $this->formatWebSuggestions( $queryData );
84            $suggestions = array_merge( $suggestions, $serviceSuggestion );
85        }
86
87        $suggestions = TtmServer::sortSuggestions( $suggestions );
88        // Must be here to not mess up the sorting function
89        $suggestions['**'] = 'suggestion';
90
91        return $suggestions;
92    }
93
94    protected function formatWebSuggestions( array $queryData ): array {
95        $service = $queryData['service'];
96        $response = $queryData['response'];
97        $sourceLanguage = $queryData['language'];
98        $sourceText = $queryData['text'];
99
100        // getResultData returns a null on failure instead of throwing an exception
101        $items = $service->getResultData( $response );
102        if ( $items === null ) {
103            return [];
104        }
105
106        $localPrefix = Title::makeTitle( NS_MAIN, '' )->getFullURL( '', false, PROTO_CANONICAL );
107        $localPrefixLength = strlen( $localPrefix );
108
109        foreach ( $items as &$item ) {
110            $local = strncmp( $item['uri'], $localPrefix, $localPrefixLength ) === 0;
111            $item = array_merge( $item, [
112                'service' => $service->getName(),
113                'source_language' => $sourceLanguage,
114                'source' => $sourceText,
115                'local' => $local,
116            ] );
117
118            // TtmServerActionApi expands this... need to fix it again to be the bare name
119            if ( $local ) {
120                $pageName = urldecode( substr( $item['location'], $localPrefixLength ) );
121                $handle = new MessageHandle( Title::newFromText( $pageName ) );
122                $item['editorUrl'] = Utilities::getEditorUrl( $handle );
123                $item['location'] = $handle->getTitle()->getPrefixedText();
124            }
125        }
126        return $items;
127    }
128
129    protected function formatInternalSuggestions(
130        array $queryData,
131        ReadableTtmServer $s,
132        string $serviceName,
133        string $sourceLanguage
134    ): array {
135        $items = [];
136
137        foreach ( $queryData as $item ) {
138            $local = $s->isLocalSuggestion( $item );
139
140            $item['service'] = $serviceName;
141            $item['source_language'] = $sourceLanguage;
142            $item['local'] = $local;
143            // Likely only needed for non-public DatabaseTtmServer
144            $item['uri'] ??= $s->expandLocation( $item );
145            if ( $local ) {
146                $handle = new MessageHandle( Title::newFromText( $item[ 'location' ] ) );
147                $item['editorUrl'] = Utilities::getEditorUrl( $handle );
148            }
149            $items[] = $item;
150        }
151
152        return $items;
153    }
154
155    /** @return ReadableTtmServer[] */
156    private function getInternalServices(): array {
157        $services = $this->getQueryableServices();
158        foreach ( $services as $name => $config ) {
159            if ( $config['type'] === 'ttmserver' ) {
160                $services[$name] = $this->ttmServerFactory->create( $name );
161            } else {
162                unset( $services[$name] );
163            }
164        }
165
166        return $services;
167    }
168
169    /** @return RemoteTTMServerWebService[] */
170    private function getWebServices(): array {
171        $services = $this->getQueryableServices();
172        foreach ( $services as $name => $config ) {
173            if ( $config['type'] === 'remote-ttmserver' ) {
174                $services[$name] = TranslationWebService::factory( $name, $config );
175            } else {
176                unset( $services[$name] );
177            }
178        }
179
180        return $services;
181    }
182
183    private function getQueryableServices(): array {
184        global $wgTranslateTranslationServices;
185        $this->services ??= $this->getQueryableServicesUncached( $wgTranslateTranslationServices );
186
187        return $this->services;
188    }
189
190    private function getQueryableServicesUncached( array $services ): array {
191        // Remove writable services
192        $writableServices = $this->ttmServerFactory->getWriteOnly();
193        foreach ( array_keys( $writableServices ) as $serviceId ) {
194            unset( $services[ $serviceId ] );
195        }
196
197        // Then remove non-ttmservers
198        foreach ( $services as $name => $config ) {
199            $type = $config['type'];
200            if ( $type !== 'ttmserver' && $type !== 'remote-ttmserver' ) {
201                unset( $services[$name] );
202            }
203        }
204
205        // Then determine the query method. Prefer HTTP queries that can be run parallel.
206        $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
207        foreach ( $services as $name => &$config ) {
208            $public = $config['public'] ?? false;
209            if ( $config['type'] === 'ttmserver' && $public ) {
210                $config['type'] = 'remote-ttmserver';
211                $config['service'] = $name;
212                $config['url'] = $urlUtils->expand( wfScript( 'api' ), PROTO_CANONICAL );
213            }
214        }
215
216        return $services;
217    }
218}