Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
RevTagStore
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 6
240
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 replaceTag
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 getLatestRevisionWithTag
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getLatestRevisionsForTags
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
42
 removeTags
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getTranslatableBundleIds
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use MediaWiki\Extension\Translate\Utilities\Utilities;
7use MediaWiki\Page\PageIdentity;
8use Wikimedia\Rdbms\ILoadBalancer;
9
10/**
11 * Class to manage revision tags for translatable bundles.
12 * @author Abijeet Patro
13 * @author Niklas Laxström
14 * @since 2022.04
15 * @license GPL-2.0-or-later
16 */
17class RevTagStore {
18    /** Indicates that a translation is fuzzy (outdated or not passing validation). */
19    public const FUZZY_TAG = 'fuzzy';
20    /** Stores the revision id of the source text which was translated. Used for showing
21     * diffs for outdated messages.
22     */
23    public const TRANSVER_PROP = 'tp:transver';
24    /** Indicates a revision of a page that can be marked for translation. */
25    public const TP_MARK_TAG = 'tp:mark';
26    /** Indicates a revision of a translatable page that is marked for translation. */
27    public const TP_READY_TAG = 'tp:tag';
28    /** Indicates a revision of a page that is a valid message bundle. */
29    public const MB_VALID_TAG = 'mb:valid';
30
31    private ILoadBalancer $loadBalancer;
32    private array $tagCache = [];
33
34    public function __construct( ILoadBalancer $loadBalancer ) {
35        $this->loadBalancer = $loadBalancer;
36    }
37
38    /** Add tag for the given revisionId, while deleting it from others */
39    public function replaceTag(
40        PageIdentity $identity,
41        string $tag,
42        int $revisionId,
43        ?array $value = null
44    ): void {
45        if ( !$identity->exists() ) {
46            return;
47        }
48
49        $articleId = $identity->getId();
50
51        $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
52        $conds = [
53            'rt_page' => $articleId,
54            'rt_type' => $tag
55        ];
56        $dbw->delete( 'revtag', $conds, __METHOD__ );
57
58        if ( $value !== null ) {
59            $conds['rt_value'] = serialize( implode( '|', $value ) );
60        }
61
62        $conds['rt_revision'] = $revisionId;
63        $dbw->insert( 'revtag', $conds, __METHOD__ );
64
65        $this->tagCache[$articleId][$tag] = $revisionId;
66    }
67
68    public function getLatestRevisionWithTag( PageIdentity $identity, string $tag ): ?int {
69        $response = $this->getLatestRevisionsForTags( $identity, $tag );
70        return $response[$tag] ?? null;
71    }
72
73    /** @return null|int[] */
74    public function getLatestRevisionsForTags( PageIdentity $identity, string ...$tags ): ?array {
75        if ( !$identity->exists() ) {
76            return null;
77        }
78
79        $articleId = $identity->getId();
80
81        $response = [];
82        $remainingTags = [];
83
84        // ATTENTION: Cache should only be updated on POST requests.
85        foreach ( $tags as $tag ) {
86            if ( isset( $this->tagCache[$articleId][$tag] ) ) {
87                $response[$tag] = $this->tagCache[$articleId][$tag];
88            } else {
89                $remainingTags[] = $tag;
90            }
91        }
92
93        if ( !$remainingTags ) {
94            // All tags were available in the cache, no need to run any queries.
95            return $response;
96        }
97
98        $dbr = Utilities::getSafeReadDB();
99        $results = $dbr->newSelectQueryBuilder()
100            ->select( [ 'rt_revision' => 'MAX(rt_revision)', 'rt_type' ] )
101            ->from( 'revtag' )
102            ->where( [
103                'rt_page' => $articleId,
104                'rt_type' => $remainingTags
105            ] )
106            ->groupBy( 'rt_type' )
107            ->caller( __METHOD__ )
108            ->fetchResultSet();
109
110        foreach ( $results as $row ) {
111            $response[$row->rt_type] = (int)$row->rt_revision;
112        }
113
114        return $response;
115    }
116
117    public function removeTags( PageIdentity $identity, string ...$tag ): void {
118        if ( !$identity->exists() ) {
119            return;
120        }
121
122        $articleId = $identity->getId();
123
124        $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
125        $conds = [
126            'rt_page' => $articleId,
127            'rt_type' => $tag,
128        ];
129        $dbw->delete( 'revtag', $conds, __METHOD__ );
130
131        unset( $this->tagCache[$articleId] );
132    }
133
134    /** Get a list of page ids where the latest revision is either tagged or marked */
135    public static function getTranslatableBundleIds( string ...$revTags ): array {
136        $dbr = Utilities::getSafeReadDB();
137        $res = $dbr->newSelectQueryBuilder()
138            ->select( 'rt_page' )
139            ->from( 'revtag' )
140            ->join(
141                'page',
142                null,
143                [ 'rt_page = page_id', 'rt_revision = page_latest', 'rt_type' => $revTags ]
144            )
145            ->groupBy( 'rt_page' )
146            ->caller( __METHOD__ )
147            ->fetchResultSet();
148        $results = [];
149        foreach ( $res as $row ) {
150            $results[$row->rt_page] = true;
151        }
152
153        return $results;
154    }
155}