setTable($dataTable); * echo $render; */ abstract class Renderer extends BaseFactory { protected $table; /** * @var Exception */ protected $exception; protected $renderSubTables = false; protected $hideIdSubDatatable = false; /** * Whether to translate column names (i.e. metric names) or not * @var bool */ public $translateColumnNames = false; /** * Column translations * @var array */ private $columnTranslations = false; /** * The API method that has returned the data that should be rendered * @var string */ public $apiMethod = false; /** * API metadata for the current report * @var array */ private $apiMetaData = null; /** * The current idSite * @var int */ public $idSite = 'all'; public function __construct() { } /** * Sets whether to render subtables or not * * @param bool $enableRenderSubTable */ public function setRenderSubTables($enableRenderSubTable) { $this->renderSubTables = (bool)$enableRenderSubTable; } /** * @param bool $bool */ public function setHideIdSubDatableFromResponse($bool) { $this->hideIdSubDatatable = (bool)$bool; } /** * Returns whether to render subtables or not * * @return bool */ protected function isRenderSubtables() { return $this->renderSubTables; } /** * Output HTTP Content-Type header */ protected function renderHeader() { Common::sendHeader('Content-Type: text/plain; charset=utf-8'); } /** * Computes the dataTable output and returns the string/binary * * @return mixed */ abstract public function render(); /** * @see render() * @return string */ public function __toString() { return $this->render(); } /** * Set the DataTable to be rendered * * @param DataTableInterface $table table to be rendered * @throws Exception */ public function setTable($table) { if (!is_array($table) && !($table instanceof DataTableInterface) ) { throw new Exception("DataTable renderers renderer accepts only DataTable, Simple and Map instances, and arrays."); } $this->table = $table; } /** * @var array */ protected static $availableRenderers = array('xml', 'json', 'csv', 'tsv', 'html' ); /** * Returns available renderers * * @return array */ public static function getRenderers() { return self::$availableRenderers; } protected static function getClassNameFromClassId($id) { $className = ucfirst(strtolower($id)); $className = 'Piwik\DataTable\Renderer\\' . $className; return $className; } protected static function getInvalidClassIdExceptionMessage($id) { $availableRenderers = implode(', ', self::getRenderers()); $klassName = self::getClassNameFromClassId($id); return Piwik::translate('General_ExceptionInvalidRendererFormat', array($klassName, $availableRenderers)); } /** * Format a value to xml * * @param string|number|bool $value value to format * @return int|string */ public static function formatValueXml($value) { if (is_string($value) && !is_numeric($value) ) { $value = html_entity_decode($value, ENT_COMPAT, 'UTF-8'); // make sure non-UTF-8 chars don't cause htmlspecialchars to choke if (function_exists('mb_convert_encoding')) { $value = @mb_convert_encoding($value, 'UTF-8', 'UTF-8'); } $value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); $htmlentities = array(" ", "¡", "¢", "£", "¤", "¥", "¦", "§", "¨", "©", "ª", "«", "¬", "­", "®", "¯", "°", "±", "²", "³", "´", "µ", "¶", "·", "¸", "¹", "º", "»", "¼", "½", "¾", "¿", "À", "Á", "Â", "Ã", "Ä", "Å", "Æ", "Ç", "È", "É", "Ê", "Ë", "Ì", "Í", "Î", "Ï", "Ð", "Ñ", "Ò", "Ó", "Ô", "Õ", "Ö", "×", "Ø", "Ù", "Ú", "Û", "Ü", "Ý", "Þ", "ß", "à", "á", "â", "ã", "ä", "å", "æ", "ç", "è", "é", "ê", "ë", "ì", "í", "î", "ï", "ð", "ñ", "ò", "ó", "ô", "õ", "ö", "÷", "ø", "ù", "ú", "û", "ü", "ý", "þ", "ÿ", "€"); $xmlentities = array("¢", "£", "¤", "¥", "¦", "§", "¨", "©", "ª", "«", "¬", "­", "®", "¯", "°", "±", "²", "³", "´", "µ", "¶", "·", "¸", "¹", "º", "»", "¼", "½", "¾", "¿", "À", "Á", "Â", "Ã", "Ä", "Å", "Æ", "Ç", "È", "É", "Ê", "Ë", "Ì", "Í", "Î", "Ï", "Ð", "Ñ", "Ò", "Ó", "Ô", "Õ", "Ö", "×", "Ø", "Ù", "Ú", "Û", "Ü", "Ý", "Þ", "ß", "à", "á", "â", "ã", "ä", "å", "æ", "ç", "è", "é", "ê", "ë", "ì", "í", "î", "ï", "ð", "ñ", "ò", "ó", "ô", "õ", "ö", "÷", "ø", "ù", "ú", "û", "ü", "ý", "þ", "ÿ", "€"); $value = str_replace($htmlentities, $xmlentities, $value); } elseif ($value === false) { $value = 0; } return $value; } /** * Translate column names to the current language. * Used in subclasses. * * @param array $names * @return array */ protected function translateColumnNames($names) { if (!$this->apiMethod) { return $names; } // load the translations only once // when multiple dates are requested (date=...,...&period=day), the meta data would // be loaded lots of times otherwise if ($this->columnTranslations === false) { $meta = $this->getApiMetaData(); if ($meta === false) { return $names; } $t = Metrics::getDefaultMetricTranslations(); foreach (array('metrics', 'processedMetrics', 'metricsGoal', 'processedMetricsGoal') as $index) { if (isset($meta[$index]) && is_array($meta[$index])) { $t = array_merge($t, $meta[$index]); } } foreach (Dimension::getAllDimensions() as $dimension) { $dimensionId = str_replace('.', '_', $dimension->getId()); $dimensionName = $dimension->getName(); if (!empty($dimensionId) && !empty($dimensionName)) { $t[$dimensionId] = $dimensionName; } } $this->columnTranslations = & $t; } foreach ($names as &$name) { if (isset($this->columnTranslations[$name])) { $name = $this->columnTranslations[$name]; } } return $names; } /** * @return array|null */ protected function getApiMetaData() { if ($this->apiMetaData === null) { list($apiModule, $apiAction) = explode('.', $this->apiMethod); if (!$apiModule || !$apiAction) { $this->apiMetaData = false; } $api = \Piwik\Plugins\API\API::getInstance(); $meta = $api->getMetadata($this->idSite, $apiModule, $apiAction); if (isset($meta[0]) && is_array($meta[0])) { $meta = $meta[0]; } $this->apiMetaData = & $meta; } return $this->apiMetaData; } /** * Translates the given column name * * @param string $column * @return mixed */ protected function translateColumnName($column) { $columns = array($column); $columns = $this->translateColumnNames($columns); return $columns[0]; } /** * Enables column translating * * @param bool $bool */ public function setTranslateColumnNames($bool) { $this->translateColumnNames = $bool; } /** * Sets the api method * * @param $method */ public function setApiMethod($method) { $this->apiMethod = $method; } /** * Sets the site id * * @param int $idSite */ public function setIdSite($idSite) { $this->idSite = $idSite; } /** * Returns true if an array should be wrapped before rendering. This is used to * mimic quirks in the old rendering logic (for backwards compatibility). The * specific meaning of 'wrap' is left up to the Renderer. For XML, this means a * new node. For JSON, this means wrapping in an array. * * In the old code, arrays were added to new DataTable instances, and then rendered. * This transformation wrapped associative arrays except under certain circumstances, * including: * - single element (ie, array('nb_visits' => 0)) (not wrapped for some renderers) * - empty array (ie, array()) * - array w/ arrays/DataTable instances as values (ie, * array('name' => 'myreport', * 'reportData' => new DataTable()) * OR array('name' => 'myreport', * 'reportData' => array(...)) ) * * @param array $array * @param bool $wrapSingleValues Whether to wrap array('key' => 'value') arrays. Some * renderers wrap them and some don't. * @param bool|null $isAssociativeArray Whether the array is associative or not. * If null, it is determined. * @return bool */ protected static function shouldWrapArrayBeforeRendering( $array, $wrapSingleValues = true, $isAssociativeArray = null) { if (empty($array)) { return false; } if ($isAssociativeArray === null) { $isAssociativeArray = Piwik::isAssociativeArray($array); } $wrap = true; if ($isAssociativeArray) { // we don't wrap if the array has one element that is a value $firstValue = reset($array); if (!$wrapSingleValues && count($array) === 1 && (!is_array($firstValue) && !is_object($firstValue)) ) { $wrap = false; } else { foreach ($array as $value) { if (is_array($value) || is_object($value) ) { $wrap = false; break; } } } } else { $wrap = false; } return $wrap; } /** * Produces a flat php array from the DataTable, putting "columns" and "metadata" on the same level. * * For example, when a originalRender() would be * array( 'columns' => array( 'col1_name' => value1, 'col2_name' => value2 ), * 'metadata' => array( 'metadata1_name' => value_metadata) ) * * a flatRender() is * array( 'col1_name' => value1, * 'col2_name' => value2, * 'metadata1_name' => value_metadata ) * * @param null|DataTable|DataTable\Map|Simple $dataTable * @return array Php array representing the 'flat' version of the datatable */ protected function convertDataTableToArray($dataTable = null) { if (is_null($dataTable)) { $dataTable = $this->table; } if (is_array($dataTable)) { $flatArray = $dataTable; if (self::shouldWrapArrayBeforeRendering($flatArray)) { $flatArray = array($flatArray); } } elseif ($dataTable instanceof DataTable\Map) { $flatArray = array(); foreach ($dataTable->getDataTables() as $keyName => $table) { $flatArray[$keyName] = $this->convertDataTableToArray($table); } } elseif ($dataTable instanceof Simple) { $flatArray = $this->convertSimpleTable($dataTable); reset($flatArray); $firstKey = key($flatArray); // if we return only one numeric value then we print out the result in a simple tag // keep it simple! if (count($flatArray) == 1 && $firstKey !== DataTable\Row::COMPARISONS_METADATA_NAME ) { $flatArray = current($flatArray); } } // A normal DataTable needs to be handled specifically else { $array = $this->convertTable($dataTable); $flatArray = $this->flattenArray($array); } return $flatArray; } /** * Converts the given data table to an array * * @param DataTable $table * @return array */ protected function convertTable($table) { $array = []; foreach ($table->getRows() as $id => $row) { $newRow = array( 'columns' => $row->getColumns(), 'metadata' => $row->getMetadata(), 'idsubdatatable' => $row->getIdSubDataTable(), ); if ($id == DataTable::ID_SUMMARY_ROW) { $newRow['issummaryrow'] = true; } if (isset($newRow['metadata'][DataTable\Row::COMPARISONS_METADATA_NAME])) { $newRow['metadata'][DataTable\Row::COMPARISONS_METADATA_NAME] = $row->getComparisons(); } $subTable = $row->getSubtable(); if ($this->isRenderSubtables() && $subTable ) { $subTable = $this->convertTable($subTable); $newRow['subtable'] = $subTable; if ($this->hideIdSubDatatable === false && isset($newRow['metadata']['idsubdatatable_in_db']) ) { $newRow['columns']['idsubdatatable'] = $newRow['metadata']['idsubdatatable_in_db']; } unset($newRow['metadata']['idsubdatatable_in_db']); } if ($this->hideIdSubDatatable !== false) { unset($newRow['idsubdatatable']); } $array[] = $newRow; } return $array; } /** * Converts the simple data table to an array * * @param Simple $table * @return array */ protected function convertSimpleTable($table) { $array = []; $row = $table->getFirstRow(); if ($row === false) { return $array; } foreach ($row->getColumns() as $columnName => $columnValue) { $array[$columnName] = $columnValue; } $comparisons = $row->getComparisons(); if (!empty($comparisons)) { $array[DataTable\Row::COMPARISONS_METADATA_NAME] = $comparisons; } return $array; } /** * * @param array $array * @return array */ protected function flattenArray($array) { $flatArray = []; foreach ($array as $row) { $newRow = $row['columns'] + $row['metadata']; if (isset($row['idsubdatatable']) && $this->hideIdSubDatatable === false ) { $newRow += array('idsubdatatable' => $row['idsubdatatable']); } if (isset($row['subtable'])) { $newRow += array('subtable' => $this->flattenArray($row['subtable'])); } $flatArray[] = $newRow; } return $flatArray; } }