Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 130
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
BannerMessageGroup
0.00% covered (danger)
0.00%
0 / 130
0.00% covered (danger)
0.00%
0 / 9
702
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getKeys
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getDefinitions
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 isUsingGroupReview
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getTranslateGroupName
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 updateBannerGroupStateHook
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
72
 getMessageGroupStates
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 registerGroupHook
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
6
 getLanguagesInState
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3use MediaWiki\Content\ContentHandler;
4use MediaWiki\Content\TextContent;
5use MediaWiki\Context\RequestContext;
6use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
7use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroupStates;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\Title\Title;
10
11/**
12 * Generate a group of message definitions for a banner so they can be translated
13 */
14class 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}