Pwirth has submitted this change and it was merged. Change subject: API base classes: ExtJS store and Tasks ......................................................................
API base classes: ExtJS store and Tasks This change contains some refactoring of class names and also changes internal implemenation details! This introduces a potential breaking change for subclasses. But as those classes are not used very much this should not be a problem. The goal is to provide a common API infrastructure within BlueSpice. * Also added a CSS fix for CRUDGridPanels when a grouping feature is being used * Added more dataset processing logic to ExtJSStore base class * Created abstract 'makeData' and implemented 'execute' to have even less code in the subclass * Added some generic hooks * Added standardized taskData for Tasks API Change-Id: Id8e1ad8bd2a14e9f8bddbc5c6873b1522d294b31 --- M includes/AutoLoader.php D includes/api/ApiExtJSBase.php A includes/api/BSApiBase.php A includes/api/BSApiExtJSStoreBase.php R includes/api/BSApiTasksBase.php A includes/api/BSStandardAPIResponse.php M includes/utility/CacheHelper.class.php M resources/bluespice.extjs/bluespice.extjs.fixes.css 8 files changed, 396 insertions(+), 138 deletions(-) Approvals: Pwirth: Verified; Looks good to me, approved diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 59f6263..ffadfb5 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -51,7 +51,10 @@ $GLOBALS['wgAutoloadClasses']['BsExtJSSortParam'] = __DIR__."/ExtJSStoreParams.php"; //api -$GLOBALS['wgAutoloadClasses']['BsApiBase'] = __DIR__."/api/BsApiBase.php"; +$GLOBALS['wgAutoloadClasses']['BSStandardAPIResponse'] = __DIR__."/api/BSStandardAPIResponse.php"; +$GLOBALS['wgAutoloadClasses']['BSApiBase'] = __DIR__."/api/BSApiBase.php"; +$GLOBALS['wgAutoloadClasses']['BSApiTasksBase'] = __DIR__."/api/BSApiTasksBase.php"; +$GLOBALS['wgAutoloadClasses']['BSApiExtJSStoreBase'] = __DIR__."/api/BSApiExtJSStoreBase.php"; //adapter $GLOBALS['wgAutoloadClasses']['BsExtensionMW'] = __DIR__."/ExtensionMW.class.php"; diff --git a/includes/api/ApiExtJSBase.php b/includes/api/ApiExtJSBase.php deleted file mode 100644 index 90546f7..0000000 --- a/includes/api/ApiExtJSBase.php +++ /dev/null @@ -1,69 +0,0 @@ -<?php - -abstract class ApiExtJSBase extends ApiBase { - - public function getAllowedParams() { - return array( - 'sort' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => false - ), - 'page' => array( - ApiBase::PARAM_TYPE => 'integer', - ApiBase::PARAM_REQUIRED => false - ), - 'limit' => array( - ApiBase::PARAM_TYPE => 'integer', - ApiBase::PARAM_REQUIRED => false - ), - 'start' => array( - ApiBase::PARAM_TYPE => 'integer', - ApiBase::PARAM_REQUIRED => false - ), - '_dc' => array( - ApiBase::PARAM_TYPE => 'integer', - ApiBase::PARAM_REQUIRED => false - ), - 'format' => array( - ApiBase::PARAM_DFLT => 'json', - ApiBase::PARAM_TYPE => array( 'json', 'jsonfm' ), - ) - ); - } - - public function getParamDescription() { - return array( - 'sort' => 'JSON string with sorting info', - 'page' => 'Allows server side calculation of start/limit', - 'limit' => 'Number of results to return', - 'start' => 'The offset to start the result list from', - '_dc' => '"Disable cache" flag', - 'format' => 'The format of the output' - ); - } - - public function getResultProperties() { - return array( - '' => array( - 'success' => 'boolean', - 'results' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => false - ), - 'rows' => array( - ApiBase::PROP_TYPE => 'array', - ApiBase::PROP_NULLABLE => false - ) - ) - ); - } - - protected function getParameterFromSettings($paramName, $paramSettings, $parseLimit) { - $value = parent::getParameterFromSettings($paramName, $paramSettings, $parseLimit); - //Unfortunately there is no way to register custom types for parameters - if( $paramName === 'sort' ) { - $value = FormatJson::decode($value); - } - return $value; - } -} \ No newline at end of file diff --git a/includes/api/BSApiBase.php b/includes/api/BSApiBase.php new file mode 100644 index 0000000..3646340 --- /dev/null +++ b/includes/api/BSApiBase.php @@ -0,0 +1,50 @@ +<?php + +abstract class BSApiBase extends ApiBase { + /** + * Checks access permissions based on a list of titles and permissions. If + * one of it fails the API processing is ended with an appropriate message + * @param array $aTitles Array of Title objects to check the requires permissions against + * @param User $oUser the User object of the requesting user. Does a fallback to $this->getUser(); + */ + protected function checkPermissions( $aTitles = array(), $oUser = null ) { + $aRequiredPermissions = $this->getRequiredPermissions(); + if( empty( $aRequiredPermissions ) ) { + return; //No need for further checking + } + foreach( $aTitles as $oTitle ) { + if( $oTitle instanceof Title === false ) { + continue; + } + foreach( $aRequiredPermissions as $sPermission ) { + if( $oTitle->userCan( $sPermission ) === false ) { + //TODO: Reflect title and permission in error message + $this->dieUsageMsg( 'badaccess-groups' ); + } + } + } + + //Fallback if not conrete title was provided + if( empty( $aTitles ) ) { + if( $oUser instanceof User === false ) { + $oUser = $this->getUser(); + } + foreach( $aRequiredPermissions as $sPermission ) { + if( $oUser->isAllowed( $sPermission ) === false ) { + //TODO: Reflect permission in error message + $this->dieUsageMsg( 'badaccess-groups' ); + } + } + } + } + + protected function getRequiredPermissions() { + return array( 'read' ); + } + + protected function getExamples() { + return array( + 'api.php?action='.$this->getModuleName(), + ); + } +} \ No newline at end of file diff --git a/includes/api/BSApiExtJSStoreBase.php b/includes/api/BSApiExtJSStoreBase.php new file mode 100644 index 0000000..9e40541 --- /dev/null +++ b/includes/api/BSApiExtJSStoreBase.php @@ -0,0 +1,281 @@ +<?php + +/** + * This class serves as a backend for ExtJS stores. It allows all + * necessary parameters and provides convenience methods and a standard ouput + * format + * + * Example request parameters of an ExtJS store + + _dc:1430126252980 + filter:[ + { + "type":"string", + "comparison":"ct", + "value":"some text ...", + "field":"someField" + } + ] + group:[ + { + "property":"someOtherField", + "direction":"ASC" + } + ] + sort:[ + { + "property":"someOtherField", + "direction":"ASC" + } + ] + page:1 + start:0 + limit:25 + */ +abstract class BSApiExtJSStoreBase extends BSApiBase { + + /** + * The current parameters sent by the ExtJS store + * @var BsExtJSStoreParams + */ + protected $oStoreParams = null; + + /** + * Automatically set within 'postProcessData' method + * @var int + */ + protected $iFinalDataSetCount = 0; + + /** + * May be overwritten by subclass + * @var string + */ + protected $root = 'results'; + + /** + * May be overwritten by subclass + * @var string + */ + protected $totalProperty = 'total'; + + public function execute() { + $aData = $this->makeData(); + $FinalData = $this->postProcessData( $aData ); + $this->returnData( $FinalData ); + } + + protected abstract function makeData(); + + /** + * Creates a proper output format based on the classes properties + * @param array $aData An array of plain old data objects + */ + public function returnData($aData) { + wfRunHooks( 'BSApiExtJSStoreBaseBeforeReturnData', array( $this, &$aData ) ); + $result = $this->getResult(); + $result->setIndexedTagName( $aData, $this->root ); + $result->addValue( null, $this->root, $aData ); + $result->addValue( null, $this->totalProperty, $this->iFinalDataSetCount ); + } + + /** + * + * @return BsExtJSStoreParams + */ + protected function getStoreParams() { + if( $this->oStoreParams === null ) { + $this->oStoreParams = BsExtJSStoreParams::newFromRequest(); + } + return $this->oStoreParams; + } + + public function getAllowedParams() { + return array( + 'sort' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => false, + ApiBase::PARAM_DFLT => '[]' + ), + 'group' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => false, + ApiBase::PARAM_DFLT => '[]' + ), + 'filter' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => false, + ApiBase::PARAM_DFLT => '[]' + ), + 'page' => array( + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_REQUIRED => false, + ApiBase::PARAM_DFLT => 0 + ), + 'limit' => array( + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_REQUIRED => false, + ApiBase::PARAM_DFLT => 25 + ), + 'start' => array( + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_REQUIRED => false, + ApiBase::PARAM_DFLT => 0 + ), + + 'callback' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => false + ), + + 'query' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => false + ), + '_dc' => array( + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_REQUIRED => false + ), + 'format' => array( + ApiBase::PARAM_DFLT => 'json', + ApiBase::PARAM_TYPE => array( 'json', 'jsonfm' ) + ) + ); + } + + public function getParamDescription() { + return array( + 'sort' => 'JSON string with sorting info; deserializes to array of objects that hold filed name and direction for each sorting option', + 'group' => 'JSON string with grouping info; deserializes to array of objects that hold filed name and direction for each grouping option', + 'filter' => 'JSON string with filter info; deserializes to array of objects that hold filed name, filter type, and filter value for each sorting option', + 'page' => 'Allows server side calculation of start/limit', + 'limit' => 'Number of results to return', + 'start' => 'The offset to start the result list from', + 'query' => 'This is similar to "filter", but the provided value serves as a filter only for the "value" field of an ExtJS component', + 'callback' => 'The offset to start the result list from', + '_dc' => '"Disable cache" flag', + 'format' => 'The format of the output (only JSON or formatted JSON)' + ); + } + + protected function getParameterFromSettings($paramName, $paramSettings, $parseLimit) { + $value = parent::getParameterFromSettings($paramName, $paramSettings, $parseLimit); + //Unfortunately there is no way to register custom types for parameters + if( in_array( $paramName, array( 'sort', 'group', 'filter' ) ) ) { + $value = FormatJson::decode($value); + if( empty($value) ) { + return array(); + } + } + return $value; + } + + /** + * Filter, sort and trim the result according to the call parameters and + * apply security trimming + * @param array $aData + * @return array + */ + public function postProcessData( $aData ) { + wfRunHooks( 'BSApiExtJSStoreBaseBeforePostProcessData', array( $this, &$aData ) ); + $aProcessedData = array(); + + //First, apply filter + $aProcessedData = array_filter($aData, array( $this, 'filterCallback') ); + wfRunHooks( 'BSApiExtJSStoreBaseAfterFilterData', array( $this, &$aProcessedData ) ); + + //Next, apply sort + usort($aProcessedData, array( $this, 'sortCallback') ); + + //Before we trim, we save the count + $this->iFinalDataSetCount = count( $aProcessedData ); + + //Last, do trimming + $aProcessedData = $this->trimData( $aProcessedData ); + + return $aProcessedData; + } + + /** + * Applies all sorters provided by the store + * @param object $oA + * @param object $oB + * @return int + */ + public function sortCallback( $oA, $oB ) { + $aSort = $this->getParameter('sort'); + $iCount = count( $aSort ); + for( $i = 0; $i < $iCount; $i++ ) { + $sProperty = $aSort[$i]->property; + $sDirection = strtoupper( $aSort[$i]->direction ); + + if( $oA->$sProperty !== $oB->$sProperty ) { + if( $sDirection === 'ASC' ) { + return $oA->$sProperty > $oB->$sProperty ? -1 : 1; + } + else { //'DESC' + return $oA->$sProperty < $oB->$sProperty ? -1 : 1; + } + } + } + return 0; + } + + public function filterCallback( $aDataSet ) { + $aFilter = $this->getParameter('filter'); + foreach( $aFilter as $oFilter ) { + //If just one of these filters does not apply, the dataset needs + //to be removed + + if( $oFilter->type == 'string' ) { + $bFilterApplies = $this->filterString($oFilter, $aDataSet); + if( !$bFilterApplies ) { + return false; + } + } + //TODO: Implement for type 'date', 'datetime', 'boolean' and 'numeric' + //... and even maybe 'list' + } + + return true; + } + + public function filterString($oFilter, $aDataSet) { + $sFieldValue = $aDataSet->{$oFilter->field}; + $sFilterValue = $oFilter->value; + + //TODO: Add string functions to BsStringHelper + //HINT: http://stackoverflow.com/a/10473026 + Case insensitive + switch( $oFilter->comparison ) { + case 'sw': + return $sFilterValue === '' || + strripos($sFieldValue, $sFilterValue, -strlen($sFieldValue)) !== false; + case 'ew': + return $sFilterValue === '' || + (($temp = strlen($sFieldValue) - strlen($sFilterValue)) >= 0 + && stripos($sFieldValue, $sFilterValue, $temp) !== false); + case 'ct': + return stripos($sFieldValue, $sFilterValue) !== false; + case 'nct': + return stripos($sFieldValue, $sFilterValue) === false; + case 'eq': + return $sFieldValue === $sFilterValue; + case 'neq': + return $sFieldValue !== $sFilterValue; + } + } + + public function trimData($aProcessedData) { + $iStart = $this->getParameter('start'); + $iEnd = $this->getParameter('limit') + $iStart; + if( $iEnd >= $this->iFinalDataSetCount ) { + $iEnd = $this->iFinalDataSetCount - 1; + } + + $aTrimmedData = array(); + for( $i = $iStart; $i <= $iEnd; $i++ ) { + $aTrimmedData[] = $aProcessedData[$i]; + } + + return $aTrimmedData; + } +} diff --git a/includes/api/BsApiBase.php b/includes/api/BSApiTasksBase.php similarity index 62% rename from includes/api/BsApiBase.php rename to includes/api/BSApiTasksBase.php index 6548962..986a7d3 100644 --- a/includes/api/BsApiBase.php +++ b/includes/api/BSApiTasksBase.php @@ -27,26 +27,16 @@ */ /** - * Api base class for BlueSpice + * Api base class for simple tasks in BlueSpice * @package BlueSpice_Foundation */ -abstract class BsApiBase extends ApiBase { +abstract class BSApiTasksBase extends BSApiBase { /** * Methods that can be called by task param * @var array */ - protected static $aTasks = array(); - - /** - * Constructor - * @param $mainModule ApiMain object - * @param string $moduleName Name of this module - * @param string $modulePrefix Prefix to use for parameter names - */ - public function __construct( $query, $moduleName, $modulePrefix = '' ) { - parent::__construct( $query, $moduleName, $modulePrefix ); - } + protected $aTasks = array(); /** * The execute() method will be invoked directly by ApiMain immediately @@ -62,33 +52,31 @@ //Avoid API warning: register the parameter used to bust browser cache $this->getMain()->getVal( '_' ); - if( !isset($aParams['task']) ) return; + $sMethod = 'task_'.$aParams['task']; - if( in_array($aParams['task'], static::$aTasks) ) { - $oResult = call_user_func( - array($this, $aParams['task']), - $aParams - ); + if( !is_callable( array( $this, $sMethod ) ) ) { + $oResult = $this->makeStandardReturn(); + $oResult->errors['task'] = 'Task '.$aParams['task'].' not implemented'; + } + else { + $oResult = $this->$sMethod( $this->getParameter('taskData'), $aParams ); } - $this->getResult()->addValue(null, 'bs', $oResult); + foreach( $oResult as $sFieldName => $mFieldValue ) { + if( $mFieldValue === null ) { + continue; //MW Api doesn't like NULL values + } + $this->getResult()->addValue(null, $sFieldName, $mFieldValue); + } } /** * Standard return object * Every task should return this! - * @return object + * @return BSStandardAPIResponse */ - protected static function stdReturn() { - return $oReturn = (object) array( - 'result' => array( - 'payload' => null, - 'success' => false, - 'message' => '', - 'errors' => array(), - 'payload_count' => 0, - ) - ); + protected function makeStandardReturn() { + return new BSStandardAPIResponse(); } /** @@ -99,13 +87,30 @@ return array( 'task' => array( ApiBase::PARAM_REQUIRED => true, - ApiBase::PARAM_TYPE => 'string' + ApiBase::PARAM_TYPE => $this->aTasks, + ), + 'taskData' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => false, + ApiBase::PARAM_DFLT => '{}' ), 'format' => array( ApiBase::PARAM_DFLT => 'json', ApiBase::PARAM_TYPE => array( 'json', 'jsonfm' ), ) ); + } + + protected function getParameterFromSettings($paramName, $paramSettings, $parseLimit) { + $value = parent::getParameterFromSettings($paramName, $paramSettings, $parseLimit); + //Unfortunately there is no way to register custom types for parameters + if( $paramName === 'taskData' ) { + $value = FormatJson::decode($value); + if( empty($value) ) { + return array(); + } + } + return $value; } /** @@ -115,40 +120,9 @@ public function getParamDescription() { return array( 'task' => 'The task you would like to execute', + 'taskData' => 'JSON string encoded object with arbitrary data for the task', 'format' => 'The format of the result', ); - } - - /** - * Default to false - * @return boolean - */ - public function needsToken() { - return false; - } - - /** - * Default to empty string - * @return string - */ - public function getTokenSalt() { - return ''; - } - - /** - * Default to true - * @return boolean - */ - public function mustBePosted() { - return true; - } - - /** - * Default to false - * @return boolean - */ - public function isWriteMode() { - return false; } /** @@ -157,7 +131,7 @@ */ public function getDescription() { return array( - 'BsApiBase: This should be implemented by subclass' + 'BSApiTasksBase: This should be implemented by subclass' ); } @@ -167,7 +141,7 @@ */ public function getExamples() { return array( - 'api.php?action=<childapimodule>&task=<taskofchildapimodule>', + 'api.php?action='.$this->getModuleName().'&task='.$this->aTasks[0].'&taskData={someKey:"someValue",isFalse:true}', ); } } \ No newline at end of file diff --git a/includes/api/BSStandardAPIResponse.php b/includes/api/BSStandardAPIResponse.php new file mode 100644 index 0000000..83fd84f --- /dev/null +++ b/includes/api/BSStandardAPIResponse.php @@ -0,0 +1,18 @@ +<?php + +/** + * This response should implement the ExtJS standard format for serverside + * form validations: + * http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.form.action.Submit + * + * TODO: do a clean implemenation with gettes and setters + */ +class BSStandardAPIResponse { + public $errors = array(); //ExtJS + public $success = false; //ExtJS + + //Custom fields + public $message = ''; + public $payload = array(); + public $payload_count = 0; +} \ No newline at end of file diff --git a/includes/utility/CacheHelper.class.php b/includes/utility/CacheHelper.class.php index 770c379..d5d89de 100644 --- a/includes/utility/CacheHelper.class.php +++ b/includes/utility/CacheHelper.class.php @@ -18,7 +18,7 @@ */ public static function getCache() { if ( self::$oCache === null ) { - self::$oCache = wfGetCache( CACHE_ANYTHING ); + self::$oCache = wfGetMainCache(); } return self::$oCache; diff --git a/resources/bluespice.extjs/bluespice.extjs.fixes.css b/resources/bluespice.extjs/bluespice.extjs.fixes.css index 3404c88..f69f67c 100644 --- a/resources/bluespice.extjs/bluespice.extjs.fixes.css +++ b/resources/bluespice.extjs/bluespice.extjs.fixes.css @@ -77,7 +77,8 @@ .bs-extjs-crud-grid tr.x-grid-row:hover .bs-extjs-actioncolumn-icon { display: inline-block; } -.bs-extjs-crud-grid tr.x-grid-row { +.bs-extjs-crud-grid tr.x-grid-row, +.bs-extjs-crud-grid tr.x-grid-data-row { height: 34px; } -- To view, visit https://gerrit.wikimedia.org/r/206801 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Id8e1ad8bd2a14e9f8bddbc5c6873b1522d294b31 Gerrit-PatchSet: 5 Gerrit-Project: mediawiki/extensions/BlueSpiceFoundation Gerrit-Branch: master Gerrit-Owner: Robert Vogel <vo...@hallowelt.biz> Gerrit-Reviewer: Mglaser <gla...@hallowelt.biz> Gerrit-Reviewer: Pwirth <wi...@hallowelt.biz> Gerrit-Reviewer: Robert Vogel <vo...@hallowelt.biz> Gerrit-Reviewer: Tweichart <weich...@hallowelt.biz> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits