PageRenderTime 55ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/piwik/core/DataTable/Renderer/Csv.php

https://github.com/imagesdesmaths/idm
PHP | 427 lines | 241 code | 52 blank | 134 comment | 47 complexity | f7e1284d10d995f3448aa1da4b61ff3d MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, BSD-2-Clause, GPL-3.0, LGPL-2.1
  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. $str = $this->convertToUnicode($str);
  78. return $str;
  79. }
  80. /**
  81. * Enables / Disables unicode converting
  82. *
  83. * @param $bool
  84. */
  85. public function setConvertToUnicode($bool)
  86. {
  87. $this->convertToUnicode = $bool;
  88. }
  89. /**
  90. * Sets the column separator
  91. *
  92. * @param $separator
  93. */
  94. public function setSeparator($separator)
  95. {
  96. $this->separator = $separator;
  97. }
  98. /**
  99. * Computes the output of the given data table
  100. *
  101. * @param DataTable|array $table
  102. * @param array $allColumns
  103. * @return string
  104. */
  105. protected function renderTable($table, &$allColumns = array())
  106. {
  107. if (is_array($table)) // convert array to DataTable
  108. {
  109. $table = DataTable::makeFromSimpleArray($table);
  110. }
  111. if ($table instanceof DataTable\Map) {
  112. $str = $this->renderDataTableMap($table, $allColumns);
  113. } else {
  114. $str = $this->renderDataTable($table, $allColumns);
  115. }
  116. return $str;
  117. }
  118. /**
  119. * Computes the output of the given data table array
  120. *
  121. * @param DataTable\Map $table
  122. * @param array $allColumns
  123. * @return string
  124. */
  125. protected function renderDataTableMap($table, &$allColumns = array())
  126. {
  127. $str = '';
  128. foreach ($table->getDataTables() as $currentLinePrefix => $dataTable) {
  129. $returned = explode("\n", $this->renderTable($dataTable, $allColumns));
  130. // get rid of the columns names
  131. $returned = array_slice($returned, 1);
  132. // case empty datatable we dont print anything in the CSV export
  133. // when in xml we would output <result date="2008-01-15" />
  134. if (!empty($returned)) {
  135. foreach ($returned as &$row) {
  136. $row = $currentLinePrefix . $this->separator . $row;
  137. }
  138. $str .= "\n" . implode("\n", $returned);
  139. }
  140. }
  141. // prepend table key to column list
  142. $allColumns = array_merge(array($table->getKeyName() => true), $allColumns);
  143. // add header to output string
  144. $str = $this->getHeaderLine(array_keys($allColumns)) . $str;
  145. return $str;
  146. }
  147. /**
  148. * Converts the output of the given simple data table
  149. *
  150. * @param DataTable|Simple $table
  151. * @param array $allColumns
  152. * @return string
  153. */
  154. protected function renderDataTable($table, &$allColumns = array())
  155. {
  156. if ($table instanceof Simple) {
  157. $row = $table->getFirstRow();
  158. if ($row !== false) {
  159. $columnNameToValue = $row->getColumns();
  160. if (count($columnNameToValue) == 1) {
  161. // simple tables should only have one column, the value
  162. $allColumns['value'] = true;
  163. $value = array_values($columnNameToValue);
  164. $str = 'value' . $this->lineEnd . $this->formatValue($value[0]);
  165. return $str;
  166. }
  167. }
  168. }
  169. $csv = $this->makeArrayFromDataTable($table, $allColumns);
  170. // now we make sure that all the rows in the CSV array have all the columns
  171. foreach ($csv as &$row) {
  172. foreach ($allColumns as $columnName => $true) {
  173. if (!isset($row[$columnName])) {
  174. $row[$columnName] = '';
  175. }
  176. }
  177. }
  178. $str = $this->buildCsvString($allColumns, $csv);
  179. return $str;
  180. }
  181. /**
  182. * Returns the CSV header line for a set of metrics. Will translate columns if desired.
  183. *
  184. * @param array $columnMetrics
  185. * @return array
  186. */
  187. private function getHeaderLine($columnMetrics)
  188. {
  189. if ($this->translateColumnNames) {
  190. $columnMetrics = $this->translateColumnNames($columnMetrics);
  191. }
  192. foreach ($columnMetrics as &$value) {
  193. $value = $this->formatValue($value);
  194. }
  195. return implode($this->separator, $columnMetrics);
  196. }
  197. /**
  198. * Formats/Escapes the given value
  199. *
  200. * @param mixed $value
  201. * @return string
  202. */
  203. protected function formatValue($value)
  204. {
  205. if (is_string($value)
  206. && !is_numeric($value)
  207. ) {
  208. $value = html_entity_decode($value, ENT_COMPAT, 'UTF-8');
  209. } elseif ($value === false) {
  210. $value = 0;
  211. }
  212. if (is_string($value)
  213. && (strpos($value, '"') !== false
  214. || strpos($value, $this->separator) !== false)
  215. ) {
  216. $value = '"' . str_replace('"', '""', $value) . '"';
  217. }
  218. // in some number formats (e.g. German), the decimal separator is a comma
  219. // we need to catch and replace this
  220. if (is_numeric($value)) {
  221. $value = (string)$value;
  222. $value = str_replace(',', '.', $value);
  223. }
  224. return $value;
  225. }
  226. /**
  227. * Sends the http headers for csv file
  228. */
  229. protected function renderHeader()
  230. {
  231. $fileName = 'Piwik ' . Piwik::translate('General_Export');
  232. $period = Common::getRequestVar('period', false);
  233. $date = Common::getRequestVar('date', false);
  234. if ($period || $date) // in test cases, there are no request params set
  235. {
  236. if ($period == 'range') {
  237. $period = new Range($period, $date);
  238. } else if (strpos($date, ',') !== false) {
  239. $period = new Range('range', $date);
  240. } else {
  241. $period = Period\Factory::build($period, Date::factory($date));
  242. }
  243. $prettyDate = $period->getLocalizedLongString();
  244. $meta = $this->getApiMetaData();
  245. $fileName .= ' _ ' . $meta['name']
  246. . ' _ ' . $prettyDate . '.csv';
  247. }
  248. // silent fail otherwise unit tests fail
  249. Common::sendHeader('Content-Disposition: attachment; filename="' . $fileName . '"', true);
  250. ProxyHttp::overrideCacheControlHeaders();
  251. }
  252. /**
  253. * Flattens an array of column values so they can be outputted as CSV (which does not support
  254. * nested structures).
  255. */
  256. private function flattenColumnArray($columns, &$csvRow = array(), $csvColumnNameTemplate = '%s')
  257. {
  258. foreach ($columns as $name => $value) {
  259. $csvName = sprintf($csvColumnNameTemplate, $this->getCsvColumnName($name));
  260. if (is_array($value)) {
  261. // if we're translating column names and this is an array of arrays, the column name
  262. // format becomes a bit more complicated. also in this case, we assume $value is not
  263. // nested beyond 2 levels (ie, array(0 => array(0 => 1, 1 => 2)), but not array(
  264. // 0 => array(0 => array(), 1 => array())) )
  265. if ($this->translateColumnNames
  266. && is_array(reset($value))
  267. ) {
  268. foreach ($value as $level1Key => $level1Value) {
  269. $inner = $name == 'goals' ? Piwik::translate('Goals_GoalX', $level1Key) : $name . ' ' . $level1Key;
  270. $columnNameTemplate = '%s (' . $inner . ')';
  271. $this->flattenColumnArray($level1Value, $csvRow, $columnNameTemplate);
  272. }
  273. } else {
  274. $this->flattenColumnArray($value, $csvRow, $csvName . '_%s');
  275. }
  276. } else {
  277. $csvRow[$csvName] = $value;
  278. }
  279. }
  280. return $csvRow;
  281. }
  282. private function getCsvColumnName($name)
  283. {
  284. if ($this->translateColumnNames) {
  285. return $this->translateColumnName($name);
  286. } else {
  287. return $name;
  288. }
  289. }
  290. /**
  291. * @param $allColumns
  292. * @param $csv
  293. * @return array
  294. */
  295. private function buildCsvString($allColumns, $csv)
  296. {
  297. $str = '';
  298. // specific case, we have only one column and this column wasn't named properly (indexed by a number)
  299. // we don't print anything in the CSV file => an empty line
  300. if (sizeof($allColumns) == 1
  301. && reset($allColumns)
  302. && !is_string(key($allColumns))
  303. ) {
  304. $str .= '';
  305. } else {
  306. // render row names
  307. $str .= $this->getHeaderLine(array_keys($allColumns)) . $this->lineEnd;
  308. }
  309. // we render the CSV
  310. foreach ($csv as $theRow) {
  311. $rowStr = '';
  312. foreach ($allColumns as $columnName => $true) {
  313. $rowStr .= $this->formatValue($theRow[$columnName]) . $this->separator;
  314. }
  315. // remove the last separator
  316. $rowStr = substr_replace($rowStr, "", -strlen($this->separator));
  317. $str .= $rowStr . $this->lineEnd;
  318. }
  319. $str = substr($str, 0, -strlen($this->lineEnd));
  320. return $str;
  321. }
  322. /**
  323. * @param $table
  324. * @param $allColumns
  325. * @return array of csv data
  326. */
  327. private function makeArrayFromDataTable($table, &$allColumns)
  328. {
  329. $csv = array();
  330. foreach ($table->getRows() as $row) {
  331. $csvRow = $this->flattenColumnArray($row->getColumns());
  332. if ($this->exportMetadata) {
  333. $metadata = $row->getMetadata();
  334. foreach ($metadata as $name => $value) {
  335. if ($name == 'idsubdatatable_in_db') {
  336. continue;
  337. }
  338. //if a metadata and a column have the same name make sure they dont overwrite
  339. if ($this->translateColumnNames) {
  340. $name = Piwik::translate('General_Metadata') . ': ' . $name;
  341. } else {
  342. $name = 'metadata_' . $name;
  343. }
  344. $csvRow[$name] = $value;
  345. }
  346. }
  347. foreach ($csvRow as $name => $value) {
  348. $allColumns[$name] = true;
  349. }
  350. if ($this->exportIdSubtable) {
  351. $idsubdatatable = $row->getIdSubDataTable();
  352. if ($idsubdatatable !== false
  353. && $this->hideIdSubDatatable === false
  354. ) {
  355. $csvRow['idsubdatatable'] = $idsubdatatable;
  356. }
  357. }
  358. $csv[] = $csvRow;
  359. }
  360. return $csv;
  361. }
  362. /**
  363. * @param $str
  364. * @return string
  365. */
  366. private function convertToUnicode($str)
  367. {
  368. if ($this->convertToUnicode
  369. && function_exists('mb_convert_encoding')
  370. ) {
  371. $str = chr(255) . chr(254) . mb_convert_encoding($str, 'UTF-16LE', 'UTF-8');
  372. }
  373. return $str;
  374. }
  375. }