Project

General

Profile

Bug #78258 » DatabaseRecordList.php

Luis García, 2018-03-26 15:48

 
<?php
namespace Vendor\Package\Xclass;

use TYPO3\CMS\Backend\RecordList\RecordListGetTableHookInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
* Xclass for TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList
*
* Makes CSV exports without generating HTML code and accumulating rows to save memory.
*/
class DatabaseRecordList extends \TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList
{
/**
* The output stream to write the CSV lines to.
*
* @var
*/
protected $outputStream;

/**
* Table name (only for CSV export)
*
* @var string
*/
protected $exportedTable;

/**
* Table field (column) where header value is found
*
* @var string
*/
protected $titleCol;

/**
* Creates the listing of records from a single table
*
* @param string $table Table name
* @param int $id Page id
* @param string $rowList List of fields to show in the listing. Pseudo fields will be added including the record header.
* @throws \UnexpectedValueException
* @throws \TYPO3\CMS\Core\Exception
* @return string HTML table with the listing for the record.
*/
public function getTable($table, $id, $rowList = '')
{
$rowListArray = GeneralUtility::trimExplode(',', $rowList, true);
// if no columns have been specified, show description (if configured)
if (!empty($GLOBALS['TCA'][$table]['ctrl']['descriptionColumn']) && empty($rowListArray)) {
array_push($rowListArray, $GLOBALS['TCA'][$table]['ctrl']['descriptionColumn']);
}
$backendUser = $this->getBackendUserAuthentication();
$lang = $this->getLanguageService();
$db = $this->getDatabaseConnection();
// Init
$addWhere = '';
$titleCol = $GLOBALS['TCA'][$table]['ctrl']['label'];
$thumbsCol = $GLOBALS['TCA'][$table]['ctrl']['thumbnail'];
$l10nEnabled = $GLOBALS['TCA'][$table]['ctrl']['languageField']
&& $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
&& !$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable'];
$tableCollapsed = (bool)$this->tablesCollapsed[$table];
// prepare space icon
$this->spaceIcon = '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
// Cleaning rowlist for duplicates and place the $titleCol as the first column always!
$this->fieldArray = [];
// title Column
// Add title column
$this->fieldArray[] = $titleCol;
// Control-Panel
if (!GeneralUtility::inList($rowList, '_CONTROL_')) {
$this->fieldArray[] = '_CONTROL_';
}
// Clipboard
if ($this->showClipboard) {
$this->fieldArray[] = '_CLIPBOARD_';
}
// Ref
if (!$this->dontShowClipControlPanels) {
$this->fieldArray[] = '_REF_';
}
// Path
if ($this->searchLevels) {
$this->fieldArray[] = '_PATH_';
}
// Localization
if ($this->localizationView && $l10nEnabled) {
$this->fieldArray[] = '_LOCALIZATION_';
$this->fieldArray[] = '_LOCALIZATION_b';
// Only restrict to the default language if no search request is in place
if ($this->searchString === '') {
$addWhere .= ' AND (
' . $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '<=0
OR
' . $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] . ' = 0
)';
}
}
// Cleaning up:
$this->fieldArray = array_unique(array_merge($this->fieldArray, $rowListArray));
if ($this->noControlPanels) {
$tempArray = array_flip($this->fieldArray);
unset($tempArray['_CONTROL_']);
unset($tempArray['_CLIPBOARD_']);
$this->fieldArray = array_keys($tempArray);
}
// Creating the list of fields to include in the SQL query:
$selectFields = $this->fieldArray;
$selectFields[] = 'uid';
$selectFields[] = 'pid';
// adding column for thumbnails
if ($thumbsCol) {
$selectFields[] = $thumbsCol;
}
if ($table == 'pages') {
$selectFields[] = 'module';
$selectFields[] = 'extendToSubpages';
$selectFields[] = 'nav_hide';
$selectFields[] = 'doktype';
$selectFields[] = 'shortcut';
$selectFields[] = 'shortcut_mode';
$selectFields[] = 'mount_pid';
}
if (is_array($GLOBALS['TCA'][$table]['ctrl']['enablecolumns'])) {
$selectFields = array_merge($selectFields, $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']);
}
foreach (['type', 'typeicon_column', 'editlock'] as $field) {
if ($GLOBALS['TCA'][$table]['ctrl'][$field]) {
$selectFields[] = $GLOBALS['TCA'][$table]['ctrl'][$field];
}
}
if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
$selectFields[] = 't3ver_id';
$selectFields[] = 't3ver_state';
$selectFields[] = 't3ver_wsid';
}
if ($l10nEnabled) {
$selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
$selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
}
if ($GLOBALS['TCA'][$table]['ctrl']['label_alt']) {
$selectFields = array_merge(
$selectFields,
GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true)
);
}
// Unique list!
$selectFields = array_unique($selectFields);
$fieldListFields = $this->makeFieldList($table, 1);
if (empty($fieldListFields) && $GLOBALS['TYPO3_CONF_VARS']['BE']['debug']) {
$message = sprintf($lang->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:missingTcaColumnsMessage', true), $table, $table);
$messageTitle = $lang->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:missingTcaColumnsMessageTitle', true);
/** @var FlashMessage $flashMessage */
$flashMessage = GeneralUtility::makeInstance(
FlashMessage::class,
$message,
$messageTitle,
FlashMessage::WARNING,
true
);
/** @var $flashMessageService FlashMessageService */
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
/** @var $defaultFlashMessageQueue \TYPO3\CMS\Core\Messaging\FlashMessageQueue */
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
$defaultFlashMessageQueue->enqueue($flashMessage);
}
// Making sure that the fields in the field-list ARE in the field-list from TCA!
$selectFields = array_intersect($selectFields, $fieldListFields);
// Implode it into a list of fields for the SQL-statement.
$selFieldList = implode(',', $selectFields);
$this->selFieldList = $selFieldList;
if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['getTable'])) {
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['getTable'] as $classData) {
$hookObject = GeneralUtility::getUserObj($classData);
if (!$hookObject instanceof RecordListGetTableHookInterface) {
throw new \UnexpectedValueException($classData . ' must implement interface ' . RecordListGetTableHookInterface::class, 1195114460);
}
$hookObject->getDBlistQuery($table, $id, $addWhere, $selFieldList, $this);
}
}
// Create the SQL query for selecting the elements in the listing:
// do not do paging when outputting as CSV
if ($this->csvOutput) {
$this->iLimit = 0;
}
if ($this->firstElementNumber > 2 && $this->iLimit > 0) {
// Get the two previous rows for sorting if displaying page > 1
$this->firstElementNumber = $this->firstElementNumber - 2;
$this->iLimit = $this->iLimit + 2;
// (API function from TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRecordList)
$queryParts = $this->makeQueryArray($table, $id, $addWhere);

$this->firstElementNumber = $this->firstElementNumber + 2;
$this->iLimit = $this->iLimit - 2;
} else {
// (API function from TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRecordList)
$queryParts = $this->makeQueryArray($table, $id, $addWhere);
}

