Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.33% covered (warning)
83.33%
60 / 72
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
QueryManageMessageGroupsActionApi
83.33% covered (warning)
83.33%
60 / 72
40.00% covered (danger)
40.00%
2 / 5
11.56
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
72.00% covered (warning)
72.00%
18 / 25
0.00% covered (danger)
0.00%
0 / 1
5.55
 getPossibleRenames
96.43% covered (success)
96.43%
27 / 28
0.00% covered (danger)
0.00%
0 / 1
3
 getAllowedParams
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use ApiQuery;
7use ApiQueryBase;
8use MediaWiki\Extension\Translate\MessageSync\MessageSourceChange;
9use MediaWiki\Extension\Translate\Synchronization\MessageChangeStorage;
10use MediaWiki\Extension\Translate\Utilities\StringComparators\EditDistanceStringComparator;
11use MediaWiki\Extension\Translate\Utilities\Utilities;
12use MediaWiki\Title\Title;
13use Wikimedia\ParamValidator\ParamValidator;
14
15/**
16 * API module for querying message group changes.
17 * @author Abijeet Patro
18 * @since 2019.10
19 * @license GPL-2.0-or-later
20 * @ingroup API TranslateAPI
21 */
22class QueryManageMessageGroupsActionApi extends ApiQueryBase {
23    private const RIGHT = 'translate-manage';
24
25    public function __construct( ApiQuery $query, string $moduleName ) {
26        parent::__construct( $query, $moduleName, 'mmg' );
27    }
28
29    public function execute() {
30        $params = $this->extractRequestParams();
31        $groupId = $params['groupId'];
32        $msgKey = $params['messageKey'];
33        $name = $params['changesetName'] ?? MessageChangeStorage::DEFAULT_NAME;
34
35        $user = $this->getUser();
36        $allowed = $user->isAllowed( self::RIGHT );
37
38        if ( !$allowed ) {
39            $this->dieWithError( 'apierror-permissiondenied-generic', 'permissiondenied' );
40        }
41
42        $group = MessageGroups::getGroup( $groupId );
43        if ( !$group ) {
44            $this->dieWithError( 'apierror-translate-invalidgroup', 'invalidgroup' );
45        }
46
47        if ( !MessageChangeStorage::isValidCdbName( $name ) ) {
48            $this->dieWithError(
49                [ 'apierror-translate-invalid-changeset-name', wfEscapeWikiText( $name ) ],
50                'invalidchangeset'
51            );
52        }
53        $cdbPath = MessageChangeStorage::getCdbPath( $name );
54
55        $sourceChanges = MessageChangeStorage::getGroupChanges( $cdbPath, $groupId );
56
57        if ( $sourceChanges->getAllModifications() === [] ) {
58            $this->dieWithError( [ 'apierror-translate-smg-nochanges' ] );
59        }
60
61        $messages = $this->getPossibleRenames(
62            $sourceChanges, $group->getNamespace(), $msgKey, $group->getSourceLanguage()
63        );
64
65        $result = $this->getResult();
66        $result->addValue( [ 'query', $this->getModuleName() ], null, $messages );
67    }
68
69    /** Fetches the messages that can be used as possible renames for a given message. */
70    protected function getPossibleRenames(
71        MessageSourceChange $sourceChanges,
72        int $groupNamespace,
73        string $msgKey,
74        string $languageCode
75    ): array {
76        $deletions = $sourceChanges->getDeletions( $languageCode );
77        $targetMsg = $sourceChanges->findMessage(
78            $languageCode, $msgKey, [ MessageSourceChange::ADDITION, MessageSourceChange::RENAME ]
79        );
80        $stringComparator = new EditDistanceStringComparator();
81        $renameList = [];
82
83        // compare deleted messages with the target message and get the similarity.
84        foreach ( $deletions as $deletion ) {
85            if ( $deletion['content'] === null ) {
86                continue;
87            }
88
89            $similarity = $stringComparator->getSimilarity(
90                $deletion['content'],
91                // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
92                $targetMsg['content']
93            );
94
95            $title = Title::makeTitle(
96                $groupNamespace,
97                Utilities::title( $deletion['key'], $languageCode, $groupNamespace )
98            );
99
100            $renameList[] = [
101                'key' => $deletion['key'],
102                'content' => $deletion['content'],
103                'similarity' => $similarity,
104                'link' => $title->getFullURL(),
105                'title' => $title->getPrefixedText()
106            ];
107        }
108
109        // sort them based on similarity
110        usort( $renameList, static function ( $a, $b ) {
111            return -( $a['similarity'] <=> $b['similarity'] );
112        } );
113
114        return $renameList;
115    }
116
117    protected function getAllowedParams(): array {
118        $params = parent::getAllowedParams();
119        $params['groupId'] = [
120            ParamValidator::PARAM_TYPE => 'string',
121            ParamValidator::PARAM_REQUIRED => true,
122        ];
123
124        $params['messageKey'] = [
125            ParamValidator::PARAM_TYPE => 'string',
126            ParamValidator::PARAM_REQUIRED => true,
127        ];
128
129        $params['changesetName'] = [
130            ParamValidator::PARAM_TYPE => 'string',
131            ParamValidator::PARAM_DEFAULT => MessageChangeStorage::DEFAULT_NAME
132        ];
133
134        return $params;
135    }
136
137    protected function getExamplesMessages(): array {
138        return [
139            'action=query&meta=managemessagegroup&mmggroupId=hello
140                &mmgchangesetName=default&mmgmessageKey=world' => 'apihelp-query+managemessagegroups-example-1',
141        ];
142    }
143}