27 private const FIELD_DELIMITER =
"\x7F";
28 private const ROW_DELIMITER =
"\n";
30 private const TYPE_AGGREGATE = AggregateMessageGroup::class;
31 private const TYPE_MESSAGE_BUNDLE = MessageBundleMessageGroup::class;
32 private const TYPE_WIKIPAGE = WikiPageMessageGroup::class;
33 private const TYPE_OTHERS =
'o';
34 private const TYPE_MAPPING = [
35 self::TYPE_AGGREGATE => [
'aggregate-groups',
'a' ],
36 self::TYPE_MESSAGE_BUNDLE => [
'message-bundles',
'm' ],
37 self::TYPE_WIKIPAGE => [
'translatable-pages',
'w' ]
40 private array $mappedTypes;
42 public function __construct(
43 private readonly WANObjectCache $cache,
44 private readonly Collation $collation,
46 private readonly NamespaceInfo $namespaceInfo,
48 private readonly TitleParser $titleParser,
49 private readonly TitleFormatter $titleFormatter,
54 public function searchStaticMessageGroups(
string $query,
int $maxResults, array $types = [] ): array {
55 $cache = $this->cache;
58 $key = $cache->makeKey(
'Translate',
'EntitySearch',
'static-groups',
'-v2' );
59 $haystack = $cache->getWithSetCallback(
61 ExpirationAwareness::TTL_WEEK,
63 return $this->getStaticMessageGroupsHaystack();
67 'checkKeys' => [ $this->messageGroupFactory->getCacheKey() ],
69 'pcTTL' => ExpirationAwareness::TTL_PROC_LONG
78 $delimiter = self::FIELD_DELIMITER;
79 $anything =
"[^$delimiter\n]";
80 $query = preg_quote( $query,
'/' );
82 $groupTypeFilter = [];
83 foreach ( $types as $filter ) {
84 $groupTypeFilter[] = $this->mappedTypes[$filter] ??
'';
86 $groupTypeFilter = count( $groupTypeFilter )
87 ? (
'[' . implode( $groupTypeFilter ) .
']' )
91 $pattern =
"/^$groupTypeFilter$delimiter($query$anything*)$delimiter($anything+)$/miu";
92 preg_match_all( $pattern, $haystack, $matches, PREG_SET_ORDER );
93 foreach ( $matches as [ , $label, $groupId ] ) {
95 $results[$groupId] = [
100 if ( count( $results ) >= $maxResults ) {
101 return array_values( $results );
106 $pattern =
"/^$groupTypeFilter$delimiter($anything*\b$query$anything*)$delimiter($anything+)$/miu";
107 preg_match_all( $pattern, $haystack, $matches, PREG_SET_ORDER );
108 foreach ( $matches as [ , $label, $groupId ] ) {
109 $results[$groupId] = [
114 if ( count( $results ) >= $maxResults ) {
115 return array_values( $results );
119 return array_values( $results );
135 $haystack = $this->getMessagesHaystack();
136 $rowDelimiter = self::ROW_DELIMITER;
137 $anything =
"[^$rowDelimiter]";
140 $query = rtrim( $query,
'*' );
141 $query = preg_quote( $query,
'/' );
144 $pattern =
"/^($anything*\b$query)$anything*$/miu";
145 preg_match_all( $pattern, $haystack, $matches, PREG_SET_ORDER );
148 $previousPrefixMatch =
null;
149 foreach ( $matches as [ $full, $prefixMatch ] ) {
152 if ( count( $results ) >= $maxResults && $previousPrefixMatch !== $prefixMatch ) {
156 if ( $full === $prefixMatch ) {
157 $results[$full] = [ $full, 1,
true, $full ];
159 if ( !isset( $results[
"$prefixMatch*"] ) ) {
160 $results[
"$prefixMatch*"] = [
"$prefixMatch*", 0,
false, $full ];
162 $results[
"$prefixMatch*"][1]++;
164 $previousPrefixMatch = $prefixMatch;
168 foreach ( $results as $index => [ $label, $count, $isExact, $full ] ) {
169 if ( $count === 1 && !$isExact ) {
170 $results[$index][0] = $full;
175 foreach ( $results as &$value ) {
177 $title = $this->titleParser->parseTitle( $value[0] );
178 $label = $this->titleFormatter->getPrefixedText( $title );
179 }
catch ( MalformedTitleException ) {
188 return array_values( $results );
193 $haystack = $this->getMessagesHaystack();
194 $rowDelimiter = self::ROW_DELIMITER;
195 $anything =
"[^$rowDelimiter]*";
198 $placeholder = bin2hex( random_bytes( 16 ) );
199 $query = str_replace(
'*', $placeholder, $query );
200 $query = preg_quote( $query,
'/' );
201 $query = str_replace( $placeholder, $anything, $query );
203 $pattern =
"/^$query/miu";
204 preg_match_all( $pattern, $haystack, $matches, PREG_SET_ORDER );
205 return array_column( $matches, 0 );
208 private function getStaticMessageGroupsHaystack(): string {
209 $groups = $this->messageGroupFactory->getGroups();
210 $data =
new SplMinHeap();
211 foreach ( $groups as $group ) {
212 $label = $group->getLabel();
214 $label = strtr( $label, [ self::FIELD_DELIMITER =>
'', self::ROW_DELIMITER =>
'' ] );
215 $sortKey = $this->collation->getSortKey( $label );
218 $groupType = get_class( $group );
219 $type = self::TYPE_MAPPING[$groupType][1] ?? self::TYPE_OTHERS;
220 $data->insert( [ $sortKey, $label, $group->getId(), $type ] );
224 foreach ( $data as [ , $label, $groupId, $type ] ) {
227 . self::FIELD_DELIMITER
229 . self::FIELD_DELIMITER
231 . self::ROW_DELIMITER;
236 private function getMessageHaystackUncached(): string {
238 $data =
new SplMinHeap();
239 $keys = $this->messageIndex->getKeys();
240 foreach ( $keys as $key ) {
242 $key = strtr( $key, [
'_' =>
' ' ] );
244 [ $namespaceId, $label ] = explode(
':', $key, 2 );
245 if ( !isset( $namespaceMap[$namespaceId] ) ) {
246 $namespaceMap[$namespaceId] = $this->namespaceInfo->getCanonicalName( (
int)$namespaceId );
248 $label = $namespaceMap[$namespaceId] .
":$label";
251 $label = strtr( $label, [ self::ROW_DELIMITER =>
'' ] );
252 $sortKey = $this->collation->getSortKey( $label );
253 $data->insert( [ $sortKey, $label ] );
257 foreach ( $data as [ , $label ] ) {
258 $haystack .= $label . self::ROW_DELIMITER;
264 private function getMessagesHaystack(): string {
265 $cache = $this->cache;
266 $key = $cache->makeKey(
'Translate',
'EntitySearch',
'messages' );
267 return $cache->getWithSetCallback(
269 ExpirationAwareness::TTL_WEEK,
270 function (): string {
274 return $this->getMessageHaystackUncached();
278 'checkKeys' => [ $this->messageIndex->getStatusCacheKey() ],
280 'pcTTL' => ExpirationAwareness::TTL_PROC_LONG
287 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'), $services->get( 'Translate:MessageGroupFactory'),);}, '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(), $services->get( 'Translate:MessageGroupFactory'), $services->getContentLanguageCode() ->toString(), 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:MessageGroupFactory'=> static function(MediaWikiServices $services):MessageGroupFactory { return new MessageGroupFactory($services->get( 'Translate:MessageGroupTypeRegistry'));}, '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(), $services->getNotificationService(), LoggerFactory::getInstance(LogNames::GROUP_SUBSCRIPTION), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):?MessageGroupSubscriptionHookHandler { if(! $services->getExtensionRegistry() ->isLoaded( 'Echo')) { return null;} return new MessageGroupSubscriptionHookHandler();}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getConnectionProvider());}, 'Translate:MessageGroupTypeRegistry'=> static function():MessageGroupTypeRegistry { return new MessageGroupTypeRegistry();}, '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(), $services->get( 'Translate:HookRunner'),);}, '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->getConnectionProvider());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);}, 'Translate:WorkflowStatesMessageGroupLoader'=> static function(MediaWikiServices $services):WorkflowStatesMessageGroupLoader { return new WorkflowStatesMessageGroupLoader(new ServiceOptions(WorkflowStatesMessageGroupLoader::CONSTRUCTOR_OPTIONS, $services->getMainConfig()),);},]
@phpcs-require-sorted-array