| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\DataTable\Renderer; |
|
|
| use Piwik\Common; |
| use Piwik\DataTable\Renderer; |
| use Piwik\DataTable\Simple; |
| use Piwik\DataTable; |
| use Piwik\Filesystem; |
| use Piwik\Period; |
| use Piwik\Period\Range; |
| use Piwik\Piwik; |
| use Piwik\ProxyHttp; |
| use Piwik\Request; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| class Csv extends Renderer |
| { |
| |
| |
| |
| |
| |
| public $separator = ","; |
|
|
| |
| |
| |
| |
| |
| public $lineEnd = "\n"; |
|
|
| |
| |
| |
| |
| |
| public $convertToUnicode = true; |
|
|
| |
| |
| |
| public const NO_DATA_AVAILABLE = 'No data available'; |
|
|
| |
| |
| |
| private $unsupportedColumns = []; |
|
|
| |
| |
| |
| public function render(): string |
| { |
| $str = $this->renderTable($this->table); |
| if (empty($str)) { |
| return self::NO_DATA_AVAILABLE; |
| } |
|
|
| $this->renderHeader(); |
|
|
| return $this->convertToUnicode($str); |
| } |
|
|
| |
| |
| |
| public function setConvertToUnicode(bool $convertToUnicode): void |
| { |
| $this->convertToUnicode = $convertToUnicode; |
| } |
|
|
| |
| |
| |
| public function setSeparator(string $separator): void |
| { |
| $this->separator = $separator; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| protected function renderTable($table, array &$allColumns = []): string |
| { |
| if (is_array($table)) { |
| |
| $table = DataTable::makeFromSimpleArray($table); |
| } |
|
|
| if ($table instanceof DataTable\Map) { |
| $str = $this->renderDataTableMap($table, $allColumns); |
| } else { |
| $str = $this->renderDataTable($table, $allColumns); |
| } |
| return $str; |
| } |
|
|
| |
| |
| |
| |
| |
| protected function renderDataTableMap(DataTable\Map $table, array &$allColumns = []): string |
| { |
| $str = ''; |
| foreach ($table->getDataTables() as $currentLinePrefix => $dataTable) { |
| $returned = explode("\n", $this->renderTable($dataTable, $allColumns)); |
|
|
| |
| $returned = array_slice($returned, 1); |
|
|
| |
| |
| if (!empty($returned)) { |
| foreach ($returned as &$row) { |
| $row = $this->formatValue($currentLinePrefix) . $this->separator . $row; |
| } |
| $str .= "\n" . implode("\n", $returned); |
| } |
| } |
|
|
| |
| $allColumns = array_merge(array($table->getKeyName() => true), $allColumns); |
|
|
| |
| return $this->getHeaderLine(array_keys($allColumns)) . $str; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| protected function renderDataTable($table, array &$allColumns = []): string |
| { |
| if ($table instanceof Simple) { |
| $row = $table->getFirstRow(); |
| if ($row !== false) { |
| $columnNameToValue = $row->getColumns(); |
| if (count($columnNameToValue) === 1) { |
| |
| $allColumns['value'] = true; |
|
|
| $value = array_values($columnNameToValue); |
| return 'value' . $this->lineEnd . $this->formatValue($value[0]); |
| } |
| } |
| } |
|
|
| $csv = $this->makeArrayFromDataTable($table, $allColumns); |
|
|
| return $this->buildCsvString($allColumns, $csv); |
| } |
|
|
| |
| |
| |
| |
| |
| private function getHeaderLine(array $columnMetrics): string |
| { |
| foreach ($columnMetrics as $index => $value) { |
| if (in_array($value, $this->unsupportedColumns)) { |
| unset($columnMetrics[$index]); |
| } |
| } |
|
|
| if ($this->translateColumnNames) { |
| $columnMetrics = $this->translateColumnNames($columnMetrics); |
| } |
|
|
| foreach ($columnMetrics as &$value) { |
| $value = $this->formatValue($value); |
| } |
|
|
| return implode($this->separator, $columnMetrics); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function formatValue($value) |
| { |
| if ( |
| is_string($value) |
| && !is_numeric($value) |
| ) { |
| $value = html_entity_decode($value, ENT_QUOTES, 'UTF-8'); |
| } elseif ($value === false) { |
| $value = 0; |
| } |
|
|
| $value = $this->formatFormulas($value); |
|
|
| if (is_string($value)) { |
| $value = str_replace(["\t", "\r"], ' ', $value); |
|
|
| |
| if ( |
| strpos($value, '"') !== false |
| || strpos($value, $this->separator) !== false |
| || strpos($value, $this->lineEnd) !== false |
| || strpos($value, ',') !== false |
| || strpos($value, ';') !== false |
| ) { |
| $value = '"' . str_replace('"', '""', $value) . '"'; |
| } |
| } |
|
|
| |
| |
| if (is_numeric($value)) { |
| $value = (string)$value; |
| $value = str_replace(',', '.', $value); |
| } |
|
|
| return $value; |
| } |
|
|
| |
| |
| |
| |
| protected function formatFormulas($value) |
| { |
| |
| $formulaStartsWith = array('=', '+', '-', '@'); |
|
|
| |
| $valueWithoutFirstPercentSign = $this->removeFirstPercentSign((string)$value); |
|
|
| if ( |
| empty($valueWithoutFirstPercentSign) |
| || !is_string($value) |
| || is_numeric($valueWithoutFirstPercentSign) |
| ) { |
| return $value; |
| } |
|
|
| $firstCharCellValue = $valueWithoutFirstPercentSign[0]; |
| $isFormula = in_array($firstCharCellValue, $formulaStartsWith); |
| if ($isFormula) { |
| return "'" . $value; |
| } |
|
|
| return $value; |
| } |
|
|
| |
| |
| |
| protected function renderHeader(): void |
| { |
| $fileName = Piwik::translate('General_Export'); |
|
|
| $period = Request::fromRequest()->getStringParameter('period', ''); |
| $date = Request::fromRequest()->getStringParameter('date', ''); |
|
|
| if ($period || $date) { |
| |
|
|
| if ($period === 'range') { |
| $period = new Range($period, $date); |
| } elseif (strpos($date, ',') !== false) { |
| $period = new Range('range', $date); |
| } else { |
| $period = Period\Factory::build($period, $date); |
| } |
|
|
| $prettyDate = $period->getLocalizedLongString(); |
|
|
| $meta = $this->getApiMetaData(); |
| $name = !empty($meta['name']) ? $meta['name'] : ''; |
|
|
| $fileName .= ' _ ' . $name |
| . ' _ ' . $prettyDate . '.csv'; |
| } |
|
|
| $fileName = Filesystem::sanitizeFilename($fileName); |
|
|
| |
| Common::sendHeader("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($fileName), true); |
| ProxyHttp::overrideCacheControlHeaders(); |
| } |
|
|
| |
| |
| |
| |
| private function flattenColumnArray(array $columns, array &$csvRow = [], string $csvColumnNameTemplate = '%s'): array |
| { |
| foreach ($columns as $name => $value) { |
| $csvName = sprintf($csvColumnNameTemplate, $this->getCsvColumnName($name)); |
|
|
| if (is_array($value)) { |
| |
| |
| |
| |
| if ( |
| $this->translateColumnNames |
| && is_array(reset($value)) |
| ) { |
| foreach ($value as $level1Key => $level1Value) { |
| $inner = $name === 'goals' ? Piwik::translate('Goals_GoalX', $level1Key) : $name . ' ' . $level1Key; |
| $columnNameTemplate = '%s (' . $inner . ')'; |
|
|
| $this->flattenColumnArray($level1Value, $csvRow, $columnNameTemplate); |
| } |
| } else { |
| $this->flattenColumnArray($value, $csvRow, $csvName . '_%s'); |
| } |
| } else { |
| $csvRow[$csvName] = $value; |
| } |
| } |
|
|
| return $csvRow; |
| } |
|
|
| private function getCsvColumnName(string $name): string |
| { |
| if ($this->translateColumnNames) { |
| return $this->translateColumnName($name); |
| } else { |
| return $name; |
| } |
| } |
|
|
| |
| |
| |
| |
| private function buildCsvString(array $allColumns, array $csv): string |
| { |
| $str = ''; |
|
|
| |
| |
| if ( |
| sizeof($allColumns) === 1 |
| && reset($allColumns) |
| && !is_string(key($allColumns)) |
| ) { |
| $str .= ''; |
| } else { |
| |
| $str .= $this->getHeaderLine(array_keys($allColumns)) . $this->lineEnd; |
| } |
|
|
| |
| foreach ($csv as $theRow) { |
| $rowStr = ''; |
| foreach ($allColumns as $columnName => $true) { |
| $rowStr .= $this->formatValue($theRow[$columnName] ?? '') . $this->separator; |
| } |
| |
| $rowStr = substr_replace($rowStr, "", -strlen($this->separator)); |
| $str .= $rowStr . $this->lineEnd; |
| } |
|
|
| return substr($str, 0, -strlen($this->lineEnd)); |
| } |
|
|
| |
| |
| |
| |
| |
| private function makeArrayFromDataTable($table, array &$allColumns): array |
| { |
| $csv = []; |
| foreach ($table->getRows() as $row) { |
| $csvRow = $this->flattenColumnArray($row->getColumns()); |
|
|
| if (!$this->hideMetadata) { |
| $metadata = $row->getMetadata(); |
| foreach ($metadata as $name => $value) { |
| if ($name === 'idsubdatatable_in_db') { |
| continue; |
| } |
| |
| if ($this->translateColumnNames) { |
| $name = Piwik::translate('General_Metadata') . ': ' . $name; |
| } else { |
| $name = 'metadata_' . $name; |
| } |
|
|
| if ( |
| is_array($value) |
| || is_object($value) |
| ) { |
| if (!in_array($name, $this->unsupportedColumns)) { |
| $this->unsupportedColumns[] = $name; |
| } |
| } else { |
| $csvRow[$name] = $value; |
| } |
| } |
| } |
|
|
| foreach ($csvRow as $name => $value) { |
| if (in_array($name, $this->unsupportedColumns)) { |
| unset($allColumns[$name]); |
| } else { |
| $allColumns[$name] = true; |
| } |
| } |
|
|
| if (!$this->hideIdSubDatatable && !$this->hideMetadata) { |
| $idsubdatatable = $row->getIdSubDataTable(); |
| if ($idsubdatatable !== false) { |
| $csvRow['idsubdatatable'] = $idsubdatatable; |
| } |
| } |
|
|
| $csv[] = $csvRow; |
| } |
|
|
| if (!empty($this->unsupportedColumns)) { |
| foreach ($this->unsupportedColumns as $unsupportedColumn) { |
| foreach ($csv as $index => $row) { |
| unset($row[$index][$unsupportedColumn]); |
| } |
| } |
| } |
|
|
| return $csv; |
| } |
|
|
| |
| |
| |
| |
| private function convertToUnicode($str) |
| { |
| if ( |
| $this->convertToUnicode |
| && function_exists('mb_convert_encoding') |
| ) { |
| $str = chr(255) . chr(254) . mb_convert_encoding($str, 'UTF-16LE', 'UTF-8'); |
| } |
| return $str; |
| } |
|
|
| protected function removeFirstPercentSign(string $value): string |
| { |
| |
| $value = ltrim($value, "\0"); |
|
|
| while (0 === strpos($value, '%00')) { |
| $value = ltrim(substr($value, 3), "\0"); |
| } |
|
|
| $posPercent = strpos($value, '%'); |
| if ($posPercent !== false) { |
| return substr_replace($value, '', $posPercent, 1); |
| } |
| return $value; |
| } |
| } |
|
|