// Finding the total amount of records on the page
// (API function from TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRecordList)
$this->setTotalItems($queryParts);

// Init:
$dbCount = 0;
$out = '';
$tableHeader = '';
$result = null;
$listOnlyInSingleTableMode = $this->listOnlyInSingleTableMode && !$this->table;
// If the count query returned any number of records, we perform the real query,
// selecting records.
if ($this->totalItems) {
// Fetch records only if not in single table mode
if ($listOnlyInSingleTableMode) {
$dbCount = $this->totalItems;
} else {
// Set the showLimit to the number of records when outputting as CSV
if ($this->csvOutput) {
$this->showLimit = $this->totalItems;
$this->iLimit = $this->totalItems;
}
$result = $db->exec_SELECT_queryArray($queryParts);
$dbCount = $db->sql_num_rows($result);
}
}
// If any records was selected, render the list:
if ($dbCount) {
$tableTitle = $lang->sL($GLOBALS['TCA'][$table]['ctrl']['title'], true);
if ($tableTitle === '') {
$tableTitle = $table;
}
// Header line is drawn
$theData = [];
if ($this->disableSingleTableView) {
$theData[$titleCol] = '<span class="c-table">' . BackendUtility::wrapInHelp($table, '', $tableTitle)
. '</span> (<span class="t3js-table-total-items">' . $this->totalItems . '</span>)';
} else {
$icon = $this->table
? '<span title="' . $lang->getLL('contractView', true) . '">' . $this->iconFactory->getIcon('actions-view-table-collapse', Icon::SIZE_SMALL)->render() . '</span>'
: '<span title="' . $lang->getLL('expandView', true) . '">' . $this->iconFactory->getIcon('actions-view-table-expand', Icon::SIZE_SMALL)->render() . '</span>';
$theData[$titleCol] = $this->linkWrapTable($table, $tableTitle . ' (<span class="t3js-table-total-items">' . $this->totalItems . '</span>) ' . $icon);
}
if ($listOnlyInSingleTableMode) {
$tableHeader .= BackendUtility::wrapInHelp($table, '', $theData[$titleCol]);
} else {
// Render collapse button if in multi table mode
$collapseIcon = '';
if (!$this->table) {
$href = htmlspecialchars(($this->listURL() . '&collapse[' . $table . ']=' . ($tableCollapsed ? '0' : '1')));
$title = $tableCollapsed
? $lang->sL('LLL:EXT:lang/locallang_core.xlf:labels.expandTable', true)
: $lang->sL('LLL:EXT:lang/locallang_core.xlf:labels.collapseTable', true);
$icon = '<span class="collapseIcon">' . $this->iconFactory->getIcon(($tableCollapsed ? 'actions-view-list-expand' : 'actions-view-list-collapse'), Icon::SIZE_SMALL)->render() . '</span>';
$collapseIcon = '<a href="' . $href . '" title="' . $title . '" class="pull-right t3js-toggle-recordlist" data-table="' . htmlspecialchars($table) . '" data-toggle="collapse" data-target="#recordlist-' . htmlspecialchars($table) . '">' . $icon . '</a>';
}
$tableHeader .= $theData[$titleCol] . $collapseIcon;
}
// Render table rows only if in multi table view or if in single table view
$rowOutput = '';
if (!$listOnlyInSingleTableMode || $this->table) {
// Fixing an order table for sortby tables
$this->currentTable = [];
$currentIdList = [];
$doSort = $GLOBALS['TCA'][$table]['ctrl']['sortby'] && !$this->sortField;
$prevUid = 0;
$prevPrevUid = 0;
// Get first two rows and initialize prevPrevUid and prevUid if on page > 1
if ($this->firstElementNumber > 2 && $this->iLimit > 0) {
$row = $db->sql_fetch_assoc($result);
$prevPrevUid = -((int)$row['uid']);
$row = $db->sql_fetch_assoc($result);
$prevUid = $row['uid'];
}

// XCLASS: Handle CSV output without generating HTML code or accumulating rows.
if ($this->csvOutput) {
$this->outputCsvFile($table, $result);
}

$accRows = [];
// Accumulate rows here
while ($row = $db->sql_fetch_assoc($result)) {
if (!$this->isRowListingConditionFulfilled($table, $row)) {
continue;
}
// In offline workspace, look for alternative record:
BackendUtility::workspaceOL($table, $row, $backendUser->workspace, true);
if (is_array($row)) {
$accRows[] = $row;
$currentIdList[] = $row['uid'];
if ($doSort) {
if ($prevUid) {
$this->currentTable['prev'][$row['uid']] = $prevPrevUid;
$this->currentTable['next'][$prevUid] = '-' . $row['uid'];
$this->currentTable['prevUid'][$row['uid']] = $prevUid;
}
$prevPrevUid = isset($this->currentTable['prev'][$row['uid']]) ? -$prevUid : $row['pid'];
$prevUid = $row['uid'];
}
}
}
$db->sql_free_result($result);
$this->totalRowCount = count($accRows);
// CSV initiated
if ($this->csvOutput) {
$this->initCSV();
}
// Render items:
$this->CBnames = [];
$this->duplicateStack = [];
$this->eCounter = $this->firstElementNumber;
$cc = 0;
foreach ($accRows as $row) {
// Render item row if counter < limit
if ($cc < $this->iLimit) {
$cc++;
$this->translations = false;
$rowOutput .= $this->renderListRow($table, $row, $cc, $titleCol, $thumbsCol);
// If localization view is enabled and no search happened it means that the selected
// records are either default or All language and here we will not select translations
// which point to the main record:
if ($this->localizationView && $l10nEnabled && $this->searchString === '') {
// For each available translation, render the record:
if (is_array($this->translations)) {
foreach ($this->translations as $lRow) {
// $lRow isn't always what we want - if record was moved we've to work with the
// placeholder records otherwise the list is messed up a bit
if ($row['_MOVE_PLH_uid'] && $row['_MOVE_PLH_pid']) {
$where = 't3ver_move_id="' . (int)$lRow['uid'] . '" AND pid="' . $row['_MOVE_PLH_pid']
. '" AND t3ver_wsid=' . $row['t3ver_wsid'] . BackendUtility::deleteClause($table);
$tmpRow = BackendUtility::getRecordRaw($table, $where, $selFieldList);
$lRow = is_array($tmpRow) ? $tmpRow : $lRow;
}
// In offline workspace, look for alternative record:
BackendUtility::workspaceOL($table, $lRow, $backendUser->workspace, true);
if (is_array($lRow) && $backendUser->checkLanguageAccess($lRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
$currentIdList[] = $lRow['uid'];
$rowOutput .= $this->renderListRow($table, $lRow, $cc, $titleCol, $thumbsCol, 18);
}
}
}
}
}
// Counter of total rows incremented:
$this->eCounter++;
}
// Record navigation is added to the beginning and end of the table if in single
// table mode
if ($this->table) {
$rowOutput = $this->renderListNavigation('top') . $rowOutput . $this->renderListNavigation('bottom');
} else {
// Show that there are more records than shown
if ($this->totalItems > $this->itemsLimitPerTable) {
$countOnFirstPage = $this->totalItems > $this->itemsLimitSingleTable ? $this->itemsLimitSingleTable : $this->totalItems;
$hasMore = $this->totalItems > $this->itemsLimitSingleTable;
$colspan = $this->showIcon ? count($this->fieldArray) + 1 : count($this->fieldArray);
$rowOutput .= '<tr><td colspan="' . $colspan . '">
<a href="' . htmlspecialchars(($this->listURL() . '&table=' . rawurlencode($table))) . '" class="btn btn-default">'
. '<span class="t3-icon fa fa-chevron-down"></span> <i>[1 - ' . $countOnFirstPage . ($hasMore ? '+' : '') . ']</i></a>
</td></tr>';
}
}
// The header row for the table is now created:
$out .= $this->renderListHeader($table, $currentIdList);
}

$collapseClass = $tableCollapsed && !$this->table ? 'collapse' : 'collapse in';
$dataState = $tableCollapsed && !$this->table ? 'collapsed' : 'expanded';

// The list of records is added after the header:
$out .= $rowOutput;
// ... and it is all wrapped in a table:
$out = '



<!--
DB listing of elements: "' . htmlspecialchars($table) . '"
-->
<div class="panel panel-space panel-default recordlist">
<div class="panel-heading">
' . $tableHeader . '
</div>
<div class="' . $collapseClass . '" data-state="' . $dataState . '" id="recordlist-' . htmlspecialchars($table) . '">
<div class="table-fit">
<table data-table="' . htmlspecialchars($table) . '" class="table table-striped table-hover' . ($listOnlyInSingleTableMode ? ' typo3-dblist-overview' : '') . '">
' . $out . '
</table>
</div>
</div>
</div>
';
// Output csv if...
// This ends the page with exit.
if ($this->csvOutput) {
$this->outputCSV($table);
}
}
// Return content:
return $out;
}

/**
* Initializes internal csvLines array with the header of field names
*
* @param string $prefix Filename prefix (table name)
* @return void
*/
protected function initCsvOutput($prefix)
{
// Setting filename:
$filename = $prefix . '_' . date('dmy-Hi') . '.csv';

// Deactivate buffered output! (For huge files)
// ob_end_clean();

// Prepare output stream
$this->outputStream = fopen('php://output', 'w');

// Creating output header:
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $filename . '"');

// Headers to show CSV content in browser
// header('Content-Type: text/plain');
// header('Content-Disposition: inline; filename="' . $filename . '"');

// Cache-Control header is needed here to solve an issue with browser IE and
// versions lower than 9. See for more information: http://support.microsoft.com/kb/323308
header("Cache-Control: ''");

$this->addHeaderRowToCSV();
}

/**
* Adds input row of values to the internal csvLines array as a CSV formatted line
*
* @param array $csvRow Array with values to be listed.
* @return void
*/
public function setCsvRow($csvRow)
{
if ($this->outputStream !== false) {
fputcsv($this->outputStream, $csvRow);
}
}

/**
* Output records of a single table to a downloadable CSV file before the rows get fetched to be displayed
* by the List module.
*
* @param string $table The table to be exported.
* @param bool|\mysqli_result|object $result The DB handle to read the records.
*/
protected function outputCsvFile($table, $result)
{
$backendUser = $this->getBackendUserAuthentication();
$db = $this->getDatabaseConnection();

$this->exportedTable = $table;
$this->titleCol = $GLOBALS['TCA'][$table]['ctrl']['label'];
$l10nEnabled = $GLOBALS['TCA'][$table]['ctrl']['languageField']
&& $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
&& !$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable'];

$this->initCsvOutput($table);

$cc = 0;

// Accumulate rows here
while ($row = $db->sql_fetch_assoc($result)) {
if (!$this->isRowListingConditionFulfilled($table, $row)) {
continue;
}

// In offline workspace, look for alternative record:
BackendUtility::workspaceOL($table, $row, $backendUser->workspace, true);
if (is_array($row)) {
// Render item row if counter < limit
if ($cc < $this->iLimit) {
$cc++;
$this->translations = false;

$this->renderListRowForCsv($row);

// If localization view is enabled and no search happened it means that the selected
// records are either default or All language and here we will not select translations
// which point to the main record:
if ($this->localizationView && $l10nEnabled && $this->searchString === '') {
// For each available translation, render the record:
if (is_array($this->translations)) {
foreach ($this->translations as $lRow) {
// $lRow isn't always what we want - if record was moved we've to work with the
// placeholder records otherwise the list is messed up a bit
if ($row['_MOVE_PLH_uid'] && $row['_MOVE_PLH_pid']) {
$where = 't3ver_move_id="' . (int)$lRow['uid'] . '" AND pid="' . $row['_MOVE_PLH_pid']
. '" AND t3ver_wsid=' . $row['t3ver_wsid'] . BackendUtility::deleteClause($table);
$tmpRow = BackendUtility::getRecordRaw($table, $where, $this->selFieldList);
$lRow = is_array($tmpRow) ? $tmpRow : $lRow;
}

// In offline workspace, look for alternative record:
BackendUtility::workspaceOL($table, $lRow, $backendUser->workspace, true);
if (is_array($lRow) && $backendUser->checkLanguageAccess($lRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
$this->renderListRowForCsv($lRow);
}
}
}
}
}
// Counter of total rows incremented:
$this->eCounter++;
}
}

$db->sql_free_result($result);

fclose($this->outputStream);

exit();
}

/**
* Render a single row for CSV
*
* @param mixed[] $row Current record
* @return void
*/
protected function renderListRowForCsv($row)
{
if (!is_array($row)) {
return;
}

$id_orig = null;

// If in search mode, make sure the preview will show the correct page
if ((string)$this->searchString !== '') {
$id_orig = $this->id;
$this->id = $row['pid'];
}

// Incr. counter.
$this->counter++;

foreach ($this->fieldArray as $fCol) {
if ($fCol != $this->titleCol
&& $fCol != 'pid'
&& $fCol != '_PATH_'
&& $fCol != '_REF_'
&& $fCol != '_CONTROL_'
&& $fCol != '_CLIPBOARD_'
&& $fCol != '_LOCALIZATION_'
&& $fCol != '_LOCALIZATION_b'
) {
$row[$fCol] = BackendUtility::getProcessedValueExtra($this->exportedTable, $fCol, $row[$fCol], 0, $row['uid']);
}
}

// Reset the ID if it was overwritten
if ((string)$this->searchString !== '') {
$this->id = $id_orig;
}

// Add row to CSV list:
$this->addToCSV($row);
}
}
    (1-1/1)