PageRenderTime 50ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/core/DataTable/Renderer/Csv.php

https://github.com/CodeYellowBV/piwik
PHP | 403 lines | 230 code | 48 blank | 125 comment | 47 complexity | 144ff4deeae01dded4e9240567a7defd MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\DataTable\Renderer;
  10. use Piwik\Common;
  11. use Piwik\DataTable\Renderer;
  12. use Piwik\DataTable\Simple;
  13. use Piwik\DataTable;
  14. use Piwik\Date;
  15. use Piwik\Period;
  16. use Piwik\Period\Range;
  17. use Piwik\Piwik;
  18. use Piwik\ProxyHttp;
  19. /**
  20. * CSV export
  21. *
  22. * When rendered using the default settings, a CSV report has the following characteristics:
  23. * The first record contains headers for all the columns in the report.
  24. * All rows have the same number of columns.
  25. * The default field delimiter string is a comma (,).
  26. * Formatting and layout are ignored.
  27. *
  28. */
  29. class Csv extends Renderer
  30. {
  31. /**
  32. * Column separator
  33. *
  34. * @var string
  35. */
  36. public $separator = ",";
  37. /**
  38. * Line end
  39. *
  40. * @var string
  41. */
  42. public $lineEnd = "\n";
  43. /**
  44. * 'metadata' columns will be exported, prefixed by 'metadata_'
  45. *
  46. * @var bool
  47. */
  48. public $exportMetadata = true;
  49. /**
  50. * Converts the content to unicode so that UTF8 characters (eg. chinese) can be imported in Excel
  51. *
  52. * @var bool
  53. */
  54. public $convertToUnicode = true;
  55. /**
  56. * idSubtable will be exported in a column called 'idsubdatatable'
  57. *
  58. * @var bool
  59. */
  60. public $exportIdSubtable = true;
  61. /**
  62. * This string is also hardcoded in archive,sh
  63. */
  64. const NO_DATA_AVAILABLE = 'No data available';
  65. /**
  66. * Computes the dataTable output and returns the string/binary
  67. *
  68. * @return string
  69. */
  70. public function render()
  71. {
  72. $str = $this->renderTable($this->table);
  73. if (empty($str)) {
  74. return self::NO_DATA_AVAILABLE;
  75. }
  76. $this->renderHeader();
  77. if ($this->convertToUnicode
  78. && function_exists('mb_convert_encoding')
  79. ) {
  80. $str = chr(255) . chr(254) . mb_convert_encoding($str, 'UTF-16LE', 'UTF-8');
  81. }
  82. return $str;
  83. }
  84. /**
  85. * Computes the exception output and returns the string/binary
  86. *
  87. * @return string
  88. */
  89. function renderException()
  90. {
  91. @header('Content-Type: text/html; charset=utf-8');
  92. $exceptionMessage = $this->getExceptionMessage();
  93. return 'Error: ' . $exceptionMessage;
  94. }
  95. /**
  96. * Enables / Disables unicode converting
  97. *
  98. * @param $bool
  99. */
  100. public function setConvertToUnicode($bool)
  101. {
  102. $this->convertToUnicode = $bool;
  103. }
  104. /**
  105. * Sets the column separator
  106. *
  107. * @param $separator
  108. */
  109. public function setSeparator($separator)
  110. {
  111. $this->separator = $separator;
  112. }
  113. /**
  114. * Computes the output of the given data table
  115. *
  116. * @param DataTable|array $table
  117. * @param array $allColumns
  118. * @return string
  119. */
  120. protected function renderTable($table, &$allColumns = array())
  121. {
  122. if (is_array($table)) // convert array to DataTable
  123. {
  124. $table = DataTable::makeFromSimpleArray($table);
  125. }
  126. if ($table instanceof DataTable\Map) {
  127. $str = $this->renderDataTableMap($table, $allColumns);
  128. } else {
  129. $str = $this->renderDataTable($table, $allColumns);
  130. }
  131. return $str;
  132. }
  133. /**
  134. * Computes the output of the given data table array
  135. *
  136. * @param DataTable\Map $table
  137. * @param array $allColumns
  138. * @return string
  139. */
  140. protected function renderDataTableMap($table, &$allColumns = array())
  141. {
  142. $str = '';
  143. foreach ($table->getDataTables() as $currentLinePrefix => $dataTable) {
  144. $returned = explode("\n", $this->renderTable($dataTable, $allColumns));
  145. // get rid of the columns names
  146. $returned = array_slice($returned, 1);
  147. // case empty datatable we dont print anything in the CSV export
  148. // when in xml we would output <result date="2008-01-15" />
  149. if (!empty($returned)) {
  150. foreach ($returned as &$row) {
  151. $row = $currentLinePrefix . $this->separator . $row;
  152. }
  153. $str .= "\n" . implode("\n", $returned);
  154. }
  155. }
  156. // prepend table key to column list
  157. $allColumns = array_merge(array($table->getKeyName() => true), $allColumns);
  158. // add header to output string
  159. $str = $this->getHeaderLine(array_keys($allColumns)) . $str;
  160. return $str;
  161. }
  162. /**
  163. * Converts the output of the given simple data table
  164. *
  165. * @param DataTable|Simple $table
  166. * @param array $allColumns
  167. * @return string
  168. */
  169. protected function renderDataTable($table, &$allColumns = array())
  170. {
  171. if ($table instanceof Simple) {
  172. $row = $table->getFirstRow();
  173. if ($row !== false) {
  174. $columnNameToValue = $row->getColumns();
  175. if (count($columnNameToValue) == 1) {
  176. // simple tables should only have one column, the value
  177. $allColumns['value'] = true;
  178. $value = array_values($columnNameToValue);
  179. $str = 'value' . $this->lineEnd . $this->formatValue($value[0]);
  180. return $str;
  181. }
  182. }
  183. }
  184. $csv = array();
  185. foreach ($table->getRows() as $row) {
  186. $csvRow = $this->flattenColumnArray($row->getColumns());
  187. if ($this->exportMetadata) {
  188. $metadata = $row->getMetadata();
  189. foreach ($metadata as $name => $value) {
  190. if ($name == 'idsubdatatable_in_db') {
  191. continue;
  192. }
  193. //if a metadata and a column have the same name make sure they dont overwrite
  194. if ($this->translateColumnNames) {
  195. $name = Piwik::translate('General_Metadata') . ': ' . $name;
  196. } else {
  197. $name = 'metadata_' . $name;
  198. }
  199. $csvRow[$name] = $value;
  200. }
  201. }
  202. foreach ($csvRow as $name => $value) {
  203. $allColumns[$name] = true;
  204. }
  205. if ($this->exportIdSubtable) {
  206. $idsubdatatable = $row->getIdSubDataTable();
  207. if ($idsubdatatable !== false
  208. && $this->hideIdSubDatatable === false
  209. ) {
  210. $csvRow['idsubdatatable'] = $idsubdatatable;
  211. }
  212. }
  213. $csv[] = $csvRow;
  214. }
  215. // now we make sure that all the rows in the CSV array have all the columns
  216. foreach ($csv as &$row) {
  217. foreach ($allColumns as $columnName => $true) {
  218. if (!isset($row[$columnName])) {
  219. $row[$columnName] = '';
  220. }
  221. }
  222. }
  223. $str = '';
  224. // specific case, we have only one column and this column wasn't named properly (indexed by a number)
  225. // we don't print anything in the CSV file => an empty line
  226. if (sizeof($allColumns) == 1
  227. && reset($allColumns)
  228. && !is_string(key($allColumns))
  229. ) {
  230. $str .= '';
  231. } else {
  232. // render row names
  233. $str .= $this->getHeaderLine(array_keys($allColumns)) . $this->lineEnd;
  234. }
  235. // we render the CSV
  236. foreach ($csv as $theRow) {
  237. $rowStr = '';
  238. foreach ($allColumns as $columnName => $true) {
  239. $rowStr .= $this->formatValue($theRow[$columnName]) . $this->separator;
  240. }
  241. // remove the last separator
  242. $rowStr = substr_replace($rowStr, "", -strlen($this->separator));
  243. $str .= $rowStr . $this->lineEnd;
  244. }
  245. $str = substr($str, 0, -strlen($this->lineEnd));
  246. return $str;
  247. }
  248. /**
  249. * Returns the CSV header line for a set of metrics. Will translate columns if desired.
  250. *
  251. * @param array $columnMetrics
  252. * @return array
  253. */
  254. private function getHeaderLine($columnMetrics)
  255. {
  256. if ($this->translateColumnNames) {
  257. $columnMetrics = $this->translateColumnNames($columnMetrics);
  258. }
  259. return implode($this->separator, $columnMetrics);
  260. }
  261. /**
  262. * Formats/Escapes the given value
  263. *
  264. * @param mixed $value
  265. * @return string
  266. */
  267. protected function formatValue($value)
  268. {
  269. if (is_string($value)
  270. && !is_numeric($value)
  271. ) {
  272. $value = html_entity_decode($value, ENT_COMPAT, 'UTF-8');
  273. } elseif ($value === false) {
  274. $value = 0;
  275. }
  276. if (is_string($value)
  277. && (strpos($value, '"') !== false
  278. || strpos($value, $this->separator) !== false)
  279. ) {
  280. $value = '"' . str_replace('"', '""', $value) . '"';
  281. }
  282. // in some number formats (e.g. German), the decimal separator is a comma
  283. // we need to catch and replace this
  284. if (is_numeric($value)) {
  285. $value = (string)$value;
  286. $value = str_replace(',', '.', $value);
  287. }
  288. return $value;
  289. }
  290. /**
  291. * Sends the http headers for csv file
  292. */
  293. protected function renderHeader()
  294. {
  295. $fileName = 'Piwik ' . Piwik::translate('General_Export');
  296. $period = Common::getRequestVar('period', false);
  297. $date = Common::getRequestVar('date', false);
  298. if ($period || $date) // in test cases, there are no request params set
  299. {
  300. if ($period == 'range') {
  301. $period = new Range($period, $date);
  302. } else if (strpos($date, ',') !== false) {
  303. $period = new Range('range', $date);
  304. } else {
  305. $period = Period\Factory::build($period, Date::factory($date));
  306. }
  307. $prettyDate = $period->getLocalizedLongString();
  308. $meta = $this->getApiMetaData();
  309. $fileName .= ' _ ' . $meta['name']
  310. . ' _ ' . $prettyDate . '.csv';
  311. }
  312. // silent fail otherwise unit tests fail
  313. @header('Content-Type: application/vnd.ms-excel');
  314. @header('Content-Disposition: attachment; filename="' . $fileName . '"');
  315. ProxyHttp::overrideCacheControlHeaders();
  316. }
  317. /**
  318. * Flattens an array of column values so they can be outputted as CSV (which does not support
  319. * nested structures).
  320. */
  321. private function flattenColumnArray($columns, &$csvRow = array(), $csvColumnNameTemplate = '%s')
  322. {
  323. foreach ($columns as $name => $value) {
  324. $csvName = sprintf($csvColumnNameTemplate, $this->getCsvColumnName($name));
  325. if (is_array($value)) {
  326. // if we're translating column names and this is an array of arrays, the column name
  327. // format becomes a bit more complicated. also in this case, we assume $value is not
  328. // nested beyond 2 levels (ie, array(0 => array(0 => 1, 1 => 2)), but not array(
  329. // 0 => array(0 => array(), 1 => array())) )
  330. if ($this->translateColumnNames
  331. && is_array(reset($value))
  332. ) {
  333. foreach ($value as $level1Key => $level1Value) {
  334. $inner = $name == 'goals' ? Piwik::translate('Goals_GoalX', $level1Key) : $name . ' ' . $level1Key;
  335. $columnNameTemplate = '%s (' . $inner . ')';
  336. $this->flattenColumnArray($level1Value, $csvRow, $columnNameTemplate);
  337. }
  338. } else {
  339. $this->flattenColumnArray($value, $csvRow, $csvName . '_%s');
  340. }
  341. } else {
  342. $csvRow[$csvName] = $value;
  343. }
  344. }
  345. return $csvRow;
  346. }
  347. private function getCsvColumnName($name)
  348. {
  349. if ($this->translateColumnNames) {
  350. return $this->translateColumnName($name);
  351. } else {
  352. return $name;
  353. }
  354. }
  355. }