Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
71.64% |
48 / 67 |
|
60.00% |
3 / 5 |
CRAP | |
0.00% |
0 / 1 |
ListsEntriesHandler | |
71.64% |
48 / 67 |
|
60.00% |
3 / 5 |
20.13 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
postInitSetup | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
run | |
82.35% |
28 / 34 |
|
0.00% |
0 / 1 |
7.27 | |||
getResultTitle | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
getParamSettings | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\ReadingLists\Rest; |
4 | |
5 | use MediaWiki\Config\Config; |
6 | use MediaWiki\Extension\ReadingLists\Doc\ReadingListEntryRow; |
7 | use MediaWiki\Extension\ReadingLists\ReadingListRepositoryException; |
8 | use MediaWiki\Extension\ReadingLists\ReverseInterwikiLookup; |
9 | use MediaWiki\Logger\LoggerFactory; |
10 | use MediaWiki\Rest\Handler; |
11 | use MediaWiki\Rest\SimpleHandler; |
12 | use MediaWiki\Title\TitleValue; |
13 | use MediaWiki\User\CentralId\CentralIdLookup; |
14 | use Psr\Log\LoggerInterface; |
15 | use Wikimedia\ParamValidator\ParamValidator; |
16 | use Wikimedia\Rdbms\LBFactory; |
17 | |
18 | /** |
19 | * Handle GET requests to /readinglists/v0/lists/{id}/entries |
20 | * |
21 | * Gets reading list entries |
22 | */ |
23 | class ListsEntriesHandler extends SimpleHandler { |
24 | use ReadingListsHandlerTrait; |
25 | |
26 | private LBFactory $dbProvider; |
27 | |
28 | private Config $config; |
29 | |
30 | private CentralIdLookup $centralIdLookup; |
31 | |
32 | private ReverseInterwikiLookup $reverseInterwikiLookup; |
33 | |
34 | private LoggerInterface $logger; |
35 | |
36 | private const MAX_LIMIT = 100; |
37 | |
38 | /** |
39 | * @param LBFactory $dbProvider |
40 | * @param Config $config |
41 | * @param CentralIdLookup $centralIdLookup |
42 | */ |
43 | public function __construct( |
44 | LBFactory $dbProvider, |
45 | Config $config, |
46 | CentralIdLookup $centralIdLookup, |
47 | ReverseInterwikiLookup $reverseInterwikiLookup |
48 | ) { |
49 | $this->dbProvider = $dbProvider; |
50 | $this->config = $config; |
51 | $this->centralIdLookup = $centralIdLookup; |
52 | $this->reverseInterwikiLookup = $reverseInterwikiLookup; |
53 | $this->logger = LoggerFactory::getInstance( 'readinglists' ); |
54 | } |
55 | |
56 | /** |
57 | * Create the repository data access object instance. |
58 | * |
59 | * @return void |
60 | */ |
61 | public function postInitSetup() { |
62 | $this->repository = $this->createRepository( |
63 | $this->getAuthority()->getUser(), $this->dbProvider, $this->config, $this->centralIdLookup, $this->logger |
64 | ); |
65 | } |
66 | |
67 | /** |
68 | * @param int $id the list to get entries from |
69 | * @return array |
70 | */ |
71 | public function run( int $id ) { |
72 | $resultPageSet = null; |
73 | |
74 | // The parameters are somewhat different between the RESTBase contract for this endpoint |
75 | // and the Action API endpoint that it forwards to. We mostly follow the RESTBase naming |
76 | // herein, and note where we differ from that. |
77 | $params = $this->getValidatedParams(); |
78 | |
79 | $sort = $params['sort'] ?? 'name'; |
80 | |
81 | // In the original RESTBase => Action API implementation, limit and dir were not exposed |
82 | // by RESTBase to the caller. Instead, dir was implied by sort type and limit was sent as |
83 | // the hard-coded special string 'max'. Callers who used the Action API directly could |
84 | // specify dir or limit if desired. To not lose this ability when replacing the RESTBase |
85 | // and Action API endpoints with MW REST endpoints, we extended the RESTBase contract to |
86 | // accept optional limit and parameters. Because the MW REST API does not support "limit" |
87 | // types, the special string 'max' is not allowed. |
88 | $limit = $params['limit']; |
89 | $dir = $params['dir'] ?? null; |
90 | $next = $params['next'] ?? null; |
91 | |
92 | // Action API allowed multiple lists, but RESTBase (the contract we're matching) did not. |
93 | // Exposing an equivalent while still matching the RESTBase contract would be problematic, |
94 | // because the list id is part of the path. We internally mirror the Action API code, |
95 | // which allowed multiple lists, in case we decide we need to add that ability in some |
96 | // way. But we do not expose that capability to callers. |
97 | // |
98 | // Also, Action API offered a "changedsince" parameter to query list entries by timestamp |
99 | // instead of by list. Because that was incompatible with querying by list, and because |
100 | // we now always require a list id, we eliminated the "changedsince" functionality. It was |
101 | // never exposed by RESTBase. |
102 | $lists = [ $id ]; |
103 | |
104 | $repository = $this->getRepository(); |
105 | |
106 | $this->checkAuthority( $this->getAuthority() ); |
107 | |
108 | $sort = self::$sortParamMap[$sort]; |
109 | $dir = self::$sortParamMap[$dir]; |
110 | $next = $this->decodeNext( $next, $sort ); |
111 | |
112 | $result = [ |
113 | 'entries' => [] |
114 | ]; |
115 | |
116 | try { |
117 | $res = $repository->getListEntries( $lists, $sort, $dir, $limit + 1, $next ); |
118 | } catch ( ReadingListRepositoryException $e ) { |
119 | $this->die( $e->getMessageObject() ); |
120 | } |
121 | |
122 | '@phan-var stdClass[] $res'; |
123 | $titles = []; |
124 | $fits = true; |
125 | foreach ( $res as $i => $row ) { |
126 | // @phan-suppress-next-line PhanTypeMismatchArgument |
127 | $item = $this->getListEntryFromRow( $row ); |
128 | if ( $i >= $limit ) { |
129 | $result['next'] = $this->makeNext( $item, $sort, $item['title'] ); |
130 | break; |
131 | } |
132 | if ( $resultPageSet ) { |
133 | // @phan-suppress-next-line PhanTypeMismatchArgument |
134 | $titles[] = $this->getResultTitle( $row ); |
135 | } else { |
136 | $result['entries'][] = $item; |
137 | } |
138 | if ( !$fits ) { |
139 | $result['next'] = $this->makeNext( $item, $sort, $item['title'] ); |
140 | break; |
141 | } |
142 | } |
143 | if ( $resultPageSet ) { |
144 | $resultPageSet->populateFromTitles( $titles ); |
145 | } |
146 | |
147 | return $result; |
148 | } |
149 | |
150 | /** |
151 | * Transform a row into an API result item |
152 | * @param ReadingListEntryRow $row |
153 | * @return ?TitleValue|string |
154 | */ |
155 | private function getResultTitle( $row ) { |
156 | $interwikiPrefix = $this->reverseInterwikiLookup->lookup( $row->rlp_project ); |
157 | if ( is_string( $interwikiPrefix ) ) { |
158 | if ( $interwikiPrefix === '' ) { |
159 | $title = TitleValue::tryNew( NS_MAIN, $row->rle_title ); |
160 | if ( !$title ) { |
161 | // Validation differences between wikis? Let's just return it as it is. |
162 | $title = TitleValue::tryNew( NS_MAIN, $row->rle_title ); |
163 | } |
164 | } else { |
165 | // We have no way of telling what the namespace is, but Title does not support |
166 | // foreign namespaces anyway. Let's just pretend it's in the main namespace so |
167 | // the prefixed title string works out as expected. |
168 | $title = TitleValue::tryNew( NS_MAIN, $row->rle_title, '', $interwikiPrefix ); |
169 | } |
170 | return $title; |
171 | } elseif ( is_array( $interwikiPrefix ) ) { |
172 | $title = implode( ':', array_slice( $interwikiPrefix, 1 ) ) . ':' . $row->rle_title; |
173 | $prefix = $interwikiPrefix[0]; |
174 | return TitleValue::tryNew( NS_MAIN, $title, '', $prefix ); |
175 | } |
176 | // For lack of a better option let's create an invalid title. |
177 | // ApiPageSet::populateFromTitles() is not documented to accept strings |
178 | // but it will actually work. |
179 | return 'Invalid project|' . $row->rlp_project . '|' . $row->rle_title; |
180 | } |
181 | |
182 | /** |
183 | * @return array[] |
184 | */ |
185 | public function getParamSettings() { |
186 | return [ |
187 | 'id' => [ |
188 | ParamValidator::PARAM_TYPE => 'integer', |
189 | ParamValidator::PARAM_REQUIRED => true, |
190 | Handler::PARAM_SOURCE => 'path', |
191 | ], |
192 | 'next' => [ |
193 | self::PARAM_SOURCE => 'query', |
194 | ParamValidator::PARAM_TYPE => 'string', |
195 | ParamValidator::PARAM_REQUIRED => false, |
196 | ], |
197 | ] + $this->getSortParamSettings( self::MAX_LIMIT, 'name' ); |
198 | } |
199 | } |