Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.64% covered (warning)
71.64%
48 / 67
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ListsEntriesHandler
71.64% covered (warning)
71.64%
48 / 67
60.00% covered (warning)
60.00%
3 / 5
20.13
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
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
 run
82.35% covered (warning)
82.35%
28 / 34
0.00% covered (danger)
0.00%
0 / 1
7.27
 getResultTitle
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 getParamSettings
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\ReadingLists\Rest;
4
5use MediaWiki\Config\Config;
6use MediaWiki\Extension\ReadingLists\Doc\ReadingListEntryRow;
7use MediaWiki\Extension\ReadingLists\ReadingListRepositoryException;
8use MediaWiki\Extension\ReadingLists\ReverseInterwikiLookup;
9use MediaWiki\Logger\LoggerFactory;
10use MediaWiki\Rest\Handler;
11use MediaWiki\Rest\SimpleHandler;
12use MediaWiki\Title\TitleValue;
13use MediaWiki\User\CentralId\CentralIdLookup;
14use Psr\Log\LoggerInterface;
15use Wikimedia\ParamValidator\ParamValidator;
16use Wikimedia\Rdbms\LBFactory;
17
18/**
19 * Handle GET requests to /readinglists/v0/lists/{id}/entries
20 *
21 * Gets reading list entries
22 */
23class 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}