27 private const FIELD_DELIMITER =
"\x7F";
28 private const ROW_DELIMITER =
"\n";
30 private WANObjectCache $cache;
31 private Collation $collation;
33 private NamespaceInfo $namespaceInfo;
35 private TitleParser $titleParser;
36 private TitleFormatter $titleFormatter;
37 private const TYPE_AGGREGATE = AggregateMessageGroup::class;
38 private const TYPE_MESSAGE_BUNDLE = MessageBundleMessageGroup::class;
39 private const TYPE_WIKIPAGE = WikiPageMessageGroup::class;
40 private const TYPE_OTHERS =
'o';
41 private const TYPE_MAPPING = [
42 self::TYPE_AGGREGATE => [
'aggregate-groups',
'a' ],
43 self::TYPE_MESSAGE_BUNDLE => [
'message-bundles',
'm' ],
44 self::TYPE_WIKIPAGE => [
'translatable-pages',
'w' ]
47 private array $mappedTypes;
49 public function __construct(
50 WANObjectCache $cache,
53 NamespaceInfo $namespaceInfo,
55 TitleParser $titleParser,
56 TitleFormatter $titleFormatter
58 $this->cache = $cache;
59 $this->collation = $collation;
60 $this->messageGroupFactory = $messageGroupFactory;
61 $this->namespaceInfo = $namespaceInfo;
62 $this->messageIndex = $messageIndex;
63 $this->titleParser = $titleParser;
64 $this->titleFormatter = $titleFormatter;
68 public function searchStaticMessageGroups(
string $query,
int $maxResults, array $types = [] ): array {
69 $cache = $this->cache;
72 $key = $cache->makeKey(
'Translate',
'EntitySearch',
'static-groups',
'-v2' );
73 $haystack = $cache->getWithSetCallback(
75 ExpirationAwareness::TTL_WEEK,
77 return $this->getStaticMessageGroupsHaystack();
81 'checkKeys' => [ $this->messageGroupFactory->getCacheKey() ],
83 'pcTTL' => ExpirationAwareness::TTL_PROC_LONG
92 $delimiter = self::FIELD_DELIMITER;
93 $anything =
"[^$delimiter\n]";
94 $query = preg_quote( $query,
'/' );
96 $groupTypeFilter = [];
97 foreach ( $types as $filter ) {
98 $groupTypeFilter[] = $this->mappedTypes[$filter] ??
'';
100 $groupTypeFilter = count( $groupTypeFilter )
101 ? (
'[' . implode( $groupTypeFilter ) .
']' )
105 $pattern =
"/^$groupTypeFilter$delimiter($query$anything*)$delimiter($anything+)$/miu";
106 preg_match_all( $pattern, $haystack, $matches, PREG_SET_ORDER );
107 foreach ( $matches as [ , $label, $groupId ] ) {
109 $results[$groupId] = [
114 if ( count( $results ) >= $maxResults ) {
115 return array_values( $results );
120 $pattern =
"/^$groupTypeFilter$delimiter($anything*\b$query$anything*)$delimiter($anything+)$/miu";
121 preg_match_all( $pattern, $haystack, $matches, PREG_SET_ORDER );
122 foreach ( $matches as [ , $label, $groupId ] ) {
123 $results[$groupId] = [
128 if ( count( $results ) >= $maxResults ) {
129 return array_values( $results );
133 return array_values( $results );
149 $haystack = $this->getMessagesHaystack();
150 $rowDelimiter = self::ROW_DELIMITER;
151 $anything =
"[^$rowDelimiter]";
152 $query = preg_quote( $query,
'/' );
155 $pattern =
"/^($anything*\b$query)$anything*$/miu";
156 preg_match_all( $pattern, $haystack, $matches, PREG_SET_ORDER );
159 $previousPrefixMatch =
null;
160 foreach ( $matches as [ $full, $prefixMatch ] ) {
163 if ( count( $results ) >= $maxResults && $previousPrefixMatch !== $prefixMatch ) {
167 if ( $full === $prefixMatch ) {
168 $results[$full] = [ $full, 1,
true, $full ];
170 if ( !isset( $results[
"$prefixMatch*"] ) ) {
171 $results[
"$prefixMatch*"] = [
"$prefixMatch*", 0,
false, $full ];
173 $results[
"$prefixMatch*"][1]++;
175 $previousPrefixMatch = $prefixMatch;
179 foreach ( $results as $index => [ $label, $count, $isExact, $full ] ) {
180 if ( $count === 1 && !$isExact ) {
181 $results[$index][0] = $full;
186 foreach ( $results as &$value ) {
188 $title = $this->titleParser->parseTitle( $value[0] );
189 $label = $this->titleFormatter->getPrefixedText( $title );
190 }
catch ( MalformedTitleException $e ) {
199 return array_values( $results );
204 $haystack = $this->getMessagesHaystack();
205 $rowDelimiter = self::ROW_DELIMITER;
206 $anything =
"[^$rowDelimiter]*";
209 $placeholder = bin2hex( random_bytes( 16 ) );
210 $query = str_replace(
'*', $placeholder, $query );
211 $query = preg_quote( $query,
'/' );
212 $query = str_replace( $placeholder, $anything, $query );
214 $pattern =
"/^$query/miu";
215 preg_match_all( $pattern, $haystack, $matches, PREG_SET_ORDER );
216 return array_column( $matches, 0 );
219 private function getStaticMessageGroupsHaystack(): string {
220 $groups = $this->messageGroupFactory->getGroups();
221 $data =
new SplMinHeap();
222 foreach ( $groups as $group ) {
223 $label = $group->getLabel();
225 $label = strtr( $label, [ self::FIELD_DELIMITER =>
'', self::ROW_DELIMITER =>
'' ] );
226 $sortKey = $this->collation->getSortKey( $label );
229 $groupType = get_class( $group );
230 $type = self::TYPE_MAPPING[$groupType][1] ?? self::TYPE_OTHERS;
231 $data->insert( [ $sortKey, $label, $group->getId(), $type ] );
235 foreach ( $data as [ , $label, $groupId, $type ] ) {
238 . self::FIELD_DELIMITER
240 . self::FIELD_DELIMITER
242 . self::ROW_DELIMITER;
247 private function getMessageHaystackUncached(): string {
249 $data =
new SplMinHeap();
250 $keys = $this->messageIndex->getKeys();
251 foreach ( $keys as $key ) {
253 $key = strtr( $key, [
'_' =>
' ' ] );
255 [ $namespaceId, $label ] = explode(
':', $key, 2 );
256 if ( !isset( $namespaceMap[$namespaceId] ) ) {
257 $namespaceMap[$namespaceId] = $this->namespaceInfo->getCanonicalName( (
int)$namespaceId );
259 $label = $namespaceMap[$namespaceId] .
":$label";
262 $label = strtr( $label, [ self::ROW_DELIMITER =>
'' ] );
263 $sortKey = $this->collation->getSortKey( $label );
264 $data->insert( [ $sortKey, $label ] );
268 foreach ( $data as [ , $label ] ) {
269 $haystack .= $label . self::ROW_DELIMITER;
275 private function getMessagesHaystack(): string {
276 $cache = $this->cache;
277 $key = $cache->makeKey(
'Translate',
'EntitySearch',
'messages' );
278 return $cache->getWithSetCallback(
280 ExpirationAwareness::TTL_WEEK,
281 function (): string {
285 return $this->getMessageHaystackUncached();
289 'checkKeys' => [ $this->messageIndex->getStatusCacheKey() ],
291 'pcTTL' => ExpirationAwareness::TTL_PROC_LONG
298 return array_column( self::TYPE_MAPPING, 1, 0 );
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager($services->getTitleFactory(), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:AggregateGroupMessageGroupFactory'=> static function(MediaWikiServices $services):AggregateGroupMessageGroupFactory { return new AggregateGroupMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateComparator'=> static function(MediaWikiServices $services):ExternalMessageSourceStateComparator { return new ExternalMessageSourceStateComparator(new SimpleStringComparator(), $services->getRevisionLookup(), $services->getPageStore());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance(LogNames::GROUP_SYNCHRONIZATION), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), $services->get( 'Translate:MessageGroupSubscription'), new ServiceOptions(ExternalMessageSourceStateImporter::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:FileBasedMessageGroupFactory'=> static function(MediaWikiServices $services):FileBasedMessageGroupFactory { return new FileBasedMessageGroupFactory(new MessageGroupConfigurationParser(), new ServiceOptions(FileBasedMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookDefinedMessageGroupFactory'=> static function(MediaWikiServices $services):HookDefinedMessageGroupFactory { return new HookDefinedMessageGroupFactory( $services->get( 'Translate:HookRunner'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleDependencyPurger'=> static function(MediaWikiServices $services):MessageBundleDependencyPurger { return new MessageBundleDependencyPurger( $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:MessageBundleMessageGroupFactory'=> static function(MediaWikiServices $services):MessageBundleMessageGroupFactory { return new MessageBundleMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'), new ServiceOptions(MessageBundleMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:MessageBundleTranslationLoader'=> static function(MediaWikiServices $services):MessageBundleTranslationLoader { return new MessageBundleTranslationLoader( $services->getLanguageFallback());}, 'Translate:MessageGroupMetadata'=> static function(MediaWikiServices $services):MessageGroupMetadata { return new MessageGroupMetadata( $services->getConnectionProvider());}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getConnectionProvider(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->get( 'Translate:MessageGroupMetadata'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageGroupSubscription'=> static function(MediaWikiServices $services):MessageGroupSubscription { return new MessageGroupSubscription($services->get( 'Translate:MessageGroupSubscriptionStore'), $services->getJobQueueGroup(), $services->getUserIdentityLookup(), LoggerFactory::getInstance(LogNames::GROUP_SUBSCRIPTION), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):MessageGroupSubscriptionHookHandler { return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getConnectionProvider());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=(array) $services->getMainConfig() ->get( 'TranslateMessageIndex');$class=array_shift( $params);$implementationMap=['HashMessageIndex'=> HashMessageIndex::class, 'CDBMessageIndex'=> CDBMessageIndex::class, 'DatabaseMessageIndex'=> DatabaseMessageIndex::class, 'hash'=> HashMessageIndex::class, 'cdb'=> CDBMessageIndex::class, 'database'=> DatabaseMessageIndex::class,];$messageIndexStoreClass=$implementationMap[$class] ?? $implementationMap['database'];return new MessageIndex(new $messageIndexStoreClass, $services->getMainWANObjectCache(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), LoggerFactory::getInstance(LogNames::MAIN), $services->getMainObjectStash(), $services->getConnectionProvider(), new ServiceOptions(MessageIndex::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getConnectionProvider(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore( $services->getConnectionProvider());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleDeleter'=> static function(MediaWikiServices $services):TranslatableBundleDeleter { return new TranslatableBundleDeleter($services->getMainObjectStash(), $services->getJobQueueGroup(), $services->get( 'Translate:SubpageListBuilder'), $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getConnectionProvider());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup(), $services->getNamespaceInfo(), $services->getTitleFactory(), $services->getFormatterFactory());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getConnectionProvider(), $services->getObjectCacheFactory(), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getConnectionProvider() ->getPrimaryDatabase(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getConnectionProvider(), $services->getJobQueueGroup(), $services->getLinkRenderer(), MessageGroups::singleton(), $services->get( 'Translate:MessageIndex'), $services->getTitleFormatter(), $services->getTitleParser(), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:TranslatablePageStateStore'), $services->get( 'Translate:TranslationUnitStoreFactory'), $services->get( 'Translate:MessageGroupMetadata'), $services->getWikiPageFactory(), $services->get( 'Translate:TranslatablePageView'), $services->get( 'Translate:MessageGroupSubscription'), $services->getFormatterFactory());}, 'Translate:TranslatablePageMessageGroupFactory'=> static function(MediaWikiServices $services):TranslatablePageMessageGroupFactory { return new TranslatablePageMessageGroupFactory(new ServiceOptions(TranslatablePageMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStateStore'=> static function(MediaWikiServices $services):TranslatablePageStateStore { return new TranslatablePageStateStore($services->get( 'Translate:PersistentCache'), $services->getPageStore());}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getConnectionProvider(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getConnectionProvider(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getConnectionProvider(), $services->getPermissionManager(), $services->getAuthManager(), $services->getUserGroupManager(), $services->getActorStore(), $services->getUserOptionsManager(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), new ServiceOptions(TranslateSandbox::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { return new TranslationStashStorage( $services->getConnectionProvider() ->getPrimaryDatabase());}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getConnectionProvider());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array