Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 130 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
| BannerMessageGroup | |
0.00% |
0 / 130 |
|
0.00% |
0 / 9 |
702 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| getKeys | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
| getDefinitions | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
| isUsingGroupReview | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
| getTranslateGroupName | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
| updateBannerGroupStateHook | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
72 | |||
| getMessageGroupStates | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
| registerGroupHook | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
6 | |||
| getLanguagesInState | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
| 1 | <?php |
| 2 | |
| 3 | use MediaWiki\Content\ContentHandler; |
| 4 | use MediaWiki\Content\TextContent; |
| 5 | use MediaWiki\Context\RequestContext; |
| 6 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups; |
| 7 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroupStates; |
| 8 | use MediaWiki\MediaWikiServices; |
| 9 | use MediaWiki\Title\Title; |
| 10 | |
| 11 | /** |
| 12 | * Generate a group of message definitions for a banner so they can be translated |
| 13 | */ |
| 14 | class BannerMessageGroup extends WikiMessageGroup { |
| 15 | |
| 16 | private const TRANSLATE_GROUP_NAME_BASE = 'Centralnotice-tgroup'; |
| 17 | |
| 18 | /** @var string */ |
| 19 | private $bannerName = ''; |
| 20 | |
| 21 | /** |
| 22 | * @var int |
| 23 | */ |
| 24 | protected $namespace = NS_CN_BANNER; |
| 25 | |
| 26 | /** |
| 27 | * @param int $namespace ID of the namespace holding CentralNotice messages |
| 28 | * @param string $title The page name of the CentralNotice banner |
| 29 | */ |
| 30 | public function __construct( $namespace, $title ) { |
| 31 | $titleObj = Title::makeTitle( $namespace, $title ); |
| 32 | $this->id = static::getTranslateGroupName( $title ); |
| 33 | |
| 34 | // For internal usage we just want the name of the banner. In the MediaWiki namespace |
| 35 | // this is stored with a prefix. Elsewhere (like the CentralNotice namespace) it is |
| 36 | // just the page name. |
| 37 | $this->bannerName = str_replace( 'Centralnotice-template-', '', $title ); |
| 38 | |
| 39 | // And now set the label for the Translate UI |
| 40 | $this->setLabel( $titleObj->getPrefixedText() ); |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * This is optimized version of getDefinitions that only returns |
| 45 | * message keys to speed up message index creation. |
| 46 | * @return array |
| 47 | */ |
| 48 | public function getKeys() { |
| 49 | $keys = []; |
| 50 | |
| 51 | $banner = Banner::fromName( $this->bannerName ); |
| 52 | $fields = $banner->getMessageFieldsFromCache(); |
| 53 | |
| 54 | // The MediaWiki page name convention for messages is the same as the |
| 55 | // convention for banners themselves, except that it doesn't include |
| 56 | // the 'template' designation. |
| 57 | if ( $this->namespace === NS_CN_BANNER ) { |
| 58 | $msgKeyPrefix = $this->bannerName . '-'; |
| 59 | } else { |
| 60 | $msgKeyPrefix = "Centralnotice-{$this->bannerName}-"; |
| 61 | } |
| 62 | |
| 63 | foreach ( array_keys( $fields ) as $msgName ) { |
| 64 | $keys[] = $msgKeyPrefix . $msgName; |
| 65 | } |
| 66 | |
| 67 | return $keys; |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Fetch the messages for the banner |
| 72 | * @return array Array of message keys with definitions. |
| 73 | */ |
| 74 | public function getDefinitions() { |
| 75 | $definitions = []; |
| 76 | |
| 77 | $banner = Banner::fromName( $this->bannerName ); |
| 78 | $fields = $banner->getMessageFieldsFromCache(); |
| 79 | |
| 80 | // The MediaWiki page name convention for messages is the same as the |
| 81 | // convention for banners themselves, except that it doesn't include |
| 82 | // the 'template' designation. |
| 83 | $msgDefKeyPrefix = "Centralnotice-{$this->bannerName}-"; |
| 84 | if ( $this->namespace === NS_CN_BANNER ) { |
| 85 | $msgKeyPrefix = $this->bannerName . '-'; |
| 86 | } else { |
| 87 | $msgKeyPrefix = $msgDefKeyPrefix; |
| 88 | } |
| 89 | |
| 90 | // Build the array of message definitions. |
| 91 | foreach ( $fields as $msgName => $msgCount ) { |
| 92 | $defkey = $msgDefKeyPrefix . $msgName; |
| 93 | $msgkey = $msgKeyPrefix . $msgName; |
| 94 | $definitions[$msgkey] = wfMessage( $defkey )->inContentLanguage()->plain(); |
| 95 | } |
| 96 | |
| 97 | return $definitions; |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * Determine if the CentralNotice banner group is using the group review feature of translate |
| 102 | * @return bool |
| 103 | */ |
| 104 | public static function isUsingGroupReview() { |
| 105 | static $useGroupReview = null; |
| 106 | |
| 107 | if ( $useGroupReview === null ) { |
| 108 | $group = MessageGroups::getGroup( self::TRANSLATE_GROUP_NAME_BASE ); |
| 109 | if ( $group && $group->getMessageGroupStates() ) { |
| 110 | $useGroupReview = true; |
| 111 | } else { |
| 112 | $useGroupReview = false; |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | return $useGroupReview; |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * Constructs the translate group name from any number of alternate forms. The group name is |
| 121 | * defined to be 'Centralnotice-tgroup-<BannerName>' |
| 122 | * |
| 123 | * This function can handle input in the form of: |
| 124 | * - raw banner name |
| 125 | * - Centralnotice-template-<banner name> |
| 126 | * |
| 127 | * @param string $bannerName The name of the banner |
| 128 | * |
| 129 | * @return string Canonical translate group name |
| 130 | */ |
| 131 | public static function getTranslateGroupName( $bannerName ) { |
| 132 | if ( str_starts_with( $bannerName, 'Centralnotice-template' ) ) { |
| 133 | return str_replace( |
| 134 | 'Centralnotice-template', |
| 135 | self::TRANSLATE_GROUP_NAME_BASE, |
| 136 | $bannerName |
| 137 | ); |
| 138 | } else { |
| 139 | return self::TRANSLATE_GROUP_NAME_BASE . '-' . $bannerName; |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | /** |
| 144 | * Hook to handle message group review state changes. If the $newState |
| 145 | * for a group is equal to $wgNoticeTranslateDeployStates then this |
| 146 | * function will copy from the CNBanners namespace into the MW namespace |
| 147 | * and protect them with right $wgCentralNoticeMessageProtectRight. This |
| 148 | * implies that the user calling this hook must have site-edit permissions |
| 149 | * and the $wgCentralNoticeMessageProtectRight granted. |
| 150 | * |
| 151 | * @param MessageGroup $group Effected group object |
| 152 | * @param string $code Language code that was modified |
| 153 | * @param string $currentState Review state the group is transitioning from |
| 154 | * @param string $newState Review state the group is transitioning to |
| 155 | * |
| 156 | * @return bool |
| 157 | */ |
| 158 | public static function updateBannerGroupStateHook( $group, $code, $currentState, $newState ) { |
| 159 | global $wgNoticeTranslateDeployStates; |
| 160 | |
| 161 | // We only need to run this if we're actually using group review |
| 162 | if ( !self::isUsingGroupReview() ) { |
| 163 | return true; |
| 164 | } |
| 165 | |
| 166 | if ( $group instanceof AggregateMessageGroup ) { |
| 167 | // Deal with an aggregate group object having changed |
| 168 | $groups = $group->getGroups(); |
| 169 | foreach ( $groups as $subgroup ) { |
| 170 | self::updateBannerGroupStateHook( |
| 171 | $subgroup, $code, $currentState, $newState ); |
| 172 | } |
| 173 | } elseif ( ( $group instanceof BannerMessageGroup ) |
| 174 | && in_array( $newState, $wgNoticeTranslateDeployStates ) |
| 175 | ) { |
| 176 | // Finally an object we can deal with directly and it's in the right state! |
| 177 | $collection = $group->initCollection( $code ); |
| 178 | $collection->loadTranslations(); |
| 179 | $keys = $collection->getMessageKeys(); |
| 180 | $user = RequestContext::getMain()->getUser(); |
| 181 | $wikiPageFactory = MediaWikiServices::getInstance()->getWikiPageFactory(); |
| 182 | |
| 183 | // Now copy each key into the MW namespace |
| 184 | foreach ( $keys as $key ) { |
| 185 | $wikiPage = $wikiPageFactory->newFromTitle( |
| 186 | Title::makeTitleSafe( NS_CN_BANNER, $key . '/' . $code ) |
| 187 | ); |
| 188 | |
| 189 | // Make sure the translation actually exists :p |
| 190 | if ( $wikiPage->exists() ) { |
| 191 | $content = $wikiPage->getContent(); |
| 192 | /** @var TextContent $content */ |
| 193 | '@phan-var TextContent $content'; |
| 194 | $text = $content->getText(); |
| 195 | |
| 196 | $wikiPage = $wikiPageFactory->newFromTitle( |
| 197 | Title::makeTitleSafe( NS_MEDIAWIKI, 'Centralnotice-' . $key . '/' . $code ) |
| 198 | ); |
| 199 | $wikiPage->doUserEditContent( |
| 200 | ContentHandler::makeContent( $text, $wikiPage->getTitle() ), |
| 201 | $user, |
| 202 | 'Update from translation plugin', |
| 203 | EDIT_FORCE_BOT, |
| 204 | false, |
| 205 | [ 'centralnotice translation' ] |
| 206 | ); |
| 207 | Banner::protectBannerContent( $wikiPage, $user, true ); |
| 208 | } |
| 209 | } |
| 210 | } else { |
| 211 | // We do nothing; we don't care about this type of group; or it's in the wrong state |
| 212 | } |
| 213 | |
| 214 | return true; |
| 215 | } |
| 216 | |
| 217 | public function getMessageGroupStates(): MessageGroupStates { |
| 218 | $conf = [ |
| 219 | 'progress' => [ 'color' => 'E00' ], |
| 220 | 'proofreading' => [ 'color' => 'FFBF00' ], |
| 221 | 'ready' => [ 'color' => 'FF0' ], |
| 222 | 'published' => [ 'color' => 'AEA', 'right' => 'centralnotice-admin' ], |
| 223 | 'state conditions' => [ |
| 224 | [ 'ready', [ 'PROOFREAD' => 'MAX' ] ], |
| 225 | [ 'proofreading', [ 'TRANSLATED' => 'MAX' ] ], |
| 226 | [ 'progress', [ 'UNTRANSLATED' => 'NONZERO' ] ], |
| 227 | [ 'unset', [ 'UNTRANSLATED' => 'MAX', 'OUTDATED' => 'ZERO', |
| 228 | 'TRANSLATED' => 'ZERO' ] ], |
| 229 | ], |
| 230 | ]; |
| 231 | |
| 232 | return new MessageGroupStates( $conf ); |
| 233 | } |
| 234 | |
| 235 | /** |
| 236 | * TranslatePostInitGroups hook handler |
| 237 | * Add banner message groups to the list of message groups that should be |
| 238 | * translated through the Translate extension. |
| 239 | * |
| 240 | * @param array &$list |
| 241 | * @return bool |
| 242 | */ |
| 243 | public static function registerGroupHook( &$list ) { |
| 244 | // Must be explicitly primary for runs under a jobqueue |
| 245 | $dbr = CNDatabase::getPrimaryDb(); |
| 246 | |
| 247 | // Create the base aggregate group |
| 248 | $conf = []; |
| 249 | $conf['BASIC'] = [ |
| 250 | 'id' => self::TRANSLATE_GROUP_NAME_BASE, |
| 251 | 'label' => 'CentralNotice Banners', |
| 252 | 'description' => '{{int:centralnotice-aggregate-group-desc}}', |
| 253 | 'meta' => 1, |
| 254 | 'class' => AggregateMessageGroup::class, |
| 255 | 'namespace' => NS_CN_BANNER, |
| 256 | ]; |
| 257 | $conf['GROUPS'] = []; |
| 258 | |
| 259 | // Find all the banners marked for translation |
| 260 | $res = $dbr->newSelectQueryBuilder() |
| 261 | ->select( [ 'page_id', 'page_namespace', 'page_title' ] ) |
| 262 | ->from( 'page' ) |
| 263 | ->join( 'revtag', null, 'page_id=rt_page' ) |
| 264 | ->where( [ 'rt_type' => Banner::TRANSLATE_BANNER_TAG ] ) |
| 265 | ->groupBy( [ 'rt_page', 'page_id', 'page_namespace', 'page_title' ] ) |
| 266 | ->caller( __METHOD__ ) |
| 267 | ->fetchResultSet(); |
| 268 | |
| 269 | foreach ( $res as $r ) { |
| 270 | $grp = new BannerMessageGroup( $r->page_namespace, $r->page_title ); |
| 271 | $id = $grp::getTranslateGroupName( $r->page_title ); |
| 272 | $list[$id] = $grp; |
| 273 | |
| 274 | // Add the banner group to the aggregate group |
| 275 | $conf['GROUPS'][] = $id; |
| 276 | } |
| 277 | |
| 278 | // Update the subgroup meta with any new groups since the last time this was run |
| 279 | $list[$conf['BASIC']['id']] = MessageGroupBase::factory( $conf ); |
| 280 | |
| 281 | return true; |
| 282 | } |
| 283 | |
| 284 | /** |
| 285 | * @param string $banner |
| 286 | * @param string $state |
| 287 | * @return string[] |
| 288 | */ |
| 289 | public static function getLanguagesInState( $banner, $state ) { |
| 290 | if ( !self::isUsingGroupReview() ) { |
| 291 | throw new LogicException( |
| 292 | 'CentralNotice is not using group review. Cannot query group review state.' |
| 293 | ); |
| 294 | } |
| 295 | |
| 296 | $groupName = self::getTranslateGroupName( $banner ); |
| 297 | |
| 298 | $db = CNDatabase::getReplicaDb(); |
| 299 | return $db->newSelectQueryBuilder() |
| 300 | ->select( 'tgr_lang' ) |
| 301 | ->from( 'translate_groupreviews' ) |
| 302 | ->where( [ |
| 303 | 'tgr_group' => $groupName, |
| 304 | 'tgr_state' => $state, |
| 305 | ] ) |
| 306 | ->caller( __METHOD__ ) |
| 307 | ->fetchFieldValues(); |
| 308 | } |
| 309 | } |