478 lines
17 KiB
PHP
478 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* Piwik - Open source web analytics
|
|
*
|
|
* @link http://piwik.org
|
|
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
|
*
|
|
*/
|
|
namespace Piwik\API;
|
|
|
|
use Exception;
|
|
use Piwik\API\DataTableManipulator\Flattener;
|
|
use Piwik\API\DataTableManipulator\LabelFilter;
|
|
use Piwik\API\DataTableManipulator\ReportTotalsCalculator;
|
|
use Piwik\Common;
|
|
use Piwik\DataTable\Renderer\Json;
|
|
use Piwik\DataTable\Renderer;
|
|
use Piwik\DataTable\Simple;
|
|
use Piwik\DataTable;
|
|
|
|
/**
|
|
*/
|
|
class ResponseBuilder
|
|
{
|
|
private $request = null;
|
|
private $outputFormat = null;
|
|
|
|
private $apiModule = false;
|
|
private $apiMethod = false;
|
|
|
|
/**
|
|
* @param string $outputFormat
|
|
* @param array $request
|
|
*/
|
|
public function __construct($outputFormat, $request = array())
|
|
{
|
|
$this->request = $request;
|
|
$this->outputFormat = $outputFormat;
|
|
}
|
|
|
|
/**
|
|
* This method processes the data resulting from the API call.
|
|
*
|
|
* - If the data resulted from the API call is a DataTable then
|
|
* - we apply the standard filters if the parameters have been found
|
|
* in the URL. For example to offset,limit the Table you can add the following parameters to any API
|
|
* call that returns a DataTable: filter_limit=10&filter_offset=20
|
|
* - we apply the filters that have been previously queued on the DataTable
|
|
* @see DataTable::queueFilter()
|
|
* - we apply the renderer that generate the DataTable in a given format (XML, PHP, HTML, JSON, etc.)
|
|
* the format can be changed using the 'format' parameter in the request.
|
|
* Example: format=xml
|
|
*
|
|
* - If there is nothing returned (void) we display a standard success message
|
|
*
|
|
* - If there is a PHP array returned, we try to convert it to a dataTable
|
|
* It is then possible to convert this datatable to any requested format (xml/etc)
|
|
*
|
|
* - If a bool is returned we convert to a string (true is displayed as 'true' false as 'false')
|
|
*
|
|
* - If an integer / float is returned, we simply return it
|
|
*
|
|
* @param mixed $value The initial returned value, before post process. If set to null, success response is returned.
|
|
* @param bool|string $apiModule The API module that was called
|
|
* @param bool|string $apiMethod The API method that was called
|
|
* @return mixed Usually a string, but can still be a PHP data structure if the format requested is 'original'
|
|
*/
|
|
public function getResponse($value = null, $apiModule = false, $apiMethod = false)
|
|
{
|
|
$this->apiModule = $apiModule;
|
|
$this->apiMethod = $apiMethod;
|
|
|
|
if($this->outputFormat == 'original') {
|
|
@header('Content-Type: text/plain; charset=utf-8');
|
|
}
|
|
return $this->renderValue($value);
|
|
}
|
|
|
|
/**
|
|
* Returns an error $message in the requested $format
|
|
*
|
|
* @param Exception $e
|
|
* @throws Exception
|
|
* @return string
|
|
*/
|
|
public function getResponseException(Exception $e)
|
|
{
|
|
$format = strtolower($this->outputFormat);
|
|
|
|
if ($format == 'original') {
|
|
throw $e;
|
|
}
|
|
|
|
try {
|
|
$renderer = Renderer::factory($format);
|
|
} catch (Exception $exceptionRenderer) {
|
|
return "Error: " . $e->getMessage() . " and: " . $exceptionRenderer->getMessage();
|
|
}
|
|
|
|
$e = $this->decorateExceptionWithDebugTrace($e);
|
|
|
|
$renderer->setException($e);
|
|
|
|
if ($format == 'php') {
|
|
$renderer->setSerialize($this->caseRendererPHPSerialize());
|
|
}
|
|
|
|
return $renderer->renderException();
|
|
}
|
|
|
|
/**
|
|
* @param $value
|
|
* @return string
|
|
*/
|
|
protected function renderValue($value)
|
|
{
|
|
// when null or void is returned from the api call, we handle it as a successful operation
|
|
if (!isset($value)) {
|
|
return $this->handleSuccess();
|
|
}
|
|
|
|
// If the returned value is an object DataTable we
|
|
// apply the set of generic filters if asked in the URL
|
|
// and we render the DataTable according to the format specified in the URL
|
|
if ($value instanceof DataTable
|
|
|| $value instanceof DataTable\Map
|
|
) {
|
|
return $this->handleDataTable($value);
|
|
}
|
|
|
|
// Case an array is returned from the API call, we convert it to the requested format
|
|
// - if calling from inside the application (format = original)
|
|
// => the data stays unchanged (ie. a standard php array or whatever data structure)
|
|
// - if any other format is requested, we have to convert this data structure (which we assume
|
|
// to be an array) to a DataTable in order to apply the requested DataTable_Renderer (for example XML)
|
|
if (is_array($value)) {
|
|
return $this->handleArray($value);
|
|
}
|
|
|
|
// original data structure requested, we return without process
|
|
if ($this->outputFormat == 'original') {
|
|
return $value;
|
|
}
|
|
|
|
if (is_object($value)
|
|
|| is_resource($value)
|
|
) {
|
|
return $this->getResponseException(new Exception('The API cannot handle this data structure.'));
|
|
}
|
|
|
|
// bool // integer // float // serialized object
|
|
return $this->handleScalar($value);
|
|
}
|
|
|
|
/**
|
|
* @param Exception $e
|
|
* @return Exception
|
|
*/
|
|
protected function decorateExceptionWithDebugTrace(Exception $e)
|
|
{
|
|
// If we are in tests, show full backtrace
|
|
if (defined('PIWIK_PATH_TEST_TO_ROOT')) {
|
|
if (\Piwik_ShouldPrintBackTraceWithMessage()) {
|
|
$message = $e->getMessage() . " in \n " . $e->getFile() . ":" . $e->getLine() . " \n " . $e->getTraceAsString();
|
|
} else {
|
|
$message = $e->getMessage() . "\n \n --> To temporarily debug this error further, set const PIWIK_PRINT_ERROR_BACKTRACE=true; in index.php";
|
|
}
|
|
return new Exception($message);
|
|
}
|
|
return $e;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the user requested to serialize the output data (&serialize=1 in the request)
|
|
*
|
|
* @param mixed $defaultSerializeValue Default value in case the user hasn't specified a value
|
|
* @return bool
|
|
*/
|
|
protected function caseRendererPHPSerialize($defaultSerializeValue = 1)
|
|
{
|
|
$serialize = Common::getRequestVar('serialize', $defaultSerializeValue, 'int', $this->request);
|
|
if ($serialize) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Apply the specified renderer to the DataTable
|
|
*
|
|
* @param DataTable|array $dataTable
|
|
* @return string
|
|
*/
|
|
protected function getRenderedDataTable($dataTable)
|
|
{
|
|
$format = strtolower($this->outputFormat);
|
|
|
|
// if asked for original dataStructure
|
|
if ($format == 'original') {
|
|
// by default "original" data is not serialized
|
|
if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) {
|
|
$dataTable = serialize($dataTable);
|
|
}
|
|
return $dataTable;
|
|
}
|
|
|
|
$method = Common::getRequestVar('method', '', 'string', $this->request);
|
|
|
|
$renderer = Renderer::factory($format);
|
|
$renderer->setTable($dataTable);
|
|
$renderer->setRenderSubTables(Common::getRequestVar('expanded', false, 'int', $this->request));
|
|
$renderer->setHideIdSubDatableFromResponse(Common::getRequestVar('hideIdSubDatable', false, 'int', $this->request));
|
|
|
|
if ($format == 'php') {
|
|
$renderer->setSerialize($this->caseRendererPHPSerialize());
|
|
$renderer->setPrettyDisplay(Common::getRequestVar('prettyDisplay', false, 'int', $this->request));
|
|
} else if ($format == 'html') {
|
|
$renderer->setTableId($this->request['method']);
|
|
} else if ($format == 'csv' || $format == 'tsv') {
|
|
$renderer->setConvertToUnicode(Common::getRequestVar('convertToUnicode', true, 'int', $this->request));
|
|
}
|
|
|
|
// prepare translation of column names
|
|
if ($format == 'html' || $format == 'csv' || $format == 'tsv' || $format = 'rss') {
|
|
$renderer->setApiMethod($method);
|
|
$renderer->setIdSite(Common::getRequestVar('idSite', false, 'int', $this->request));
|
|
$renderer->setTranslateColumnNames(Common::getRequestVar('translateColumnNames', false, 'int', $this->request));
|
|
}
|
|
|
|
return $renderer->render();
|
|
}
|
|
|
|
/**
|
|
* Returns a success $message in the requested $format
|
|
*
|
|
* @param string $message
|
|
* @return string
|
|
*/
|
|
protected function handleSuccess($message = 'ok')
|
|
{
|
|
// return a success message only if no content has already been buffered, useful when APIs return raw text or html content to the browser
|
|
if (!ob_get_contents()) {
|
|
switch ($this->outputFormat) {
|
|
case 'xml':
|
|
@header("Content-Type: text/xml;charset=utf-8");
|
|
$return =
|
|
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" .
|
|
"<result>\n" .
|
|
"\t<success message=\"" . $message . "\" />\n" .
|
|
"</result>";
|
|
break;
|
|
case 'json':
|
|
@header("Content-Type: application/json");
|
|
$return = '{"result":"success", "message":"' . $message . '"}';
|
|
break;
|
|
case 'php':
|
|
$return = array('result' => 'success', 'message' => $message);
|
|
if ($this->caseRendererPHPSerialize()) {
|
|
$return = serialize($return);
|
|
}
|
|
break;
|
|
|
|
case 'csv':
|
|
@header("Content-Type: application/vnd.ms-excel");
|
|
@header("Content-Disposition: attachment; filename=piwik-report-export.csv");
|
|
$return = "message\n" . $message;
|
|
break;
|
|
|
|
default:
|
|
$return = 'Success:' . $message;
|
|
break;
|
|
}
|
|
return $return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts the given scalar to an data table
|
|
*
|
|
* @param mixed $scalar
|
|
* @return string
|
|
*/
|
|
protected function handleScalar($scalar)
|
|
{
|
|
$dataTable = new Simple();
|
|
$dataTable->addRowsFromArray(array($scalar));
|
|
return $this->getRenderedDataTable($dataTable);
|
|
}
|
|
|
|
/**
|
|
* Handles the given data table
|
|
*
|
|
* @param DataTable $datatable
|
|
* @return string
|
|
*/
|
|
protected function handleDataTable($datatable)
|
|
{
|
|
// if requested, flatten nested tables
|
|
if (Common::getRequestVar('flat', '0', 'string', $this->request) == '1') {
|
|
$flattener = new Flattener($this->apiModule, $this->apiMethod, $this->request);
|
|
if (Common::getRequestVar('include_aggregate_rows', '0', 'string', $this->request) == '1') {
|
|
$flattener->includeAggregateRows();
|
|
}
|
|
$datatable = $flattener->flatten($datatable);
|
|
}
|
|
|
|
if (1 == Common::getRequestVar('totals', '1', 'integer', $this->request)) {
|
|
$genericFilter = new ReportTotalsCalculator($this->apiModule, $this->apiMethod, $this->request);
|
|
$datatable = $genericFilter->calculate($datatable);
|
|
}
|
|
|
|
// if the flag disable_generic_filters is defined we skip the generic filters
|
|
if (0 == Common::getRequestVar('disable_generic_filters', '0', 'string', $this->request)) {
|
|
$genericFilter = new DataTableGenericFilter($this->request);
|
|
$genericFilter->filter($datatable);
|
|
}
|
|
|
|
// we automatically safe decode all datatable labels (against xss)
|
|
$datatable->queueFilter('SafeDecodeLabel');
|
|
|
|
// if the flag disable_queued_filters is defined we skip the filters that were queued
|
|
if (Common::getRequestVar('disable_queued_filters', 0, 'int', $this->request) == 0) {
|
|
$datatable->applyQueuedFilters();
|
|
}
|
|
|
|
// use the ColumnDelete filter if hideColumns/showColumns is provided (must be done
|
|
// after queued filters are run so processed metrics can be removed, too)
|
|
$hideColumns = Common::getRequestVar('hideColumns', '', 'string', $this->request);
|
|
$showColumns = Common::getRequestVar('showColumns', '', 'string', $this->request);
|
|
if ($hideColumns !== '' || $showColumns !== '') {
|
|
$datatable->filter('ColumnDelete', array($hideColumns, $showColumns));
|
|
}
|
|
|
|
// apply label filter: only return rows matching the label parameter (more than one if more than one label)
|
|
$label = $this->getLabelFromRequest($this->request);
|
|
if (!empty($label)) {
|
|
$addLabelIndex = Common::getRequestVar('labelFilterAddLabelIndex', 0, 'int', $this->request) == 1;
|
|
|
|
$filter = new LabelFilter($this->apiModule, $this->apiMethod, $this->request);
|
|
$datatable = $filter->filter($label, $datatable, $addLabelIndex);
|
|
}
|
|
return $this->getRenderedDataTable($datatable);
|
|
}
|
|
|
|
/**
|
|
* Converts the given simple array to a data table
|
|
*
|
|
* @param array $array
|
|
* @return string
|
|
*/
|
|
protected function handleArray($array)
|
|
{
|
|
if ($this->outputFormat == 'original') {
|
|
// we handle the serialization. Because some php array have a very special structure that
|
|
// couldn't be converted with the automatic DataTable->addRowsFromSimpleArray
|
|
// the user may want to request the original PHP data structure serialized by the API
|
|
// in case he has to setup serialize=1 in the URL
|
|
if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) {
|
|
return serialize($array);
|
|
}
|
|
return $array;
|
|
}
|
|
|
|
$multiDimensional = $this->handleMultiDimensionalArray($array);
|
|
if ($multiDimensional !== false) {
|
|
return $multiDimensional;
|
|
}
|
|
|
|
return $this->getRenderedDataTable($array);
|
|
}
|
|
|
|
/**
|
|
* Is this a multi dimensional array?
|
|
* Multi dim arrays are not supported by the Datatable renderer.
|
|
* We manually render these.
|
|
*
|
|
* array(
|
|
* array(
|
|
* 1,
|
|
* 2 => array( 1,
|
|
* 2
|
|
* )
|
|
* ),
|
|
* array( 2,
|
|
* 3
|
|
* )
|
|
* );
|
|
*
|
|
* @param array $array
|
|
* @return string|bool false if it isn't a multidim array
|
|
*/
|
|
protected function handleMultiDimensionalArray($array)
|
|
{
|
|
$first = reset($array);
|
|
foreach ($array as $first) {
|
|
if (is_array($first)) {
|
|
foreach ($first as $key => $value) {
|
|
// Yes, this is a multi dim array
|
|
if (is_array($value)) {
|
|
switch ($this->outputFormat) {
|
|
case 'json':
|
|
@header("Content-Type: application/json");
|
|
return self::convertMultiDimensionalArrayToJson($array);
|
|
break;
|
|
|
|
case 'php':
|
|
if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) {
|
|
return serialize($array);
|
|
}
|
|
return $array;
|
|
|
|
case 'xml':
|
|
@header("Content-Type: text/xml;charset=utf-8");
|
|
return $this->getRenderedDataTable($array);
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Render a multidimensional array to Json
|
|
* Handle DataTable|Set elements in the first dimension only, following case does not work:
|
|
* array(
|
|
* array(
|
|
* DataTable,
|
|
* 2 => array(
|
|
* 1,
|
|
* 2
|
|
* ),
|
|
* ),
|
|
* );
|
|
*
|
|
* @param array $array can contain scalar, arrays, DataTable and Set
|
|
* @return string
|
|
*/
|
|
public static function convertMultiDimensionalArrayToJson($array)
|
|
{
|
|
$jsonRenderer = new Json();
|
|
$jsonRenderer->setTable($array);
|
|
$renderedReport = $jsonRenderer->render();
|
|
return $renderedReport;
|
|
}
|
|
|
|
/**
|
|
* Returns the value for the label query parameter which can be either a string
|
|
* (ie, label=...) or array (ie, label[]=...).
|
|
*
|
|
* @param array $request
|
|
* @return array
|
|
*/
|
|
static public function getLabelFromRequest($request)
|
|
{
|
|
$label = Common::getRequestVar('label', array(), 'array', $request);
|
|
if (empty($label)) {
|
|
$label = Common::getRequestVar('label', '', 'string', $request);
|
|
if (!empty($label)) {
|
|
$label = array($label);
|
|
}
|
|
}
|
|
|
|
$label = self::unsanitizeLabelParameter($label);
|
|
return $label;
|
|
}
|
|
|
|
static public function unsanitizeLabelParameter($label)
|
|
{
|
|
// this is needed because Proxy uses Common::getRequestVar which in turn
|
|
// uses Common::sanitizeInputValue. This causes the > that separates recursive labels
|
|
// to become > and we need to undo that here.
|
|
$label = Common::unsanitizeInputValues($label);
|
|
return $label;
|
|
}
|
|
}
|