Commit 4e418049 authored by Felix Kopp's avatar Felix Kopp Committed by Christian Kuhn
Browse files

[TASK] Revamp EXT:recycler

Refactors the recycler extension to a modern architecture. The backend
is based on Extbase and Fluid, the UI is based on jQuery and
Twitter Bootstrap now.

Due to restrictions in the core, non-admin users cannot restore deleted
pages for now.

Kudos to Felix Kopp for porting the base to Extbase and Fluid.

Releases: master
Resolves: #64420
Change-Id: I9d330981af0b42703b8352c1d61bec818e08b38e
Reviewed-on: http://review.typo3.org/36109


Reviewed-by: Benni Mack's avatarBenjamin Mack <benni@typo3.org>
Tested-by: Benni Mack's avatarBenjamin Mack <benni@typo3.org>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent 75323122
......@@ -89,7 +89,7 @@ define('TYPO3/CMS/Backend/Modal', ['jquery', 'TYPO3/CMS/Backend/FlashMessages'],
Modal.currentModal.trigger('modal-dismiss');
});
if (content instanceof $) {
if (typeof content === 'object') {
Modal.currentModal.find('.modal-body').append(content);
} else {
// we need html, check if we have to wrap content in <p>
......
......@@ -15,6 +15,9 @@ namespace TYPO3\CMS\Recycler\Controller;
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\Utility\IconUtility;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Recycler\Utility\RecyclerUtility;
/**
......@@ -26,22 +29,25 @@ use TYPO3\CMS\Recycler\Utility\RecyclerUtility;
class DeletedRecordsController {
/**
* @var \TYPO3\CMS\Lang\LanguageService
* @var \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
*/
protected $languageService;
protected $runtimeCache = NULL;
/**
* Constructor
* @var DataHandler
*/
protected $tce;
public function __construct() {
$this->languageService = $GLOBALS['LANG'];
$this->runtimeCache = $this->getMemoryCache();
$this->tce = GeneralUtility::makeInstance(DataHandler::class);
}
/**
* Transforms the rows for the deleted Records into the Array View necessary for ExtJS Ext.data.ArrayReader
* Transforms the rows for the deleted records
*
* @param array $deletedRowsArray Array with table as key and array with all deleted rows
* @param int $totalDeleted Number of deleted records in total, for PagingToolbar
* @param int $totalDeleted Number of deleted records in total
* @return string JSON array
*/
public function transform($deletedRowsArray, $totalDeleted) {
......@@ -49,30 +55,93 @@ class DeletedRecordsController {
$jsonArray = array(
'rows' => array()
);
// iterate
if (is_array($deletedRowsArray) && count($deletedRowsArray) > 0) {
if (is_array($deletedRowsArray)) {
$lang = $this->getLanguageService();
$backendUser = $this->getBackendUser();
foreach ($deletedRowsArray as $table => $rows) {
$total += count($deletedRowsArray[$table]);
foreach ($rows as $row) {
$pageTitle = $this->getPageTitle((int)$row['pid']);
$backendUser = BackendUtility::getRecord('be_users', $row[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']], 'username', '', FALSE);
$jsonArray['rows'][] = array(
'uid' => $row['uid'],
'pid' => $row['pid'],
'icon' => IconUtility::getSpriteIconForRecord($table, $row),
'pageTitle' => RecyclerUtility::getUtf8String($pageTitle),
'table' => $table,
'crdate' => BackendUtility::datetime($row[$GLOBALS['TCA'][$table]['ctrl']['crdate']]),
'tstamp' => BackendUtility::datetime($row[$GLOBALS['TCA'][$table]['ctrl']['tstamp']]),
'owner' => htmlspecialchars($backendUser['username']),
'owner_uid' => $row[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']],
'tableTitle' => RecyclerUtility::getUtf8String($this->languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title'])),
'title' => htmlspecialchars(RecyclerUtility::getUtf8String(
BackendUtility::getRecordTitle($table, $row))),
'tableTitle' => RecyclerUtility::getUtf8String($lang->sL($GLOBALS['TCA'][$table]['ctrl']['title'])),
'title' => htmlspecialchars(RecyclerUtility::getUtf8String(BackendUtility::getRecordTitle($table, $row))),
'path' => RecyclerUtility::getRecordPath($row['pid'])
);
}
}
}
$jsonArray['total'] = $totalDeleted;
return json_encode($jsonArray);
return $jsonArray;
}
/**
* Gets the page title of the given page id
*
* @param int $pageId
* @return string
*/
protected function getPageTitle($pageId) {
$cacheId = 'recycler-pagetitle-' . $pageId;
if ($this->runtimeCache->has($cacheId)) {
$pageTitle = $this->runtimeCache->get($cacheId);
} else {
if ($pageId === 0) {
$pageTitle = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
} else {
$recordInfo = $this->tce->recordInfo('pages', $pageId, 'title');
$pageTitle = $recordInfo['title'];
}
$this->runtimeCache->set($cacheId, $pageTitle);
}
return $pageTitle;
}
/**
* Returns an instance of LanguageService
*
* @return \TYPO3\CMS\Lang\LanguageService
*/
protected function getLanguageService() {
return $GLOBALS['LANG'];
}
/**
* Returns the current BE user.
*
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
*/
protected function getBackendUser() {
return $GLOBALS['BE_USER'];
}
/**
* Create and returns an instance of the CacheManager
*
* @return \TYPO3\CMS\Core\Cache\CacheManager
*/
protected function getCacheManager() {
return GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class);
}
/**
* Gets an instance of the memory cache.
*
* @return \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
*/
protected function getMemoryCache() {
return $this->getCacheManager()->getCache('cache_runtime');
}
}
\ No newline at end of file
......@@ -14,7 +14,14 @@ namespace TYPO3\CMS\Recycler\Controller;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Http\AjaxRequestHandler;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3\CMS\Recycler\Domain\Model\Tables;
use TYPO3\CMS\Recycler\Domain\Model\DeletedRecords;
use TYPO3\CMS\Recycler\Controller\DeletedRecordsController;
/**
* Controller class for the 'recycler' extension. Handles the AJAX Requests
......@@ -25,133 +32,149 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
class RecyclerAjaxController {
/**
* Stores the content for the ajax output
* The local configuration array
*
* @var string
* @var array
*/
protected $content;
protected $conf = array();
/**
* Command to be processed
*
* @var string
*/
protected $command;
/**
* Stores relevant data from extJS
* Example: Json format
* [ ["pages",1],["pages",2],["tt_content",34] ]
*
* @var string
* The constructor of this class
*/
protected $data;
/**
* Initialize method
*
* @return void
*/
public function init() {
$this->mapCommand();
$this->getContent();
public function __construct() {
// Configuration, variable assignment
$this->conf['action'] = GeneralUtility::_GP('action');
$this->conf['table'] = GeneralUtility::_GP('table') ? GeneralUtility::_GP('table') : '';
$this->conf['limit'] = GeneralUtility::_GP('limit') ? (int)GeneralUtility::_GP('limit') : 25;
$this->conf['start'] = GeneralUtility::_GP('start') ? (int)GeneralUtility::_GP('limit') : 0;
$this->conf['filterTxt'] = GeneralUtility::_GP('filterTxt') ? GeneralUtility::_GP('filterTxt') : '';
$this->conf['startUid'] = GeneralUtility::_GP('startUid') ? (int)GeneralUtility::_GP('startUid') : 0;
$this->conf['depth'] = GeneralUtility::_GP('depth') ? (int)GeneralUtility::_GP('depth') : 0;
$this->conf['records'] = GeneralUtility::_GP('records') ? GeneralUtility::_GP('records') : NULL;
$this->conf['recursive'] = GeneralUtility::_GP('recursive') ? (bool)(int)GeneralUtility::_GP('recursive') : FALSE;
}
/**
* Maps the command to the correct Model and View
* The main dispatcher function. Collect data and prepare HTML output.
*
* @param array $params array of parameters from the AJAX interface, currently unused
* @param AjaxRequestHandler $ajaxObj object of type AjaxRequestHandler
* @return void
*/
public function mapCommand() {
$this->command = GeneralUtility::_GP('cmd');
$this->data = GeneralUtility::_GP('data');
// check params
if (!is_string($this->command)) {
// @TODO make devlog output
return;
}
// Create content
$this->createContent();
}
public function dispatch($params = array(), AjaxRequestHandler $ajaxObj = NULL) {
$extPath = ExtensionManagementUtility::extPath('recycler');
$view = GeneralUtility::makeInstance(StandaloneView::class);
$view->setPartialRootPaths(array('default' => $extPath . 'Resources/Private/Partials'));
/**
* Creates the content
*
* @return void
*/
protected function createContent() {
switch ($this->command) {
$content = '';
// Determine the scripts to execute
switch ($this->conf['action']) {
case 'getTables':
$this->setDataInSession('depthSelection', $this->conf['depth']);
/* @var $model Tables */
$model = GeneralUtility::makeInstance(Tables::class);
$content = $model->getTables($this->conf['startUid'], $this->conf['depth']);
break;
case 'getDeletedRecords':
$table = GeneralUtility::_GP('table') ? GeneralUtility::_GP('table') : GeneralUtility::_GP('tableDefault');
$limit = GeneralUtility::_GP('limit') ? (int)GeneralUtility::_GP('limit') : (int)GeneralUtility::_GP('pagingSizeDefault');
$start = GeneralUtility::_GP('start') ? (int)GeneralUtility::_GP('start') : 0;
$filter = GeneralUtility::_GP('filterTxt') ? GeneralUtility::_GP('filterTxt') : '';
$startUid = GeneralUtility::_GP('startUid') ? GeneralUtility::_GP('startUid') : '';
$depth = GeneralUtility::_GP('depth') ? GeneralUtility::_GP('depth') : '';
$this->setDataInSession('tableSelection', $table);
/* @var $model \TYPO3\CMS\Recycler\Domain\Model\DeletedRecords */
$model = GeneralUtility::makeInstance(\TYPO3\CMS\Recycler\Domain\Model\DeletedRecords::class);
$model->loadData($startUid, $table, $depth, $start . ',' . $limit, $filter);
$this->setDataInSession('tableSelection', $this->conf['table']);
$this->setDataInSession('depthSelection', $this->conf['depth']);
$this->setDataInSession('resultLimit', $this->conf['limit']);
/* @var $model DeletedRecords */
$model = GeneralUtility::makeInstance(DeletedRecords::class);
$model->loadData($this->conf['startUid'], $this->conf['table'], $this->conf['depth'], $this->conf['start'] . ',' . $this->conf['limit'], $this->conf['filterTxt']);
$deletedRowsArray = $model->getDeletedRows();
$model = GeneralUtility::makeInstance(\TYPO3\CMS\Recycler\Domain\Model\DeletedRecords::class);
$totalDeleted = $model->getTotalCount($startUid, $table, $depth, $filter);
// load view
/* @var $view \TYPO3\CMS\Recycler\Controller\DeletedRecordsController */
$view = GeneralUtility::makeInstance(\TYPO3\CMS\Recycler\Controller\DeletedRecordsController::class);
$str = $view->transform($deletedRowsArray, $totalDeleted);
/* @var $model DeletedRecords */
$model = GeneralUtility::makeInstance(DeletedRecords::class);
$totalDeleted = $model->getTotalCount($this->conf['startUid'], $this->conf['table'], $this->conf['depth'], $this->conf['filter']);
/* @var $view DeletedRecordsController */
$controller = GeneralUtility::makeInstance(DeletedRecordsController::class);
$recordsArray = $controller->transform($deletedRowsArray, $totalDeleted);
$modTS = $this->getBackendUser()->getTSConfig('mod.recycler');
$allowDelete = (bool)$this->getBackendUser()->user['admin'] ? TRUE : (bool)$modTS['properties']['allowDelete'];
$view->setTemplatePathAndFilename($extPath . 'Resources/Private/Templates/Ajax/RecordsTable.html');
$view->assign('records', $recordsArray['rows']);
$view->assign('allowDelete', $allowDelete);
$view->assign('total', $recordsArray['total']);
$content = json_encode(array(
'rows' => $view->render(),
'totalItems' => $recordsArray['total']
));
break;
case 'doDelete':
$str = FALSE;
/* @var $model \TYPO3\CMS\Recycler\Domain\Model\DeletedRecords */
$model = GeneralUtility::makeInstance(\TYPO3\CMS\Recycler\Domain\Model\DeletedRecords::class);
if ($model->deleteData($this->data)) {
$str = TRUE;
case 'undoRecords':
if (empty($this->conf['records']) || !is_array($this->conf['records'])) {
$content = json_encode(array(
'success' => FALSE,
'message' => LocalizationUtility::translate('flashmessage.delete.norecordsselected', 'recycler')
));
break;
}
/* @var $model DeletedRecords */
$model = GeneralUtility::makeInstance(DeletedRecords::class);
$success = $model->undeleteData($this->conf['records'], $this->conf['recursive']);
$affectedRecords = count($this->conf['records']);
$messageKey = 'flashmessage.undo.' . ($success ? 'success' : 'failure') . '.' . ($affectedRecords === 1 ? 'singular' : 'plural');
$content = json_encode(array(
'success' => TRUE,
'message' => sprintf(LocalizationUtility::translate($messageKey, 'recycler'), $affectedRecords)
));
break;
case 'doUndelete':
$str = FALSE;
$recursive = GeneralUtility::_GP('recursive');
/* @var $model \TYPO3\CMS\Recycler\Domain\Model\DeletedRecords */
$model = GeneralUtility::makeInstance(\TYPO3\CMS\Recycler\Domain\Model\DeletedRecords::class);
if ($model->undeleteData($this->data, $recursive)) {
$str = TRUE;
case 'deleteRecords':
if (empty($this->conf['records']) || !is_array($this->conf['records'])) {
$content = json_encode(array(
'success' => FALSE,
'message' => LocalizationUtility::translate('flashmessage.delete.norecordsselected', 'recycler')
));
break;
}
/* @var $model DeletedRecords */
$model = GeneralUtility::makeInstance(DeletedRecords::class);
$success = $model->deleteData($this->conf['records']);
$affectedRecords = count($this->conf['records']);
$messageKey = 'flashmessage.delete.' . ($success ? 'success' : 'failure') . '.' . ($affectedRecords === 1 ? 'singular' : 'plural');
$content = json_encode(array(
'success' => TRUE,
'message' => sprintf(LocalizationUtility::translate($messageKey, 'recycler'), $affectedRecords)
));
break;
case 'getTables':
$depth = GeneralUtility::_GP('depth') ? GeneralUtility::_GP('depth') : 0;
$startUid = GeneralUtility::_GP('startUid') ? GeneralUtility::_GP('startUid') : '';
$this->setDataInSession('depthSelection', $depth);
/* @var $model \TYPO3\CMS\Recycler\Domain\Model\Tables */
$model = GeneralUtility::makeInstance(\TYPO3\CMS\Recycler\Domain\Model\Tables::class);
$str = $model->getTables('json', TRUE, $startUid, $depth);
break;
default:
$str = 'No command was recognized.';
}
$this->content = $str;
}
/**
* Returns the content that was created in the mapCommand method
*
* @return string
*/
public function getContent() {
echo $this->content;
$ajaxObj->addContent($this->conf['table'] . '_' . $this->conf['start'], $content);
}
/**
* Sets data in the session of the current backend user.
*
* @param string $identifier: The identifier to be used to set the data
* @param string $data: The data to be stored in the session
* @param string $identifier The identifier to be used to set the data
* @param string $data The data to be stored in the session
* @return void
*/
protected function setDataInSession($identifier, $data) {
/* @var $beUser \TYPO3\CMS\Core\Authentication\BackendUserAuthentication */
$beUser = $GLOBALS['BE_USER'];
$beUser = $this->getBackendUser();
$beUser->uc['tx_recycler'][$identifier] = $data;
$beUser->writeUC();
}
/**
* Returns the BackendUser
*
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
*/
protected function getBackendUser() {
return $GLOBALS['BE_USER'];
}
/**
* @return \TYPO3\CMS\Lang\LanguageService
*/
protected function getLanguageService() {
return $GLOBALS['LANG'];
}
}
......@@ -14,26 +14,22 @@ namespace TYPO3\CMS\Recycler\Controller;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Module 'Recycler' for the 'recycler' extension.
*
* @author Julian Kleinhans <typo3@kj187.de>
*/
class RecyclerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClass {
class RecyclerModuleController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController {
/**
* @var \TYPO3\CMS\Backend\Template\DocumentTemplate
* @var string
*/
public $doc;
protected $relativePath;
/**
* @var string
*/
protected $relativePath;
public $perms_clause;
/**
* @var array
......@@ -55,70 +51,33 @@ class RecyclerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClass
*/
protected $recordsPageLimit = 50;
/**
* @var \TYPO3\CMS\Core\Page\PageRenderer
*/
protected $pageRenderer;
/**
* @var \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
*/
protected $backendUser;
/**
* @var \TYPO3\CMS\Lang\LanguageService
*/
protected $languageService;
/**
* The name of the module
*
* @var string
*/
protected $moduleName = 'web_txrecyclerM1';
/**
* Constructor
*/
public function __construct() {
$this->languageService = $GLOBALS['LANG'];
$this->languageService->includeLLFile('EXT:recycler/mod1/locallang.xlf');
$this->backendUser = $GLOBALS['BE_USER'];
$this->MCONF = array(
'name' => $this->moduleName,
);
}
/**
* Initializes the Module
*
* @return void
*/
public function initialize() {
parent::init();
$this->doc = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Template\DocumentTemplate::class);
$this->doc->setModuleTemplate(ExtensionManagementUtility::extPath('recycler') . 'mod1/mod_template.html');
$this->doc->backPath = $GLOBALS['BACK_PATH'];
$this->doc->setExtDirectStateProvider();
$this->pageRenderer = $this->doc->getPageRenderer();
$this->relativePath = ExtensionManagementUtility::extRelPath('recycler');
$this->pageRecord = BackendUtility::readPageAccess($this->id, $this->perms_clause);
public function initializeAction() {
$this->id = GeneralUtility::_GP('id');
$backendUser = $this->getBackendUser();
$this->perms_clause = $backendUser->getPagePermsClause(1);
$this->pageRecord = \TYPO3\CMS\Backend\Utility\BackendUtility::readPageAccess($this->id, $this->perms_clause);
$this->isAccessibleForCurrentUser = $this->id && is_array($this->pageRecord) || !$this->id && $this->isCurrentUserAdmin();
//don't access in workspace
if ($this->backendUser->workspace !== 0) {
// don't access in workspace
if ($backendUser->workspace !== 0) {
$this->isAccessibleForCurrentUser = FALSE;
}
//read configuration
$modTS = $this->backendUser->getTSConfig('mod.recycler');
// read configuration
$modTS = $backendUser->getTSConfig('mod.recycler');
if ($this->isCurrentUserAdmin()) {
$this->allowDelete = TRUE;
} else {
$this->allowDelete = $modTS['properties']['allowDelete'] == '1';
$this->allowDelete = (bool)$modTS['properties']['allowDelete'];
}
if (isset($modTS['properties']['recordsPageLimit']) && (int)$modTS['properties']['recordsPageLimit'] > 0) {
$this->recordsPageLimit = (int)$modTS['properties']['recordsPageLimit'];
if (isset($modTS['properties']['recordsPageLimit']) && intval($modTS['properties']['recordsPageLimit']) > 0) {
$this->recordsPageLimit = intval($modTS['properties']['recordsPageLimit']);
}
}
......@@ -127,31 +86,15 @@ class RecyclerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClass
*
* @return void
*/
public function render() {
$this->content .= $this->doc->header($this->languageService->getLL('title'));
$this->content .= '<p class="lead">' . $this->languageService->getLL('description') . '</p>';
if ($this->isAccessibleForCurrentUser) {
$this->loadHeaderData();
// div container for renderTo
$this->content .= '<div id="recyclerContent"></div>';
} else {
// If no access or if ID == zero
$this->content .= $this->doc->spacer(10);
}
}
public function indexAction() {
// Integrate dynamic JavaScript such as configuration or lables:
$jsConfiguration = $this->getJavaScriptConfiguration();
$this->getPageRenderer()->addInlineSettingArray('Recycler', $jsConfiguration);
$this->getPageRenderer()->addInlineLanguageLabelFile('EXT:recycler/Resources/Private/Language/locallang.xlf');
/**
* Flushes the rendered content to browser.
*
* @return void
*/
public function flush() {
$content = $this->doc->moduleBody($this->pageRecord, $this->getDocHeaderButtons(), $this->getTemplateMarkers());
// Renders the module page
$content = $this->doc->render($this->languageService->getLL('title'), $content);
$this->content = NULL;
$this->doc = NULL;
echo $content;
$this->view->assign('title', $this->getLanguageService()->getLL('title'));
$this->view->assign('content', $this->content);
$this->view->assign('allowDelete', $this->allowDelete);
}
/**
......@@ -160,31 +103,7 @@ class RecyclerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClass
* @return bool Whether the current user is admin
*/