Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.87% covered (warning)
52.87%
83 / 157
55.56% covered (warning)
55.56%
5 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiSetNotificationTimestamp
53.21% covered (warning)
53.21%
83 / 156
55.56% covered (warning)
55.56%
5 / 9
194.86
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 execute
47.75% covered (danger)
47.75%
53 / 111
0.00% covered (danger)
0.00%
0 / 1
158.40
 getPageSet
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 mustBePosted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWriteMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 needsToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * API for MediaWiki 1.14+
5 *
6 * Copyright © 2012 Wikimedia Foundation and contributors
7 *
8 * @license GPL-2.0-or-later
9 * @file
10 */
11
12namespace MediaWiki\Api;
13
14use MediaWiki\Revision\RevisionStore;
15use MediaWiki\Title\Title;
16use MediaWiki\Title\TitleFactory;
17use MediaWiki\Title\TitleFormatter;
18use MediaWiki\Watchlist\WatchedItemStoreInterface;
19use Wikimedia\ParamValidator\ParamValidator;
20use Wikimedia\Rdbms\IConnectionProvider;
21use Wikimedia\Rdbms\IDBAccessObject;
22
23/**
24 * API interface for setting the wl_notificationtimestamp field
25 * @ingroup API
26 */
27class ApiSetNotificationTimestamp extends ApiBase {
28
29    /** @var ApiPageSet|null */
30    private $mPageSet = null;
31
32    private RevisionStore $revisionStore;
33    private IConnectionProvider $dbProvider;
34    private WatchedItemStoreInterface $watchedItemStore;
35    private TitleFormatter $titleFormatter;
36    private TitleFactory $titleFactory;
37
38    public function __construct(
39        ApiMain $main,
40        string $action,
41        IConnectionProvider $dbProvider,
42        RevisionStore $revisionStore,
43        WatchedItemStoreInterface $watchedItemStore,
44        TitleFormatter $titleFormatter,
45        TitleFactory $titleFactory
46    ) {
47        parent::__construct( $main, $action );
48
49        $this->dbProvider = $dbProvider;
50        $this->revisionStore = $revisionStore;
51        $this->watchedItemStore = $watchedItemStore;
52        $this->titleFormatter = $titleFormatter;
53        $this->titleFactory = $titleFactory;
54    }
55
56    public function execute() {
57        $user = $this->getUser();
58
59        if ( !$user->isRegistered() ) {
60            $this->dieWithError( 'watchlistanontext', 'notloggedin' );
61        }
62        $this->checkUserRightsAny( 'editmywatchlist' );
63
64        $params = $this->extractRequestParams();
65        $this->requireMaxOneParameter( $params, 'timestamp', 'torevid', 'newerthanrevid' );
66
67        $continuationManager = new ApiContinuationManager( $this, [], [] );
68        $this->setContinuationManager( $continuationManager );
69
70        $pageSet = $this->getPageSet();
71        if ( $params['entirewatchlist'] && $pageSet->getDataSource() !== null ) {
72            $this->dieWithError(
73                [
74                    'apierror-invalidparammix-cannotusewith',
75                    $this->encodeParamName( 'entirewatchlist' ),
76                    $pageSet->encodeParamName( $pageSet->getDataSource() )
77                ],
78                'multisource'
79            );
80        }
81
82        $dbw = $this->dbProvider->getPrimaryDatabase();
83
84        $timestamp = null;
85        if ( isset( $params['timestamp'] ) ) {
86            $timestamp = $dbw->timestamp( $params['timestamp'] );
87        }
88
89        if ( !$params['entirewatchlist'] ) {
90            $pageSet->execute();
91        }
92
93        if ( isset( $params['torevid'] ) ) {
94            if ( $params['entirewatchlist'] || $pageSet->getGoodTitleCount() > 1 ) {
95                $this->dieWithError( [ 'apierror-multpages', $this->encodeParamName( 'torevid' ) ] );
96            }
97            $titles = $pageSet->getGoodPages();
98            $title = reset( $titles );
99            if ( $title ) {
100                // XXX $title isn't actually used, can we just get rid of the previous six lines?
101                $timestamp = $this->revisionStore->getTimestampFromId(
102                    $params['torevid'],
103                    IDBAccessObject::READ_LATEST
104                );
105                if ( $timestamp ) {
106                    $timestamp = $dbw->timestamp( $timestamp );
107                } else {
108                    $timestamp = null;
109                }
110            }
111        } elseif ( isset( $params['newerthanrevid'] ) ) {
112            if ( $params['entirewatchlist'] || $pageSet->getGoodTitleCount() > 1 ) {
113                $this->dieWithError( [ 'apierror-multpages', $this->encodeParamName( 'newerthanrevid' ) ] );
114            }
115            $titles = $pageSet->getGoodPages();
116            $title = reset( $titles );
117            if ( $title ) {
118                $timestamp = null;
119                $currRev = $this->revisionStore->getRevisionById(
120                    $params['newerthanrevid'],
121                    IDBAccessObject::READ_LATEST
122                );
123                if ( $currRev ) {
124                    $nextRev = $this->revisionStore->getNextRevision(
125                        $currRev,
126                        IDBAccessObject::READ_LATEST
127                    );
128                    if ( $nextRev ) {
129                        $timestamp = $dbw->timestamp( $nextRev->getTimestamp() );
130                    }
131                }
132            }
133        }
134
135        $apiResult = $this->getResult();
136        $result = [];
137        if ( $params['entirewatchlist'] ) {
138            // Entire watchlist mode: Just update the thing and return a success indicator
139            $this->watchedItemStore->resetAllNotificationTimestampsForUser( $user, $timestamp );
140
141            $result['notificationtimestamp'] = $timestamp === null
142                ? ''
143                : wfTimestamp( TS_ISO_8601, $timestamp );
144        } else {
145            // First, log the invalid titles
146            foreach ( $pageSet->getInvalidTitlesAndReasons() as $r ) {
147                $r['invalid'] = true;
148                $result[] = $r;
149            }
150            foreach ( $pageSet->getMissingPageIDs() as $p ) {
151                $page = [];
152                $page['pageid'] = $p;
153                $page['missing'] = true;
154                $page['notwatched'] = true;
155                $result[] = $page;
156            }
157            foreach ( $pageSet->getMissingRevisionIDs() as $r ) {
158                $rev = [];
159                $rev['revid'] = $r;
160                $rev['missing'] = true;
161                $rev['notwatched'] = true;
162                $result[] = $rev;
163            }
164
165            $pages = $pageSet->getPages();
166            if ( $pages ) {
167                // Now process the valid titles
168                $this->watchedItemStore->setNotificationTimestampsForUser(
169                    $user,
170                    $timestamp,
171                    $pages
172                );
173
174                // Query the results of our update
175                $timestamps = $this->watchedItemStore->getNotificationTimestampsBatch(
176                    $user,
177                    $pages
178                );
179
180                // Now, put the valid titles into the result
181                /** @var \MediaWiki\Page\PageIdentity $page */
182                foreach ( $pages as $page ) {
183                    $ns = $page->getNamespace();
184                    $dbkey = $page->getDBkey();
185                    $r = [
186                        'ns' => $ns,
187                        'title' => $this->titleFormatter->getPrefixedText( $page ),
188                    ];
189                    if ( !$page->exists() ) {
190                        $r['missing'] = true;
191                        $title = $this->titleFactory->newFromPageIdentity( $page );
192                        if ( $title->isKnown() ) {
193                            $r['known'] = true;
194                        }
195                    }
196                    if ( isset( $timestamps[$ns] ) && array_key_exists( $dbkey, $timestamps[$ns] )
197                        && $timestamps[$ns][$dbkey] !== false
198                    ) {
199                        $r['notificationtimestamp'] = '';
200                        if ( $timestamps[$ns][$dbkey] !== null ) {
201                            $r['notificationtimestamp'] = wfTimestamp( TS_ISO_8601, $timestamps[$ns][$dbkey] );
202                        }
203                    } else {
204                        $r['notwatched'] = true;
205                    }
206                    $result[] = $r;
207                }
208            }
209
210            ApiResult::setIndexedTagName( $result, 'page' );
211        }
212        $apiResult->addValue( null, $this->getModuleName(), $result );
213
214        $this->setContinuationManager( null );
215        $continuationManager->setContinuationIntoResult( $apiResult );
216    }
217
218    /**
219     * Get a cached instance of an ApiPageSet object
220     * @return ApiPageSet
221     */
222    private function getPageSet() {
223        $this->mPageSet ??= new ApiPageSet( $this );
224
225        return $this->mPageSet;
226    }
227
228    /** @inheritDoc */
229    public function mustBePosted() {
230        return true;
231    }
232
233    /** @inheritDoc */
234    public function isWriteMode() {
235        return true;
236    }
237
238    /** @inheritDoc */
239    public function needsToken() {
240        return 'csrf';
241    }
242
243    /** @inheritDoc */
244    public function getAllowedParams( $flags = 0 ) {
245        $result = [
246            'entirewatchlist' => [
247                ParamValidator::PARAM_TYPE => 'boolean'
248            ],
249            'timestamp' => [
250                ParamValidator::PARAM_TYPE => 'timestamp'
251            ],
252            'torevid' => [
253                ParamValidator::PARAM_TYPE => 'integer'
254            ],
255            'newerthanrevid' => [
256                ParamValidator::PARAM_TYPE => 'integer'
257            ],
258            'continue' => [
259                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
260            ],
261        ];
262        if ( $flags ) {
263            $result += $this->getPageSet()->getFinalParams( $flags );
264        }
265
266        return $result;
267    }
268
269    /** @inheritDoc */
270    protected function getExamplesMessages() {
271        $title = Title::newMainPage()->getPrefixedText();
272        $mp = rawurlencode( $title );
273
274        return [
275            'action=setnotificationtimestamp&entirewatchlist=&token=123ABC'
276                => 'apihelp-setnotificationtimestamp-example-all',
277            "action=setnotificationtimestamp&titles={$mp}&token=123ABC"
278                => 'apihelp-setnotificationtimestamp-example-page',
279            "action=setnotificationtimestamp&titles={$mp}&" .
280                'timestamp=2012-01-01T00:00:00Z&token=123ABC'
281                => 'apihelp-setnotificationtimestamp-example-pagetimestamp',
282            'action=setnotificationtimestamp&generator=allpages&gapnamespace=2&token=123ABC'
283                => 'apihelp-setnotificationtimestamp-example-allpages',
284        ];
285    }
286
287    /** @inheritDoc */
288    public function getHelpUrls() {
289        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:SetNotificationTimestamp';
290    }
291}
292
293/** @deprecated class alias since 1.43 */
294class_alias( ApiSetNotificationTimestamp::class, 'ApiSetNotificationTimestamp' );