Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
AggregateGroupManager.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
9use MediaWiki\Title\Title;
10use MediaWiki\Title\TitleFactory;
11use MessageGroup;
12use RuntimeException;
14
22 private TitleFactory $titleFactory;
23 private MessageGroupMetadata $messageGroupMetadata;
24
25 private const MESSAGE_GROUP_PREFIX = 'agg-';
26
27 public function __construct(
28 TitleFactory $titleFactory,
29 MessageGroupMetadata $messageGroupMetadata
30 ) {
31 $this->titleFactory = $titleFactory;
32 $this->messageGroupMetadata = $messageGroupMetadata;
33 }
34
35 public function supportsAggregation( MessageGroup $group ): bool {
36 return $group instanceof WikiPageMessageGroup || $group instanceof MessageBundleMessageGroup;
37 }
38
39 public function getTargetTitleByGroupId( string $groupId ): Title {
40 $group = MessageGroups::getGroup( $groupId );
41 if ( $group ) {
42 return $this->getTargetTitleByGroup( $group );
43 } else {
44 /* To allow removing no longer existing groups from aggregate message groups,
45 * the message group object $group might not always be available.
46 * In this case we need to fake some title. */
47 return $this->titleFactory->newFromText( "Special:Translate/$groupId" );
48 }
49 }
50
52 public function getTargetTitleByGroup( MessageGroup $group ): Title {
53 $relatedGroupPage = $group->getRelatedPage();
54 if ( !$relatedGroupPage ) {
55 throw new RuntimeException( "No related page found for group " . $group->getId() );
56 }
57
58 return $this->titleFactory->newFromLinkTarget( $relatedGroupPage );
59 }
60
61 public function add( string $name, string $description, ?string $languageCode ): string {
62 // Throw error if the group already exists
63 if ( MessageGroups::labelExists( $name ) ) {
64 throw new DuplicateAggregateGroupException( $name );
65 }
66
67 $aggregateGroupId = $this->generateAggregateGroupId( $name );
68
69 // FIXME: Each call to set runs a DB query. Make this more efficient.
70 $this->messageGroupMetadata->set( $aggregateGroupId, 'name', $name );
71 $this->messageGroupMetadata->set( $aggregateGroupId, 'description', $description );
72 if ( $languageCode ) {
73 // TODO: Add language code validation
74 $this->messageGroupMetadata->set( $aggregateGroupId, 'sourcelanguagecode', $languageCode );
75 }
76 $this->messageGroupMetadata->setSubgroups( $aggregateGroupId, [] );
77
78 return $aggregateGroupId;
79 }
80
86 public function associate( string $aggregateGroupId, array $newSubgroupIds ): array {
87 $existingSubgroupIds = $this->getSubgroups( $aggregateGroupId );
88 $newSubgroups = MessageGroups::getGroupsById( $newSubgroupIds );
89 // Identify groups that do not exist
90 $missingGroupIds = $this->findMissingGroupIds( $newSubgroups, $newSubgroupIds );
91 $invalidGroupIds = [];
92 foreach ( $newSubgroups as $subGroup ) {
93 if ( !$this->supportsAggregation( $subGroup ) ) {
94 $invalidGroupIds[] = $subGroup->getId();
95 }
96 }
97
98 if ( $missingGroupIds || $invalidGroupIds ) {
99 $invalidGroupIds = array_unique( array_merge( $missingGroupIds, $invalidGroupIds ) );
100 throw new AggregateGroupAssociationFailure( $invalidGroupIds );
101 }
102
103 $aggregateGroupLanguage = $this->messageGroupMetadata->get( $aggregateGroupId, 'sourcelanguagecode' );
104 if ( $aggregateGroupLanguage !== false ) {
105 // Ensure that the new groups have the same language as the aggregate group.
106 $sourceLanguageMismatchGroupIds = [];
107 foreach ( $newSubgroups as $subGroup ) {
108 if ( $subGroup->getSourceLanguage() !== $aggregateGroupLanguage ) {
109 $sourceLanguageMismatchGroupIds[] = $subGroup->getId();
110 }
111 }
112
113 if ( $sourceLanguageMismatchGroupIds ) {
114 throw new AggregateGroupLanguageMismatchException(
115 $sourceLanguageMismatchGroupIds,
116 $aggregateGroupLanguage
117 );
118 }
119 }
120
121 $allSubgroupIds = array_unique( array_merge( $existingSubgroupIds, $newSubgroupIds ) );
122 if ( array_diff( $newSubgroupIds, $existingSubgroupIds ) === [] ) {
123 // No new subgroups added
124 return [];
125 }
126
127 $this->messageGroupMetadata->setSubgroups( $aggregateGroupId, $allSubgroupIds );
128 return $newSubgroupIds;
129 }
130
136 public function disassociate( string $aggregateGroupId, array $subgroupIds ): array {
137 $existingSubGroupIds = $this->getSubgroups( $aggregateGroupId );
138 $remainingSubGroupIds = array_diff( $existingSubGroupIds, $subgroupIds );
139 $this->messageGroupMetadata->setSubgroups( $aggregateGroupId, $remainingSubGroupIds );
140 return array_diff( $existingSubGroupIds, $remainingSubGroupIds );
141 }
142
144 public function hasGroupsSupportingAggregation(): bool {
145 $groups = MessageGroups::getAllGroups();
146 foreach ( $groups as $group ) {
147 if ( $this->supportsAggregation( $group ) ) {
148 return true;
149 }
150 }
151
152 return false;
153 }
154
159 public function getAll(): array {
160 $groupsPreload = MessageGroups::getGroupsByType( AggregateMessageGroup::class );
161 $this->messageGroupMetadata->preloadGroups( array_keys( $groupsPreload ), __METHOD__ );
162
163 $groups = MessageGroups::getAllGroups();
164 uasort( $groups, [ MessageGroups::class, 'groupLabelSort' ] );
165 $aggregates = [];
166 foreach ( $groups as $group ) {
167 if ( $group instanceof AggregateMessageGroup ) {
168 // Filter out AggregateGroups configured in YAML
169 $subgroups = $this->messageGroupMetadata->getSubgroups( $group->getId() );
170 if ( $subgroups !== null ) {
171 $aggregates[] = $group;
172 }
173 }
174 }
175
176 return $aggregates;
177 }
178
180 private function findMissingGroupIds( array $subGroups, array $subGroupIds ): array {
181 $existingIds = array_map( static fn ( $group ) => $group->getId(), $subGroups );
182 return array_diff( $subGroupIds, $existingIds );
183 }
184
185 private function getSubgroups( string $aggregateGroupId ): array {
186 $existingSubGroupIds = $this->messageGroupMetadata->getSubgroups( $aggregateGroupId );
187 if ( $existingSubGroupIds !== null ) {
188 // For a newly created aggregate group, it may contain no subgroups, but null
189 // means the group does not exist or something has gone wrong.
190 return $existingSubGroupIds;
191 }
192
193 throw new AggregateGroupNotFoundException( $aggregateGroupId );
194 }
195
196 private function generateAggregateGroupId( string $name ): string {
197 // The database field has a maximum limit of 200 bytes
198 if ( strlen( $name ) + strlen( self::MESSAGE_GROUP_PREFIX ) >= 200 ) {
199 $aggregateGroupId = self::MESSAGE_GROUP_PREFIX . substr( sha1( $name ), 0, 5 );
200 } else {
201 $pattern = '/[\x00-\x1f\x23\x27\x2c\x2e\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/i';
202 $aggregateGroupId = self::MESSAGE_GROUP_PREFIX . preg_replace( $pattern, '_', $name );
203 }
204
205 // ID already exists: Generate a new ID by adding a number to it.
206 $idExists = MessageGroups::getGroup( $aggregateGroupId );
207 if ( $idExists ) {
208 $i = 1;
209 do {
210 $tempId = $aggregateGroupId . '-' . $i;
211 $idExists = MessageGroups::getGroup( $tempId );
212 $i++;
213 } while ( $idExists );
214 $aggregateGroupId = $tempId;
215 }
216
217 return $aggregateGroupId;
218 }
219}
Groups multiple message groups together as one group.
Contains logic to manage aggregate groups and their subgroups.
hasGroupsSupportingAggregation()
Checks if there are any pages that support aggregation.
Factory class for accessing message groups individually by id or all of them as a list.
static getGroup(string $id)
Fetch a message group by id.
Offers functionality for reading and updating Translate group related metadata.
Wraps the translatable page sections into a message group.
Interface for message groups.
getId()
Returns the unique identifier for this group.