update Piwik to version 2.16 (fixes #91)
This commit is contained in:
parent
296343bf3b
commit
d885a4baa9
5833 changed files with 418860 additions and 226988 deletions
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
* Piwik - free/libre analytics platform
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
|
|
@ -9,21 +9,22 @@
|
|||
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;
|
||||
use Piwik\DataTable\Renderer;
|
||||
use Piwik\DataTable\DataTableInterface;
|
||||
use Piwik\DataTable\Filter\ColumnDelete;
|
||||
use Piwik\DataTable\Filter\Pattern;
|
||||
|
||||
/**
|
||||
*/
|
||||
class ResponseBuilder
|
||||
{
|
||||
private $request = null;
|
||||
private $outputFormat = null;
|
||||
private $apiRenderer = null;
|
||||
private $request = null;
|
||||
private $sendHeader = true;
|
||||
private $postProcessDataTable = true;
|
||||
|
||||
private $apiModule = false;
|
||||
private $apiMethod = false;
|
||||
|
|
@ -34,8 +35,19 @@ class ResponseBuilder
|
|||
*/
|
||||
public function __construct($outputFormat, $request = array())
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->outputFormat = $outputFormat;
|
||||
$this->request = $request;
|
||||
$this->apiRenderer = ApiRenderer::factory($outputFormat, $request);
|
||||
}
|
||||
|
||||
public function disableSendHeader()
|
||||
{
|
||||
$this->sendHeader = false;
|
||||
}
|
||||
|
||||
public function disableDataTablePostProcessor()
|
||||
{
|
||||
$this->postProcessDataTable = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -70,61 +82,21 @@ class ResponseBuilder
|
|||
$this->apiModule = $apiModule;
|
||||
$this->apiMethod = $apiMethod;
|
||||
|
||||
if($this->outputFormat == 'original') {
|
||||
@header('Content-Type: text/plain; charset=utf-8');
|
||||
}
|
||||
return $this->renderValue($value);
|
||||
}
|
||||
$this->sendHeaderIfEnabled();
|
||||
|
||||
/**
|
||||
* 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 (ob_get_contents()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->apiRenderer->renderSuccess('ok');
|
||||
}
|
||||
|
||||
// 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
|
||||
) {
|
||||
if ($value instanceof DataTableInterface) {
|
||||
return $this->handleDataTable($value);
|
||||
}
|
||||
|
||||
|
|
@ -137,26 +109,39 @@ class ResponseBuilder
|
|||
return $this->handleArray($value);
|
||||
}
|
||||
|
||||
// original data structure requested, we return without process
|
||||
if ($this->outputFormat == 'original') {
|
||||
return $value;
|
||||
if (is_object($value)) {
|
||||
return $this->apiRenderer->renderObject($value);
|
||||
}
|
||||
|
||||
if (is_object($value)
|
||||
|| is_resource($value)
|
||||
) {
|
||||
return $this->getResponseException(new Exception('The API cannot handle this data structure.'));
|
||||
if (is_resource($value)) {
|
||||
return $this->apiRenderer->renderResource($value);
|
||||
}
|
||||
|
||||
// bool // integer // float // serialized object
|
||||
return $this->handleScalar($value);
|
||||
return $this->apiRenderer->renderScalar($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an error $message in the requested $format
|
||||
*
|
||||
* @param Exception $e
|
||||
* @throws Exception
|
||||
* @return string
|
||||
*/
|
||||
public function getResponseException(Exception $e)
|
||||
{
|
||||
$e = $this->decorateExceptionWithDebugTrace($e);
|
||||
$message = $this->formatExceptionMessage($e);
|
||||
|
||||
$this->sendHeaderIfEnabled();
|
||||
|
||||
return $this->apiRenderer->renderException($message, $e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Exception $e
|
||||
* @return Exception
|
||||
*/
|
||||
protected function decorateExceptionWithDebugTrace(Exception $e)
|
||||
private function decorateExceptionWithDebugTrace(Exception $e)
|
||||
{
|
||||
// If we are in tests, show full backtrace
|
||||
if (defined('PIWIK_PATH_TEST_TO_ROOT')) {
|
||||
|
|
@ -165,314 +150,109 @@ class ResponseBuilder
|
|||
} 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)
|
||||
private function formatExceptionMessage(Exception $exception)
|
||||
{
|
||||
$serialize = Common::getRequestVar('serialize', $defaultSerializeValue, 'int', $this->request);
|
||||
if ($serialize) {
|
||||
$message = $exception->getMessage();
|
||||
if (\Piwik_ShouldPrintBackTraceWithMessage()) {
|
||||
$message .= "\n" . $exception->getTraceAsString();
|
||||
}
|
||||
|
||||
return Renderer::formatValueXml($message);
|
||||
}
|
||||
|
||||
private function handleDataTable(DataTableInterface $datatable)
|
||||
{
|
||||
if ($this->postProcessDataTable) {
|
||||
$postProcessor = new DataTablePostProcessor($this->apiModule, $this->apiMethod, $this->request);
|
||||
$datatable = $postProcessor->process($datatable);
|
||||
}
|
||||
|
||||
return $this->apiRenderer->renderDataTable($datatable);
|
||||
}
|
||||
|
||||
private function handleArray($array)
|
||||
{
|
||||
$firstArray = null;
|
||||
$firstKey = null;
|
||||
if (!empty($array)) {
|
||||
$firstArray = reset($array);
|
||||
$firstKey = key($array);
|
||||
}
|
||||
|
||||
$isAssoc = !empty($firstArray) && is_numeric($firstKey) && is_array($firstArray) && count(array_filter(array_keys($firstArray), 'is_string'));
|
||||
|
||||
if (is_numeric($firstKey)) {
|
||||
$columns = Common::getRequestVar('filter_column', false, 'array', $this->request);
|
||||
$pattern = Common::getRequestVar('filter_pattern', '', 'string', $this->request);
|
||||
|
||||
if ($columns != array(false) && $pattern !== '') {
|
||||
$pattern = new Pattern(new DataTable(), $columns, $pattern);
|
||||
$array = $pattern->filterArray($array);
|
||||
}
|
||||
|
||||
$limit = Common::getRequestVar('filter_limit', -1, 'integer', $this->request);
|
||||
$offset = Common::getRequestVar('filter_offset', '0', 'integer', $this->request);
|
||||
|
||||
if ($this->shouldApplyLimitOnArray($limit, $offset)) {
|
||||
$array = array_slice($array, $offset, $limit, $preserveKeys = false);
|
||||
}
|
||||
}
|
||||
|
||||
if ($isAssoc) {
|
||||
$hideColumns = Common::getRequestVar('hideColumns', '', 'string', $this->request);
|
||||
$showColumns = Common::getRequestVar('showColumns', '', 'string', $this->request);
|
||||
if ($hideColumns !== '' || $showColumns !== '') {
|
||||
$columnDelete = new ColumnDelete(new DataTable(), $hideColumns, $showColumns);
|
||||
$array = $columnDelete->filter($array);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->apiRenderer->renderArray($array);
|
||||
}
|
||||
|
||||
private function shouldApplyLimitOnArray($limit, $offset)
|
||||
{
|
||||
if ($limit === -1) {
|
||||
// all fields are requested
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($offset > 0) {
|
||||
// an offset is specified, we have to apply the limit
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
// "api_datatable_default_limit" is set by API\Controller if no filter_limit is specified by the user.
|
||||
// it holds the number of the configured default limit.
|
||||
$limitSetBySystem = Common::getRequestVar('api_datatable_default_limit', -2, 'integer', $this->request);
|
||||
|
||||
// we ignore the limit if the datatable_default_limit was set by the system as this default filter_limit is
|
||||
// only meant for dataTables but not for arrays. This way we stay BC as filter_limit was not applied pre
|
||||
// Piwik 2.6 and some fixes were made in Piwik 2.13.
|
||||
$wasFilterLimitSetBySystem = $limitSetBySystem !== -2;
|
||||
|
||||
// we check for "$limitSetBySystem === $limit" as an API method could request another API method with
|
||||
// another limit. In this case we need to apply it again.
|
||||
$isLimitStillDefaultLimit = $limitSetBySystem === $limit;
|
||||
|
||||
if ($wasFilterLimitSetBySystem && $isLimitStillDefaultLimit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the specified renderer to the DataTable
|
||||
*
|
||||
* @param DataTable|array $dataTable
|
||||
* @return string
|
||||
*/
|
||||
protected function getRenderedDataTable($dataTable)
|
||||
private function sendHeaderIfEnabled()
|
||||
{
|
||||
$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;
|
||||
if ($this->sendHeader) {
|
||||
$this->apiRenderer->sendHeader();
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue