jenkins-bot has submitted this change and it was merged. Change subject: Merge from master ......................................................................
Merge from master This change contains several new API base classes from the master branch. Those are needed by some extensions like InsertFile, NSFileRepoConnector and BookshelfUI See also https://gerrit.wikimedia.org/r/#/c/224409/ Change-Id: I6dc475aac57bfdeba0cabe02984bf61e3d153753 --- M BlueSpiceFoundation.php M includes/AutoLoader.php M includes/CoreHooks.php M includes/api/ApiFormatJsonMW.php A includes/api/BSApiExtJSStoreBase.php A includes/api/BSApiFileBackendStore.php A includes/api/BSApiTasksBase.php A includes/api/BSStandardAPIResponse.php M includes/api/BsApiBase.php A resources/bluespice.extjs/BS/model/File.js 10 files changed, 884 insertions(+), 181 deletions(-) Approvals: Robert Vogel: Looks good to me, approved Pwirth: Looks good to me, but someone else must approve jenkins-bot: Verified diff --git a/BlueSpiceFoundation.php b/BlueSpiceFoundation.php index c24d5d9..2f708ca 100644 --- a/BlueSpiceFoundation.php +++ b/BlueSpiceFoundation.php @@ -65,6 +65,8 @@ $wgAjaxExportList[] = 'BsCommonAJAXInterface::getFileUrl'; $wgAjaxExportList[] = 'BsCore::ajaxBSPing'; +$wgAPIModules['bs-filebackend-store'] = 'BSApiFileBackendStore'; + //I18N MW1.23+ $wgMessagesDirs['BlueSpice'] = __DIR__ . '/i18n/core'; $wgMessagesDirs['BlueSpiceCredits'] = __DIR__ . '/i18n/credits'; diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 03fbc59..7183f8d 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -51,7 +51,11 @@ $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"; +$GLOBALS['wgAutoloadClasses']['BSApiFileBackendStore'] = __DIR__."/api/BSApiFileBackendStore.php"; //adapter $GLOBALS['wgAutoloadClasses']['BsExtensionMW'] = __DIR__."/ExtensionMW.class.php"; diff --git a/includes/CoreHooks.php b/includes/CoreHooks.php index 7da6192..1c430c4 100644 --- a/includes/CoreHooks.php +++ b/includes/CoreHooks.php @@ -435,26 +435,26 @@ /** * Additional chances to reject an uploaded file - * @param string $saveName: destination file name - * @param string $tempName: filesystem path to the temporary file for checks - * @param string &$error: output: message key for message to show if upload canceled by returning false. May also be an array, where the first element - is the message key and the remaining elements are used as parameters to the message. - * @return bool true on success , false on failure + * @param String $sSaveName: Destination file name + * @param String $sTempName: Filesystem path to the temporary file for checks + * @param String &$sError: output: message key for message to show if upload canceled by returning false + * @return Boolean true on success , false on failure */ public static function onUploadVerification( $sSaveName, $sTempName, &$sError ) { - $aParts = explode( '.', $sSaveName ); + if( empty( $sSaveName ) || !$iFileExt = strrpos( $sSaveName, '.' ) ) { + return true; + } - if ( !empty( $aParts[0] ) ) { - $oUser = User::newFromName( $aParts[0] ); + $sUser = substr( $sSaveName, 0, $iFileExt ); + $oUser = User::newFromName( $sUser ); + if( is_null( $oUser ) || $oUser->getId() == 0 ) { + return true; + } - if ( $oUser->getId() != 0 ) { - $oCurrUser = RequestContext::getMain()->getUser(); - - if ( strcasecmp( $oUser->getName(), $oCurrUser->getName() ) !== 0 ) { - $sError = 'bs-imageofotheruser'; - return false; - } - } + $oCurrUser = RequestContext::getMain()->getUser(); + if( $oUser->getId() !== $oCurrUser->getId() ) { + $sError = 'bs-imageofotheruser'; + return false; } return true; diff --git a/includes/api/ApiFormatJsonMW.php b/includes/api/ApiFormatJsonMW.php index d087043..aa39b4b 100644 --- a/includes/api/ApiFormatJsonMW.php +++ b/includes/api/ApiFormatJsonMW.php @@ -38,14 +38,15 @@ } if ( defined( 'ApiResult::META_CONTENT' ) ) { - $data = $this->getResult()->getResultData(); - $data = ApiResult::transformForBC( $data ); - $data = ApiResult::transformForTypes( $data, array( 'BC' => true ) ); - $data = ApiResult::removeMetadata( $data ); + $data = $this->getResult()->getResultData( null, array( + 'BC' => array(), + 'Types' => array(), + 'Strip' => 'all', + ) ); } else { $data = $this->getResultData(); } - + $this->printText( $prefix . FormatJson::encode( $data, $this->getIsHtml() ) . diff --git a/includes/api/BSApiExtJSStoreBase.php b/includes/api/BSApiExtJSStoreBase.php new file mode 100644 index 0000000..9af6019 --- /dev/null +++ b/includes/api/BSApiExtJSStoreBase.php @@ -0,0 +1,433 @@ +<?php +/** + * This class serves as a backend for ExtJS stores. It allows all + * necessary parameters and provides convenience methods and a standard ouput + * format + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file is part of BlueSpice for MediaWiki + * For further information visit http://www.blue-spice.org + * + * @author Robert Vogel <[email protected]> + * @author Patric Wirth <[email protected]> + * @package Bluespice_Foundation + * @copyright Copyright (C) 2015 Hallo Welt! - Medienwerkstatt GmbH, All rights reserved. + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License v2 or later + * @filesource + * + * 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() { + $sQuery = $this->getParameter( 'query' ); + $aData = $this->makeData( $sQuery ); + $FinalData = $this->postProcessData( $aData ); + $this->returnData( $FinalData ); + } + + /** + * @param string $sQuery Potential query provided by ExtJS component. + * This is some kind of preliminary filtering. Subclass has to decide if + * and how to process it + * @return array - Full list of of data objects. Filters, paging, sorting + * will be done by the base class + */ + protected abstract function makeData( $sQuery = '' ); + + /** + * 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; + } + + public function getParameter( $paramName, $parseLimit = true ) { + //Make this public, so hook handler could get the params + return parent::getParameter( $paramName, $parseLimit ); + } + + /** + * Filter, sort and trim the result according to the call parameters and + * apply security trimming + * @param array $aData + * @return array + */ + public function postProcessData( $aData ) { + if( !wfRunHooks( 'BSApiExtJSStoreBaseBeforePostProcessData', array( $this, &$aData ) ) ) { + return $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') ); <-- had some performance issues + $aProcessedData = $this->sortData( $aProcessedData ); + + //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 + * --> has performance issues on large dataset; Code kept for documentation + * @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; + } + + /** + * + * @param object $aDataSet + * @return boolean + */ + 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; + } + } + if( $oFilter->type == 'list' ) { + $bFilterApplies = $this->filterList( $oFilter, $aDataSet ); + if( !$bFilterApplies ) { + return false; + } + } + if( $oFilter->type == 'numeric' ) { + $bFilterApplies = $this->filterNumeric( $oFilter, $aDataSet ); + if( !$bFilterApplies ) { + return false; + } + } + //TODO: Implement for type 'date', 'datetime' and 'boolean' + } + + return true; + } + + /** + * Performs string filtering based on given filter of type string on a dataset + * @param object $oFilter + * @param oject $aDataSet + * @return boolean true if filter applies, false if not + */ + public function filterString( $oFilter, $aDataSet ) { + if( !is_string( $oFilter->value ) ) { + return true; //TODO: Warning + } + $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; + } + } + + /** + * Performs numeric filtering based on given filter of type integer on a + * dataset + * @param object $oFilter + * @param oject $aDataSet + * @return boolean true if filter applies, false if not + */ + public function filterNumeric( $oFilter, $aDataSet ) { + if( !is_numeric( $oFilter->value ) ) { + return true; //TODO: Warning + } + $sFieldValue = $aDataSet->{$oFilter->field}; + $iFilterValue = (int) $oFilter->value; + + switch( $oFilter->comparison ) { + case 'gt': + return $sFieldValue < $iFilterValue; + case 'lt': + return $sFieldValue > $iFilterValue; + case 'eq': + return $sFieldValue === $iFilterValue; + case 'neq': + return $sFieldValue !== $iFilterValue; + } + } + + /** + * Performs list filtering based on given filter of type array on a dataset + * @param object $oFilter + * @param oject $aDataSet + * @return boolean true if filter applies, false if not + */ + public function filterList( $oFilter, $aDataSet ) { + if( !is_array( $oFilter->value ) ) { + return true; //TODO: Warning + } + $aFieldValues = $aDataSet->{$oFilter->field}; + if( empty( $aFieldValues ) ) { + return false; + } + $aFilterValues = $oFilter->value; + $aTemp = array_intersect( $aFieldValues, $aFilterValues ); + if( empty( $aTemp ) ) { + return false; + } + return true; + } + + /** + * Applies pagination on the result + * @param array $aProcessedData The filtered result + * @return array a trimmed version of the result + */ + public function trimData( $aProcessedData ) { + $iStart = $this->getParameter( 'start' ); + $iEnd = $this->getParameter( 'limit' ) + $iStart; + + if( $iEnd > $this->iFinalDataSetCount || $iEnd === 0 ) { + $iEnd = $this->iFinalDataSetCount; + } + + $aTrimmedData = array(); + for( $i = $iStart; $i < $iEnd; $i++ ) { + $aTrimmedData[] = $aProcessedData[$i]; + } + + return $aTrimmedData; + } + + /** + * Performs fast sorting on the results. Thanks at user "pigpen" + * @param array $aProcessedData + * @return array The sorted results + */ + public function sortData($aProcessedData) { + $aSort = $this->getParameter('sort'); + $iCount = count( $aSort ); + for( $i = 0; $i < $iCount; $i++ ) { + $sProperty = $aSort[$i]->property; + $sDirection = strtoupper( $aSort[$i]->direction ); + $a{$sProperty} = array(); + foreach( $aProcessedData as $iKey => $aDataSet ) { + $a{$sProperty}[$iKey] = $aDataSet->{$sProperty}; + } + + $aParams[] = $a{$sProperty}; + $aParams[] = SORT_NATURAL; //We might tweak this depending on the data type of the field. Mabye we should make some "getSort( $fieldName )" method + if( $sDirection === 'ASC' ) { + $aParams[] = SORT_ASC; + } + else { + $aParams[] = SORT_DESC; + } + } + $aParams[] = &$aProcessedData; + + call_user_func_array( 'array_multisort', $aParams ); + return $aProcessedData; + } + +} diff --git a/includes/api/BSApiFileBackendStore.php b/includes/api/BSApiFileBackendStore.php new file mode 100644 index 0000000..e1c81f0 --- /dev/null +++ b/includes/api/BSApiFileBackendStore.php @@ -0,0 +1,180 @@ +<?php +/** + * This class serves as a backend for ExtJS stores. It allows all + * necessary parameters and provides convenience methods and a standard ouput + * format + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file is part of BlueSpice for MediaWiki + * For further information visit http://www.blue-spice.org + * + * @author Robert Vogel <[email protected]> + * @author Patric Wirth <[email protected]> + * @package Bluespice_Foundation + * @copyright Copyright (C) 2015 Hallo Welt! - Medienwerkstatt GmbH, All rights reserved. + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License v2 or later + * @filesource + */ +class BSApiFileBackendStore extends BSApiExtJSStoreBase { + + public function makeData( $sQuery = '' ) { + $res = $this->fetchCaseInsensitive( $sQuery ); + + //The initial query is made against the searchindex table, which holds + //lowercased and otherwise normalized titles. Unfornunately if + //one queries an exact title with dots (and colons) the result will be + //empty because the searchindex table data is stripped from those + //characters. We will fallback to a query without the use of + //searchindex, just in case... + if( $res->numRows() === 0 ) { + $res = $this->fetchCaseSensitive( $sQuery ); + } + + $bUseSecureFileStore = BsExtensionManager::isContextActive( + 'MW::SecureFileStore::Active' + ); + + //First query: Get all files and their pages + $aReturn = array(); + foreach( $res as $oRow ) { + try { + $oImg = RepoGroup::singleton()->getLocalRepo() + ->newFileFromRow( $oRow ); + } catch (Exception $ex) { + continue; + } + + $oTitle = Title::newFromRow( $oRow ); + //No "user can read" check here, because it may be expensive. + //This may be done by hook handlers + + //TODO: use 'thumb.php'? + //TODO: Make thumb size a parameter + $sThumb = $oImg->createThumb( 48, 48 ); + $sUrl = $oImg->getUrl(); + if( $bUseSecureFileStore ) { //TODO: Remove + $sThumb = html_entity_decode( SecureFileStore::secureStuff( $sThumb, true ) ); + $sUrl = html_entity_decode( SecureFileStore::secureStuff( $sUrl, true ) ); + } + + $aReturn[ $oRow->page_id ] = (object) array( + 'file_url' => $sUrl, + 'file_name' => $oImg->getName(), + 'file_size' => $oImg->getSize(), + 'file_bits' => $oImg->getBitDepth(), + 'file_user' => $oImg->getUser( 'id' ), + 'file_width' => $oImg->getWidth(), + 'file_height' => $oImg->getHeight(), + 'file_mimetype' => $oImg->getMimeType(), # major/minor + 'file_metadata' => unserialize( $oImg->getMetadata() ), + 'file_user_text' => $oImg->getUser( 'text' ), + 'file_extension' => $oImg->getExtension(), + 'file_timestamp' => $oImg->getTimestamp(), + 'file_mediatype' => $oImg->getMediaType(), + 'file_description' => $oImg->getDescription(), + 'file_display_text' => $oImg->getName(), + 'file_thumbnail_url' => $sThumb, + 'page_id' => $oTitle->getArticleID(), + 'page_title' => $oTitle->getText(), + 'page_latest' => $oTitle->getLatestRevID(), + 'page_namespace' => $oTitle->getNamespace(), + 'page_categories' => array(), //Filled by a second step below + 'page_is_redirect' => $oTitle->isRedirect(), + + //For some reason 'page_is_new' and 'page_touched' are not + //initialized by 'Title::newFromRow'; Instead when calling + //'Title->isNew()' or 'Title->getTouched()' an extra query is + //being sent to the database, wich introduced a performance + //issue. As the resulting data is the same we just use the raw + //form here. + 'page_is_new' => (bool)$oRow->page_is_new, + 'page_touched' => $oRow->page_touched + ); + } + + //Second query: Get all categories of each file page + $aPageIds = array_keys( $aReturn ); + if( !empty( $aPageIds ) ) { + $oDbr = wfGetDB( DB_SLAVE ); + $oCatRes = $oDbr->select( + 'categorylinks', + array( 'cl_from', 'cl_to' ), + array( 'cl_from' => $aPageIds ) + ); + foreach( $oCatRes as $oCatRow ) { + $aReturn[$oCatRow->cl_from]->page_categories[] = $oCatRow->cl_to; + } + } + + return array_values( $aReturn ); + //TODO: Find out if or where this hook was used before + //wfRunHooks( 'BSInsertFileGetFilesBeforeQuery', array( &$aConds, &$aNameFilters ) ); + } + + public function fetchCaseInsensitive( $sQuery ) { + $oDbr = wfGetDB( DB_SLAVE ); + + $aContidions = array( + 'page_namespace' => NS_FILE, + 'page_title = img_name', + 'page_id = si_page' //Needed for case insensitive quering; Maybe + //implement 'query' as a implicit filter on 'img_name' field? + ); + + if( !empty( $sQuery ) ) { + $aContidions[] = "si_title ".$oDbr->buildLike( + $oDbr->anyString(), + strtolower( $sQuery ), //make case insensitive! + $oDbr->anyString() + ); + } + + $res = $oDbr->select( + array( 'image', 'page', 'searchindex' ), + '*', + $aContidions, + __METHOD__ + ); + + return $res; + } + + public function fetchCaseSensitive( $sQuery ) { + $oDbr = wfGetDB( DB_SLAVE ); + + $aContidions = array( + 'page_namespace' => NS_FILE, + 'page_title = img_name', + ); + + if( !empty( $sQuery ) ) { + $aContidions[] = "img_name ".$oDbr->buildLike( + $oDbr->anyString(), + str_replace(' ', '_', $sQuery ), + $oDbr->anyString() + ); + } + + $res = $oDbr->select( + array( 'image', 'page' ), + '*', + $aContidions, + __METHOD__ + ); + + return $res; + } +} diff --git a/includes/api/BSApiTasksBase.php b/includes/api/BSApiTasksBase.php new file mode 100644 index 0000000..986a7d3 --- /dev/null +++ b/includes/api/BSApiTasksBase.php @@ -0,0 +1,147 @@ +<?php +/** + * Provides the base api for BlueSpice. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * This file is part of BlueSpice for MediaWiki + * For further information visit http://www.blue-spice.org + * + * @author Patric Wirth <[email protected]> + * @package Bluespice_Foundation + * @copyright Copyright (C) 2011 Hallo Welt! - Medienwerkstatt GmbH, All rights reserved. + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License v2 or later + * @filesource + */ + +/** + * Api base class for simple tasks in BlueSpice + * @package BlueSpice_Foundation + */ +abstract class BSApiTasksBase extends BSApiBase { + + /** + * Methods that can be called by task param + * @var array + */ + protected $aTasks = array(); + + /** + * The execute() method will be invoked directly by ApiMain immediately + * before the result of the module is output. Aside from the + * constructor, implementations should assume that no other methods + * will be called externally on the module before the result is + * processed. + * @return null + */ + public function execute() { + $aParams = $this->extractRequestParams(); + + //Avoid API warning: register the parameter used to bust browser cache + $this->getMain()->getVal( '_' ); + + $sMethod = 'task_'.$aParams['task']; + + 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 ); + } + + 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 BSStandardAPIResponse + */ + protected function makeStandardReturn() { + return new BSStandardAPIResponse(); + } + + /** + * Returns an array of allowed parameters + * @return array + */ + protected function getAllowedParams() { + return array( + 'task' => array( + ApiBase::PARAM_REQUIRED => true, + 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; + } + + /** + * Returns the basic param descriptions + * @return array + */ + 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', + ); + } + + /** + * Returns the bsic description for this module + * @return type + */ + public function getDescription() { + return array( + 'BSApiTasksBase: This should be implemented by subclass' + ); + } + + /** + * Returns the basic example + * @return type + */ + public function getExamples() { + return array( + '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/api/BsApiBase.php b/includes/api/BsApiBase.php index 6548962..3646340 100644 --- a/includes/api/BsApiBase.php +++ b/includes/api/BsApiBase.php @@ -1,173 +1,50 @@ <?php -/** - * Provides the base api for BlueSpice. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - * This file is part of BlueSpice for MediaWiki - * For further information visit http://www.blue-spice.org - * - * @author Patric Wirth <[email protected]> - * @package Bluespice_Foundation - * @copyright Copyright (C) 2011 Hallo Welt! - Medienwerkstatt GmbH, All rights reserved. - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License v2 or later - * @filesource - */ -/** - * Api base class for BlueSpice - * @package BlueSpice_Foundation - */ -abstract class BsApiBase extends ApiBase { - +abstract class BSApiBase extends ApiBase { /** - * Methods that can be called by task param - * @var array + * 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 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 ); - } - - /** - * The execute() method will be invoked directly by ApiMain immediately - * before the result of the module is output. Aside from the - * constructor, implementations should assume that no other methods - * will be called externally on the module before the result is - * processed. - * @return null - */ - public function execute() { - $aParams = $this->extractRequestParams(); - - //Avoid API warning: register the parameter used to bust browser cache - $this->getMain()->getVal( '_' ); - - if( !isset($aParams['task']) ) return; - - if( in_array($aParams['task'], static::$aTasks) ) { - $oResult = call_user_func( - array($this, $aParams['task']), - $aParams - ); + 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' ); + } + } } - $this->getResult()->addValue(null, 'bs', $oResult); + //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' ); + } + } + } } - /** - * Standard return object - * Every task should return this! - * @return object - */ - protected static function stdReturn() { - return $oReturn = (object) array( - 'result' => array( - 'payload' => null, - 'success' => false, - 'message' => '', - 'errors' => array(), - 'payload_count' => 0, - ) - ); + protected function getRequiredPermissions() { + return array( 'read' ); } - /** - * Returns an array of allowed parameters - * @return array - */ - protected function getAllowedParams() { + protected function getExamples() { return array( - 'task' => array( - ApiBase::PARAM_REQUIRED => true, - ApiBase::PARAM_TYPE => 'string' - ), - 'format' => array( - ApiBase::PARAM_DFLT => 'json', - ApiBase::PARAM_TYPE => array( 'json', 'jsonfm' ), - ) - ); - } - - /** - * Returns the basic param descriptions - * @return array - */ - public function getParamDescription() { - return array( - 'task' => 'The task you would like to execute', - '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; - } - - /** - * Returns the bsic description for this module - * @return type - */ - public function getDescription() { - return array( - 'BsApiBase: This should be implemented by subclass' - ); - } - - /** - * Returns the basic example - * @return type - */ - public function getExamples() { - return array( - 'api.php?action=<childapimodule>&task=<taskofchildapimodule>', + 'api.php?action='.$this->getModuleName(), ); } } \ No newline at end of file diff --git a/resources/bluespice.extjs/BS/model/File.js b/resources/bluespice.extjs/BS/model/File.js new file mode 100644 index 0000000..8ee12c1 --- /dev/null +++ b/resources/bluespice.extjs/BS/model/File.js @@ -0,0 +1,41 @@ +/** + * This model is used to have a unified representation of all MediaWiki repo + * files. The information is a combination of the page and the file table + */ + +Ext.define('BS.model.File', { + extend: 'Ext.data.Model', + + idProperty: 'file_name', + + fields: [ + //Those are values we can gather from the MediaWiki 'page' table. + { name: 'page_id', type: 'int', defaultValue: 0 }, + { name: 'page_namespace', type: 'int', defaultValue: -99 }, + { name: 'page_title', type: 'string', defaultValue: '' }, + { name: 'page_is_new', type: 'bool', defaultValue: true }, + { name: 'page_touched', type: 'date', defaultValue: '19700101000000', dateFormat: 'YmdHis' }, + { name: 'page_is_redirect', type: 'bool', defaultValue: false }, + { name: 'page_latest', type: 'date', defaultValue: '19700101000000', dateFormat: 'YmdHis' }, + + //Here come custom fields that are calculated on the server side + { name: 'page_categories', type: 'array', defaultValue: [] }, + + { name: 'file_name', type: 'string' }, + { name: 'file_size', type: 'int', defaultValue: 0 }, + { name: 'file_bits', type: 'int', defaultValue: 0 }, + { name: 'file_user', type: 'int', defaultValue: 0 }, + { name: 'file_width', type: 'int', defaultValue: 0 }, + { name: 'file_height', type: 'int', defaultValue: 0 }, + { name: 'file_mimetype', type: 'string', defaultValue: 'unknown/unknown' }, + { name: 'file_metadata', type: 'object', defaultValue: {} }, + { name: 'file_extension', type: 'string', defaultValue: '' }, + { name: 'file_timestamp', type: 'date', defaultValue: '19700101000000', dateFormat: 'YmdHis' }, + { name: 'file_mediatype', type: 'string', defaultValue: '' }, + { name: 'file_description', type: 'string', defaultValue: '' }, + { name: 'file_display_text', type: 'string', defaultValue: '' }, //TODO: Maybe fallback to 'file_name' + { name: 'file_thumbnail_url', type: 'string', defaultValue: '' } + ] + + //TODO: Implement getter +}); \ No newline at end of file -- To view, visit https://gerrit.wikimedia.org/r/224410 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I6dc475aac57bfdeba0cabe02984bf61e3d153753 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/BlueSpiceFoundation Gerrit-Branch: REL1_23 Gerrit-Owner: Robert Vogel <[email protected]> Gerrit-Reviewer: Mglaser <[email protected]> Gerrit-Reviewer: Pwirth <[email protected]> Gerrit-Reviewer: Robert Vogel <[email protected]> Gerrit-Reviewer: Tweichart <[email protected]> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
