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