PageRenderTime 47ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/core/API/ResponseBuilder.php

https://github.com/CodeYellowBV/piwik
PHP | 478 lines | 263 code | 50 blank | 165 comment | 65 complexity | 4e5176f82078ddb5f742a61bfd7beb4c 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\API;
  10. use Exception;
  11. use Piwik\API\DataTableManipulator\Flattener;
  12. use Piwik\API\DataTableManipulator\LabelFilter;
  13. use Piwik\API\DataTableManipulator\ReportTotalsCalculator;
  14. use Piwik\Common;
  15. use Piwik\DataTable\Renderer\Json;
  16. use Piwik\DataTable\Renderer;
  17. use Piwik\DataTable\Simple;
  18. use Piwik\DataTable;
  19. /**
  20. */
  21. class ResponseBuilder
  22. {
  23. private $request = null;
  24. private $outputFormat = null;
  25. private $apiModule = false;
  26. private $apiMethod = false;
  27. /**
  28. * @param string $outputFormat
  29. * @param array $request
  30. */
  31. public function __construct($outputFormat, $request = array())
  32. {
  33. $this->request = $request;
  34. $this->outputFormat = $outputFormat;
  35. }
  36. /**
  37. * This method processes the data resulting from the API call.
  38. *
  39. * - If the data resulted from the API call is a DataTable then
  40. * - we apply the standard filters if the parameters have been found
  41. * in the URL. For example to offset,limit the Table you can add the following parameters to any API
  42. * call that returns a DataTable: filter_limit=10&filter_offset=20
  43. * - we apply the filters that have been previously queued on the DataTable
  44. * @see DataTable::queueFilter()
  45. * - we apply the renderer that generate the DataTable in a given format (XML, PHP, HTML, JSON, etc.)
  46. * the format can be changed using the 'format' parameter in the request.
  47. * Example: format=xml
  48. *
  49. * - If there is nothing returned (void) we display a standard success message
  50. *
  51. * - If there is a PHP array returned, we try to convert it to a dataTable
  52. * It is then possible to convert this datatable to any requested format (xml/etc)
  53. *
  54. * - If a bool is returned we convert to a string (true is displayed as 'true' false as 'false')
  55. *
  56. * - If an integer / float is returned, we simply return it
  57. *
  58. * @param mixed $value The initial returned value, before post process. If set to null, success response is returned.
  59. * @param bool|string $apiModule The API module that was called
  60. * @param bool|string $apiMethod The API method that was called
  61. * @return mixed Usually a string, but can still be a PHP data structure if the format requested is 'original'
  62. */
  63. public function getResponse($value = null, $apiModule = false, $apiMethod = false)
  64. {
  65. $this->apiModule = $apiModule;
  66. $this->apiMethod = $apiMethod;
  67. if($this->outputFormat == 'original') {
  68. @header('Content-Type: text/plain; charset=utf-8');
  69. }
  70. return $this->renderValue($value);
  71. }
  72. /**
  73. * Returns an error $message in the requested $format
  74. *
  75. * @param Exception $e
  76. * @throws Exception
  77. * @return string
  78. */
  79. public function getResponseException(Exception $e)
  80. {
  81. $format = strtolower($this->outputFormat);
  82. if ($format == 'original') {
  83. throw $e;
  84. }
  85. try {
  86. $renderer = Renderer::factory($format);
  87. } catch (Exception $exceptionRenderer) {
  88. return "Error: " . $e->getMessage() . " and: " . $exceptionRenderer->getMessage();
  89. }
  90. $e = $this->decorateExceptionWithDebugTrace($e);
  91. $renderer->setException($e);
  92. if ($format == 'php') {
  93. $renderer->setSerialize($this->caseRendererPHPSerialize());
  94. }
  95. return $renderer->renderException();
  96. }
  97. /**
  98. * @param $value
  99. * @return string
  100. */
  101. protected function renderValue($value)
  102. {
  103. // when null or void is returned from the api call, we handle it as a successful operation
  104. if (!isset($value)) {
  105. return $this->handleSuccess();
  106. }
  107. // If the returned value is an object DataTable we
  108. // apply the set of generic filters if asked in the URL
  109. // and we render the DataTable according to the format specified in the URL
  110. if ($value instanceof DataTable
  111. || $value instanceof DataTable\Map
  112. ) {
  113. return $this->handleDataTable($value);
  114. }
  115. // Case an array is returned from the API call, we convert it to the requested format
  116. // - if calling from inside the application (format = original)
  117. // => the data stays unchanged (ie. a standard php array or whatever data structure)
  118. // - if any other format is requested, we have to convert this data structure (which we assume
  119. // to be an array) to a DataTable in order to apply the requested DataTable_Renderer (for example XML)
  120. if (is_array($value)) {
  121. return $this->handleArray($value);
  122. }
  123. // original data structure requested, we return without process
  124. if ($this->outputFormat == 'original') {
  125. return $value;
  126. }
  127. if (is_object($value)
  128. || is_resource($value)
  129. ) {
  130. return $this->getResponseException(new Exception('The API cannot handle this data structure.'));
  131. }
  132. // bool // integer // float // serialized object
  133. return $this->handleScalar($value);
  134. }
  135. /**
  136. * @param Exception $e
  137. * @return Exception
  138. */
  139. protected function decorateExceptionWithDebugTrace(Exception $e)
  140. {
  141. // If we are in tests, show full backtrace
  142. if (defined('PIWIK_PATH_TEST_TO_ROOT')) {
  143. if (\Piwik_ShouldPrintBackTraceWithMessage()) {
  144. $message = $e->getMessage() . " in \n " . $e->getFile() . ":" . $e->getLine() . " \n " . $e->getTraceAsString();
  145. } else {
  146. $message = $e->getMessage() . "\n \n --> To temporarily debug this error further, set const PIWIK_PRINT_ERROR_BACKTRACE=true; in index.php";
  147. }
  148. return new Exception($message);
  149. }
  150. return $e;
  151. }
  152. /**
  153. * Returns true if the user requested to serialize the output data (&serialize=1 in the request)
  154. *
  155. * @param mixed $defaultSerializeValue Default value in case the user hasn't specified a value
  156. * @return bool
  157. */
  158. protected function caseRendererPHPSerialize($defaultSerializeValue = 1)
  159. {
  160. $serialize = Common::getRequestVar('serialize', $defaultSerializeValue, 'int', $this->request);
  161. if ($serialize) {
  162. return true;
  163. }
  164. return false;
  165. }
  166. /**
  167. * Apply the specified renderer to the DataTable
  168. *
  169. * @param DataTable|array $dataTable
  170. * @return string
  171. */
  172. protected function getRenderedDataTable($dataTable)
  173. {
  174. $format = strtolower($this->outputFormat);
  175. // if asked for original dataStructure
  176. if ($format == 'original') {
  177. // by default "original" data is not serialized
  178. if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) {
  179. $dataTable = serialize($dataTable);
  180. }
  181. return $dataTable;
  182. }
  183. $method = Common::getRequestVar('method', '', 'string', $this->request);
  184. $renderer = Renderer::factory($format);
  185. $renderer->setTable($dataTable);
  186. $renderer->setRenderSubTables(Common::getRequestVar('expanded', false, 'int', $this->request));
  187. $renderer->setHideIdSubDatableFromResponse(Common::getRequestVar('hideIdSubDatable', false, 'int', $this->request));
  188. if ($format == 'php') {
  189. $renderer->setSerialize($this->caseRendererPHPSerialize());
  190. $renderer->setPrettyDisplay(Common::getRequestVar('prettyDisplay', false, 'int', $this->request));
  191. } else if ($format == 'html') {
  192. $renderer->setTableId($this->request['method']);
  193. } else if ($format == 'csv' || $format == 'tsv') {
  194. $renderer->setConvertToUnicode(Common::getRequestVar('convertToUnicode', true, 'int', $this->request));
  195. }
  196. // prepare translation of column names
  197. if ($format == 'html' || $format == 'csv' || $format == 'tsv' || $format = 'rss') {
  198. $renderer->setApiMethod($method);
  199. $renderer->setIdSite(Common::getRequestVar('idSite', false, 'int', $this->request));
  200. $renderer->setTranslateColumnNames(Common::getRequestVar('translateColumnNames', false, 'int', $this->request));
  201. }
  202. return $renderer->render();
  203. }
  204. /**
  205. * Returns a success $message in the requested $format
  206. *
  207. * @param string $message
  208. * @return string
  209. */
  210. protected function handleSuccess($message = 'ok')
  211. {
  212. // return a success message only if no content has already been buffered, useful when APIs return raw text or html content to the browser
  213. if (!ob_get_contents()) {
  214. switch ($this->outputFormat) {
  215. case 'xml':
  216. @header("Content-Type: text/xml;charset=utf-8");
  217. $return =
  218. "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" .
  219. "<result>\n" .
  220. "\t<success message=\"" . $message . "\" />\n" .
  221. "</result>";
  222. break;
  223. case 'json':
  224. @header("Content-Type: application/json");
  225. $return = '{"result":"success", "message":"' . $message . '"}';
  226. break;
  227. case 'php':
  228. $return = array('result' => 'success', 'message' => $message);
  229. if ($this->caseRendererPHPSerialize()) {
  230. $return = serialize($return);
  231. }
  232. break;
  233. case 'csv':
  234. @header("Content-Type: application/vnd.ms-excel");
  235. @header("Content-Disposition: attachment; filename=piwik-report-export.csv");
  236. $return = "message\n" . $message;
  237. break;
  238. default:
  239. $return = 'Success:' . $message;
  240. break;
  241. }
  242. return $return;
  243. }
  244. }
  245. /**
  246. * Converts the given scalar to an data table
  247. *
  248. * @param mixed $scalar
  249. * @return string
  250. */
  251. protected function handleScalar($scalar)
  252. {
  253. $dataTable = new Simple();
  254. $dataTable->addRowsFromArray(array($scalar));
  255. return $this->getRenderedDataTable($dataTable);
  256. }
  257. /**
  258. * Handles the given data table
  259. *
  260. * @param DataTable $datatable
  261. * @return string
  262. */
  263. protected function handleDataTable($datatable)
  264. {
  265. // if requested, flatten nested tables
  266. if (Common::getRequestVar('flat', '0', 'string', $this->request) == '1') {
  267. $flattener = new Flattener($this->apiModule, $this->apiMethod, $this->request);
  268. if (Common::getRequestVar('include_aggregate_rows', '0', 'string', $this->request) == '1') {
  269. $flattener->includeAggregateRows();
  270. }
  271. $datatable = $flattener->flatten($datatable);
  272. }
  273. if (1 == Common::getRequestVar('totals', '1', 'integer', $this->request)) {
  274. $genericFilter = new ReportTotalsCalculator($this->apiModule, $this->apiMethod, $this->request);
  275. $datatable = $genericFilter->calculate($datatable);
  276. }
  277. // if the flag disable_generic_filters is defined we skip the generic filters
  278. if (0 == Common::getRequestVar('disable_generic_filters', '0', 'string', $this->request)) {
  279. $genericFilter = new DataTableGenericFilter($this->request);
  280. $genericFilter->filter($datatable);
  281. }
  282. // we automatically safe decode all datatable labels (against xss)
  283. $datatable->queueFilter('SafeDecodeLabel');
  284. // if the flag disable_queued_filters is defined we skip the filters that were queued
  285. if (Common::getRequestVar('disable_queued_filters', 0, 'int', $this->request) == 0) {
  286. $datatable->applyQueuedFilters();
  287. }
  288. // use the ColumnDelete filter if hideColumns/showColumns is provided (must be done
  289. // after queued filters are run so processed metrics can be removed, too)
  290. $hideColumns = Common::getRequestVar('hideColumns', '', 'string', $this->request);
  291. $showColumns = Common::getRequestVar('showColumns', '', 'string', $this->request);
  292. if ($hideColumns !== '' || $showColumns !== '') {
  293. $datatable->filter('ColumnDelete', array($hideColumns, $showColumns));
  294. }
  295. // apply label filter: only return rows matching the label parameter (more than one if more than one label)
  296. $label = $this->getLabelFromRequest($this->request);
  297. if (!empty($label)) {
  298. $addLabelIndex = Common::getRequestVar('labelFilterAddLabelIndex', 0, 'int', $this->request) == 1;
  299. $filter = new LabelFilter($this->apiModule, $this->apiMethod, $this->request);
  300. $datatable = $filter->filter($label, $datatable, $addLabelIndex);
  301. }
  302. return $this->getRenderedDataTable($datatable);
  303. }
  304. /**
  305. * Converts the given simple array to a data table
  306. *
  307. * @param array $array
  308. * @return string
  309. */
  310. protected function handleArray($array)
  311. {
  312. if ($this->outputFormat == 'original') {
  313. // we handle the serialization. Because some php array have a very special structure that
  314. // couldn't be converted with the automatic DataTable->addRowsFromSimpleArray
  315. // the user may want to request the original PHP data structure serialized by the API
  316. // in case he has to setup serialize=1 in the URL
  317. if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) {
  318. return serialize($array);
  319. }
  320. return $array;
  321. }
  322. $multiDimensional = $this->handleMultiDimensionalArray($array);
  323. if ($multiDimensional !== false) {
  324. return $multiDimensional;
  325. }
  326. return $this->getRenderedDataTable($array);
  327. }
  328. /**
  329. * Is this a multi dimensional array?
  330. * Multi dim arrays are not supported by the Datatable renderer.
  331. * We manually render these.
  332. *
  333. * array(
  334. * array(
  335. * 1,
  336. * 2 => array( 1,
  337. * 2
  338. * )
  339. * ),
  340. * array( 2,
  341. * 3
  342. * )
  343. * );
  344. *
  345. * @param array $array
  346. * @return string|bool false if it isn't a multidim array
  347. */
  348. protected function handleMultiDimensionalArray($array)
  349. {
  350. $first = reset($array);
  351. foreach ($array as $first) {
  352. if (is_array($first)) {
  353. foreach ($first as $key => $value) {
  354. // Yes, this is a multi dim array
  355. if (is_array($value)) {
  356. switch ($this->outputFormat) {
  357. case 'json':
  358. @header("Content-Type: application/json");
  359. return self::convertMultiDimensionalArrayToJson($array);
  360. break;
  361. case 'php':
  362. if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) {
  363. return serialize($array);
  364. }
  365. return $array;
  366. case 'xml':
  367. @header("Content-Type: text/xml;charset=utf-8");
  368. return $this->getRenderedDataTable($array);
  369. default:
  370. break;
  371. }
  372. }
  373. }
  374. }
  375. }
  376. return false;
  377. }
  378. /**
  379. * Render a multidimensional array to Json
  380. * Handle DataTable|Set elements in the first dimension only, following case does not work:
  381. * array(
  382. * array(
  383. * DataTable,
  384. * 2 => array(
  385. * 1,
  386. * 2
  387. * ),
  388. * ),
  389. * );
  390. *
  391. * @param array $array can contain scalar, arrays, DataTable and Set
  392. * @return string
  393. */
  394. public static function convertMultiDimensionalArrayToJson($array)
  395. {
  396. $jsonRenderer = new Json();
  397. $jsonRenderer->setTable($array);
  398. $renderedReport = $jsonRenderer->render();
  399. return $renderedReport;
  400. }
  401. /**
  402. * Returns the value for the label query parameter which can be either a string
  403. * (ie, label=...) or array (ie, label[]=...).
  404. *
  405. * @param array $request
  406. * @return array
  407. */
  408. static public function getLabelFromRequest($request)
  409. {
  410. $label = Common::getRequestVar('label', array(), 'array', $request);
  411. if (empty($label)) {
  412. $label = Common::getRequestVar('label', '', 'string', $request);
  413. if (!empty($label)) {
  414. $label = array($label);
  415. }
  416. }
  417. $label = self::unsanitizeLabelParameter($label);
  418. return $label;
  419. }
  420. static public function unsanitizeLabelParameter($label)
  421. {
  422. // this is needed because Proxy uses Common::getRequestVar which in turn
  423. // uses Common::sanitizeInputValue. This causes the > that separates recursive labels
  424. // to become &gt; and we need to undo that here.
  425. $label = Common::unsanitizeInputValues($label);
  426. return $label;
  427. }
  428. }