Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.02% covered (warning)
83.02%
44 / 53
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ListsHandler
83.02% covered (warning)
83.02%
44 / 53
50.00% covered (danger)
50.00%
4 / 8
16.10
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 postInitSetup
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLists
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
5
 doGetLists
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
2.09
 getResultItem
28.57% covered (danger)
28.57%
2 / 7
0.00% covered (danger)
0.00%
0 / 1
6.28
 needsWriteAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getParamSettings
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\ReadingLists\Rest;
4
5use DateTime;
6use DateTimeZone;
7use LogicException;
8use MediaWiki\Config\Config;
9use MediaWiki\Extension\ReadingLists\Doc\ReadingListRow;
10use MediaWiki\Extension\ReadingLists\ReadingListRepositoryException;
11use MediaWiki\Logger\LoggerFactory;
12use MediaWiki\Rest\Handler;
13use MediaWiki\User\CentralId\CentralIdLookup;
14use MediaWiki\Utils\MWTimestamp;
15use Psr\Log\LoggerInterface;
16use Wikimedia\ParamValidator\ParamValidator;
17use Wikimedia\Rdbms\IResultWrapper;
18use Wikimedia\Rdbms\LBFactory;
19
20/**
21 * Handle GET requests to /readinglists/v0/lists
22 *
23 * Gets reading lists
24 *
25 * We derive from Handler, not SimpleHandler, even though child classes use path parameters.
26 * This is because the child classes would need to override run() with different signatures.
27 */
28class ListsHandler extends Handler {
29    use ReadingListsHandlerTrait;
30    use ReadingListsTokenAwareHandlerTrait;
31
32    private LBFactory $dbProvider;
33
34    private Config $config;
35
36    private CentralIdLookup $centralIdLookup;
37
38    private LoggerInterface $logger;
39
40    protected bool $allowDeletedRowsInResponse = false;
41
42    // Temporarily limit paging sizes per T164990#3264314 / T168984#3659998
43    private const MAX_LIMIT = 10;
44
45    /**
46     * @param LBFactory $dbProvider
47     * @param Config $config
48     * @param CentralIdLookup $centralIdLookup
49     */
50    public function __construct(
51        LBFactory $dbProvider,
52        Config $config,
53        CentralIdLookup $centralIdLookup
54    ) {
55        $this->dbProvider = $dbProvider;
56        $this->config = $config;
57        $this->centralIdLookup = $centralIdLookup;
58        $this->logger = LoggerFactory::getInstance( 'readinglists' );
59    }
60
61    /**
62     * Create the repository data access object instance.
63     *
64     * @return void
65     */
66    public function postInitSetup() {
67        $this->repository = $this->createRepository(
68            $this->getAuthority()->getUser(), $this->dbProvider, $this->config, $this->centralIdLookup, $this->logger
69        );
70    }
71
72    /**
73     * @return array
74     */
75    public function execute() {
76        return $this->getLists( $this->getValidatedParams() );
77    }
78
79    /**
80     * Common function for getting Reading Lists.
81     *
82     * @param array $params all parameters (path and query)
83     * @return array
84     */
85    protected function getLists( array $params ): array {
86        $result = [];
87
88        $this->checkAuthority( $this->getAuthority() );
89
90        $params['sort'] = self::$sortParamMap[$params['sort']];
91        $params['dir'] = self::$sortParamMap[$params['dir']];
92        $params['next'] = $this->decodeNext( $params['next'], $params['sort'] );
93
94        // timestamp from before querying the DB
95        $timestamp = new DateTime( 'now', new DateTimeZone( 'GMT' ) );
96
97        // perform database query and get results
98        $res = $this->doGetLists( $params );
99
100        $lists = [];
101        foreach ( $res as $i => $row ) {
102            '@phan-var ReadingListRow $row';
103            $item = $this->getResultItem( $row );
104            if ( $i >= $params['limit'] ) {
105                // We reached the extra row. Create and return a "next" value that the client
106                // can send with a subsequent request for pagination.
107                $result['next'] = $this->makeNext( $item, $params['sort'], $item['name'] );
108                break;
109            }
110            $lists[$i] = $item;
111        }
112        $result['lists'] = $lists;
113
114        // Add a timestamp that, when used in the date parameter in the \lists
115        // and \entries endpoints, guarantees that no change will be skipped (at the
116        // cost of possibly repeating some changes in the current query). See T182706 for details.
117        if ( !$params['next'] ) {
118            // Ignore continuations (the client should just use the timestamp received in the
119            // first step). Otherwise, backdate more than the max transaction duration, to
120            // prevent the situation where a DB write happens before the current request
121            // but is only committed after it. (If there is no max transaction duration, the
122            // client is on its own.)
123            $maxUserDBWriteDuration = $this->config->get( 'MaxUserDBWriteDuration' );
124            if ( $maxUserDBWriteDuration ) {
125                $timestamp->modify( '-' . ( $maxUserDBWriteDuration + 1 ) . ' seconds' );
126            }
127            $syncTimestamp = ( new MWTimestamp( $timestamp ) )->getTimestamp( TS_ISO_8601 );
128            $result['continue-from'] = $syncTimestamp;
129        }
130
131        return $result;
132    }
133
134    /**
135     * Worker function for getting Reading Lists.
136     *
137     * @param array $params all parameters (path and query)
138     * @return IResultWrapper<ReadingListRow>
139     */
140    protected function doGetLists( array $params ) {
141        $repository = $this->getRepository();
142
143        try {
144            $result = $repository->getAllLists(
145                $params['sort'], $params['dir'], $params['limit'] + 1, $params['next']
146            );
147        } catch ( ReadingListRepositoryException $e ) {
148            $this->die( $e->getMessageObject() );
149        }
150
151        return $result;
152    }
153
154    /**
155     * Transform a row into an API result item
156     * @param ReadingListRow $row List row, with additions from addExtraData().
157     * @return array
158     */
159    private function getResultItem( $row ): array {
160        if ( $row->rl_deleted && !$this->allowDeletedRowsInResponse ) {
161            $this->logger->error( 'Deleted row returned in non-changes mode', [
162                'rl_id' => $row->rl_id,
163                'user_central_id' => $row->rl_user_id,
164            ] );
165            throw new LogicException( 'Deleted row returned in non-changes mode' );
166        }
167        return $this->getListFromRow( $row );
168    }
169
170    /**
171     * @return false
172     */
173    public function needsWriteAccess() {
174        return false;
175    }
176
177    /**
178     * @return array[]
179     */
180    public function getParamSettings() {
181        return [
182            'next' => [
183                self::PARAM_SOURCE => 'query',
184                ParamValidator::PARAM_TYPE => 'string',
185                ParamValidator::PARAM_REQUIRED => false,
186                ParamValidator::PARAM_DEFAULT => '',
187            ],
188        ] + $this->getSortParamSettings( self::MAX_LIMIT, 'name' );
189    }
190}