Gergő Tisza has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/366980 )

Change subject: [WIP] API
......................................................................

[WIP] API

Adds API endpoints:
* action=readinglists
* meta=readinglists
* list=readinglistentries
* list=readinglistchanges

TODO:
* return list/entry order
* add i18n/doc boilerplate
* return ids from write modules
* fix useless sync return format
* prevent adding/removing entries from deleted lists?
* review DB index coverage
* use non-offset-based continuation where possible
* T164990#3460750
* turn ApiQueryReadingListEntries into a generator
  * create ReverseInterwikiLookup (cf InterwikiLookup::getAllPrefixes)

Longer term TODO: how will the REST API use tokens?

Bug: T168980
Bug: T168988
Change-Id: I3615581fb06c599dea50877d3678d1ba3e82bb20
---
M extension.json
M i18n/en.json
M i18n/qqq.json
A maintenance/purge.php
A src/Api/ApiQueryReadingListChanges.php
A src/Api/ApiQueryReadingListEntries.php
A src/Api/ApiQueryReadingLists.php
A src/Api/ApiReadingLists.php
A src/Api/ApiReadingListsCreate.php
A src/Api/ApiReadingListsCreateEntry.php
A src/Api/ApiReadingListsDelete.php
A src/Api/ApiReadingListsDeleteEntry.php
A src/Api/ApiReadingListsOrder.php
A src/Api/ApiReadingListsOrderEntry.php
A src/Api/ApiReadingListsSetup.php
A src/Api/ApiReadingListsTeardown.php
A src/Api/ApiReadingListsUpdate.php
A src/Api/ApiTrait.php
M src/ReadingListRepositoryException.php
19 files changed, 1,631 insertions(+), 4 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/ReadingLists 
refs/changes/80/366980/1

diff --git a/extension.json b/extension.json
index 3049a86..c0c04cb 100644
--- a/extension.json
+++ b/extension.json
@@ -12,7 +12,43 @@
                "MediaWiki\\Extensions\\ReadingLists\\HookHandler": 
"src/HookHandler.php",
                "MediaWiki\\Extensions\\ReadingLists\\Utils": "src/Utils.php",
                "MediaWiki\\Extensions\\ReadingLists\\ReadingListRepository": 
"src/ReadingListRepository.php",
-               
"MediaWiki\\Extensions\\ReadingLists\\ReadingListRepositoryException": 
"src/ReadingListRepositoryException.php"
+               
"MediaWiki\\Extensions\\ReadingLists\\ReadingListRepositoryException": 
"src/ReadingListRepositoryException.php",
+               
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiQueryReadingListChanges": 
"src/Api/ApiQueryReadingListChanges.php",
+               
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiQueryReadingListEntries": 
"src/Api/ApiQueryReadingListEntries.php",
+               
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiQueryReadingLists": 
"src/Api/ApiQueryReadingLists.php",
+               "MediaWiki\\Extensions\\ReadingLists\\Api\\ApiReadingLists": 
"src/Api/ApiReadingLists.php",
+               
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiReadingListsCreateEntry": 
"src/Api/ApiReadingListsCreateEntry.php",
+               
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiReadingListsCreate": 
"src/Api/ApiReadingListsCreate.php",
+               
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiReadingListsDeleteEntry": 
"src/Api/ApiReadingListsDeleteEntry.php",
+               
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiReadingListsDelete": 
"src/Api/ApiReadingListsDelete.php",
+               
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiReadingListsOrderEntry": 
"src/Api/ApiReadingListsOrderEntry.php",
+               
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiReadingListsOrder": 
"src/Api/ApiReadingListsOrder.php",
+               
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiReadingListsSetup": 
"src/Api/ApiReadingListsSetup.php",
+               
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiReadingListsTeardown": 
"src/Api/ApiReadingListsTeardown.php",
+               
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiReadingListsUpdate": 
"src/Api/ApiReadingListsUpdate.php",
+               "MediaWiki\\Extensions\\ReadingLists\\Api\\ApiTrait": 
"src/Api/ApiTrait.php"
+       },
+       "APIModules": {
+               "readinglists": 
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiReadingLists"
+       },
+       "APIListModules": {
+               "readinglistentries": {
+                       "class": 
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiQueryReadingListEntries",
+                       "factory": 
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiQueryReadingListEntries::factory"
+               },
+               "readinglistchanges": {
+                       "class": 
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiQueryReadingListChanges",
+                       "factory": 
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiQueryReadingListChanges::factory"
+               }
+       },
+       "APIMetaModules": {
+               "readinglists": {
+                       "class": 
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiQueryReadingLists",
+                       "factory": 
"MediaWiki\\Extensions\\ReadingLists\\Api\\ApiQueryReadingLists::factory"
+               }
+       },
+       "ConfigRegistry": {
+               "ReadingLists": "GlobalVarConfig::newInstance"
        },
        "config": {
                "ReadingListsCluster": {
diff --git a/i18n/en.json b/i18n/en.json
index acdeb77..3e9a795 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -18,5 +18,6 @@
        "readinglists-db-error-empty-list-ids": "List ids parameter must not be 
empty.",
        "readinglists-db-error-empty-order": "Order parameter must not be 
empty.",
        "readinglists-db-error-entry-not-in-list": "List entry $1 does not 
belong to this list.",
-       "readinglists-db-error-user-required": "This method cannot be called 
without specifying the user."
+       "readinglists-db-error-user-required": "This method cannot be called 
without specifying the user.",
+       "readinglists-apierror-project-title-param": "<var>project</var> and 
<var>title</var> must be used together."
 }
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 7db2849..9cd3c5c 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -18,5 +18,6 @@
        "readinglists-db-error-empty-list-ids": "Error message used when 
querying list entries without specifying any lists.",
        "readinglists-db-error-empty-order": "Error message used when trying to 
set the order of lists or list entries but passing an empty order array.",
        "readinglists-db-error-entry-not-in-list": "Error message used when 
trying to set the order of list entries but some of them do not belong to the 
list.",
-       "readinglists-db-error-user-required": "Error message used when calling 
a method that operates on a single user, but the user was not specified when 
the repository object was constructed."
+       "readinglists-db-error-user-required": "Error message used when calling 
a method that operates on a single user, but the user was not specified when 
the repository object was constructed.",
+       "readinglists-apierror-project-title-param": "{{doc-apierror}}"
 }
