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