/** * Checks if a given string is a Uniform Resource Locator (URL). * * On seriously malformed URLs, parse_url may return FALSE and emit an * E_WARNING. * * filter_var() requires a scheme to be present. * * http://www.faqs.org/rfcs/rfc2396.html * Scheme names consist of a sequence of characters beginning with a * lower case letter and followed by any combination of lower case letters, * digits, plus ("+"), period ("."), or hyphen ("-"). For resiliency, * programs interpreting URI should treat upper case letters as equivalent to * lower case in scheme names (e.g., allow "HTTP" as well as "http"). * scheme = alpha *( alpha | digit | "+" | "-" | "." ) * * Convert the domain part to punicode if it does not look like a regular * domain name. Only the domain part because RFC3986 specifies the the rest of * the url may not contain special characters: * http://tools.ietf.org/html/rfc3986#appendix-A * * @param string $url The URL to be validated * @return boolean Whether the given URL is valid */ public static function isValidUrl($url) { $parsedUrl = parse_url($url); if (!$parsedUrl || !isset($parsedUrl['scheme'])) { return FALSE; } // HttpUtility::buildUrl() will always build urls with <scheme>:// // our original $url might only contain <scheme>: (e.g. mail:) // so we convert that to the double-slashed version to ensure // our check against the $recomposedUrl is proper if (!self::isFirstPartOfStr($url, $parsedUrl['scheme'] . '://')) { $url = str_replace($parsedUrl['scheme'] . ':', $parsedUrl['scheme'] . '://', $url); } $recomposedUrl = HttpUtility::buildUrl($parsedUrl); if ($recomposedUrl !== $url) { // The parse_url() had to modify characters, so the URL is invalid return FALSE; } if (isset($parsedUrl['host']) && !preg_match('/^[a-z0-9.\\-]*$/i', $parsedUrl['host'])) { $parsedUrl['host'] = self::idnaEncode($parsedUrl['host']); } return filter_var(HttpUtility::buildUrl($parsedUrl), FILTER_VALIDATE_URL) !== FALSE; }
/** * 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 * @return string HTML table with the listing for the record. */ public function getTable($table, $id, $rowList = '') { $backendLayout = $this->getBackendLayoutView()->getSelectedBackendLayout($id); $backendLayoutColumns = array(); if (is_array($backendLayout['__items'])) { foreach ($backendLayout['__items'] as $backendLayoutItem) { $backendLayoutColumns[$backendLayoutItem[1]] = htmlspecialchars($backendLayoutItem[0]); } } $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']); } $this->backendUser = $this->getBackendUserAuthentication(); $lang = $this->getLanguageService(); $db = $this->getDatabaseConnection(); // Init $addWhere = ''; $titleCol = $GLOBALS['TCA'][$table]['ctrl']['label']; $thumbsCol = $GLOBALS['TCA'][$table]['ctrl']['thumbnail']; $this->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 = array(); // 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 && $this->l10nEnabled) { $this->fieldArray[] = '_LOCALIZATION_'; $this->fieldArray[] = '_LOCALIZATION_b'; $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 ($table === 'tt_content') { $selectFields[] = 'CType'; $selectFields[] = 'tx_gridelements_container'; $selectFields[] = 'tx_gridelements_columns'; } if (is_array($GLOBALS['TCA'][$table]['ctrl']['enablecolumns'])) { $selectFields = array_merge($selectFields, $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']); } foreach (array('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 ($this->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. $this->selFieldList = implode(',', $selectFields); 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('$hookObject must implement interface ' . RecordListGetTableHookInterface::class, 1195114460); } $hookObject->getDBlistQuery($table, $id, $addWhere, $this->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->selFieldList); $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, $this->selFieldList); } // Finding the total amount of records on the page // (API function from TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRecordList) $totalItemsQueryParts = $queryParts; $totalItemsQueryParts['WHERE'] = str_replace('AND colPos != -1', '', $totalItemsQueryParts['WHERE']); $this->setTotalItems($totalItemsQueryParts); // 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 = array(); 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; } // Check if gridelements containers are expanded or collapsed if ($table === 'tt_content') { $this->expandedGridelements = array(); $backendUser = $this->getBackendUserAuthentication(); if (is_array($backendUser->uc['moduleData']['list']['gridelementsExpanded'])) { $this->expandedGridelements = $backendUser->uc['moduleData']['list']['gridelementsExpanded']; } $expandOverride = GeneralUtility::_GP('gridelementsExpand'); if (is_array($expandOverride)) { foreach ($expandOverride as $expandContainer => $expandValue) { if ($expandValue) { $this->expandedGridelements[$expandContainer] = 1; } else { unset($this->expandedGridelements[$expandContainer]); } } $backendUser->uc['moduleData']['list']['gridelementsExpanded'] = $this->expandedGridelements; // Save modified user uc $backendUser->writeUC($backendUser->uc); $returnUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl')); if ($returnUrl !== '') { HttpUtility::redirect($returnUrl); } } } // 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 = array(); $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']; } $accRows = array(); // 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, $this->backendUser->workspace, true); if (is_array($row)) { $accRows[] = $row; $this->currentIdList[] = $row['uid']; if ($row['CType'] === 'gridelements_pi1') { $this->currentContainerIdList[] = $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 = array(); $this->duplicateStack = array(); $this->eCounter = $this->firstElementNumber; $cc = 0; $lastColPos = ''; foreach ($accRows as $key => $row) { // Render item row if counter < limit if ($cc < $this->iLimit) { $cc++; $this->translations = false; if (isset($row['colPos']) && $row['colPos'] != $lastColPos) { $lastColPos = $row['colPos']; $this->showMoveUp = false; $rowOutput .= '<tr> <td colspan="2"></td> <td colspan="' . (count($this->fieldArray) - 1 + $this->maxDepth) . '" style="padding:5px;"> <br /> <strong>' . $this->getLanguageService()->sL('LLL:EXT:gridelements/Resources/Private/Language/locallang_db.xml:list.columnName') . ' ' . ($backendLayoutColumns[$row['colPos']] ? $backendLayoutColumns[$row['colPos']] : (int) $row['colPos']) . '</strong> </td> </tr>'; } else { $this->showMoveUp = true; } if (isset($row['colPos']) && isset($accRows[$key + 1]) && $row['colPos'] != $accRows[$key + 1]['colPos']) { $this->showMoveDown = false; } else { $this->showMoveDown = true; } $rowOutput .= $this->renderListRow($table, $row, $cc, $titleCol, $thumbsCol); // If localization view is enabled 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: } // 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 + $this->maxDepth : 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, $this->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"> <div class="panel-heading"> ' . $tableHeader . ' </div> <div class="table-fit ' . $collapseClass . '" id="recordlist-' . htmlspecialchars($table) . '" data-state="' . $dataState . '"> <table data-table="' . htmlspecialchars($table) . '" class="table table-striped table-hover' . ($listOnlyInSingleTableMode ? ' typo3-dblist-overview' : '') . '"> ' . $out . ' </table> </div> </div> '; // Output csv if... // This ends the page with exit. if ($this->csvOutput) { $this->outputCSV($table); } } // Return content: return $out; }