diff --git a/maintenance/purge.php b/maintenance/purge.php
new file mode 100644
index 0000000..e2ecdd6
--- /dev/null
+++ b/maintenance/purge.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Maintenance;
+
+use CentralIdLookup;
+use Maintenance;
+use MediaWiki\Extensions\ReadingLists\ReadingListRepository;
+use MediaWiki\Extensions\ReadingLists\Utils;
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
+use User;
+
+require_once getenv( 'MW_INSTALL_PATH' ) !== false
+       ? getenv( 'MW_INSTALL_PATH' ) . '/maintenance/Maintenance.php'
+       : __DIR__ . '/../../../maintenance/Maintenance.php';
+
+/**
+ * Maintenance script for purging unneeded DB rows (deleted lists/entries or 
orphaned sortkeys).
+ * Purging deleted lists/entries limits clients' ability to sync deletes.
+ * Purging orphaned sortkeys has no user-visible effect.
+ * @ingroup Maintenance
+ */
+class Purge extends Maintenance {
+
+       public function __construct() {
+               parent::__construct();
+               $this->addDescription(
+                       'Purge unneeded database rows (deleted lists/entries or 
orphaned sortkeys).' );
+               $this->addOption( 'before', 'Purge deleted lists/entries before 
this timestamp', true, true );
+       }
+
+       /**
+        * @inheritdoc
+        */
+       public function execute() {
+               $now = wfTimestampNow();
+               $before = wfTimestamp( TS_MW, $this->getOption( 'before' ) );
+               if ( !$before || $now <= $before ) {
+                       // Let's not delete all rows if the user entered an 
invalid timestamp.
+                       $this->error( 'Invalid timestamp', 1 );
+               }
+               $this->output( "...puring deleted rows\n" );
+               $this->getReadingListRepository()->purgeOldDeleted( $before );
+               $this->output( "...puring orphaned sortkeys\n" );
+               $this->getReadingListRepository()->purgeSortkeys();
+               $this->output( "done.\n" );
+       }
+
+       /**
+        * Initializes the repository.
+        * @return ReadingListRepository
+        */
+       private function getReadingListRepository() {
+               $services = MediaWikiServices::getInstance();
+               $loadBalancerFactory = $services->getDBLoadBalancerFactory();
+               $dbw = Utils::getDB( DB_MASTER, $services );
+               $dbr = Utils::getDB( DB_REPLICA, $services );
+               $user = User::newSystemUser( 'Maintenance script', [ 'steal' => 
true ] );
+               // There isn't really any way for this user to be non-local, 
but let's be future-proof.
+               $centralId = 
CentralIdLookup::factory()->centralIdFromLocalUser( $user );
+               $repository = new ReadingListRepository( $centralId, $dbw, 
$dbr, $loadBalancerFactory );
+               $repository->setLogger( LoggerFactory::getInstance( 
'readinglists' ) );
+               return $repository;
+       }
+
+}
+
+$maintClass = Purge::class;
+require_once RUN_MAINTENANCE_IF_MAIN;
diff --git a/src/Api/ApiQueryReadingListChanges.php 
b/src/Api/ApiQueryReadingListChanges.php
new file mode 100644
index 0000000..baf0269
--- /dev/null
+++ b/src/Api/ApiQueryReadingListChanges.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Api;
+
+use ApiQueryBase;
+use MediaWiki\Extensions\ReadingLists\Doc\ReadingListAndEntryRow;
+use MediaWiki\Extensions\ReadingLists\ReadingListRepositoryException;
+
+/**
+ * API list module for syncing reading list changes.
+ */
+class ApiQueryReadingListChanges extends ApiQueryBase {
+
+       use ApiTrait;
+
+       /** @var string API module prefix */
+       private static $prefix = 'rlc';
+
+       /**
+        * @inheritdoc
+        * @return void
+        */
+       public function execute() {
+               try {
+                       if ( $this->getUser()->isAnon() ) {
+                               $this->dieWithError( [ 
'apierror-mustbeloggedin',
+                                       $this->msg( 'action-viewmyprivateinfo' 
) ], 'notloggedin' );
+                       }
+                       $this->checkUserRightsAny( 'viewmyprivateinfo' );
+
+                       $since = $this->getParameter( 'since' );
+                       $limit = $this->getParameter( 'limit' );
+                       $offset = $this->getParameter( 'offset' );
+
+                       $path = [ 'query', $this->getModuleName() ];
+                       $result = $this->getResult();
+                       $result->addIndexedTagName( $path, 'change' );
+
+                       $res = $this->getReadingListRepository( 
$this->getUser() )
+                               ->getListsByDateUpdated( $since, $limit + 1, 
$offset );
+                       $nextOffset = 0;
+                       $fits = true;
+                       foreach ( $res as $row ) {
+                               if ( !$fits || $nextOffset === $limit ) {
+                                       $this->setContinueEnumParameter( 
'offset', $nextOffset );
+                                       break;
+                               }
+                               $nextOffset++;
+                               // FIXME not a useful format for clients
+                               $fits = $result->addValue( $path, null, 
$this->getResultItem( $row, $since ) );
+                       }
+               } catch ( ReadingListRepositoryException $e ) {
+                       $this->dieWithException( $e );
+               }
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getAllowedParams() {
+               return [
+                       'since' => [
+                               self::PARAM_TYPE => 'timestamp',
+                               self::PARAM_REQUIRED => true,
+                       ],
+                       'limit' => [
+                               self::PARAM_DFLT => 'max',
+                               self::PARAM_TYPE => 'limit',
+                               self::PARAM_MIN => 1,
+                               self::PARAM_MAX => self::LIMIT_BIG1,
+                               self::PARAM_MAX2 => self::LIMIT_BIG2,
+                       ],
+                       'offset' => [
+                               self::PARAM_TYPE => 'integer',
+                               self::PARAM_DFLT => 0,
+                               self::PARAM_HELP_MSG => 
'api-help-param-continue',
+                       ],
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       public function getHelpUrls() {
+               return [
+                       
'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API',
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getExamplesMessages() {
+               return [
+
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isInternal() {
+               // ReadingLists API is still experimental
+               return true;
+       }
+
+       /**
+        * Transform a row into an API result item
+        * @param ReadingListAndEntryRow $row
+        * @param string $since 'since' API parameter
+        * @return array
+        */
+       private function getResultItem( $row, $since ) {
+               $listChanged = wfTimestamp( TS_UNIX, $row->rl_date_updated )
+                                          > wfTimestamp( TS_UNIX, $since );
+               $entryChanged = wfTimestamp( TS_UNIX, $row->rle_date_updated )
+                                          > wfTimestamp( TS_UNIX, $since );
+               $data = [
+                       'listId' => $row->rle_rl_id,
+                       'entryid' => $row->rle_id,
+                       'project' => $row->rle_project,
+                       'title' => $row->rle_title,
+                       'entryCreated' => $row->rle_date_created,
+                       'entryUpdated' => $row->rle_date_updated,
+                       'entryDeleted' => $row->rle_deleted,
+               ];
+               if ( $listChanged ) {
+                       $data += [
+                               'name' => $row->rl_name,
+                               'default' => $row->rl_is_default,
+                               'description' => $row->rl_description,
+                               'color' => $row->rl_color,
+                               'image' => $row->rl_image,
+                               'icon' => $row->rl_icon,
+                               'listCreated' => $row->rl_date_created,
+                               'listUpdated' => $row->rl_date_updated,
+                               'listDeleted' => $row->rl_deleted,
+                       ];
+               }
+               return $data;
+       }
+
+}
diff --git a/src/Api/ApiQueryReadingListEntries.php 
b/src/Api/ApiQueryReadingListEntries.php
new file mode 100644
index 0000000..a5a8bdc
--- /dev/null
+++ b/src/Api/ApiQueryReadingListEntries.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Api;
+
+use ApiQueryBase;
+use MediaWiki\Extensions\ReadingLists\Doc\ReadingListEntryRow;
+use MediaWiki\Extensions\ReadingLists\ReadingListRepositoryException;
+
+/**
+ * API list module for getting list contents.
+ */
+class ApiQueryReadingListEntries extends ApiQueryBase {
+
+       use ApiTrait;
+
+       /** @var string API module prefix */
+       private static $prefix = 'rle';
+
+       /**
+        * @inheritdoc
+        * @return void
+        */
+       public function execute() {
+               try {
+                       if ( $this->getUser()->isAnon() ) {
+                               $this->dieWithError( [ 
'apierror-mustbeloggedin',
+                                       $this->msg( 'action-viewmyprivateinfo' 
) ], 'notloggedin' );
+                       }
+                       $this->checkUserRightsAny( 'viewmyprivateinfo' );
+
+                       $lists = $this->getParameter( 'lists' );
+                       $limit = $this->getParameter( 'limit' );
+                       $offset = $this->getParameter( 'offset' );
+
+                       $path = [ 'query', $this->getModuleName() ];
+                       $result = $this->getResult();
+                       $result->addIndexedTagName( $path, 'entry' );
+
+                       $res = $this->getReadingListRepository( 
$this->getUser() )
+                               ->getListEntries( $lists, $limit + 1, $offset );
+                       $nextOffset = 0;
+                       $fits = true;
+                       foreach ( $res as $row ) {
+                               if ( !$fits || $nextOffset === $limit ) {
+                                       $this->setContinueEnumParameter( 
'offset', $nextOffset );
+                                       break;
+                               }
+                               $nextOffset++;
+                               $fits = $result->addValue( $path, null, 
$this->getResultItem( $row ) );
+                       }
+               } catch ( ReadingListRepositoryException $e ) {
+                       $this->dieWithException( $e );
+               }
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getAllowedParams() {
+               return [
+                       'lists' => [
+                               self::PARAM_REQUIRED => true,
+                               self::PARAM_TYPE => 'integer',
+                               self::PARAM_ISMULTI => true,
+                       ],
+                       'limit' => [
+                               self::PARAM_DFLT => 'max',
+                               self::PARAM_TYPE => 'limit',
+                               self::PARAM_MIN => 1,
+                               self::PARAM_MAX => self::LIMIT_BIG1,
+                               self::PARAM_MAX2 => self::LIMIT_BIG2,
+                       ],
+                       'offset' => [
+                               self::PARAM_TYPE => 'integer',
+                               self::PARAM_DFLT => 0,
+                               self::PARAM_HELP_MSG => 
'api-help-param-continue',
+                       ],
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       public function getHelpUrls() {
+               return [
+                       
'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API',
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getExamplesMessages() {
+               return [
+
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isInternal() {
+               // ReadingLists API is still experimental
+               return true;
+       }
+
+       /**
+        * Transform a row into an API result item
+        * @param ReadingListEntryRow $row
+        * @return array
+        */
+       private function getResultItem( $row ) {
+               return [
+                       'id' => $row->rle_id,
+                       'listId' => $row->rle_rl_id,
+                       'project' => $row->rle_project,
+                       'title' => $row->rle_title,
+                       'created' => $row->rle_date_created,
+                       'updated' => $row->rle_date_updated,
+               ];
+       }
+
+}
diff --git a/src/Api/ApiQueryReadingLists.php b/src/Api/ApiQueryReadingLists.php
new file mode 100644
index 0000000..e229252
--- /dev/null
+++ b/src/Api/ApiQueryReadingLists.php
@@ -0,0 +1,145 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Api;
+
+use ApiQueryBase;
+use MediaWiki\Extensions\ReadingLists\Doc\ReadingListRow;
+use MediaWiki\Extensions\ReadingLists\ReadingListRepositoryException;
+
+/**
+ * API meta module for getting list metadata.
+ */
+class ApiQueryReadingLists extends ApiQueryBase {
+
+       use ApiTrait;
+
+       /** @var string API module prefix */
+       private static $prefix = 'rl';
+
+       /**
+        * @inheritdoc
+        * @return void
+        */
+       public function execute() {
+               try {
+                       if ( $this->getUser()->isAnon() ) {
+                               $this->dieWithError( [ 
'apierror-mustbeloggedin',
+                                       $this->msg( 'action-viewmyprivateinfo' 
) ], 'notloggedin' );
+                       }
+                       $this->checkUserRightsAny( 'viewmyprivateinfo' );
+
+                       $project = $this->getParameter( 'project' );
+                       $title = $this->getParameter( 'title' );
+                       $limit = $this->getParameter( 'limit' );
+                       $offset = $this->getParameter( 'offset' );
+
+                       $path = [ 'query', $this->getModuleName() ];
+                       $result = $this->getResult();
+                       $result->addIndexedTagName( $path, 'list' );
+
+                       if ( $project && $title ) {
+                               $res = $this->getReadingListRepository( 
$this->getUser() )
+                                               ->getListsByPage( $project, 
$title );
+                               foreach ( $res as $row ) {
+                                       // TODO do we need continuation for 
getListsByPage?
+                                       $result->addValue( $path, null, 
$this->getResultItem( $row ) );
+                               }
+                       } elseif ( $project || $title ) {
+                               $this->dieWithError( 
'readinglists-apierror-project-title-param' );
+                       } else {
+                               $res = $this->getReadingListRepository( 
$this->getUser() )->getAllLists( $limit + 1, $offset );
+                               $nextOffset = 0;
+                               $fits = true;
+                               foreach ( $res as $row ) {
+                                       if ( !$fits || $nextOffset === $limit ) 
{
+                                               
$this->setContinueEnumParameter( 'offset', $nextOffset );
+                                               break;
+                                       }
+                                       $nextOffset++;
+                                       $fits = $result->addValue( $path, null, 
$this->getResultItem( $row ) );
+                               }
+                       }
+               } catch ( ReadingListRepositoryException $e ) {
+                       $this->dieWithException( $e );
+               }
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getAllowedParams() {
+               return [
+                       'project' => [
+                               self::PARAM_TYPE => 'string',
+                       ],
+                       'title' => [
+                               self::PARAM_TYPE => 'string',
+                       ],
+                       'limit' => [
+                               self::PARAM_DFLT => 'max',
+                               self::PARAM_TYPE => 'limit',
+                               self::PARAM_MIN => 1,
+                               self::PARAM_MAX => self::LIMIT_BIG1,
+                               self::PARAM_MAX2 => self::LIMIT_BIG2,
+                       ],
+                       'continue' => [
+                               self::PARAM_HELP_MSG => 
'api-help-param-continue',
+                       ],
+                       'offset' => [
+                               self::PARAM_TYPE => 'integer',
+                               self::PARAM_DFLT => 0,
+                               self::PARAM_HELP_MSG => 
'api-help-param-continue',
+                       ],
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       public function getHelpUrls() {
+               return [
+                       
'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API',
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getExamplesMessages() {
+               return [
+
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isInternal() {
+               // ReadingLists API is still experimental
+               return true;
+       }
+
+       /**
+        * Transform a row into an API result item
+        * @param ReadingListRow $row
+        * @return array
+        */
+       private function getResultItem( $row ) {
+               return [
+                       'id' => $row->rl_id,
+                       'name' => $row->rl_name,
+                       'default' => $row->rl_is_default,
+                       'description' => $row->rl_description,
+                       'color' => $row->rl_color,
+                       'image' => $row->rl_image,
+                       'icon' => $row->rl_icon,
+                       'created' => $row->rl_date_created,
+                       'updated' => $row->rl_date_updated,
+               ];
+       }
+
+}
diff --git a/src/Api/ApiReadingLists.php b/src/Api/ApiReadingLists.php
new file mode 100644
index 0000000..346b136
--- /dev/null
+++ b/src/Api/ApiReadingLists.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Api;
+
+use ApiBase;
+use ApiModuleManager;
+use MediaWiki\Extensions\ReadingLists\ReadingListRepositoryException;
+
+/**
+ * API parent module for all write operations.
+ * Each operation (command) is implemented as a submodule.
+ */
+class ApiReadingLists extends ApiBase {
+
+       /** @var array Module name => module class */
+       private static $submodules = [
+               'setup' => ApiReadingListsSetup::class,
+               'teardown' => ApiReadingListsTeardown::class,
+               'create' => ApiReadingListsCreate::class,
+               'update' => ApiReadingListsUpdate::class,
+               'delete' => ApiReadingListsDelete::class,
+               'createentry' => ApiReadingListsCreateEntry::class,
+               'deleteentry' => ApiReadingListsDeleteEntry::class,
+               'order' => ApiReadingListsOrder::class,
+               'orderentry' => ApiReadingListsOrderEntry::class,
+       ];
+
+       /** @var ApiModuleManager */
+       private $moduleManager;
+
+       /**
+        * Entry point for executing the module
+        * @inheritdoc
+        * @return void
+        */
+       public function execute() {
+               $command = $this->getParameter( 'command' );
+               $module = $this->moduleManager->getModule( $command, 'command' 
);
+               $module->extractRequestParams();
+               try {
+                       $module->execute();
+                       $module->getResult()->addValue( null, 
$module->getModuleName(), [ 'result' => 'Success' ] );
+               } catch ( ReadingListRepositoryException $e ) {
+                       $module->getResult()->addValue( null, 
$module->getModuleName(), [ 'result' => 'Failure' ] );
+                       $this->dieWithException( $e );
+               }
+       }
+
+       /**
+        * @inheritdoc
+        * @return ApiModuleManager
+        */
+       public function getModuleManager() {
+               if ( !$this->moduleManager ) {
+                       $modules = array_map( function ( $class ) {
+                               return [
+                                       'class' => $class,
+                                       'factory' => "$class::factory",
+                               ];
+                       }, self::$submodules );
+                       $this->moduleManager = new ApiModuleManager( $this );
+                       $this->moduleManager->addModules( $modules, 'command' );
+               }
+               return $this->moduleManager;
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getAllowedParams() {
+               return [
+                       'command' => [
+                               self::PARAM_TYPE => 'submodule',
+                               self::PARAM_REQUIRED => true,
+                       ],
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       public function getHelpUrls() {
+               return [
+                       
'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API',
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isInternal() {
+               // ReadingLists API is still experimental
+               return true;
+       }
+
+}
diff --git a/src/Api/ApiReadingListsCreate.php 
b/src/Api/ApiReadingListsCreate.php
new file mode 100644
index 0000000..c46b690
--- /dev/null
+++ b/src/Api/ApiReadingListsCreate.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Api;
+
+use ApiBase;
+use ApiModuleManager;
+
+/**
+ * API module for all write operations.
+ * Each operation (command) is implemented as a submodule.
+ */
+class ApiReadingListsCreate extends ApiBase {
+
+       use ApiTrait;
+
+       /** @var string API module prefix */
+       private static $prefix = '';
+
+       /**
+        * Entry point for executing the module
+        * @inheritdoc
+        * @return void
+        */
+       public function execute() {
+               if ( $this->getUser()->isAnon() ) {
+                       $this->dieWithError(
+                               [ 'apierror-mustbeloggedin', $this->msg( 
'action-editmyprivateinfo' ) ], 'notloggedin'
+                       );
+               }
+               $this->checkUserRightsAny( 'editmyprivateinfo' );
+
+               $params = $this->extractRequestParams();
+
+               $this->getReadingListRepository( $this->getUser() )->addList( 
$params['name'],
+                       $params['description'], $params['color'], 
$params['image'], $params['icon'] );
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getAllowedParams() {
+               return [
+                       'name' => [
+                               self::PARAM_TYPE => 'string',
+                               self::PARAM_REQUIRED => true,
+                       ],
+                       'description' => [
+                               self::PARAM_TYPE => 'string',
+                               self::PARAM_DFLT => '',
+
+                       ],
+                       'color' => [
+                               self::PARAM_TYPE => 'string',
+                               self::PARAM_DFLT => '',
+                       ],
+                       'image' => [
+                               self::PARAM_TYPE => 'string',
+                               self::PARAM_DFLT => '',
+                       ],
+                       'icon' => [
+                               self::PARAM_TYPE => 'string',
+                               self::PARAM_DFLT => '',
+                       ],
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       public function getHelpUrls() {
+               return [
+                       
'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API',
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getExamplesMessages() {
+               return [
+
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isWriteMode() {
+               return true;
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isInternal() {
+               // ReadingLists API is still experimental
+               return true;
+       }
+
+}
diff --git a/src/Api/ApiReadingListsCreateEntry.php 
b/src/Api/ApiReadingListsCreateEntry.php
new file mode 100644
index 0000000..cd6fe3b
--- /dev/null
+++ b/src/Api/ApiReadingListsCreateEntry.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Api;
+
+use ApiBase;
+use ApiModuleManager;
+
+/**
+ * API module for all write operations.
+ * Each operation (command) is implemented as a submodule.
+ */
+class ApiReadingListsCreateEntry extends ApiBase {
+
+       use ApiTrait;
+
+       /** @var string API module prefix */
+       private static $prefix = '';
+
+       /**
+        * Entry point for executing the module
+        * @inheritdoc
+        * @return void
+        */
+       public function execute() {
+               if ( $this->getUser()->isAnon() ) {
+                       $this->dieWithError(
+                               [ 'apierror-mustbeloggedin', $this->msg( 
'action-editmyprivateinfo' ) ], 'notloggedin'
+                       );
+               }
+               $this->checkUserRightsAny( 'editmyprivateinfo' );
+
+               $listId = $this->getParameter( 'list' );
+               $project = $this->getParameter( 'project' );
+               $title = $this->getParameter( 'title' );
+
+               $this->getReadingListRepository( $this->getUser() 
)->addListEntry( $listId, $project, $title );
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getAllowedParams() {
+               return [
+                       'list' => [
+                               self::PARAM_TYPE => 'integer',
+                               self::PARAM_REQUIRED => true,
+                       ],
+                       'project' => [
+                               self::PARAM_TYPE => 'string',
+                               self::PARAM_REQUIRED => true,
+                       ],
+                       'title' => [
+                               self::PARAM_TYPE => 'string',
+                               self::PARAM_REQUIRED => true,
+                       ],
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       public function getHelpUrls() {
+               return [
+                       
'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API',
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getExamplesMessages() {
+               return [
+
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isWriteMode() {
+               return true;
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isInternal() {
+               // ReadingLists API is still experimental
+               return true;
+       }
+
+}
diff --git a/src/Api/ApiReadingListsDelete.php 
b/src/Api/ApiReadingListsDelete.php
new file mode 100644
index 0000000..0b3f3f4
--- /dev/null
+++ b/src/Api/ApiReadingListsDelete.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Api;
+
+use ApiBase;
+use ApiModuleManager;
+
+/**
+ * API module for all write operations.
+ * Each operation (command) is implemented as a submodule.
+ */
+class ApiReadingListsDelete extends ApiBase {
+
+       use ApiTrait;
+
+       /** @var string API module prefix */
+       private static $prefix = '';
+
+       /**
+        * Entry point for executing the module
+        * @inheritdoc
+        * @return void
+        */
+       public function execute() {
+               if ( $this->getUser()->isAnon() ) {
+                       $this->dieWithError(
+                               [ 'apierror-mustbeloggedin', $this->msg( 
'action-editmyprivateinfo' ) ], 'notloggedin'
+                       );
+               }
+               $this->checkUserRightsAny( 'editmyprivateinfo' );
+
+               $listId = $this->getParameter( 'list' );
+
+               $this->getReadingListRepository( $this->getUser() 
)->deleteList( $listId );
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getAllowedParams() {
+               return [
+                       'list' => [
+                               self::PARAM_TYPE => 'integer',
+                               self::PARAM_REQUIRED => true,
+                       ],
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       public function getHelpUrls() {
+               return [
+                       
'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API',
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getExamplesMessages() {
+               return [
+
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isWriteMode() {
+               return true;
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isInternal() {
+               // ReadingLists API is still experimental
+               return true;
+       }
+
+}
diff --git a/src/Api/ApiReadingListsDeleteEntry.php 
b/src/Api/ApiReadingListsDeleteEntry.php
new file mode 100644
index 0000000..8a50f82
--- /dev/null
+++ b/src/Api/ApiReadingListsDeleteEntry.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Api;
+
+use ApiBase;
+use ApiModuleManager;
+
+/**
+ * API module for all write operations.
+ * Each operation (command) is implemented as a submodule.
+ */
+class ApiReadingListsDeleteEntry extends ApiBase {
+
+       use ApiTrait;
+
+       /** @var string API module prefix */
+       private static $prefix = '';
+
+       /**
+        * Entry point for executing the module
+        * @inheritdoc
+        * @return void
+        */
+       public function execute() {
+               if ( $this->getUser()->isAnon() ) {
+                       $this->dieWithError(
+                               [ 'apierror-mustbeloggedin', $this->msg( 
'action-editmyprivateinfo' ) ], 'notloggedin'
+                       );
+               }
+               $this->checkUserRightsAny( 'editmyprivateinfo' );
+
+               $entryId = $this->getParameter( 'entry' );
+
+               $this->getReadingListRepository( $this->getUser() 
)->deleteListEntry( $entryId );
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getAllowedParams() {
+               return [
+                       'entry' => [
+                               self::PARAM_TYPE => 'integer',
+                               self::PARAM_REQUIRED => true,
+                       ],
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       public function getHelpUrls() {
+               return [
+                       
'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API',
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getExamplesMessages() {
+               return [
+
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isWriteMode() {
+               return true;
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isInternal() {
+               // ReadingLists API is still experimental
+               return true;
+       }
+
+}
diff --git a/src/Api/ApiReadingListsOrder.php b/src/Api/ApiReadingListsOrder.php
new file mode 100644
index 0000000..f756e77
--- /dev/null
+++ b/src/Api/ApiReadingListsOrder.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Api;
+
+use ApiBase;
+use ApiModuleManager;
+
+/**
+ * API module for all write operations.
+ * Each operation (command) is implemented as a submodule.
+ */
+class ApiReadingListsOrder extends ApiBase {
+
+       use ApiTrait;
+
+       /** @var string API module prefix */
+       private static $prefix = '';
+
+       /**
+        * Entry point for executing the module
+        * @inheritdoc
+        * @return void
+        */
+       public function execute() {
+               if ( $this->getUser()->isAnon() ) {
+                       $this->dieWithError(
+                               [ 'apierror-mustbeloggedin', $this->msg( 
'action-editmyprivateinfo' ) ], 'notloggedin'
+                       );
+               }
+               $this->checkUserRightsAny( 'editmyprivateinfo' );
+
+               $order = $this->getParameter( 'order' );
+
+               $this->getReadingListRepository( $this->getUser() 
)->setListOrder( $order );
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getAllowedParams() {
+               return [
+                       'order' => [
+                               self::PARAM_TYPE => 'integer',
+                               self::PARAM_ISMULTI => true,
+                               self::PARAM_REQUIRED => true,
+                       ],
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       public function getHelpUrls() {
+               return [
+                       
'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API',
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getExamplesMessages() {
+               return [
+
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isWriteMode() {
+               return true;
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isInternal() {
+               // ReadingLists API is still experimental
+               return true;
+       }
+
+}
diff --git a/src/Api/ApiReadingListsOrderEntry.php 
b/src/Api/ApiReadingListsOrderEntry.php
new file mode 100644
index 0000000..9d93716
--- /dev/null
+++ b/src/Api/ApiReadingListsOrderEntry.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Api;
+
+use ApiBase;
+use ApiModuleManager;
+
+/**
+ * API module for all write operations.
+ * Each operation (command) is implemented as a submodule.
+ */
+class ApiReadingListsOrderEntry extends ApiBase {
+
+       use ApiTrait;
+
+       /** @var string API module prefix */
+       private static $prefix = '';
+
+       /**
+        * Entry point for executing the module
+        * @inheritdoc
+        * @return void
+        */
+       public function execute() {
+               if ( $this->getUser()->isAnon() ) {
+                       $this->dieWithError(
+                               [ 'apierror-mustbeloggedin', $this->msg( 
'action-editmyprivateinfo' ) ], 'notloggedin'
+                       );
+               }
+               $this->checkUserRightsAny( 'editmyprivateinfo' );
+
+               $listId = $this->getParameter( 'list' );
+               $order = $this->getParameter( 'order' );
+
+               $this->getReadingListRepository( $this->getUser() 
)->setListEntryOrder( $listId, $order );
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getAllowedParams() {
+               return [
+                       'list' => [
+                               self::PARAM_TYPE => 'integer',
+                               self::PARAM_REQUIRED => true,
+                       ],
+                       'order' => [
+                               self::PARAM_TYPE => 'integer',
+                               self::PARAM_ISMULTI => true,
+                               self::PARAM_REQUIRED => true,
+                       ],
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       public function getHelpUrls() {
+               return [
+                       
'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API',
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getExamplesMessages() {
+               return [
+
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isWriteMode() {
+               return true;
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isInternal() {
+               // ReadingLists API is still experimental
+               return true;
+       }
+
+}
diff --git a/src/Api/ApiReadingListsSetup.php b/src/Api/ApiReadingListsSetup.php
new file mode 100644
index 0000000..6ebe80f
--- /dev/null
+++ b/src/Api/ApiReadingListsSetup.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Api;
+
+use ApiBase;
+use ApiModuleManager;
+
+/**
+ * API module for all write operations.
+ * Each operation (command) is implemented as a submodule.
+ */
+class ApiReadingListsSetup extends ApiBase {
+
+       use ApiTrait;
+
+       /** @var string API module prefix */
+       private static $prefix = '';
+
+       /**
+        * Entry point for executing the module
+        * @inheritdoc
+        * @return void
+        */
+       public function execute() {
+               if ( $this->getUser()->isAnon() ) {
+                       $this->dieWithError(
+                               [ 'apierror-mustbeloggedin', $this->msg( 
'action-editmyprivateinfo' ) ], 'notloggedin'
+                       );
+               }
+               $this->checkUserRightsAny( 'editmyprivateinfo' );
+
+               $this->getReadingListRepository( $this->getUser() 
)->setupForUser();
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getAllowedParams() {
+               return [];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       public function getHelpUrls() {
+               return [
+                       
'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API',
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getExamplesMessages() {
+               return [
+
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isWriteMode() {
+               return true;
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isInternal() {
+               // ReadingLists API is still experimental
+               return true;
+       }
+
+}
diff --git a/src/Api/ApiReadingListsTeardown.php 
b/src/Api/ApiReadingListsTeardown.php
new file mode 100644
index 0000000..c2d45b2
--- /dev/null
+++ b/src/Api/ApiReadingListsTeardown.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Api;
+
+use ApiBase;
+use ApiModuleManager;
+
+/**
+ * API module for all write operations.
+ * Each operation (command) is implemented as a submodule.
+ */
+class ApiReadingListsTeardown extends ApiBase {
+
+       use ApiTrait;
+
+       /** @var string API module prefix */
+       private static $prefix = '';
+
+       /**
+        * Entry point for executing the module
+        * @inheritdoc
+        * @return void
+        */
+       public function execute() {
+               if ( $this->getUser()->isAnon() ) {
+                       $this->dieWithError(
+                               [ 'apierror-mustbeloggedin', $this->msg( 
'action-editmyprivateinfo' ) ], 'notloggedin'
+                       );
+               }
+               $this->checkUserRightsAny( 'editmyprivateinfo' );
+
+               $this->getReadingListRepository( $this->getUser() 
)->teardownForUser();
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getAllowedParams() {
+               return [];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       public function getHelpUrls() {
+               return [
+                       
'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API',
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getExamplesMessages() {
+               return [
+
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isWriteMode() {
+               return true;
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isInternal() {
+               // ReadingLists API is still experimental
+               return true;
+       }
+
+}
diff --git a/src/Api/ApiReadingListsUpdate.php 
b/src/Api/ApiReadingListsUpdate.php
new file mode 100644
index 0000000..0afe63f
--- /dev/null
+++ b/src/Api/ApiReadingListsUpdate.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Api;
+
+use ApiBase;
+use ApiModuleManager;
+
+/**
+ * API module for all write operations.
+ * Each operation (command) is implemented as a submodule.
+ */
+class ApiReadingListsUpdate extends ApiBase {
+
+       use ApiTrait;
+
+       /** @var string API module prefix */
+       private static $prefix = '';
+
+       /**
+        * Entry point for executing the module
+        * @inheritdoc
+        * @return void
+        */
+       public function execute() {
+               if ( $this->getUser()->isAnon() ) {
+                       $this->dieWithError(
+                               [ 'apierror-mustbeloggedin', $this->msg( 
'action-editmyprivateinfo' ) ], 'notloggedin'
+                       );
+               }
+               $this->checkUserRightsAny( 'editmyprivateinfo' );
+
+               $params = $this->extractRequestParams();
+
+               $this->getReadingListRepository( $this->getUser() 
)->updateList( $params['list'],
+                       $params['name'], $params['description'], 
$params['color'], $params['image'], $params['icon'] );
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getAllowedParams() {
+               return [
+                       'list' => [
+                               self::PARAM_TYPE => 'integer',
+                               self::PARAM_REQUIRED => true,
+                       ],
+                       'name' => [
+                               self::PARAM_TYPE => 'string',
+                               self::PARAM_REQUIRED => true,
+                       ],
+                       'description' => [
+                               self::PARAM_TYPE => 'string',
+                               self::PARAM_DFLT => '',
+
+                       ],
+                       'color' => [
+                               self::PARAM_TYPE => 'string',
+                               self::PARAM_DFLT => '',
+                       ],
+                       'image' => [
+                               self::PARAM_TYPE => 'string',
+                               self::PARAM_DFLT => '',
+                       ],
+                       'icon' => [
+                               self::PARAM_TYPE => 'string',
+                               self::PARAM_DFLT => '',
+                       ],
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       public function getHelpUrls() {
+               return [
+                       
'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:ReadingLists#API',
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return array
+        */
+       protected function getExamplesMessages() {
+               return [
+
+               ];
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isWriteMode() {
+               return true;
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       /**
+        * @inheritdoc
+        * @return bool
+        */
+       public function isInternal() {
+               // ReadingLists API is still experimental
+               return true;
+       }
+
+}
diff --git a/src/Api/ApiTrait.php b/src/Api/ApiTrait.php
new file mode 100644
index 0000000..bb7dd13
--- /dev/null
+++ b/src/Api/ApiTrait.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace MediaWiki\Extensions\ReadingLists\Api;
+
+use ApiBase;
+use ApiMain;
+use ApiQueryBase;
+use CentralIdLookup;
+use MediaWiki\Extensions\ReadingLists\Api\ApiQueryReadingListChanges;
+use MediaWiki\Extensions\ReadingLists\Api\ApiQueryReadingListEntries;
+use MediaWiki\Extensions\ReadingLists\Api\ApiQueryReadingLists;
+use MediaWiki\Extensions\ReadingLists\Api\ApiReadingLists;
+use MediaWiki\Extensions\ReadingLists\ReadingListRepository;
+use MediaWiki\Extensions\ReadingLists\ReadingListRepositoryException;
+use MediaWiki\Extensions\ReadingLists\Utils;
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
+use User;
+use Wikimedia\Rdbms\DBConnRef;
+use Wikimedia\Rdbms\LBFactory;
+
+/**
+ * Shared initialization for the APIs.
+ * Classes using it must have a static $prefix property (the API module 
prefix).
+ */
+trait ApiTrait {
+       /** @var ReadingListRepository */
+       private $repository;
+
+       /** @var LBFactory */
+       private $loadBalancerFactory;
+
+       /** @var DBConnRef */
+       private $dbw;
+
+       /** @var DBConnRef */
+       private $dbr;
+
+       /** @var ApiBase */
+       private $parent;
+
+       /**
+        * Static entry point for initializing the module
+        * @param ApiBase $parent Parent module
+        * @param string $name Module name
+        * @return static
+        */
+       public static function factory( ApiBase $parent, $name ) {
+               $services = MediaWikiServices::getInstance();
+               $loadBalancerFactory = $services ->getDBLoadBalancerFactory();
+               $dbw = Utils::getDB( DB_MASTER, $services );
+               $dbr = Utils::getDB( DB_REPLICA, $services );
+               if ( static::$prefix ) {
+                       // We are in one of the read modules, $parent is 
ApiQuery.
+                       // This is an ApiQueryBase subclass so we need to pass 
ApiQuery.
+                       $module = new static( $parent, $name, static::$prefix );
+               } else {
+                       // We are in one of the write submodules, $parent is 
ApiReadingLists.
+                       // This is an ApiBase subclass so we need to pass 
ApiMain.
+                       $module = new static( $parent->getMain(), $name, 
static::$prefix );
+               }
+               $module->parent = $parent;
+               $module->injectDatabaseDependencies( $loadBalancerFactory, 
$dbw, $dbr );
+               return $module;
+       }
+
+       /**
+        * Get the parent module.
+        * @return ApiBase
+        */
+       public function getParent() {
+               return $this->parent;
+       }
+
+       /**
+        * Set database-related dependencies. Required when initializing a 
module that uses this trait.
+        * @param LBFactory $loadBalancerFactory
+        * @param DBConnRef $dbw Master connection
+        * @param DBConnRef $dbr Replica connection
+        */
+       protected function injectDatabaseDependencies(
+               LBFactory $loadBalancerFactory, DBConnRef $dbw, DBConnRef $dbr
+       ) {
+               $this->loadBalancerFactory = $loadBalancerFactory;
+               $this->dbw = $dbw;
+               $this->dbr = $dbr;
+       }
+
+       /**
+        * Get the repository for the given user.
+        * @param User $user
+        * @return ReadingListRepository
+        */
+       protected function getReadingListRepository( User $user = null ) {
+               $centralId = 
CentralIdLookup::factory()->centralIdFromLocalUser( $user,
+                       CentralIdLookup::AUDIENCE_RAW );
+               $repository = new ReadingListRepository( $centralId, 
$this->dbw, $this->dbr,
+                       $this->loadBalancerFactory );
+               $repository->setLogger( LoggerFactory::getInstance( 
'readinglists' ) );
+               return $repository;
+       }
+
+}
diff --git a/src/ReadingListRepositoryException.php 
b/src/ReadingListRepositoryException.php
index 2260f79..ab24c32 100644
--- a/src/ReadingListRepositoryException.php
+++ b/src/ReadingListRepositoryException.php
@@ -3,6 +3,7 @@
 namespace MediaWiki\Extensions\ReadingLists;
 
 use Exception;
+use ILocalizedException;
 use Message;
 
 /**
@@ -10,7 +11,7 @@
  * constraint (e.g. trying to add an entry to a list owned by a different 
user). Usually this is
  * a client error; in some cases it could happen for otherwise sane calls due 
to race conditions.
  */
-class ReadingListRepositoryException extends Exception {
+class ReadingListRepositoryException extends Exception implements 
ILocalizedException {
 
        /** @var Message */
        private $messageObject;

-- 
To view, visit https://gerrit.wikimedia.org/r/366980
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I3615581fb06c599dea50877d3678d1ba3e82bb20
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/ReadingLists
Gerrit-Branch: master
Gerrit-Owner: Gergő Tisza <gti...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to