BxAnalyticsCsv.php 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defgroup Analytics Analytics
  7. * @ingroup UnaModules
  8. *
  9. * @{
  10. */
  11. /**
  12. * CSV downloads of reports. Paths, titles, referrers and campaign tags come from visitors, so
  13. * a text cell that a spreadsheet would read as a formula is turned into plain text.
  14. */
  15. class BxAnalyticsCsv
  16. {
  17. /**
  18. * A character that makes a spreadsheet evaluate a cell it starts, where a cell can start:
  19. * at the start of the text, and after a ; or a line break. Spreadsheets in many locales
  20. * (Russian, German, French) split a CSV line on ;, and a quote in the middle of a field
  21. * doesn't enclose anything, so 'x;=1+1' would be two cells, the second a formula.
  22. * Every character here is ASCII, which never occurs inside a multibyte UTF-8 character,
  23. * so the pattern works on bytes and never fails on broken text.
  24. */
  25. const FORMULA_PATTERN = '/(?:^|(?<=[;\r\n]))( *)([=+\-@\t\r])/';
  26. /**
  27. * A cell: numbers as they are, text with a ' before anything that would start a formula.
  28. */
  29. public static function cell($mixedValue)
  30. {
  31. if(is_int($mixedValue) || is_float($mixedValue))
  32. return $mixedValue;
  33. if(is_bool($mixedValue))
  34. return (int)$mixedValue;
  35. $sValue = (string)$mixedValue;
  36. if($sValue === '')
  37. return $sValue;
  38. return preg_replace(self::FORMULA_PATTERN, "$1'$2", $sValue);
  39. }
  40. /**
  41. * A download's file name from known parts only: analytics-<report>-<dimension>-<from>-<to>.csv.
  42. * Anything but letters, digits, _ and - is dropped from each part.
  43. */
  44. public static function getFilename($aParts)
  45. {
  46. $aResult = ['analytics'];
  47. foreach($aParts as $sPart) {
  48. $sPart = preg_replace('/[^A-Za-z0-9_-]+/', '', (string)$sPart);
  49. if($sPart !== '')
  50. $aResult[] = $sPart;
  51. }
  52. return implode('-', $aResult) . '.csv';
  53. }
  54. /**
  55. * Write a header row and rows to a stream: UTF-8 with a byte order mark (so spreadsheets
  56. * read it as UTF-8), commas, quotes doubled inside quoted cells.
  57. *
  58. * @param resource $rStream
  59. * @param array $aHeader column titles
  60. * @param array $aRows rows of cells
  61. */
  62. public static function write($rStream, $aHeader, $aRows)
  63. {
  64. fwrite($rStream, "\xEF\xBB\xBF");
  65. fputcsv($rStream, array_map([self::class, 'cell'], array_values($aHeader)), ',', '"', '');
  66. foreach($aRows as $aRow)
  67. fputcsv($rStream, array_map([self::class, 'cell'], array_values($aRow)), ',', '"', '');
  68. }
  69. }
  70. /** @} */