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