Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.81% covered (danger)
0.81%
4 / 494
1.52% covered (danger)
1.52%
1 / 66
CRAP
0.00% covered (danger)
0.00%
0 / 1
CodeRevision
0.81% covered (danger)
0.81%
4 / 494
1.52% covered (danger)
1.52%
1 / 66
27384.01
0.00% covered (danger)
0.00%
0 / 1
 newFromSvn
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
182
 getPathFragments
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 newFromRow
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 getId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIdString
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getIdStringUnique
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getRepoId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRepo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAuthor
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWikiUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMessage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStatus
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOldStatus
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCommonPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPossibleStates
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getProtectedStates
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPossibleStateMessageKeys
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeStateMessageKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPossibleFlags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isValidStatus
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isProtectedStatus
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setStatus
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
90
 insertChunks
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 save
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
240
 insertPaths
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getUniqueAffectedRevs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAffectedRevs
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getAffectedBugRevs
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 getModifiedPaths
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 isDiffable
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 previewComment
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 saveComment
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 emailNotifyUsersOfChanges
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
90
 commentData
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 threadedSortKey
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getComments
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getCommentCount
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getPropChanges
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getPropChangeUsers
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getReviewContributingUsers
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCommentingUsers
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getFollowupRevisions
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getFollowedUpRevisions
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 addReferencesFrom
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 addReferences
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addReferencesTo
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 removeReferencesFrom
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 removeReferencesTo
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getSignoffs
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 addSignoff
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 strikeSignoffs
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 getTags
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 changeTags
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
56
 normalizeTags
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 tagData
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 normalizeTag
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 isValidTag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPrevious
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 getNext
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 getPathConds
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNextUnresolved
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 getCanonicalUrl
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 sendCommentToUDP
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 sendStatusToUDP
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 sendRecentChanges
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\Extension\CodeReview\Backend;
4
5use Exception;
6use IRCColourfulRCFeedFormatter;
7use MediaWiki\MediaWikiServices;
8use MediaWiki\Permissions\Authority;
9use SpecialPage;
10use stdClass;
11use Title;
12use User;
13use Wikimedia\Rdbms\IDatabase;
14use Wikimedia\Rdbms\IResultWrapper;
15
16class CodeRevision {
17    /**
18     * Regex to match bug mentions in comments, commit summaries, etc
19     *
20     * Examples:
21     * bug 1234, bug1234, bug #1234, bug#1234
22     */
23    public const BUG_REFERENCE = '/\bbug ?#?(\d+)\b/i';
24
25    /**
26     * @var CodeRepository
27     */
28    protected $repo;
29
30    protected $repoId;
31
32    protected $id;
33
34    protected $author;
35
36    protected $timestamp;
37
38    protected $message;
39
40    protected $paths;
41
42    protected $status;
43
44    protected $oldStatus;
45
46    protected $commonPath;
47
48    /**
49     * @param CodeRepository $repo
50     * @param array $data
51     * @return CodeRevision
52     */
53    public static function newFromSvn( CodeRepository $repo, $data ) {
54        $rev = new CodeRevision();
55        $rev->repoId = $repo->getId();
56        $rev->repo = $repo;
57        $rev->id = intval( $data['rev'] );
58        $rev->author = $data['author'];
59        $rev->timestamp = wfTimestamp( TS_MW, strtotime( $data['date'] ) );
60        $rev->message = rtrim( $data['msg'] );
61        $rev->paths = $data['paths'];
62        $rev->status = 'new';
63        $rev->oldStatus = '';
64
65        $common = null;
66        if ( $rev->paths ) {
67            if ( count( $rev->paths ) == 1 ) {
68                $common = $rev->paths[0]['path'];
69            } else {
70                $first = array_shift( $rev->paths );
71                $common = explode( '/', $first['path'] );
72
73                foreach ( $rev->paths as $path ) {
74                    $compare = explode( '/', $path['path'] );
75
76                    // make sure $common is the shortest path
77                    if ( count( $compare ) < count( $common ) ) {
78                        list( $compare, $common ) = [ $common, $compare ];
79                    }
80
81                    $tmp = [];
82                    foreach ( $common as $k => $v ) {
83                        if ( $v == $compare[$k] ) {
84                            $tmp[] = $v;
85                        } else {
86                            break;
87                        }
88                    }
89                    $common = $tmp;
90                }
91                $common = implode( '/', $common );
92
93                array_unshift( $rev->paths, $first );
94            }
95
96            $rev->paths = self::getPathFragments( $rev->paths );
97        }
98        $rev->commonPath = $common;
99
100        // Check for ignored paths
101        global $wgCodeReviewDeferredPaths;
102        if ( isset( $wgCodeReviewDeferredPaths[$repo->getName()] ) ) {
103            foreach ( $wgCodeReviewDeferredPaths[$repo->getName()] as $defer ) {
104                if ( preg_match( $defer, $rev->commonPath ) ) {
105                    $rev->status = 'deferred';
106                    break;
107                }
108            }
109        }
110
111        global $wgCodeReviewAutoTagPath;
112        if ( isset( $wgCodeReviewAutoTagPath[$repo->getName()] ) ) {
113            foreach ( $wgCodeReviewAutoTagPath[$repo->getName()] as $path => $tags ) {
114                if ( preg_match( $path, $rev->commonPath ) ) {
115                    $rev->changeTags( $tags, [] );
116                    break;
117                }
118            }
119        }
120        return $rev;
121    }
122
123    /**
124     * @param array $paths
125     * @return array
126     */
127    public static function getPathFragments( $paths = [] ) {
128        $allPaths = [];
129
130        foreach ( $paths as $path ) {
131            $currentPath = '/';
132            foreach ( explode( '/', $path['path'] ) as $fragment ) {
133                if ( $currentPath !== '/' ) {
134                    $currentPath .= '/';
135                }
136
137                $currentPath .= $fragment;
138
139                if ( $currentPath == $path['path'] ) {
140                    $action = $path['action'];
141                } else {
142                    $action = 'N';
143                }
144
145                $allPaths[] = [
146                    'path' => $currentPath,
147                    'action' => $action
148                ];
149            }
150        }
151
152        return $allPaths;
153    }
154
155    /**
156     * @throws Exception
157     * @param CodeRepository $repo
158     * @param stdClass $row
159     * @return CodeRevision
160     */
161    public static function newFromRow( CodeRepository $repo, $row ) {
162        $rev = new CodeRevision();
163        $rev->repoId = intval( $row->cr_repo_id );
164        if ( $rev->repoId != $repo->getId() ) {
165            throw new Exception( 'Invalid repo ID in ' . __METHOD__ );
166        }
167        $rev->repo = $repo;
168        $rev->id = intval( $row->cr_id );
169        $rev->author = $row->cr_author;
170        $rev->timestamp = wfTimestamp( TS_MW, $row->cr_timestamp );
171        $rev->message = $row->cr_message;
172        $rev->status = $row->cr_status;
173        $rev->oldStatus = '';
174        $rev->commonPath = $row->cr_path;
175        return $rev;
176    }
177
178    /**
179     * @return int
180     */
181    public function getId() {
182        return intval( $this->id );
183    }
184
185    /**
186     * Like getId(), but returns the result as a string, including prefix,
187     * i.e. "r123" instead of 123.
188     * @param int|null $id
189     * @return string
190     */
191    public function getIdString( $id = null ) {
192        if ( $id === null ) {
193            $id = $this->getId();
194        }
195        return $this->repo->getRevIdString( $id );
196    }
197
198    /**
199     * Like getIdString(), but if more than one repository is defined
200     * on the wiki then it includes the repo name as a prefix to the revision ID
201     * (separated with a period).
202     * This ensures you get a unique reference, as the revision ID alone can be
203     * confusing (e.g. in emails, page titles etc.). If only one repository is
204     * defined then this returns the same as getIdString() as there is no ambiguity.
205     *
206     * @param int|null $id
207     * @return string
208     */
209    public function getIdStringUnique( $id = null ) {
210        if ( $id === null ) {
211            $id = $this->getId();
212        }
213        return $this->repo->getRevIdStringUnique( $id );
214    }
215
216    /**
217     * @return int
218     */
219    public function getRepoId() {
220        return intval( $this->repoId );
221    }
222
223    /**
224     * @return CodeRepository
225     */
226    public function getRepo() {
227        return $this->repo;
228    }
229
230    /**
231     * @return string
232     */
233    public function getAuthor() {
234        return $this->author;
235    }
236
237    /**
238     * @return User
239     */
240    public function getWikiUser() {
241        return $this->repo->authorWikiUser( $this->getAuthor() );
242    }
243
244    /**
245     * @return string
246     */
247    public function getTimestamp() {
248        return $this->timestamp;
249    }
250
251    /**
252     * @return string
253     */
254    public function getMessage() {
255        return $this->message;
256    }
257
258    /**
259     * @return string
260     */
261    public function getStatus() {
262        return $this->status;
263    }
264
265    /**
266     * @return string
267     */
268    public function getOldStatus() {
269        return $this->oldStatus;
270    }
271
272    /**
273     * @return string
274     */
275    public function getCommonPath() {
276        return $this->commonPath;
277    }
278
279    /**
280     * List of all possible states a CodeRevision can be in
281     * @return array
282     */
283    public static function getPossibleStates() {
284        global $wgCodeReviewStates;
285        return $wgCodeReviewStates;
286    }
287
288    /**
289     * List of all states that a user cannot set on their own revision
290     * @return array
291     */
292    public static function getProtectedStates() {
293        global $wgCodeReviewProtectedStates;
294        return $wgCodeReviewProtectedStates;
295    }
296
297    /**
298     * @return array
299     */
300    public static function getPossibleStateMessageKeys() {
301        return array_map( [ self::class, 'makeStateMessageKey' ], self::getPossibleStates() );
302    }
303
304    /**
305     * @param string $key
306     * @return string
307     */
308    private static function makeStateMessageKey( $key ) {
309        return "code-status-$key";
310    }
311
312    /**
313     * List of all flags a user can mark themself as having done to a revision
314     * @return array
315     */
316    public static function getPossibleFlags() {
317        global $wgCodeReviewFlags;
318        return $wgCodeReviewFlags;
319    }
320
321    /**
322     * Returns whether the provided status is valid
323     * @param string $status
324     * @return bool
325     */
326    public static function isValidStatus( $status ) {
327        return in_array( $status, self::getPossibleStates(), true );
328    }
329
330    /**
331     * Returns whether the provided status is protected
332     * @param string $status
333     * @return bool
334     */
335    public static function isProtectedStatus( $status ) {
336        return in_array( $status, self::getProtectedStates(), true );
337    }
338
339    /**
340     * @throws Exception
341     * @param string $status value in CodeRevision::getPossibleStates
342     * @param Authority $performer
343     * @return bool
344     */
345    public function setStatus( $status, $performer ) {
346        if ( !self::isValidStatus( $status ) ) {
347            throw new Exception( 'Tried to save invalid code revision status' );
348        }
349
350        // Don't allow the user account tied to the committer account mark
351        // their own revisions as ok/resolved
352        // Obviously only works if user accounts are tied!
353        $wikiUser = $this->getWikiUser();
354        if ( self::isProtectedStatus( $status )
355            && $wikiUser && $performer->getUser()->getName() == $wikiUser->getName()
356        ) {
357            // allow the user to review their own code if required
358            if ( !$wikiUser->isAllowed( 'codereview-review-own' ) ) {
359                return false;
360            }
361        }
362
363        // Get the old status from the primary database
364        $dbw = wfGetDB( DB_PRIMARY );
365        $this->oldStatus = $dbw->selectField(
366            'code_rev',
367            'cr_status',
368            [ 'cr_repo_id' => $this->repoId, 'cr_id' => $this->id ],
369            __METHOD__
370        );
371        if ( $this->oldStatus === $status ) {
372            // nothing to do here
373            return false;
374        }
375        // Update status
376        $this->status = $status;
377        $dbw->update(
378            'code_rev',
379            [ 'cr_status' => $status ],
380            [
381                'cr_repo_id' => $this->repoId,
382                'cr_id' => $this->id
383            ],
384            __METHOD__
385        );
386        // Log this change
387        if ( $performer && $performer->getUser()->getId() ) {
388            $dbw->insert(
389                'code_prop_changes',
390                [
391                    'cpc_repo_id'   => $this->getRepoId(),
392                    'cpc_rev_id'    => $this->getId(),
393                    'cpc_attrib'    => 'status',
394                    'cpc_removed'   => $this->oldStatus,
395                    'cpc_added'     => $status,
396                    'cpc_timestamp' => $dbw->timestamp(),
397                    'cpc_user'      => $performer->getUser()->getId(),
398                    'cpc_user_text' => $performer->getUser()->getName()
399                ],
400                __METHOD__
401            );
402        }
403
404        $this->sendStatusToUDP( $status, $this->oldStatus, $performer );
405
406        return true;
407    }
408
409    /**
410     * Quickie protection against huuuuuuuuge batch inserts
411     *
412     * @param IDatabase $db
413     * @param string $table
414     * @param array $data
415     * @param string $method
416     * @param array $options
417     * @return void
418     */
419    protected static function insertChunks(
420        $db, $table, $data, $method = __METHOD__, $options = []
421    ) {
422        $chunkSize = 100;
423        for ( $i = 0, $count = count( $data ); $i < $count; $i += $chunkSize ) {
424            $db->insert(
425                $table,
426                array_slice( $data, $i, $chunkSize ),
427                $method,
428                $options
429            );
430        }
431    }
432
433    /**
434     * @return void
435     */
436    public function save() {
437        $dbw = wfGetDB( DB_PRIMARY );
438        $dbw->startAtomic( __METHOD__ );
439        $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
440
441        $dbw->insert(
442            'code_rev',
443            [
444                'cr_repo_id' => $this->repoId,
445                'cr_id' => $this->id,
446                'cr_author' => $this->author,
447                'cr_timestamp' => $dbw->timestamp( $this->timestamp ),
448                'cr_message' => $this->message,
449                'cr_status' => $this->status,
450                'cr_path' => $this->commonPath,
451                'cr_flags' => ''
452            ],
453            __METHOD__,
454            [ 'IGNORE' ]
455        );
456
457        // Already exists? Update the row!
458        $newRevision = $dbw->affectedRows() > 0;
459        if ( !$newRevision ) {
460            $dbw->update(
461                'code_rev',
462                [
463                    'cr_author' => $this->author,
464                    'cr_timestamp' => $dbw->timestamp( $this->timestamp ),
465                    'cr_message' => $this->message,
466                    'cr_path' => $this->commonPath
467                ],
468                [
469                    'cr_repo_id' => $this->repoId,
470                    'cr_id' => $this->id
471                ],
472                __METHOD__
473            );
474        }
475
476        // Update path tracking used for output and searching
477        if ( $this->paths ) {
478            self::insertPaths( $dbw, $this->paths, $this->repoId, $this->id );
479        }
480
481        $affectedRevs = $this->getUniqueAffectedRevs();
482
483        if ( count( $affectedRevs ) ) {
484            $this->addReferencesTo( $affectedRevs );
485        }
486
487        global $wgEnableEmail, $wgCodeReviewDisableFollowUpNotification;
488        // Email the authors of revisions that this follows up on
489        if ( $wgEnableEmail && !$wgCodeReviewDisableFollowUpNotification
490            && $newRevision && count( $affectedRevs ) > 0
491        ) {
492            // Get committer wiki user name, or repo name at least
493            $commitAuthor = $this->getWikiUser();
494
495            if ( $commitAuthor ) {
496                $committer = $commitAuthor->getName();
497                $commitAuthorId = $commitAuthor->getId();
498            } else {
499                $committer = htmlspecialchars( $this->author );
500                $commitAuthorId = 0;
501            }
502
503            // Get the authors of these revisions
504            $res = $dbw->select(
505                'code_rev',
506                [
507                    'cr_repo_id',
508                    'cr_id',
509                    'cr_author',
510                    'cr_timestamp',
511                    'cr_message',
512                    'cr_status',
513                    'cr_path',
514                ],
515                [
516                    'cr_repo_id' => $this->repoId,
517                    'cr_id'      => $affectedRevs,
518                    // just in case
519                    'cr_id < ' . intval( $this->id ),
520                    // No sense in notifying if it's the same person
521                    'cr_author != ' . $dbw->addQuotes( $this->author )
522                ],
523                __METHOD__,
524                [ 'USE INDEX' => 'PRIMARY' ]
525            );
526
527            // Get repo and build comment title (for url)
528            $url = $this->getCanonicalUrl();
529
530            foreach ( $res as $row ) {
531                $revision = self::newFromRow( $this->repo, $row );
532                $users = $revision->getCommentingUsers();
533
534                $rowUrl = $revision->getCanonicalUrl();
535
536                $revisionAuthor = $revision->getWikiUser();
537
538                $revisionCommitSummary = $revision->getMessage();
539
540                // Add the followup revision author if they have not already
541                // been added as a commentor (they won't want dupe emails!)
542                if ( $revisionAuthor && !array_key_exists( $revisionAuthor->getId(), $users ) ) {
543                    $users[$revisionAuthor->getId()] = $revisionAuthor;
544                }
545
546                // Notify commenters and revision author of followup revision
547                foreach ( $users as $user ) {
548                    /**
549                     * @var $user User
550                     */
551
552                    // No sense in notifying the author of this rev if they are
553                    // a commenter/the author on the target rev
554                    if ( $commitAuthorId == $user->getId() ) {
555                        continue;
556                    }
557
558                    if ( $user->canReceiveEmail() ) {
559                        // Send message in receiver's language
560                        $lang = $userOptionsLookup->getOption( $user, 'language' );
561                        $user->sendMail(
562                            wfMessage( 'codereview-email-subj2', $this->repo->getName(),
563                                $this->getIdString( $row->cr_id ) )->inLanguage( $lang )->text(),
564                            wfMessage( 'codereview-email-body2', $committer,
565                                $this->getIdStringUnique( $row->cr_id ),
566                                $url, $this->message,
567                                $rowUrl, $revisionCommitSummary )->inLanguage( $lang )->text()
568                        );
569                    }
570                }
571            }
572        }
573
574        $dbw->endAtomic( __METHOD__ );
575    }
576
577    /**
578     * @param IDatabase $dbw
579     * @param array $paths
580     * @param int $repoId
581     * @param int $revId
582     */
583    public static function insertPaths( $dbw, $paths, $repoId, $revId ) {
584        $data = [];
585        foreach ( $paths as $path ) {
586            $data[] = [
587                'cp_repo_id' => $repoId,
588                'cp_rev_id'  => $revId,
589                'cp_path'    => $path['path'],
590                'cp_action'  => $path['action']
591            ];
592        }
593        self::insertChunks( $dbw, 'code_paths', $data, __METHOD__, [ 'IGNORE' ] );
594    }
595
596    /**
597     * Returns a unique value array from that of getAffectedRevs() and getAffectedBugRevs()
598     *
599     * @return array
600     */
601    public function getUniqueAffectedRevs() {
602        return array_unique( array_merge( $this->getAffectedRevs(), $this->getAffectedBugRevs() ) );
603    }
604
605    /**
606     * Get the revisions this commit references
607     *
608     * @return array
609     */
610    public function getAffectedRevs() {
611        $affectedRevs = [];
612        $m = [];
613        if ( preg_match_all( '/\br(\d{2,})\b/', $this->message, $m ) ) {
614            foreach ( $m[1] as $rev ) {
615                $affectedRev = intval( $rev );
616                if ( $affectedRev != $this->id ) {
617                    $affectedRevs[] = $affectedRev;
618                }
619            }
620        }
621        return $affectedRevs;
622    }
623
624    /**
625     * Parses references bugs in the comment, inserts them to code bugs, and returns an array of
626     * previous revs linking to the same bug
627     *
628     * @return array
629     */
630    public function getAffectedBugRevs() {
631        $dbw = wfGetDB( DB_PRIMARY );
632
633        // Update bug references table...
634        $affectedBugs = [];
635        $m = [];
636        if ( preg_match_all( self::BUG_REFERENCE, $this->message, $m ) ) {
637            $data = [];
638            foreach ( $m[1] as $bug ) {
639                $data[] = [
640                    'cb_repo_id' => $this->repoId,
641                    'cb_from'    => $this->id,
642                    'cb_bug'     => $bug
643                ];
644                $affectedBugs[] = intval( $bug );
645            }
646            $dbw->insert( 'code_bugs', $data, __METHOD__, [ 'IGNORE' ] );
647        }
648
649        // Also, get previous revisions that have bugs in common...
650        $affectedRevs = [];
651        if ( count( $affectedBugs ) ) {
652            $res = $dbw->select(
653                'code_bugs',
654                [ 'cb_from' ],
655                [
656                    'cb_repo_id' => $this->repoId,
657                    'cb_bug'     => $affectedBugs,
658                    // just in case
659                    'cb_from < ' . intval( $this->id ),
660                ],
661                __METHOD__,
662                [ 'USE INDEX' => 'cb_repo_id' ]
663            );
664            foreach ( $res as $row ) {
665                $affectedRevs[] = intval( $row->cb_from );
666            }
667        }
668
669        return $affectedRevs;
670    }
671
672    /**
673     * @return IResultWrapper
674     */
675    public function getModifiedPaths() {
676        $dbr = wfGetDB( DB_REPLICA );
677        return $dbr->select(
678            'code_paths',
679            [ 'cp_path', 'cp_action' ],
680            [ 'cp_repo_id' => $this->repoId, 'cp_rev_id' => $this->id ],
681            __METHOD__
682        );
683    }
684
685    /**
686     * @return bool
687     */
688    public function isDiffable() {
689        global $wgCodeReviewMaxDiffPaths;
690        $paths = $this->getModifiedPaths();
691        return $paths->numRows()
692            && ( $wgCodeReviewMaxDiffPaths > 0 && $paths->numRows() < $wgCodeReviewMaxDiffPaths );
693    }
694
695    /**
696     * @param string $text
697     * @param Authority $performer
698     * @param null $parent
699     * @return CodeComment
700     */
701    public function previewComment( $text, Authority $performer, $parent = null ) {
702        $data = $this->commentData( rtrim( $text ), $performer, $parent );
703        $data['cc_id'] = null;
704        return CodeComment::newFromData( $this, $data );
705    }
706
707    /**
708     * @param string $text
709     * @param Authority $performer
710     * @param null $parent
711     * @return int
712     */
713    public function saveComment( $text, Authority $performer, $parent = null ) {
714        $text = rtrim( $text );
715        if ( !strlen( $text ) ) {
716            return 0;
717        }
718        $dbw = wfGetDB( DB_PRIMARY );
719        $data = $this->commentData( $text, $performer, $parent );
720
721        $dbw->startAtomic( __METHOD__ );
722        $dbw->insert( 'code_comment', $data, __METHOD__ );
723        $commentId = $dbw->insertId();
724        $dbw->endAtomic( __METHOD__ );
725
726        $url = $this->getCanonicalUrl( $commentId );
727
728        $this->sendCommentToUDP( $commentId, $text, $performer, $url );
729
730        return $commentId;
731    }
732
733    /**
734     * @param Authority $performer Whoever made the changes
735     * @param string $subject
736     * @param string $body
737     * @param string|array ...$args
738     * @return void
739     */
740    public function emailNotifyUsersOfChanges( Authority $performer, $subject, $body, ...$args ) {
741        // Give email notices to committer and commenters
742        global $wgCodeReviewENotif, $wgEnableEmail, $wgCodeReviewCommentWatcherEmail,
743            $wgCodeReviewCommentWatcherName;
744        $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
745        if ( !$wgCodeReviewENotif || !$wgEnableEmail ) {
746            return;
747        }
748
749        // Make list of users to send emails to
750        $users = $this->getCommentingUsers();
751        $wikiUser = $this->getWikiUser();
752        if ( $wikiUser ) {
753            $users[$wikiUser->getId()] = $wikiUser;
754        }
755        // If we've got a spam list, send emails to it too
756        if ( $wgCodeReviewCommentWatcherEmail ) {
757            $watcher = new User();
758            $watcher->setEmail( $wgCodeReviewCommentWatcherEmail );
759            $watcher->setName( $wgCodeReviewCommentWatcherName );
760            // We don't have any anons, so using 0 is safe
761            $users[0] = $watcher;
762        }
763
764        /**
765         * @var $user User
766         */
767        foreach ( $users as $id => $user ) {
768            // No sense in notifying this commenter
769            if ( $wikiUser->getId() == $user->getId() ) {
770                continue;
771            }
772
773            // canReceiveEmail() returns false for the fake watcher user, so exempt it
774            // This is ugly
775            if ( $id == 0 || $user->canReceiveEmail() ) {
776                // Send message in receiver's language
777                $lang = $userOptionsLookup->getOption( $user, 'language' );
778
779                $localSubject = wfMessage( $subject, $this->repo->getName(), $this->getIdString() )
780                    ->inLanguage( $lang )->text();
781                $localBody = wfMessage( $body, $args )->inLanguage( $lang )->text();
782
783                $user->sendMail( $localSubject, $localBody );
784            }
785        }
786    }
787
788    /**
789     * @param string $text
790     * @param Authority $performer
791     * @param null $parent
792     * @return array
793     */
794    protected function commentData( $text, Authority $performer, $parent = null ) {
795        $dbw = wfGetDB( DB_PRIMARY );
796        $ts = wfTimestamp( TS_MW );
797        $sortkey = $this->threadedSortkey( $parent, $ts );
798        return [
799            'cc_repo_id' => $this->repoId,
800            'cc_rev_id' => $this->id,
801            'cc_text' => $text,
802            'cc_parent' => $parent,
803            'cc_user' => $performer->getUser()->getId(),
804            'cc_user_text' => $performer->getUser()->getName(),
805            'cc_timestamp' => $dbw->timestamp( $ts ),
806            'cc_sortkey' => $sortkey
807        ];
808    }
809
810    /**
811     * @throws Exception
812     * @param null $parent
813     * @param string $ts
814     * @return string
815     */
816    protected function threadedSortKey( $parent, $ts ) {
817        if ( $parent ) {
818            // We construct a threaded sort key by concatenating the timestamps
819            // of all our parent comments
820            $dbw = wfGetDB( DB_PRIMARY );
821            $parentKey = $dbw->selectField(
822                'code_comment',
823                'cc_sortkey',
824                [ 'cc_id' => $parent ],
825                __METHOD__
826            );
827            if ( $parentKey ) {
828                return $parentKey . ',' . $ts;
829            } else {
830                // hmmmm
831                throw new Exception( 'Invalid parent submission' );
832            }
833        } else {
834            return $ts;
835        }
836    }
837
838    /**
839     * @return array
840     */
841    public function getComments() {
842        $dbr = wfGetDB( DB_REPLICA );
843        $result = $dbr->select(
844            'code_comment',
845            [
846                'cc_id',
847                'cc_text',
848                'cc_user',
849                'cc_user_text',
850                'cc_timestamp',
851                'cc_sortkey' ],
852            [
853                'cc_repo_id' => $this->repoId,
854                'cc_rev_id' => $this->id
855            ],
856            __METHOD__,
857            [ 'ORDER BY' => 'cc_sortkey' ]
858        );
859        $comments = [];
860        foreach ( $result as $row ) {
861            $comments[] = CodeComment::newFromRow( $this, $row );
862        }
863        return $comments;
864    }
865
866    /**
867     * @return int
868     */
869    public function getCommentCount() {
870        $dbr = wfGetDB( DB_REPLICA );
871        $result = $dbr->select( 'code_comment',
872            [ 'cc_id' ],
873            [
874                'cc_repo_id' => $this->repoId,
875                'cc_rev_id' => $this->id ],
876            __METHOD__
877        );
878
879        if ( $result ) {
880            return intval( $result->comments );
881        } else {
882            return 0;
883        }
884    }
885
886    /**
887     * @return array
888     */
889    public function getPropChanges() {
890        $dbr = wfGetDB( DB_REPLICA );
891        $result = $dbr->select(
892            [ 'code_prop_changes', 'user' ],
893            [
894                'cpc_attrib',
895                'cpc_removed',
896                'cpc_added',
897                'cpc_timestamp',
898                'cpc_user',
899                'cpc_user_text',
900                'user_name'
901            ], [
902                'cpc_repo_id' => $this->repoId,
903                'cpc_rev_id' => $this->id,
904            ],
905            __METHOD__,
906            [ 'ORDER BY' => 'cpc_timestamp DESC' ],
907            [ 'user' => [ 'LEFT JOIN', 'cpc_user = user_id' ] ]
908        );
909        $changes = [];
910        foreach ( $result as $row ) {
911            $changes[] = CodePropChange::newFromRow( $this, $row );
912        }
913        return $changes;
914    }
915
916    /**
917     * @return array
918     */
919    public function getPropChangeUsers() {
920        $dbr = wfGetDB( DB_REPLICA );
921        $result = $dbr->select(
922            'code_prop_changes',
923            'DISTINCT(cpc_user)',
924            [
925                'cpc_repo_id' => $this->repoId,
926                'cpc_rev_id' => $this->id,
927            ],
928            __METHOD__
929        );
930        $users = [];
931        foreach ( $result as $row ) {
932            $users[$row->cpc_user] = User::newFromId( $row->cpc_user );
933        }
934        return $users;
935    }
936
937    /**
938     * "Review" being revision commenters, and people who set/removed tags and changed the status
939     *
940     * @return array
941     */
942    public function getReviewContributingUsers() {
943        return array_merge( $this->getCommentingUsers(), $this->getPropChangeUsers() );
944    }
945
946    /**
947     * @return array
948     */
949    protected function getCommentingUsers() {
950        $dbr = wfGetDB( DB_REPLICA );
951        $res = $dbr->select(
952            'code_comment',
953            'DISTINCT(cc_user)',
954            [
955                'cc_repo_id' => $this->repoId,
956                'cc_rev_id' => $this->id,
957                // users only
958                'cc_user != 0'
959            ],
960            __METHOD__
961        );
962        $users = [];
963        foreach ( $res as $row ) {
964            $users[$row->cc_user] = User::newFromId( $row->cc_user );
965        }
966        return $users;
967    }
968
969    /**
970     * Get all revisions referring to this revision (called followups of this revision in the UI).
971     *
972     * Any references from a revision to itself or from a revision to a revision in its past
973     * (i.e. with a lower revision ID) are silently dropped.
974     *
975     * @return array of code_rev database row objects
976     */
977    public function getFollowupRevisions() {
978        $refs = [];
979        $dbr = wfGetDB( DB_REPLICA );
980        $res = $dbr->select(
981            [ 'code_relations', 'code_rev' ],
982            [ 'cr_id', 'cr_status', 'cr_timestamp', 'cr_author', 'cr_message' ],
983            [
984                'cf_repo_id' => $this->repoId,
985                'cf_to' => $this->id,
986                'cr_repo_id = cf_repo_id',
987                'cr_id = cf_from'
988            ],
989            __METHOD__
990        );
991        foreach ( $res as $row ) {
992            if ( $this->id < intval( $row->cr_id ) ) {
993                $refs[] = $row;
994            }
995        }
996        return $refs;
997    }
998
999    /**
1000     * Get all revisions this revision follows up
1001     *
1002     * @return array of code_rev database row objects
1003     */
1004    public function getFollowedUpRevisions() {
1005        $refs = [];
1006        $dbr = wfGetDB( DB_REPLICA );
1007        $res = $dbr->select(
1008            [ 'code_relations', 'code_rev' ],
1009            [ 'cr_id', 'cr_status', 'cr_timestamp', 'cr_author', 'cr_message' ],
1010            [
1011                'cf_repo_id' => $this->repoId,
1012                'cf_from' => $this->id,
1013                'cr_repo_id = cf_repo_id',
1014                'cr_id = cf_to'
1015            ],
1016            __METHOD__
1017        );
1018        foreach ( $res as $row ) {
1019            if ( $this->id > intval( $row->cr_id ) ) {
1020                $refs[] = $row;
1021            }
1022        }
1023        return $refs;
1024    }
1025
1026    /**
1027     * Add references from the specified revisions to this revision. In the UI, this will
1028     * show the specified revisions as follow-ups to this one.
1029     *
1030     * This function will silently refuse to add a reference from a revision to itself or from
1031     * revisions in its past (i.e. with lower revision IDs)
1032     * @param array $revs array of revision IDs
1033     */
1034    public function addReferencesFrom( $revs ) {
1035        $data = [];
1036        foreach ( array_unique( (array)$revs ) as $rev ) {
1037            if ( $rev > $this->getId() ) {
1038                $data[] = [
1039                    'cf_repo_id' => $this->getRepoId(),
1040                    'cf_from' => $rev,
1041                    'cf_to' => $this->getId()
1042                ];
1043            }
1044        }
1045        $this->addReferences( $data );
1046    }
1047
1048    /**
1049     * @param array $data
1050     * @return void
1051     */
1052    private function addReferences( $data ) {
1053        $dbw = wfGetDB( DB_PRIMARY );
1054        $dbw->insert( 'code_relations', $data, __METHOD__, [ 'IGNORE' ] );
1055    }
1056
1057    /**
1058     * Same as addReferencesFrom(), but adds references from this revision to
1059     * the specified revisions.
1060     * @param array $revs array of revision IDs
1061     */
1062    public function addReferencesTo( $revs ) {
1063        $data = [];
1064        foreach ( array_unique( (array)$revs ) as $rev ) {
1065            if ( $rev < $this->getId() ) {
1066                $data[] = [
1067                    'cf_repo_id' => $this->getRepoId(),
1068                    'cf_from' => $this->getId(),
1069                    'cf_to' => $rev,
1070                ];
1071            }
1072        }
1073        $this->addReferences( $data );
1074    }
1075
1076    /**
1077     * Remove references from the specified revisions to this revision. In the UI, this will
1078     * no longer show the specified revisions as follow-ups to this one.
1079     * @param array $revs array of revision IDs
1080     */
1081    public function removeReferencesFrom( $revs ) {
1082        $dbw = wfGetDB( DB_PRIMARY );
1083        $dbw->delete( 'code_relations', [
1084                'cf_repo_id' => $this->getRepoId(),
1085                'cf_from' => $revs,
1086                'cf_to' => $this->getId()
1087            ], __METHOD__
1088        );
1089    }
1090
1091    /**
1092     * Remove references to the specified revisions from this revision.
1093     *
1094     * @param array $revs array of revision IDs
1095     */
1096    public function removeReferencesTo( $revs ) {
1097        $dbw = wfGetDB( DB_PRIMARY );
1098        $dbw->delete( 'code_relations', [
1099                'cf_repo_id' => $this->getRepoId(),
1100                'cf_from' => $this->getId(),
1101                'cf_to' => $revs
1102            ], __METHOD__
1103        );
1104    }
1105
1106    /**
1107     * Get all sign-offs for this revision
1108     * @param int $from DB_REPLICA or DB_PRIMARY
1109     * @return array of CodeSignoff objects
1110     */
1111    public function getSignoffs( $from = DB_REPLICA ) {
1112        $db = wfGetDB( $from );
1113        $result = $db->select(
1114            'code_signoffs',
1115            [ 'cs_user', 'cs_user_text', 'cs_flag', 'cs_timestamp', 'cs_timestamp_struck' ],
1116            [
1117                'cs_repo_id' => $this->repoId,
1118                'cs_rev_id' => $this->id,
1119            ],
1120            __METHOD__,
1121            [ 'ORDER BY' => 'cs_timestamp' ]
1122        );
1123
1124        $signoffs = [];
1125        foreach ( $result as $row ) {
1126            $signoffs[] = CodeSignoff::newFromRow( $this, $row );
1127        }
1128        return $signoffs;
1129    }
1130
1131    /**
1132     * Add signoffs for this revision
1133     * @param Authority $performer Authority object for the user who did the sign-off
1134     * @param array $flags array of flags (strings, see getPossibleFlags()). Each flag is added as
1135     *   a separate sign-off
1136     */
1137    public function addSignoff( $performer, $flags ) {
1138        $dbw = wfGetDB( DB_PRIMARY );
1139        $rows = [];
1140        foreach ( (array)$flags as $flag ) {
1141            $rows[] = [
1142                'cs_repo_id' => $this->repoId,
1143                'cs_rev_id' => $this->id,
1144                'cs_user' => $performer->getUser()->getId(),
1145                'cs_user_text' => $performer->getUser()->getName(),
1146                'cs_flag' => $flag,
1147                'cs_timestamp' => $dbw->timestamp(),
1148                'cs_timestamp_struck' => wfGetDB( DB_REPLICA )->getInfinity()
1149            ];
1150        }
1151        $dbw->insert( 'code_signoffs', $rows, __METHOD__, [ 'IGNORE' ] );
1152    }
1153
1154    /**
1155     * Strike a set of sign-offs by a given user. Any sign-offs in $ids not
1156     * by $user are silently ignored, as well as nonexistent IDs and
1157     * already-struck sign-offs.
1158     * @param Authority $performer Authority object
1159     * @param array $ids array of sign-off IDs to strike
1160     */
1161    public function strikeSignoffs( $performer, $ids ) {
1162        foreach ( $ids as $id ) {
1163            $signoff = CodeSignoff::newFromId( $this, $id );
1164            // Only allow striking own signoffs
1165            if ( $signoff && $signoff->userText === $performer->getUser()->getName() ) {
1166                $signoff->strike();
1167            }
1168        }
1169    }
1170
1171    /**
1172     * @param int $from
1173     * @return array
1174     */
1175    public function getTags( $from = DB_REPLICA ) {
1176        $db = wfGetDB( $from );
1177        $result = $db->select(
1178            'code_tags',
1179            [ 'ct_tag' ],
1180            [
1181                'ct_repo_id' => $this->repoId,
1182                'ct_rev_id' => $this->id
1183            ],
1184            __METHOD__
1185        );
1186
1187        $tags = [];
1188        foreach ( $result as $row ) {
1189            $tags[] = $row->ct_tag;
1190        }
1191        return $tags;
1192    }
1193
1194    /**
1195     * @param array $addTags
1196     * @param array $removeTags
1197     * @param Authority|null $performer
1198     */
1199    public function changeTags( $addTags, $removeTags, $performer = null ) {
1200        // Get the current tags and see what changes
1201        $tagsNow = $this->getTags( DB_PRIMARY );
1202        // Normalize our input tags
1203        $addTags = $this->normalizeTags( $addTags );
1204        $removeTags = $this->normalizeTags( $removeTags );
1205        $addTags = array_diff( $addTags, $tagsNow );
1206        $removeTags = array_intersect( $removeTags, $tagsNow );
1207        // Do the queries
1208        $dbw = wfGetDB( DB_PRIMARY );
1209        if ( $addTags ) {
1210            $dbw->insert(
1211                'code_tags',
1212                $this->tagData( $addTags ),
1213                __METHOD__,
1214                [ 'IGNORE' ]
1215            );
1216        }
1217        if ( $removeTags ) {
1218            $dbw->delete(
1219                'code_tags',
1220                [
1221                    'ct_repo_id' => $this->repoId,
1222                    'ct_rev_id'  => $this->id,
1223                    'ct_tag'     => $removeTags ],
1224                __METHOD__
1225            );
1226        }
1227        // Log this change
1228        if ( ( $removeTags || $addTags ) && $performer && $performer->getUser()->getId() ) {
1229            $dbw->insert( 'code_prop_changes',
1230                [
1231                    'cpc_repo_id'   => $this->getRepoId(),
1232                    'cpc_rev_id'    => $this->getId(),
1233                    'cpc_attrib'    => 'tags',
1234                    'cpc_removed'   => implode( ',', $removeTags ),
1235                    'cpc_added'     => implode( ',', $addTags ),
1236                    'cpc_timestamp' => $dbw->timestamp(),
1237                    'cpc_user'      => $performer->getUser()->getId(),
1238                    'cpc_user_text' => $performer->getUser()->getName()
1239                ],
1240                __METHOD__
1241            );
1242        }
1243    }
1244
1245    /**
1246     * @param array $tags
1247     * @return array
1248     */
1249    protected function normalizeTags( $tags ) {
1250        $out = [];
1251        foreach ( $tags as $tag ) {
1252            $out[] = $this->normalizeTag( $tag );
1253        }
1254        return $out;
1255    }
1256
1257    /**
1258     * @param array $tags
1259     * @return array
1260     */
1261    protected function tagData( $tags ) {
1262        $data = [];
1263        foreach ( $tags as $tag ) {
1264            if ( $tag == '' ) {
1265                continue;
1266            }
1267            $data[] = [
1268                'ct_repo_id' => $this->repoId,
1269                'ct_rev_id'  => $this->id,
1270                'ct_tag'     => $this->normalizeTag( $tag ) ];
1271        }
1272        return $data;
1273    }
1274
1275    /**
1276     * @param string $tag
1277     * @return bool
1278     */
1279    public function normalizeTag( $tag ) {
1280        $title = Title::newFromText( $tag );
1281        if ( $title ) {
1282            $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1283            return $contLang->lc( $title->getDBkey() );
1284        }
1285
1286        return false;
1287    }
1288
1289    /**
1290     * @param string $tag
1291     * @return bool
1292     */
1293    public function isValidTag( $tag ) {
1294        return ( $this->normalizeTag( $tag ) !== false );
1295    }
1296
1297    /**
1298     * @param string $path
1299     * @return bool|int
1300     */
1301    public function getPrevious( $path = '' ) {
1302        $dbr = wfGetDB( DB_REPLICA );
1303        $encId = $dbr->addQuotes( $this->id );
1304        $tables = [ 'code_rev' ];
1305        if ( $path != '' ) {
1306            $conds = $this->getPathConds( $path );
1307            $order = 'cp_rev_id DESC';
1308            $tables[] = 'code_paths';
1309        } else {
1310            $conds = [ 'cr_repo_id' => $this->repoId ];
1311            $order = 'cr_id DESC';
1312        }
1313        $conds[] = "cr_id < $encId";
1314        $row = $dbr->selectRow(
1315            $tables,
1316            'cr_id',
1317            $conds,
1318            __METHOD__,
1319            [ 'ORDER BY' => $order ]
1320        );
1321        if ( $row ) {
1322            return intval( $row->cr_id );
1323        } else {
1324            return false;
1325        }
1326    }
1327
1328    /**
1329     * @param string $path
1330     * @return bool|int
1331     */
1332    public function getNext( $path = '' ) {
1333        $dbr = wfGetDB( DB_REPLICA );
1334        $encId = $dbr->addQuotes( $this->id );
1335        $tables = [ 'code_rev' ];
1336        if ( $path != '' ) {
1337            $conds = $this->getPathConds( $path );
1338            $order = 'cp_rev_id ASC';
1339            $tables[] = 'code_paths';
1340        } else {
1341            $conds = [ 'cr_repo_id' => $this->repoId ];
1342            $order = 'cr_id ASC';
1343        }
1344        $conds[] = "cr_id > $encId";
1345        $row = $dbr->selectRow(
1346            $tables,
1347            'cr_id',
1348            $conds,
1349            __METHOD__,
1350            [ 'ORDER BY' => $order ]
1351        );
1352        if ( $row ) {
1353            return intval( $row->cr_id );
1354        } else {
1355            return false;
1356        }
1357    }
1358
1359    /**
1360     * @param string $path
1361     * @return array
1362     */
1363    protected function getPathConds( $path ) {
1364        return [
1365            'cp_repo_id' => $this->repoId,
1366            'cp_path' => $path,
1367            // join conds
1368            'cr_repo_id = cp_repo_id',
1369            'cr_id = cp_rev_id'
1370        ];
1371    }
1372
1373    /**
1374     * @param string $path
1375     * @return bool|int
1376     */
1377    public function getNextUnresolved( $path = '' ) {
1378        $dbr = wfGetDB( DB_REPLICA );
1379        $encId = $dbr->addQuotes( $this->id );
1380        $tables = [ 'code_rev' ];
1381        if ( $path != '' ) {
1382            $conds = $this->getPathConds( $path );
1383            $order = 'cp_rev_id ASC';
1384            $tables[] = 'code_paths';
1385        } else {
1386            $conds = [ 'cr_repo_id' => $this->repoId ];
1387            $order = 'cr_id ASC';
1388        }
1389        $conds[] = "cr_id > $encId";
1390        $conds['cr_status'] = [ 'new', 'fixme' ];
1391        $row = $dbr->selectRow(
1392            $tables,
1393            'cr_id',
1394            $conds,
1395            __METHOD__,
1396            [ 'ORDER BY' => $order ]
1397        );
1398        if ( $row ) {
1399            return intval( $row->cr_id );
1400        } else {
1401            return false;
1402        }
1403    }
1404
1405    /**
1406     * Get the canonical URL of a revision. Constructs a Title for this revision
1407     * along the lines of [[Special:Code/RepoName/12345#c678]] and calls getCanonicalURL().
1408     * @param string|int $commentId
1409     * @return string
1410     */
1411    public function getCanonicalUrl( $commentId = 0 ) {
1412        # Append comment ID if not null, empty string or zero
1413        $fragment = $commentId ? "c{$commentId}" : '';
1414        $title = SpecialPage::getTitleFor(
1415            'Code',
1416            $this->repo->getName() . '/' . $this->id,
1417            $fragment
1418        );
1419
1420        return $title->getCanonicalURL();
1421    }
1422
1423    /**
1424     * @param string $commentId
1425     * @param string $text
1426     * @param Authority $performer
1427     * @param null|string $url
1428     * @return void
1429     */
1430    protected function sendCommentToUDP( $commentId, $text, Authority $performer, $url = null ) {
1431        global $wgLang;
1432        if ( $url === null ) {
1433            $url = $this->getCanonicalUrl( $commentId );
1434        }
1435
1436        $line = sprintf(
1437            "%s \00314(%s)\003 \0037%s\003 \00303%s\003: \00310%s\003%s",
1438            wfMessage( 'code-rev-message' )->text(),
1439            $this->repo->getName(),
1440            $this->getIdString(),
1441            IRCColourfulRCFeedFormatter::cleanupForIRC( $performer->getUser()->getName() ),
1442            IRCColourfulRCFeedFormatter::cleanupForIRC( $wgLang->truncateForVisual( $text, 100 ) ),
1443            $url
1444        );
1445
1446        $this->sendRecentChanges( $line );
1447    }
1448
1449    /**
1450     * @param string $status
1451     * @param string $oldStatus
1452     * @param Authority $performer
1453     */
1454    protected function sendStatusToUDP( $status, $oldStatus, Authority $performer ) {
1455        $url = $this->getCanonicalUrl();
1456
1457        // Give grep a chance to find the usages:
1458        // code-status-new, code-status-fixme, code-status-reverted, code-status-resolved,
1459        // code-status-ok, code-status-deferred, code-status-old
1460        $line = sprintf(
1461            "%s \00314(%s)\00303 %s\003 %s: \00315%s\003 -> \00310%s\003%s",
1462            wfMessage( 'code-rev-status' )->text(),
1463            $this->repo->getName(),
1464            IRCColourfulRCFeedFormatter::cleanupForIRC( $performer->getUser()->getName() ),
1465            // Remove three apostrophes as they are intended for the parser
1466            str_replace(
1467                "'''",
1468                '',
1469                wfMessage(
1470                    'code-change-status',
1471                    "\0037{$this->getIdString()}\003"
1472                )->text()
1473            ),
1474            wfMessage( 'code-status-' . $oldStatus )->text(),
1475            wfMessage( 'code-status-' . $status )->text(),
1476            $url
1477        );
1478
1479        $this->sendRecentChanges( $line );
1480    }
1481
1482    /**
1483     * @param string $line
1484     */
1485    private function sendRecentChanges( $line ) {
1486        global $wgCodeReviewRC;
1487        foreach ( $wgCodeReviewRC as $rc ) {
1488            /**
1489             * @var FormattedRCFeed $engine
1490             */
1491            $engine = new $rc['formatter'];
1492            $engine->send( $rc, $line );
1493        }
1494    }
1495}