Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 128
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
WikifunctionsRecentChangesInsertJob
0.00% covered (danger)
0.00%
0 / 128
0.00% covered (danger)
0.00%
0 / 2
552
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
 run
0.00% covered (danger)
0.00%
0 / 124
0.00% covered (danger)
0.00%
0 / 1
506
1<?php
2
3/**
4 * @file
5 * @ingroup Extensions
6 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
7 * @license MIT
8 */
9
10namespace MediaWiki\Extension\WikiLambda\Jobs;
11
12use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry;
13use MediaWiki\Extension\WikiLambda\WikiLambdaServices;
14use MediaWiki\JobQueue\GenericParameterJob;
15use MediaWiki\JobQueue\Job;
16use MediaWiki\Logger\LoggerFactory;
17use MediaWiki\MediaWikiServices;
18use MediaWiki\RecentChanges\RecentChange;
19use MediaWiki\Title\Title;
20use MediaWiki\User\CentralId\CentralIdLookup;
21use Psr\Log\LoggerInterface;
22
23/**
24 * Foo
25 */
26class WikifunctionsRecentChangesInsertJob extends Job implements GenericParameterJob {
27
28    /**
29     * @var string
30     */
31    public const SRC_WIKIFUNCTIONS = 'wf';
32
33    private LoggerInterface $logger;
34    private ?CentralIdLookup $centralIdLookup;
35
36    /**
37     * @var array Array of job parameters
38     * @phan-var array<string,mixed>
39     */
40    public $params;
41
42    public function __construct( array $params ) {
43        // This job, triggered from RecentChanges activity, takes the edit and fans it out to each
44        // relevant client wiki to process locally.
45
46        parent::__construct( 'wikifunctionsRecentChangesInsert', $params );
47        $this->logger = LoggerFactory::getInstance( 'WikiLambdaClient' );
48
49        // We use a CentralIdLookupFactory if configured to convert the repo wiki's user IDs to our local wiki's
50        // ones. If null, we assume this wiki is not connected to a central system.
51        $this->centralIdLookup = MediaWikiServices::getInstance()->getCentralIdLookupFactory()->getNonLocalLookup();
52
53        $this->params = $params;
54    }
55
56    public function run(): bool {
57        // What pages would the job be updating?
58        $wikifunctionsClientStore = WikiLambdaServices::getWikifunctionsClientStore();
59        $pagesUsingFunction = $wikifunctionsClientStore->fetchWikifunctionsUsage( $this->params['target'] );
60
61        $this->logger->debug(
62            __CLASS__ . ': Processing a change for {targetZObject}',
63            [
64                'targetZObject' => $this->params['target']
65            ]
66        );
67
68        // Work out whether the job is still needed
69        if ( count( $pagesUsingFunction ) === 0 ) {
70            // We were triggered by the repo, but we aren't using that Function.
71            // Note: Until T385630 is done, this is acting-as-expected, and shouldn't be a source of concern.
72            $this->logger->debug(
73                __CLASS__ . ' triggered for {item} but it is unused; T385630 would avoid this',
74                [
75                    'item' => $this->params['target'],
76                ]
77            );
78            return true;
79        }
80
81        $services = MediaWikiServices::getInstance();
82        $dbw = $services->getConnectionProvider()->getPrimaryDatabase();
83        $commentStore = $services->getCommentStore();
84
85        // Build the RecentChange attributes common to all entries regardless of page on which it's used
86        $generalAttributes = [
87            'rc_source' => self::SRC_WIKIFUNCTIONS,
88
89            // Our standard flags, invariant between changes: never minor or deletes or creates
90            'rc_minor' => false,
91            'rc_deleted' => false,
92
93            // There's no patrollable state for this change entry, as it doesn't take place on this wiki
94            'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED,
95
96            // Log-related things, all set empty (this is not a log entry)
97            'rc_logid' => 0,
98            'rc_log_type' => null,
99            'rc_log_action' => '',
100            'rc_params' => '',
101
102            // Update-specific stuff
103            'rc_bot' => $this->params['bot'],
104            'rc_timestamp' => wfTimestamp( TS_MW, $this->params['timestamp'] ),
105        ];
106
107        $changeData = $this->params['data'];
108        $changeAction = $changeData['action'];
109
110        if ( !$changeAction || !in_array( $changeAction, [ 'delete', 'restore', 'edit' ] ) ) {
111            $this->logger->warning(
112                __CLASS__ . ' triggered for {item} with unrecognised action {action}; data error?',
113                [
114                    'item' => $this->params['target'],
115                    'action' => var_export( $this->params ),
116                    // 'action' => $changeAction,
117                ]
118            );
119            return true;
120        }
121
122        if ( $changeAction !== 'edit' ) {
123            switch ( $changeData['type'] ) {
124                case ZTypeRegistry::Z_FUNCTION:
125                    // Used messages:
126                    // - wikilambda-recentchanges-explanation-delete-function
127                    // - wikilambda-recentchanges-explanation-restore-function
128                    $changeData['message'] = "wikilambda-recentchanges-explanation-$changeAction-function";
129                    break;
130
131                case ZTypeRegistry::Z_IMPLEMENTATION:
132                    // Used messages:
133                    // - wikilambda-recentchanges-explanation-delete-implementation
134                    // - wikilambda-recentchanges-explanation-restore-implementation
135                    $changeData['message'] = "wikilambda-recentchanges-explanation-$changeAction-implementation";
136                    $changeData['messageParams'] = [ $changeData['target'] ];
137                    break;
138
139                case ZTypeRegistry::Z_TESTER:
140                    // Used messages:
141                    // - wikilambda-recentchanges-explanation-delete-tester
142                    // - wikilambda-recentchanges-explanation-restore-tester
143                    $changeData['message'] = "wikilambda-recentchanges-explanation-$changeAction-tester";
144                    $changeData['messageParams'] = [ $changeData['target'] ];
145                    break;
146
147                default:
148                    // Unrecognised type; just exit, and log for follow-up
149                    $this->logger->warning(
150                        __CLASS__ . ' triggered for {item} deletion/undeletion with unknown type {type}; data error?',
151                        [
152                            'item' => $this->params['target'],
153                            'action' => $changeData['type'],
154                        ]
155                    );
156                    return true;
157            }
158        } else {
159            // Note: We just pick the first of multiple operations, as that's what the UX allows you to do. However, if
160            // e.g. someone did an API edit that added some Implementations & removed some Testers, we'll show only one.
161            $operations = $changeData['operations'];
162
163            switch ( $changeData['type'] ) {
164                case ZTypeRegistry::Z_FUNCTION:
165                    // Changes to Functions are complex – direct errors, and changes to approved Implementations/Testers
166                    $actionPath = array_key_first( $operations );
167
168                    switch ( $actionPath ) {
169                        case ZTypeRegistry::Z_FUNCTION_IMPLEMENTATIONS:
170                        case ZTypeRegistry::Z_FUNCTION_TESTERS:
171                            $typeTouched = ( $actionPath === ZTypeRegistry::Z_FUNCTION_IMPLEMENTATIONS )
172                                ? 'implementations' : 'testers';
173                            $action = array_key_first( $operations[$actionPath] );
174                            $affected = $operations[$actionPath][$action];
175
176                            if ( $action === 'add' ) {
177                                $changeAction = 'connect';
178                            } elseif ( $action === 'remove' ) {
179                                $changeAction = 'disconnect';
180                            }
181
182                            $lang = $services->getContentLanguage();
183
184                            // Used messages:
185                            // - wikilambda-recentchanges-explanation-connect-implementation
186                            // - wikilambda-recentchanges-explanation-disconnect-implementation
187                            // - wikilambda-recentchanges-explanation-connect-tester
188                            // - wikilambda-recentchanges-explanation-disconnect-tester
189                            $changeData['message'] = "wikilambda-recentchanges-explanation-$changeAction-$typeTouched";
190                            $changeData['messageParams'] = [ count( $affected ), $lang->listToText( $affected ) ];
191                            break;
192
193                        default:
194                            // The edit was to something other than the approved Implementations or Testers; use generic
195                            $changeData['message'] = 'wikilambda-recentchanges-explanation-edit-function';
196                            break;
197                    }
198                    break;
199
200                case ZTypeRegistry::Z_IMPLEMENTATION:
201                    $changeData['message'] = 'wikilambda-recentchanges-explanation-edit-implementation';
202                    break;
203
204                case ZTypeRegistry::Z_TESTER:
205                    $changeData['message'] = 'wikilambda-recentchanges-explanation-edit-tester';
206                    break;
207
208                default:
209                    // Unrecognised type; just exit, and log for follow-up
210                    $this->logger->warning(
211                        __CLASS__ . ' triggered for {item} with unrecognised type {type}; data error?',
212                        [
213                            'item' => $this->params['target'],
214                            'action' => $changeData['type'],
215                        ]
216                    );
217                    return true;
218            }
219        }
220
221        $comment = $commentStore->createComment( $dbw, $this->params['summary'] ?? '' );
222
223        // Ideally we'd ask the CommentStore if it has an existing Comment ID for this string and re-use that, but
224        // that facility isn't available, so we'll just insert the raw string as a new field and let RC deal.
225        $generalAttributes['rc_comment'] = $comment->text;
226
227        // Ask CentralAuth for the user ID lookup, if available.
228        $generalAttributes['rc_actor'] = 0;
229        if ( $this->centralIdLookup ) {
230            $localUser = $this->centralIdLookup->localUserFromCentralId( $this->params['user'] );
231            if ( $localUser ) {
232                $generalAttributes['rc_actor'] = $localUser->getId();
233            }
234            // If there's no local user, we'll fall back to id 0
235        } else {
236            // Assume the user is a local one, and use their ID directly
237            $generalAttributes['rc_actor'] = $this->params['user'];
238        }
239
240        // We can't stuff non-strings into the rc_params field, so we need to JSON-ify it
241        $generalAttributes['rc_params'] = json_encode( $changeData );
242
243        foreach ( $pagesUsingFunction as $titleString ) {
244            $title = Title::newFromText( $titleString );
245
246            $titleSpecificAttribs = [
247                'rc_namespace' => $title->getNamespace(),
248                'rc_title' => $title->getDBkey(),
249                // As we're not adding an edit, we just re-use the most recent edit ID for the page
250                'rc_cur_id' => $title->getArticleID(),
251
252                // We're not changing the page, just faking it, so
253                // … old and new lengths are the same, and …
254                'rc_old_len' => $title->getLength(),
255                'rc_new_len' => $title->getLength(),
256
257                // … old and new revisions are the also same
258                'rc_this_oldid' => $title->getLatestRevID(),
259                'rc_last_oldid' => $title->getLatestRevID(),
260            ];
261
262            $changeAttributes = $generalAttributes + $titleSpecificAttribs;
263
264            $this->logger->debug(
265                __CLASS__ . ': Inserting a RecentChange for {targetZObject} on page {target}',
266                [
267                    'targetZObject' => $this->params['target'],
268                    'target' => $titleString
269                ]
270            );
271
272            $changeEntry = new RecentChange();
273            $changeEntry->setAttribs( $changeAttributes );
274            $changeEntry->setExtra( $changeData );
275            $changeEntry->save();
276        }
277
278        return true;
279    }
280}