PDF rausgenommen

This commit is contained in:
aschwarz
2023-01-23 11:03:31 +01:00
parent 82d562a322
commit a6523903eb
28078 changed files with 4247552 additions and 2 deletions

View File

@@ -0,0 +1,80 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable;
use Piwik\DataTable;
/**
* A filter is set of logic that manipulates a DataTable. Existing filters do things like,
*
* - add/remove rows
* - change column values (change string to lowercase, truncate, etc.)
* - add/remove columns or metadata (compute percentage values, add an 'icon' metadata based on the label, etc.)
* - add/remove/edit subtable associated with rows
* - etc.
*
* Filters are called with a DataTable instance and extra parameters that are specified
* in {@link Piwik\DataTable::filter()} and {@link Piwik\DataTable::queueFilter()}.
*
* To see examples of Filters look at the existing ones in the Piwik\DataTable\BaseFilter
* namespace.
*
* @api
*/
abstract class BaseFilter
{
/**
* @var bool
*/
protected $enableRecursive = false;
/**
* Constructor.
*
* @param DataTable $table
*/
public function __construct(DataTable $table)
{
// empty
}
/**
* Manipulates a {@link DataTable} in some way.
*
* @param DataTable $table
*/
abstract public function filter($table);
/**
* Enables/Disables recursive filtering. Whether this property is actually used
* is up to the derived BaseFilter class.
*
* @param bool $enable
*/
public function enableRecursive($enable)
{
$this->enableRecursive = (bool)$enable;
}
/**
* Filters a row's subtable, if one exists and is loaded in memory.
*
* @param Row $row The row whose subtable should be filter.
*/
public function filterSubTable(Row $row)
{
if (!$this->enableRecursive) {
return;
}
$subTable = $row->getSubtable();
if ($subTable) {
$this->filter($subTable);
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
/**
* This contains the bridge classes which were used prior to Piwik 2.0
* The serialized reports contains these classes below, which were not using namespaces yet
*/
namespace {
use Piwik\DataTable\Row;
use Piwik\DataTable\Row\DataTableSummaryRow;
class Piwik_DataTable_Row_DataTableSummary extends DataTableSummaryRow
{
}
class Piwik_DataTable_Row extends Row
{
}
// only used for BC to unserialize old archived Row instances. We cannot unserialize Row directly as it implements
// the Serializable interface and it would fail on PHP5.6+ when userializing the Row instance directly.
class Piwik_DataTable_SerializedRow
{
public $c;
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable;
/**
* The DataTable Interface
*
*/
interface DataTableInterface
{
public function getRowsCount();
public function queueFilter($className, $parameters = array());
public function applyQueuedFilters();
public function filter($className, $parameters = array());
public function getFirstRow();
public function __toString();
public function enableRecursiveSort();
public function renameColumn($oldName, $newName);
public function deleteColumns($columns, $deleteRecursiveInSubtables = false);
public function deleteRow($id);
public function deleteColumn($name);
public function getColumn($name);
public function getColumns();
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable;
use Piwik\Plugin\Metric;
use Piwik\Plugins\CoreHome\Columns\Metrics\ActionsPerVisit;
use Piwik\Plugins\CoreHome\Columns\Metrics\AverageTimeOnSite;
use Piwik\Plugins\CoreHome\Columns\Metrics\BounceRate;
use Piwik\Plugins\CoreHome\Columns\Metrics\ConversionRate;
/**
* Adds processed metrics columns to a {@link DataTable} using metrics that already exist.
*
* Columns added are:
*
* - **conversion_rate**: percent value of `nb_visits_converted / nb_visits
* - **nb_actions_per_visit**: `nb_actions / nb_visits`
* - **avg_time_on_site**: in number of seconds, `round(visit_length / nb_visits)`. Not
* pretty formatted.
* - **bounce_rate**: percent value of `bounce_count / nb_visits`
*
* Adding the **filter_add_columns_when_show_all_columns** query parameter to
* an API request will trigger the execution of this Filter.
*
* _Note: This filter must be called before {@link ReplaceColumnNames} is called._
*
* **Basic usage example**
*
* $dataTable->filter('AddColumnsProcessedMetrics');
*
* @api
*/
class AddColumnsProcessedMetrics extends BaseFilter
{
protected $invalidDivision = 0;
protected $roundPrecision = 2;
protected $deleteRowsWithNoVisit = true;
/**
* Constructor.
*
* @param DataTable $table The table to eventually filter.
* @param bool $deleteRowsWithNoVisit Whether to delete rows with no visits or not.
*/
public function __construct($table, $deleteRowsWithNoVisit = true)
{
$this->deleteRowsWithNoVisit = $deleteRowsWithNoVisit;
parent::__construct($table);
}
/**
* Adds the processed metrics. See {@link AddColumnsProcessedMetrics} for
* more information.
*
* @param DataTable $table
*/
public function filter($table)
{
if ($this->deleteRowsWithNoVisit) {
$this->deleteRowsWithNoVisit($table);
}
$extraProcessedMetrics = $table->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME);
$extraProcessedMetrics[] = new ConversionRate();
$extraProcessedMetrics[] = new ActionsPerVisit();
$extraProcessedMetrics[] = new AverageTimeOnSite();
$extraProcessedMetrics[] = new BounceRate();
$table->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, $extraProcessedMetrics);
}
private function deleteRowsWithNoVisit(DataTable $table)
{
foreach ($table->getRows() as $key => $row) {
$nbVisits = Metric::getMetric($row, 'nb_visits');
$nbActions = Metric::getMetric($row, 'nb_actions');
if ($nbVisits == 0
&& $nbActions == 0
) {
// case of keyword/website/campaign with a conversion for this day, but no visit, we don't show it
$table->deleteRow($key);
}
}
}
}

View File

@@ -0,0 +1,165 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\Archive\DataTableFactory;
use Piwik\DataTable;
use Piwik\Piwik;
use Piwik\Plugin\Metric;
use Piwik\Plugins\Goals\Columns\Metrics\GoalSpecific\AverageOrderRevenue;
use Piwik\Plugins\Goals\Columns\Metrics\GoalSpecific\ConversionRate;
use Piwik\Plugins\Goals\Columns\Metrics\GoalSpecific\Conversions;
use Piwik\Plugins\Goals\Columns\Metrics\GoalSpecific\ItemsCount;
use Piwik\Plugins\Goals\Columns\Metrics\GoalSpecific\Revenue;
use Piwik\Plugins\Goals\Columns\Metrics\GoalSpecific\RevenuePerVisit as GoalSpecificRevenuePerVisit;
use Piwik\Plugins\Goals\Columns\Metrics\RevenuePerVisit;
/**
* Adds goal related metrics to a {@link DataTable} using metrics that already exist.
*
* Metrics added are:
*
* - **revenue_per_visit**: total goal and ecommerce revenue / nb_visits
* - **goal_%idGoal%_conversion_rate**: the conversion rate. There will be one of
* these columns for each goal that exists
* for the site.
* - **goal_%idGoal%_nb_conversions**: the number of conversions. There will be one of
* these columns for each goal that exists
* for the site.
* - **goal_%idGoal%_revenue_per_visit**: goal revenue / nb_visits. There will be one of
* these columns for each goal that exists
* for the site.
* - **goal_%idGoal%_revenue**: goal revenue. There will be one of
* these columns for each goal that exists
* for the site.
* - **goal_%idGoal%_avg_order_revenue**: goal revenue / number of orders or abandoned
* carts. Only for ecommerce order and abandoned cart
* reports.
* - **goal_%idGoal%_items**: number of items. Only for ecommerce order and abandoned cart
* reports.
*
* Adding the **filter_update_columns_when_show_all_goals** query parameter to
* an API request will trigger the execution of this Filter.
*
* _Note: This filter must be called before {@link ReplaceColumnNames} is called._
*
* **Basic usage example**
*
* $dataTable->filter('AddColumnsProcessedMetricsGoal',
* array($enable = true, $idGoal = Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER));
*
* @api
*/
class AddColumnsProcessedMetricsGoal extends AddColumnsProcessedMetrics
{
/**
* Process main goal metrics: conversion rate, revenue per visit
*/
const GOALS_MINIMAL_REPORT = -2;
/**
* Process main goal metrics, and conversion rate per goal
*/
const GOALS_OVERVIEW = -1;
/**
* Process all goal and per-goal metrics
*/
const GOALS_FULL_TABLE = 0;
/**
* Constructor.
*
* @param DataTable $table The table that will eventually filtered.
* @param bool $enable Always set to true.
* @param string $processOnlyIdGoal Defines what metrics to add (don't process metrics when you don't display them).
* If self::GOALS_FULL_TABLE, all Goal metrics (and per goal metrics) will be processed.
* If self::GOALS_OVERVIEW, only the main goal metrics will be added.
* If an int > 0, then will process only metrics for this specific Goal.
*/
public function __construct($table, $enable = true, $processOnlyIdGoal, $goalsToProcess = null)
{
$this->processOnlyIdGoal = $processOnlyIdGoal;
$this->isEcommerce = $this->processOnlyIdGoal == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER || $this->processOnlyIdGoal == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART;
parent::__construct($table);
// Ensure that all rows with no visit but conversions will be displayed
$this->deleteRowsWithNoVisit = false;
$this->goalsToProcess = $goalsToProcess;
}
/**
* Adds the processed metrics. See {@link AddColumnsProcessedMetrics} for
* more information.
*
* @param DataTable $table
*/
public function filter($table)
{
// Add standard processed metrics
parent::filter($table);
$goals = $this->getGoalsInTable($table);
if (!empty($this->goalsToProcess)) {
$goals = array_unique(array_merge($goals, $this->goalsToProcess));
sort($goals);
}
$idSite = DataTableFactory::getSiteIdFromMetadata($table);
$extraProcessedMetrics = $table->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME);
$extraProcessedMetrics[] = new RevenuePerVisit();
if ($this->processOnlyIdGoal != self::GOALS_MINIMAL_REPORT) {
foreach ($goals as $idGoal) {
if (($this->processOnlyIdGoal > self::GOALS_FULL_TABLE
|| $this->isEcommerce)
&& $this->processOnlyIdGoal != $idGoal
) {
continue;
}
$extraProcessedMetrics[] = new ConversionRate($idSite, $idGoal); // PerGoal\ConversionRate
// When the table is displayed by clicking on the flag icon, we only display the columns
// Visits, Conversions, Per goal conversion rate, Revenue
if ($this->processOnlyIdGoal == self::GOALS_OVERVIEW) {
continue;
}
$extraProcessedMetrics[] = new Conversions($idSite, $idGoal); // PerGoal\Conversions or GoalSpecific\
$extraProcessedMetrics[] = new GoalSpecificRevenuePerVisit($idSite, $idGoal); // PerGoal\Revenue
$extraProcessedMetrics[] = new Revenue($idSite, $idGoal); // PerGoal\Revenue
if ($this->isEcommerce) {
$extraProcessedMetrics[] = new AverageOrderRevenue($idSite, $idGoal);
$extraProcessedMetrics[] = new ItemsCount($idSite, $idGoal);
}
}
}
$table->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, $extraProcessedMetrics);
}
private function getGoalsInTable(DataTable $table)
{
$result = array();
foreach ($table->getRows() as $row) {
$goals = Metric::getMetric($row, 'goals');
if (!$goals) {
continue;
}
foreach ($goals as $goalId => $goalMetrics) {
$goalId = str_replace("idgoal=", "", $goalId);
$result[] = $goalId;
}
}
return array_unique($result);
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
use Piwik\Development;
/**
* Executes a filter for each row of a {@link DataTable} and generates a segment filter for each row.
*
* **Basic usage example**
*
* $dataTable->filter('AddSegmentByLabel', array('segmentName'));
* $dataTable->filter('AddSegmentByLabel', array(array('segmentName1', 'segment2'), ';');
*
* @api
*/
class AddSegmentByLabel extends BaseFilter
{
private $segments;
private $delimiter;
/**
* Generates a segment filter based on the label column and the given segment names
*
* @param DataTable $table
* @param string|array $segmentOrSegments Either one segment or an array of segments.
* If more than one segment is given a delimter has to be defined.
* @param string $delimiter The delimiter by which the label should be splitted.
*/
public function __construct($table, $segmentOrSegments, $delimiter = '')
{
parent::__construct($table);
if (!is_array($segmentOrSegments)) {
$segmentOrSegments = array($segmentOrSegments);
}
$this->segments = $segmentOrSegments;
$this->delimiter = $delimiter;
}
/**
* See {@link AddSegmentByLabel}.
*
* @param DataTable $table
*/
public function filter($table)
{
if (empty($this->segments)) {
$msg = 'AddSegmentByLabel is called without having any segments defined';
Development::error($msg);
return;
}
if (count($this->segments) === 1) {
$segment = reset($this->segments);
foreach ($table->getRowsWithoutSummaryRow() as $key => $row) {
$label = $row->getColumn('label');
if (!empty($label)) {
$row->setMetadata('segment', $segment . '==' . urlencode($label));
}
}
} elseif (!empty($this->delimiter)) {
$numSegments = count($this->segments);
$conditionAnd = ';';
foreach ($table->getRowsWithoutSummaryRow() as $key => $row) {
$label = $row->getColumn('label');
if (!empty($label)) {
$parts = explode($this->delimiter, $label);
if (count($parts) === $numSegments) {
$filter = array();
foreach ($this->segments as $index => $segment) {
if (!empty($segment)) {
$filter[] = $segment . '==' . urlencode($parts[$index]);
}
}
$row->setMetadata('segment', implode($conditionAnd, $filter));
}
}
}
} else {
$names = implode(', ', $this->segments);
$msg = 'Multiple segments are given but no delimiter defined. Segments: ' . $names;
Development::error($msg);
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
/**
* Executes a filter for each row of a {@link DataTable} and generates a segment filter for each row.
* It will map the label column to a segmentValue by searching for the label in the index of the given
* mapping array.
*
* **Basic usage example**
*
* $dataTable->filter('AddSegmentByLabelMapping', array('segmentName', array('1' => 'smartphone, '2' => 'desktop')));
*
* @api
*/
class AddSegmentByLabelMapping extends BaseFilter
{
private $segment;
private $mapping;
/**
* @param DataTable $table
* @param string $segment
* @param array $mapping
*/
public function __construct($table, $segment, $mapping)
{
parent::__construct($table);
$this->segment = $segment;
$this->mapping = $mapping;
}
/**
* See {@link AddSegmentByLabelMapping}.
*
* @param DataTable $table
*/
public function filter($table)
{
if (empty($this->segment) || empty($this->mapping)) {
return;
}
foreach ($table->getRows() as $row) {
$label = $row->getColumn('label');
if (!empty($this->mapping[$label])) {
$label = $this->mapping[$label];
$row->setMetadata('segment', $this->segment . '==' . urlencode($label));
}
}
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
use Piwik\Development;
/**
* Executes a filter for each row of a {@link DataTable} and generates a segment filter for each row.
*
* **Basic usage example**
*
* $dataTable->filter('AddSegmentByRangeLabel', array('segmentName'));
*
* @api
*/
class AddSegmentByRangeLabel extends BaseFilter
{
private $segments;
private $delimiter;
/**
* Generates a segment filter based on the label column and the given segment name
*
* @param DataTable $table
* @param string $segment one segment
*/
public function __construct($table, $segment)
{
parent::__construct($table);
$this->segment = $segment;
}
/**
* @param DataTable $table
*/
public function filter($table)
{
if (empty($this->segment)) {
$msg = 'AddSegmentByRangeLabel is called without having any segment defined';
Development::error($msg);
return;
}
foreach ($table->getRowsWithoutSummaryRow() as $key => $row) {
$label = $row->getColumn('label');
if (empty($label)) {
return;
}
if ($label == 'General_NewVisits') {
$row->setMetadata('segment', 'visitorType==new');
continue;
}
// if there's more than one element, handle as a range w/ an upper bound
if (strpos($label, "-") !== false) {
// get the range
sscanf($label, "%d - %d", $lowerBound, $upperBound);
if ($lowerBound == $upperBound) {
$row->setMetadata('segment', $this->segment . '==' . urlencode($lowerBound));
} else {
$row->setMetadata('segment', $this->segment . '>=' . urlencode($lowerBound) . ';' .
$this->segment . '<=' . urlencode($upperBound));
}
} // if there's one element, handle as a range w/ no upper bound
else {
// get the lower bound
sscanf($label, "%d", $lowerBound);
if ($lowerBound !== null) {
$row->setMetadata('segment', $this->segment . '>=' . urlencode($lowerBound));
}
}
}
}
}

View File

@@ -0,0 +1,82 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable;
/**
* Converts for each row of a {@link DataTable} a segmentValue to a segment (expression). The name of the segment
* is automatically detected based on the given report.
*
* **Basic usage example**
*
* $dataTable->filter('AddSegmentBySegmentValue', array($reportInstance));
*
* @api
*/
class AddSegmentBySegmentValue extends BaseFilter
{
/**
* @var \Piwik\Plugin\Report
*/
private $report;
/**
* @param DataTable $table
* @param $report
*/
public function __construct($table, $report)
{
parent::__construct($table);
$this->report = $report;
}
/**
* See {@link AddSegmentBySegmentValue}.
*
* @param DataTable $table
* @return int The number of deleted rows.
*/
public function filter($table)
{
if (empty($this->report) || !$table->getRowsCount()) {
return;
}
$dimension = $this->report->getDimension();
if (empty($dimension)) {
return;
}
$segments = $dimension->getSegments();
if (empty($segments)) {
return;
}
$this->enableRecursive(true);
/** @var \Piwik\Plugin\Segment $segment */
$segment = reset($segments);
$segmentName = $segment->getSegment();
foreach ($table->getRows() as $row) {
$value = $row->getMetadata('segmentValue');
$filter = $row->getMetadata('segment');
if ($value !== false && $filter === false) {
$row->setMetadata('segment', sprintf('%s==%s', $segmentName, urlencode($value)));
}
$this->filterSubTable($row);
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
/**
* Executes a filter for each row of a {@link DataTable} and generates a segment filter for each row.
*
* **Basic usage example**
*
* $dataTable->filter('AddSegmentValue', array());
* $dataTable->filter('AddSegmentValue', array(function ($label) {
* $transformedValue = urldecode($transformedValue);
* return $transformedValue;
* });
*
* @api
*/
class AddSegmentValue extends ColumnCallbackAddMetadata
{
public function __construct($table, $callback = null)
{
parent::__construct($table, 'label', 'segmentValue', $callback, null, false);
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable;
use Piwik\DataTable\Row\DataTableSummaryRow;
/**
* Adds a summary row to {@link DataTable}s that contains the sum of all other table rows.
*
* **Basic usage example**
*
* $dataTable->filter('AddSummaryRow');
*
* // use a human readable label for the summary row (instead of '-1')
* $dataTable->filter('AddSummaryRow', array($labelSummaryRow = Piwik::translate('General_Total')));
*
* @api
*/
class AddSummaryRow extends BaseFilter
{
/**
* Constructor.
*
* @param DataTable $table The table that will be filtered.
* @param int $labelSummaryRow The value of the label column for the new row.
*/
public function __construct($table, $labelSummaryRow = DataTable::LABEL_SUMMARY_ROW)
{
parent::__construct($table);
$this->labelSummaryRow = $labelSummaryRow;
}
/**
* Executes the filter. See {@link AddSummaryRow}.
*
* @param DataTable $table
*/
public function filter($table)
{
$row = new DataTableSummaryRow($table);
$row->setColumn('label', $this->labelSummaryRow);
$table->addSummaryRow($row);
}
}

View File

@@ -0,0 +1,168 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\Piwik;
/**
* A {@link DataTable} filter that replaces range label columns with prettier,
* human-friendlier versions.
*
* When reports that summarize data over a set of ranges (such as the
* reports in the **VisitorInterest** plugin) are archived, they are
* archived with labels that read as: '$min-$max' or '$min+'. These labels
* have no units and can look like '1-1'.
*
* This filter can be used to clean up and add units to those range labels. To
* do this, you supply a string to use when the range specifies only
* one unit (ie '1-1') and another format string when the range specifies
* more than one unit (ie '2-2', '3-5' or '6+').
*
* This filter can be extended to vary exactly how ranges are prettified based
* on the range values found in the DataTable. To see an example of this,
* take a look at the {@link BeautifyTimeRangeLabels} filter.
*
* **Basic usage example**
*
* $dataTable->queueFilter('BeautifyRangeLabels', array("1 visit", "%s visits"));
*
* @api
*/
class BeautifyRangeLabels extends ColumnCallbackReplace
{
/**
* The string to use when the range being beautified is between 1-1 units.
* @var string
*/
protected $labelSingular;
/**
* The format string to use when the range being beautified references more than
* one unit.
* @var string
*/
protected $labelPlural;
/**
* Constructor.
*
* @param DataTable $table The DataTable that will be filtered.
* @param string $labelSingular The string to use when the range being beautified
* is equal to '1-1 units', eg `"1 visit"`.
* @param string $labelPlural The string to use when the range being beautified
* references more than one unit. This must be a format
* string that takes one string parameter, eg, `"%s visits"`.
*/
public function __construct($table, $labelSingular, $labelPlural)
{
parent::__construct($table, 'label', array($this, 'beautify'), array());
$this->labelSingular = $labelSingular;
$this->labelPlural = $labelPlural;
}
/**
* Beautifies a range label and returns the pretty result. See {@link BeautifyRangeLabels}.
*
* @param string $value The range string. This must be in either a '$min-$max' format
* a '$min+' format.
* @return string The pretty range label.
*/
public function beautify($value)
{
// if there's more than one element, handle as a range w/ an upper bound
if (strpos($value, "-") !== false) {
// get the range
sscanf($value, "%d - %d", $lowerBound, $upperBound);
// if the lower bound is the same as the upper bound make sure the singular label
// is used
if ($lowerBound == $upperBound) {
return $this->getSingleUnitLabel($value, $lowerBound);
} else {
return $this->getRangeLabel($value, $lowerBound, $upperBound);
}
} // if there's one element, handle as a range w/ no upper bound
else {
// get the lower bound
sscanf($value, "%d", $lowerBound);
if ($lowerBound !== null) {
$plusEncoded = urlencode('+');
$plusLen = strlen($plusEncoded);
$len = strlen($value);
// if the label doesn't end with a '+', append it
if ($len < $plusLen || substr($value, $len - $plusLen) != $plusEncoded) {
$value .= $plusEncoded;
}
return $this->getUnboundedLabel($value, $lowerBound);
} else {
// if no lower bound can be found, this isn't a valid range. in this case
// we assume its a translation key and try to translate it.
return Piwik::translate(trim($value));
}
}
}
/**
* Beautifies and returns a range label whose range spans over one unit, ie
* 1-1, 2-2 or 3-3.
*
* This function can be overridden in derived types to customize beautifcation
* behavior based on the range values.
*
* @param string $oldLabel The original label value.
* @param int $lowerBound The lower bound of the range.
* @return string The pretty range label.
*/
public function getSingleUnitLabel($oldLabel, $lowerBound)
{
if ($lowerBound == 1) {
return $this->labelSingular;
} else {
return sprintf($this->labelPlural, $lowerBound);
}
}
/**
* Beautifies and returns a range label whose range is bounded and spans over
* more than one unit, ie 1-5, 5-10 but NOT 11+.
*
* This function can be overridden in derived types to customize beautifcation
* behavior based on the range values.
*
* @param string $oldLabel The original label value.
* @param int $lowerBound The lower bound of the range.
* @param int $upperBound The upper bound of the range.
* @return string The pretty range label.
*/
public function getRangeLabel($oldLabel, $lowerBound, $upperBound)
{
return sprintf($this->labelPlural, $oldLabel);
}
/**
* Beautifies and returns a range label whose range is unbounded, ie
* 5+, 10+, etc.
*
* This function can be overridden in derived types to customize beautifcation
* behavior based on the range values.
*
* @param string $oldLabel The original label value.
* @param int $lowerBound The lower bound of the range.
* @return string The pretty range label.
*/
public function getUnboundedLabel($oldLabel, $lowerBound)
{
return sprintf($this->labelPlural, $oldLabel);
}
}

View File

@@ -0,0 +1,121 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
/**
* A {@link DataTable} filter that replaces range labels whose values are in seconds with
* prettier, human-friendlier versions.
*
* This filter customizes the behavior of the {@link BeautifyRangeLabels} filter
* so range values that are less than one minute are displayed in seconds but
* other ranges are displayed in minutes.
*
* **Basic usage**
*
* $dataTable->filter('BeautifyTimeRangeLabels', array("%1$s-%2$s min", "1 min", "%s min"));
*
* @api
*/
class BeautifyTimeRangeLabels extends BeautifyRangeLabels
{
/**
* A format string used to create pretty range labels when the range's
* lower bound is between 0 and 60.
*
* This format string must take two numeric parameters, one for each
* range bound.
*/
protected $labelSecondsPlural;
/**
* Constructor.
*
* @param DataTable $table The DataTable this filter will run over.
* @param string $labelSecondsPlural A string to use when beautifying range labels
* whose lower bound is between 0 and 60. Must be
* a format string that takes two numeric params.
* @param string $labelMinutesSingular A string to use when replacing a range that
* equals 60-60 (or 1 minute - 1 minute).
* @param string $labelMinutesPlural A string to use when replacing a range that
* spans multiple minutes. This must be a
* format string that takes one string parameter.
*/
public function __construct($table, $labelSecondsPlural, $labelMinutesSingular, $labelMinutesPlural)
{
parent::__construct($table, $labelMinutesSingular, $labelMinutesPlural);
$this->labelSecondsPlural = $labelSecondsPlural;
}
/**
* Beautifies and returns a range label whose range spans over one unit, ie
* 1-1, 2-2 or 3-3.
*
* If the lower bound of the range is less than 60 the pretty range label
* will be in seconds. Otherwise, it will be in minutes.
*
* @param string $oldLabel The original label value.
* @param int $lowerBound The lower bound of the range.
* @return string The pretty range label.
*/
public function getSingleUnitLabel($oldLabel, $lowerBound)
{
if ($lowerBound < 60) {
return sprintf($this->labelSecondsPlural, $lowerBound, $lowerBound);
} elseif ($lowerBound == 60) {
return $this->labelSingular;
} else {
return sprintf($this->labelPlural, ceil($lowerBound / 60));
}
}
/**
* Beautifies and returns a range label whose range is bounded and spans over
* more than one unit, ie 1-5, 5-10 but NOT 11+.
*
* If the lower bound of the range is less than 60 the pretty range label
* will be in seconds. Otherwise, it will be in minutes.
*
* @param string $oldLabel The original label value.
* @param int $lowerBound The lower bound of the range.
* @param int $upperBound The upper bound of the range.
* @return string The pretty range label.
*/
public function getRangeLabel($oldLabel, $lowerBound, $upperBound)
{
if ($lowerBound < 60) {
return sprintf($this->labelSecondsPlural, $lowerBound, $upperBound);
} else {
return sprintf($this->labelPlural, ceil($lowerBound / 60) . "-" . ceil($upperBound / 60));
}
}
/**
* Beautifies and returns a range label whose range is unbounded, ie
* 5+, 10+, etc.
*
* If the lower bound of the range is less than 60 the pretty range label
* will be in seconds. Otherwise, it will be in minutes.
*
* @param string $oldLabel The original label value.
* @param int $lowerBound The lower bound of the range.
* @return string The pretty range label.
*/
public function getUnboundedLabel($oldLabel, $lowerBound)
{
if ($lowerBound < 60) {
return sprintf($this->labelSecondsPlural, $lowerBound);
} else {
// since we're using minutes, we use floor so 1801s+ will be 30m+ and not 31m+
return sprintf($this->labelPlural, "" . floor($lowerBound / 60) . urlencode('+'));
}
}
}

View File

@@ -0,0 +1,197 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\NumberFormatter;
use Piwik\Site;
/**
* A {@link DataTable} filter that calculates the evolution of a metric and adds
* it to each row as a percentage.
*
* **This filter cannot be used as an argument to {@link Piwik\DataTable::filter()}** since
* it requires corresponding data from another DataTable. Instead,
* you must manually perform a binary filter (see the **MultiSites** API for an
* example).
*
* The evolution metric is calculated as:
*
* ((currentValue - pastValue) / pastValue) * 100
*
* @api
* @deprecated since v2.10.0 (use EvolutionMetric instead)
*/
class CalculateEvolutionFilter extends ColumnCallbackAddColumnPercentage
{
/**
* The the DataTable that contains past data.
*
* @var DataTable
*/
protected $pastDataTable;
/**
* Tells if column being added is the revenue evolution column.
*/
protected $isRevenueEvolution = null;
/**
* Constructor.
*
* @param DataTable $table The DataTable being filtered.
* @param DataTable $pastDataTable The DataTable containing data for the period in the past.
* @param string $columnToAdd The column to add evolution data to, eg, `'visits_evolution'`.
* @param string $columnToRead The column to use to calculate evolution data, eg, `'nb_visits'`.
* @param int $quotientPrecision The precision to use when rounding the evolution value.
*/
public function __construct($table, $pastDataTable, $columnToAdd, $columnToRead, $quotientPrecision = 0)
{
parent::__construct(
$table, $columnToAdd, $columnToRead, $columnToRead, $quotientPrecision, $shouldSkipRows = true);
$this->pastDataTable = $pastDataTable;
$this->isRevenueEvolution = $columnToAdd == 'revenue_evolution';
}
/**
* Returns the difference between the column in the specific row and its
* sister column in the past DataTable.
*
* @param Row $row
* @return int|float
*/
protected function getDividend($row)
{
$currentValue = $row->getColumn($this->columnValueToRead);
// if the site this is for doesn't support ecommerce & this is for the revenue_evolution column,
// we don't add the new column
if ($currentValue === false
&& $this->isRevenueEvolution
&& !Site::isEcommerceEnabledFor($row->getColumn('label'))
) {
return false;
}
$pastRow = $this->getPastRowFromCurrent($row);
if ($pastRow) {
$pastValue = $pastRow->getColumn($this->columnValueToRead);
} else {
$pastValue = 0;
}
return $currentValue - $pastValue;
}
/**
* Returns the value of the column in $row's sister row in the past
* DataTable.
*
* @param Row $row
* @return int|float
*/
protected function getDivisor($row)
{
$pastRow = $this->getPastRowFromCurrent($row);
if (!$pastRow) {
return 0;
}
return $pastRow->getColumn($this->columnNameUsedAsDivisor);
}
/**
* Calculates and formats a quotient based on a divisor and dividend.
*
* Unlike ColumnCallbackAddColumnPercentage's,
* version of this method, this method will return 100% if the past
* value of a metric is 0, and the current value is not 0. For a
* value representative of an evolution, this makes sense.
*
* @param int|float $value The dividend.
* @param int|float $divisor
* @return string
*/
protected function formatValue($value, $divisor)
{
$value = self::getPercentageValue($value, $divisor, $this->quotientPrecision);
$value = self::appendPercentSign($value);
$value = Common::forceDotAsSeparatorForDecimalPoint($value);
return $value;
}
/**
* Utility function. Returns the current row in the past DataTable.
*
* @param Row $row The row in the 'current' DataTable.
* @return bool|Row
*/
protected function getPastRowFromCurrent($row)
{
return $this->pastDataTable->getRowFromLabel($row->getColumn('label'));
}
/**
* Calculates the evolution percentage for two arbitrary values.
*
* @param float|int $currentValue The current metric value.
* @param float|int $pastValue The value of the metric in the past. We measure the % change
* from this value to $currentValue.
* @param float|int $quotientPrecision The quotient precision to round to.
* @param bool $appendPercentSign Whether to append a '%' sign to the end of the number or not.
*
* @return string The evolution percent, eg `'15%'`.
*/
public static function calculate($currentValue, $pastValue, $quotientPrecision = 0, $appendPercentSign = true)
{
$number = self::getPercentageValue($currentValue - $pastValue, $pastValue, $quotientPrecision);
if ($appendPercentSign) {
return NumberFormatter::getInstance()->formatPercent($number, $quotientPrecision);
}
return NumberFormatter::getInstance()->format($number, $quotientPrecision);
}
public static function appendPercentSign($number)
{
return $number . '%';
}
public static function prependPlusSignToNumber($number)
{
if ($number > 0) {
$number = '+' . $number;
}
return $number;
}
/**
* Returns an evolution percent based on a value & divisor.
*/
private static function getPercentageValue($value, $divisor, $quotientPrecision)
{
if ($value == 0) {
$evolution = 0;
} elseif ($divisor == 0) {
$evolution = 100;
} else {
$evolution = ($value / $divisor) * 100;
}
$evolution = round($evolution, $quotientPrecision);
return $evolution;
}
}

View File

@@ -0,0 +1,113 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
use Piwik\Plugins\CoreHome\Columns\Metrics\CallableProcessedMetric;
/**
* Adds a new column to every row of a {@link DataTable} based on the result of callback.
*
* **Basic usage example**
*
* $callback = function ($visits, $timeSpent) {
* return round($timeSpent / $visits, 2);
* };
*
* $dataTable->filter('ColumnCallbackAddColumn', array(array('nb_visits', 'sum_time_spent'), 'avg_time_on_site', $callback));
*
* @api
*/
class ColumnCallbackAddColumn extends BaseFilter
{
/**
* The names of the columns to pass to the callback.
*/
private $columns;
/**
* The name of the column to add.
*/
private $columnToAdd;
/**
* The callback to apply to each row of the DataTable. The result is added as
* the value of a new column.
*/
private $functionToApply;
/**
* Extra parameters to pass to the callback.
*/
private $functionParameters;
/**
* Constructor.
*
* @param DataTable $table The DataTable that will be filtered.
* @param array|string $columns The names of the columns to pass to the callback.
* @param string $columnToAdd The name of the column to add.
* @param callable $functionToApply The callback to apply to each row of a DataTable. The columns
* specified in `$columns` are passed to this callback.
* @param array $functionParameters deprecated - use an [anonymous function](http://php.net/manual/en/functions.anonymous.php)
* instead.
*/
public function __construct($table, $columns, $columnToAdd, $functionToApply, $functionParameters = array())
{
parent::__construct($table);
if (!is_array($columns)) {
$columns = array($columns);
}
$this->columns = $columns;
$this->columnToAdd = $columnToAdd;
$this->functionToApply = $functionToApply;
$this->functionParameters = $functionParameters;
}
/**
* See {@link ColumnCallbackAddColumn}.
*
* @param DataTable $table The table to filter.
*/
public function filter($table)
{
$columns = $this->columns;
$functionParams = $this->functionParameters;
$functionToApply = $this->functionToApply;
$extraProcessedMetrics = $table->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME);
if (empty($extraProcessedMetrics)) {
$extraProcessedMetrics = array();
}
$metric = new CallableProcessedMetric($this->columnToAdd, function (DataTable\Row $row) use ($columns, $functionParams, $functionToApply) {
$columnValues = array();
foreach ($columns as $column) {
$columnValues[] = $row->getColumn($column);
}
$parameters = array_merge($columnValues, $functionParams);
return call_user_func_array($functionToApply, $parameters);
}, $columns);
$extraProcessedMetrics[] = $metric;
$table->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, $extraProcessedMetrics);
foreach ($table->getRows() as $row) {
$row->setColumn($this->columnToAdd, $metric->compute($row));
$this->filterSubTable($row);
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\Piwik;
/**
* Calculates a percentage value for each row of a {@link DataTable} and adds the result
* to each row.
*
* See {@link ColumnCallbackAddColumnQuotient} for more information.
*
* **Basic usage example**
*
* $nbVisits = // ... get the visits for a period ...
* $dataTable->queueFilter('ColumnCallbackAddColumnPercentage', array('nb_visits', 'nb_visits_percentage', $nbVisits, 1));
*
* @api
*/
class ColumnCallbackAddColumnPercentage extends ColumnCallbackAddColumnQuotient
{
/**
* Formats the given value as a percentage.
*
* @param number $value
* @param number $divisor
* @return string
*/
protected function formatValue($value, $divisor)
{
return Piwik::getPercentageSafe($value, $divisor, $this->quotientPrecision) . '%';
}
}

View File

@@ -0,0 +1,146 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable;
use Piwik\DataTable\Row;
/**
* Calculates the quotient of two columns and adds the result as a new column
* for each row of a DataTable.
*
* This filter is used to calculate rate values (eg, `'bounce_rate'`), averages
* (eg, `'avg_time_on_page'`) and other types of values.
*
* **Basic usage example**
*
* $dataTable->queueFilter('ColumnCallbackAddColumnQuotient', array('bounce_rate', 'bounce_count', 'nb_visits', $precision = 2));
*
* @api
*/
class ColumnCallbackAddColumnQuotient extends BaseFilter
{
protected $table;
protected $columnValueToRead;
protected $columnNameToAdd;
protected $columnNameUsedAsDivisor;
protected $totalValueUsedAsDivisor;
protected $quotientPrecision;
protected $shouldSkipRows;
protected $getDivisorFromSummaryRow;
/**
* Constructor.
*
* @param DataTable $table The DataTable that will eventually be filtered.
* @param string $columnNameToAdd The name of the column to add the quotient value to.
* @param string $columnValueToRead The name of the column that holds the dividend.
* @param number|string $divisorValueOrDivisorColumnName
* Either numeric value to use as the divisor for every row,
* or the name of the column whose value should be used as the
* divisor.
* @param int $quotientPrecision The precision to use when rounding the quotient.
* @param bool|number $shouldSkipRows Whether rows w/o the column to read should be skipped or not.
* @param bool $getDivisorFromSummaryRow Whether to get the divisor from the summary row or the current
* row iteration.
*/
public function __construct($table, $columnNameToAdd, $columnValueToRead, $divisorValueOrDivisorColumnName,
$quotientPrecision = 0, $shouldSkipRows = false, $getDivisorFromSummaryRow = false)
{
parent::__construct($table);
$this->table = $table;
$this->columnValueToRead = $columnValueToRead;
$this->columnNameToAdd = $columnNameToAdd;
if (is_numeric($divisorValueOrDivisorColumnName)) {
$this->totalValueUsedAsDivisor = $divisorValueOrDivisorColumnName;
} else {
$this->columnNameUsedAsDivisor = $divisorValueOrDivisorColumnName;
}
$this->quotientPrecision = $quotientPrecision;
$this->shouldSkipRows = $shouldSkipRows;
$this->getDivisorFromSummaryRow = $getDivisorFromSummaryRow;
}
/**
* See {@link ColumnCallbackAddColumnQuotient}.
*
* @param DataTable $table
*/
public function filter($table)
{
foreach ($table->getRows() as $row) {
$value = $this->getDividend($row);
if ($value === false && $this->shouldSkipRows) {
continue;
}
// Delete existing column if it exists
$existingValue = $row->getColumn($this->columnNameToAdd);
if ($existingValue !== false) {
continue;
}
$divisor = $this->getDivisor($row);
$formattedValue = $this->formatValue($value, $divisor);
$row->addColumn($this->columnNameToAdd, $formattedValue);
$this->filterSubTable($row);
}
}
/**
* Formats the given value
*
* @param number $value
* @param number $divisor
* @return float|int
*/
protected function formatValue($value, $divisor)
{
$quotient = 0;
if ($divisor > 0 && $value > 0) {
$quotient = round($value / $divisor, $this->quotientPrecision);
}
return $quotient;
}
/**
* Returns the dividend to use when calculating the new column value. Can
* be overridden by descendent classes to customize behavior.
*
* @param Row $row The row being modified.
* @return int|float
*/
protected function getDividend($row)
{
return $row->getColumn($this->columnValueToRead);
}
/**
* Returns the divisor to use when calculating the new column value. Can
* be overridden by descendent classes to customize behavior.
*
* @param Row $row The row being modified.
* @return int|float
*/
protected function getDivisor($row)
{
if (!is_null($this->totalValueUsedAsDivisor)) {
return $this->totalValueUsedAsDivisor;
} elseif ($this->getDivisorFromSummaryRow) {
$summaryRow = $this->table->getRowFromId(DataTable::ID_SUMMARY_ROW);
return $summaryRow->getColumn($this->columnNameUsedAsDivisor);
} else {
return $row->getColumn($this->columnNameUsedAsDivisor);
}
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
/**
* Executes a callback for each row of a {@link DataTable} and adds the result as a new
* row metadata value.
*
* **Basic usage example**
*
* $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'logo', 'Piwik\Plugins\MyPlugin\getLogoFromLabel'));
*
* @api
*/
class ColumnCallbackAddMetadata extends BaseFilter
{
private $columnsToRead;
private $functionToApply;
private $functionParameters;
private $metadataToAdd;
private $applyToSummaryRow;
/**
* Constructor.
*
* @param DataTable $table The DataTable instance that will be filtered.
* @param string|array $columnsToRead The columns to read from each row and pass on to the callback.
* @param string $metadataToAdd The name of the metadata field that will be added to each row.
* @param callable $functionToApply The callback to apply for each row.
* @param array $functionParameters deprecated - use an [anonymous function](http://php.net/manual/en/functions.anonymous.php)
* instead.
* @param bool $applyToSummaryRow Whether the callback should be applied to the summary row or not.
*/
public function __construct($table, $columnsToRead, $metadataToAdd, $functionToApply = null,
$functionParameters = null, $applyToSummaryRow = true)
{
parent::__construct($table);
if (!is_array($columnsToRead)) {
$columnsToRead = array($columnsToRead);
}
$this->columnsToRead = $columnsToRead;
$this->functionToApply = $functionToApply;
$this->functionParameters = $functionParameters;
$this->metadataToAdd = $metadataToAdd;
$this->applyToSummaryRow = $applyToSummaryRow;
}
/**
* See {@link ColumnCallbackAddMetadata}.
*
* @param DataTable $table
*/
public function filter($table)
{
if ($this->applyToSummaryRow) {
$rows = $table->getRows();
} else {
$rows = $table->getRowsWithoutSummaryRow();
}
foreach ($rows as $key => $row) {
$parameters = array();
foreach ($this->columnsToRead as $columnsToRead) {
$parameters[] = $row->getColumn($columnsToRead);
}
if (!is_null($this->functionParameters)) {
$parameters = array_merge($parameters, $this->functionParameters);
}
if (!is_null($this->functionToApply)) {
$newValue = call_user_func_array($this->functionToApply, $parameters);
} else {
$newValue = $parameters[0];
}
if ($newValue !== false) {
$row->addMetadata($this->metadataToAdd, $newValue);
}
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
/**
* Executes a callback for each row of a {@link DataTable} and removes the defined metadata column from each row.
*
* **Basic usage example**
*
* $dataTable->filter('ColumnCallbackDeleteMetadata', array('segmentValue'));
*
* @api
*/
class ColumnCallbackDeleteMetadata extends BaseFilter
{
private $metadataToRemove;
/**
* Constructor.
*
* @param DataTable $table The DataTable instance that will be filtered.
* @param string $metadataToRemove The name of the metadata field that will be removed from each row.
*/
public function __construct($table, $metadataToRemove)
{
parent::__construct($table);
$this->metadataToRemove = $metadataToRemove;
}
/**
* See {@link ColumnCallbackDeleteMetadata}.
*
* @param DataTable $table
*/
public function filter($table)
{
$this->enableRecursive(true);
foreach ($table->getRows() as $row) {
$row->deleteMetadata($this->metadataToRemove);
$this->filterSubTable($row);
}
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
/**
* Deletes all rows for which a callback returns true.
*
* **Basic usage example**
*
* $labelsToRemove = array('label1', 'label2', 'label2');
* $dataTable->filter('ColumnCallbackDeleteRow', array('label', function ($label) use ($labelsToRemove) {
* return in_array($label, $labelsToRemove);
* }));
*
* @api
*/
class ColumnCallbackDeleteRow extends BaseFilter
{
private $function;
private $functionParams;
/**
* Constructor.
*
* @param DataTable $table The DataTable that will be filtered eventually.
* @param array|string $columnsToFilter The column or array of columns that should be
* passed to the callback.
* @param callback $function The callback that determines whether a row should be deleted
* or not. Should return `true` if the row should be deleted.
* @param array $functionParams deprecated - use an [anonymous function](http://php.net/manual/en/functions.anonymous.php)
* instead.
*/
public function __construct($table, $columnsToFilter, $function, $functionParams = array())
{
parent::__construct($table);
if (!is_array($functionParams)) {
$functionParams = array($functionParams);
}
if (!is_array($columnsToFilter)) {
$columnsToFilter = array($columnsToFilter);
}
$this->function = $function;
$this->columnsToFilter = $columnsToFilter;
$this->functionParams = $functionParams;
}
/**
* Filters the given data table
*
* @param DataTable $table
*/
public function filter($table)
{
foreach ($table->getRows() as $key => $row) {
$params = array();
foreach ($this->columnsToFilter as $column) {
$params[] = $row->getColumn($column);
}
$params = array_merge($params, $this->functionParams);
if (call_user_func_array($this->function, $params) === true) {
$table->deleteRow($key);
}
$this->filterSubTable($row);
}
}
}

View File

@@ -0,0 +1,131 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable;
use Piwik\DataTable\Row;
/**
* Replaces one or more column values in each row of a DataTable with the results
* of a callback.
*
* **Basic usage example**
*
* $truncateString = function ($value, $truncateLength) {
* if (strlen($value) > $truncateLength) {
* return substr(0, $truncateLength);
* } else {
* return $value;
* }
* };
*
* // label, url and truncate_length are columns in $dataTable
* $dataTable->filter('ColumnCallbackReplace', array('label', 'url'), $truncateString, null, array('truncate_length'));
*
* @api
*/
class ColumnCallbackReplace extends BaseFilter
{
private $columnsToFilter;
private $functionToApply;
private $functionParameters;
private $extraColumnParameters;
/**
* Constructor.
*
* @param DataTable $table The DataTable to filter.
* @param array|string $columnsToFilter The columns whose values should be passed to the callback
* and then replaced with the callback's result.
* @param callable $functionToApply The function to execute. Must take the column value as a parameter
* and return a value that will be used to replace the original.
* @param array|null $functionParameters deprecated - use an [anonymous function](http://php.net/manual/en/functions.anonymous.php)
* instead.
* @param array $extraColumnParameters Extra column values that should be passed to the callback, but
* shouldn't be replaced.
*/
public function __construct($table, $columnsToFilter, $functionToApply, $functionParameters = null,
$extraColumnParameters = array())
{
parent::__construct($table);
$this->functionToApply = $functionToApply;
$this->functionParameters = $functionParameters;
if (!is_array($columnsToFilter)) {
$columnsToFilter = array($columnsToFilter);
}
$this->columnsToFilter = $columnsToFilter;
$this->extraColumnParameters = $extraColumnParameters;
}
/**
* See {@link ColumnCallbackReplace}.
*
* @param DataTable $table
*/
public function filter($table)
{
foreach ($table->getRows() as $row) {
$extraColumnParameters = array();
foreach ($this->extraColumnParameters as $columnName) {
$extraColumnParameters[] = $row->getColumn($columnName);
}
foreach ($this->columnsToFilter as $column) {
// when a value is not defined, we set it to zero by default (rather than displaying '-')
$value = $this->getElementToReplace($row, $column);
if ($value === false) {
$value = 0;
}
$parameters = array_merge(array($value), $extraColumnParameters);
if (!is_null($this->functionParameters)) {
$parameters = array_merge($parameters, $this->functionParameters);
}
$newValue = call_user_func_array($this->functionToApply, $parameters);
$this->setElementToReplace($row, $column, $newValue);
$this->filterSubTable($row);
}
}
if (in_array('label', $this->columnsToFilter)) {
// we need to force rebuilding the index
$table->setLabelsHaveChanged();
}
}
/**
* Replaces the given column within given row with the given value
*
* @param Row $row
* @param string $columnToFilter
* @param mixed $newValue
*/
protected function setElementToReplace($row, $columnToFilter, $newValue)
{
$row->setColumn($columnToFilter, $newValue);
}
/**
* Returns the element that should be replaced
*
* @param Row $row
* @param string $columnToFilter
* @return mixed
*/
protected function getElementToReplace($row, $columnToFilter)
{
return $row->getColumn($columnToFilter);
}
}

View File

@@ -0,0 +1,191 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
/**
* Filter that will remove columns from a {@link DataTable} using either a blacklist,
* whitelist or both.
*
* This filter is used to handle the **hideColumn** and **showColumn** query parameters.
*
* **Basic usage example**
*
* $columnsToRemove = array('nb_hits', 'nb_pageviews');
* $dataTable->filter('ColumnDelete', array($columnsToRemove));
*
* $columnsToKeep = array('nb_visits');
* $dataTable->filter('ColumnDelete', array(array(), $columnsToKeep));
*
* @api
*/
class ColumnDelete extends BaseFilter
{
/**
* The columns that should be removed from DataTable rows.
*
* @var array
*/
private $columnsToRemove;
/**
* The columns that should be kept in DataTable rows. All other columns will be
* removed. If a column is in $columnsToRemove and this variable, it will NOT be kept.
*
* @var array
*/
private $columnsToKeep;
/**
* Hack: when specifying "showColumns", sometimes we'd like to also keep columns that "look" like a given column,
* without manually specifying all these columns (which may not be possible if column names are generated dynamically)
*
* Column will be kept, if they match any name in the $columnsToKeep, or if they look like anyColumnToKeep__anythingHere
*/
const APPEND_TO_COLUMN_NAME_TO_KEEP = '__';
/**
* Delete the column, only if the value was zero
*
* @var bool
*/
private $deleteIfZeroOnly;
/**
* Constructor.
*
* @param DataTable $table The DataTable instance that will eventually be filtered.
* @param array|string $columnsToRemove An array of column names or a comma-separated list of
* column names. These columns will be removed.
* @param array|string $columnsToKeep An array of column names that should be kept or a
* comma-separated list of column names. Columns not in
* this list will be removed.
* @param bool $deleteIfZeroOnly If true, columns will be removed only if their value is 0.
*/
public function __construct($table, $columnsToRemove, $columnsToKeep = array(), $deleteIfZeroOnly = false)
{
parent::__construct($table);
if (is_string($columnsToRemove)) {
$columnsToRemove = $columnsToRemove == '' ? array() : explode(',', $columnsToRemove);
}
if (is_string($columnsToKeep)) {
$columnsToKeep = $columnsToKeep == '' ? array() : explode(',', $columnsToKeep);
}
$this->columnsToRemove = $columnsToRemove;
$this->columnsToKeep = array_flip($columnsToKeep); // flip so we can use isset instead of in_array
$this->deleteIfZeroOnly = $deleteIfZeroOnly;
}
/**
* See {@link ColumnDelete}.
*
* @param DataTable $table
* @return DataTable
*/
public function filter($table)
{
// always do recursive filter
$this->enableRecursive(true);
$recurse = false; // only recurse if there are columns to remove/keep
// remove columns specified in $this->columnsToRemove
if (!empty($this->columnsToRemove)) {
$this->removeColumnsFromTable($table);
$recurse = true;
}
// remove columns not specified in $columnsToKeep
if (!empty($this->columnsToKeep)) {
foreach ($table as $index => $row) {
$columnsToDelete = array();
foreach ($row as $name => $value) {
$keep = false;
// @see self::APPEND_TO_COLUMN_NAME_TO_KEEP
foreach ($this->columnsToKeep as $nameKeep => $true) {
if (strpos($name, $nameKeep . self::APPEND_TO_COLUMN_NAME_TO_KEEP) === 0) {
$keep = true;
}
}
if (!$keep
&& $name != 'label' // label cannot be removed via whitelisting
&& !isset($this->columnsToKeep[$name])
) {
// we cannot remove row directly to prevent notice "ArrayIterator::next(): Array was modified
// outside object and internal position is no longer valid in /var/www..."
$columnsToDelete[] = $name;
}
}
foreach ($columnsToDelete as $columnToDelete) {
unset($table[$index][$columnToDelete]);
}
}
$recurse = true;
}
// recurse
if ($recurse && !is_array($table)) {
foreach ($table as $row) {
$this->filterSubTable($row);
}
}
return $table;
}
/**
* @param $table
* @return array
*/
protected function removeColumnsFromTable(&$table)
{
if(!$this->isArrayAccess($table)) {
return;
}
foreach ($table as $index => $row) {
if(!$this->isArrayAccess($row)) {
continue;
}
foreach ($this->columnsToRemove as $column) {
if (!array_key_exists($column, $row)) {
continue;
}
if ($this->deleteIfZeroOnly) {
$value = $row[$column];
if ($value === false || !empty($value)) {
continue;
}
}
unset($table[$index][$column]);
}
// Restore me in Piwik 4
//$this->removeColumnsFromTable($row);
}
}
/**
* @param $table
* @return bool
*/
protected function isArrayAccess(&$table)
{
return is_array($table) || $table instanceof \ArrayAccess;
}
}

View File

@@ -0,0 +1,125 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
use Piwik\Metrics;
/**
* Deletes all rows for which a specific column has a value that is lower than
* specified minimum threshold value.
*
* **Basic usage examples**
*
* // remove all countries from UserCountry.getCountry that have less than 3 visits
* $dataTable = // ... get a DataTable whose queued filters have been run ...
* $dataTable->filter('ExcludeLowPopulation', array('nb_visits', 3));
*
* // remove all countries from UserCountry.getCountry whose percent of total visits is less than 5%
* $dataTable = // ... get a DataTable whose queued filters have been run ...
* $dataTable->filter('ExcludeLowPopulation', array('nb_visits', false, 0.05));
*
* // remove all countries from UserCountry.getCountry whose bounce rate is less than 10%
* $dataTable = // ... get a DataTable that has a numerical bounce_rate column ...
* $dataTable->filter('ExcludeLowPopulation', array('bounce_rate', 0.10));
*
* @api
*/
class ExcludeLowPopulation extends BaseFilter
{
const MINIMUM_SIGNIFICANT_PERCENTAGE_THRESHOLD = 0.02;
/**
* The minimum value to enforce in a datatable for a specified column. Rows found with
* a value less than this are removed.
*
* @var number
*/
private $minimumValue;
/**
* Constructor.
*
* @param DataTable $table The DataTable that will be filtered eventually.
* @param string $columnToFilter The name of the column whose value will determine whether
* a row is deleted or not.
* @param number|false $minimumValue The minimum column value. Rows with column values <
* this number will be deleted. If false,
* `$minimumPercentageThreshold` is used.
* @param bool|float $minimumPercentageThreshold If supplied, column values must be a greater
* percentage of the sum of all column values than
* this percentage.
*/
public function __construct($table, $columnToFilter, $minimumValue, $minimumPercentageThreshold = false)
{
parent::__construct($table);
$row = $table->getFirstRow();
if ($row === false) {
return;
}
$this->columnToFilter = $this->selectColumnToExclude($columnToFilter, $row);
if ($minimumValue == 0) {
if ($minimumPercentageThreshold === false) {
$minimumPercentageThreshold = self::MINIMUM_SIGNIFICANT_PERCENTAGE_THRESHOLD;
}
$allValues = $table->getColumn($this->columnToFilter);
$sumValues = array_sum($allValues);
$minimumValue = $sumValues * $minimumPercentageThreshold;
}
$this->minimumValue = $minimumValue;
}
/**
* See {@link ExcludeLowPopulation}.
*
* @param DataTable $table
*/
public function filter($table)
{
if(empty($this->columnToFilter)) {
return;
}
$minimumValue = $this->minimumValue;
$isValueLowPopulation = function ($value) use ($minimumValue) {
return $value < $minimumValue;
};
$table->filter('ColumnCallbackDeleteRow', array($this->columnToFilter, $isValueLowPopulation));
}
/**
* Sets the column to be used for Excluding low population
*
* @param DataTable\Row $row
* @return int
*/
private function selectColumnToExclude($columnToFilter, $row)
{
if ($row->hasColumn($columnToFilter)) {
return $columnToFilter;
}
// filter_excludelowpop=nb_visits but the column name is still Metrics::INDEX_NB_VISITS in the table
$columnIdToName = Metrics::getMappingFromNameToId();
if (isset($columnIdToName[$columnToFilter])) {
$column = $columnIdToName[$columnToFilter];
if ($row->hasColumn($column)) {
return $column;
}
}
return $columnToFilter;
}
}

View File

@@ -0,0 +1,109 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable\Row;
/**
* DataTable filter that will group {@link DataTable} rows together based on the results
* of a reduce function. Rows with the same reduce result will be summed and merged.
*
* _NOTE: This filter should never be queued, it must be applied directly on a {@link DataTable}._
*
* **Basic usage example**
*
* // group URLs by host
* $dataTable->filter('GroupBy', array('label', function ($labelUrl) {
* return parse_url($labelUrl, PHP_URL_HOST);
* }));
*
* @api
*/
class GroupBy extends BaseFilter
{
/**
* The name of the columns to reduce.
* @var string
*/
private $groupByColumn;
/**
* A callback that modifies the $groupByColumn of each row in some way. Rows with
* the same reduction result will be added together.
*/
private $reduceFunction;
/**
* Extra parameters to pass to the reduce function.
*/
private $parameters;
/**
* Constructor.
*
* @param DataTable $table The DataTable to filter.
* @param string $groupByColumn The column name to reduce.
* @param callable $reduceFunction The reduce function. This must alter the `$groupByColumn`
* columng in some way. If not set then the filter will group by the raw column value.
* @param array $parameters deprecated - use an [anonymous function](http://php.net/manual/en/functions.anonymous.php)
* instead.
*/
public function __construct($table, $groupByColumn, $reduceFunction = null, $parameters = array())
{
parent::__construct($table);
$this->groupByColumn = $groupByColumn;
$this->reduceFunction = $reduceFunction;
$this->parameters = $parameters;
}
/**
* See {@link GroupBy}.
*
* @param DataTable $table
*/
public function filter($table)
{
/** @var Row[] $groupByRows */
$groupByRows = array();
$nonGroupByRowIds = array();
foreach ($table->getRowsWithoutSummaryRow() as $rowId => $row) {
$groupByColumnValue = $row->getColumn($this->groupByColumn);
$groupByValue = $groupByColumnValue;
// reduce the group by column of this row
if ($this->reduceFunction) {
$parameters = array_merge(array($groupByColumnValue), $this->parameters);
$groupByValue = call_user_func_array($this->reduceFunction, $parameters);
}
if (!isset($groupByRows[$groupByValue])) {
// if we haven't encountered this group by value before, we mark this row as a
// row to keep, and change the group by column to the reduced value.
$groupByRows[$groupByValue] = $row;
$row->setColumn($this->groupByColumn, $groupByValue);
} else {
// if we have already encountered this group by value, we add this row to the
// row that will be kept, and mark this one for deletion
$groupByRows[$groupByValue]->sumRow($row, $copyMeta = true, $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME));
$nonGroupByRowIds[] = $rowId;
}
}
if ($this->groupByColumn === 'label') {
$table->setLabelsHaveChanged();
}
// delete the unneeded rows.
$table->deleteRows($nonGroupByRowIds);
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
/**
* Delete all rows from the table that are not in the given [offset, offset+limit) range.
*
* **Basic example usage**
*
* // delete all rows from 5 -> 15
* $dataTable->filter('Limit', array(5, 10));
*
* @api
*/
class Limit extends BaseFilter
{
/**
* Constructor.
*
* @param DataTable $table The DataTable that will be filtered eventually.
* @param int $offset The starting row index to keep.
* @param int $limit Number of rows to keep (specify -1 to keep all rows).
* @param bool $keepSummaryRow Whether to keep the summary row or not.
*/
public function __construct($table, $offset, $limit = -1, $keepSummaryRow = false)
{
parent::__construct($table);
$this->offset = $offset;
$this->limit = $limit;
$this->keepSummaryRow = $keepSummaryRow;
}
/**
* See {@link Limit}.
*
* @param DataTable $table
*/
public function filter($table)
{
$table->setMetadata(DataTable::TOTAL_ROWS_BEFORE_LIMIT_METADATA_NAME, $table->getRowsCount());
if ($this->keepSummaryRow) {
$summaryRow = $table->getRowFromId(DataTable::ID_SUMMARY_ROW);
}
// we delete from 0 to offset
if ($this->offset > 0) {
$table->deleteRowsOffset(0, $this->offset);
}
// at this point the array has offset less elements. We delete from limit to the end
if ($this->limit >= 0) {
$table->deleteRowsOffset($this->limit);
}
if ($this->keepSummaryRow && !empty($summaryRow)) {
$table->addSummaryRow($summaryRow);
}
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
/**
* Executes a callback for each row of a {@link DataTable} and adds the result to the
* row as a metadata value. Only metadata values are passed to the callback.
*
* **Basic usage example**
*
* // add a logo metadata based on the url metadata
* $dataTable->filter('MetadataCallbackAddMetadata', array('url', 'logo', 'Piwik\Plugins\MyPlugin\getLogoFromUrl'));
*
* @api
*/
class MetadataCallbackAddMetadata extends BaseFilter
{
private $metadataToRead;
private $functionToApply;
private $metadataToAdd;
private $applyToSummaryRow;
/**
* Constructor.
*
* @param DataTable $table The DataTable that will eventually be filtered.
* @param string|array $metadataToRead The metadata to read from each row and pass to the callback.
* @param string $metadataToAdd The name of the metadata to add.
* @param callable $functionToApply The callback to execute for each row. The result will be
* added as metadata with the name `$metadataToAdd`.
* @param bool $applyToSummaryRow True if the callback should be applied to the summary row, false
* if otherwise.
*/
public function __construct($table, $metadataToRead, $metadataToAdd, $functionToApply,
$applyToSummaryRow = true)
{
parent::__construct($table);
$this->functionToApply = $functionToApply;
if (!is_array($metadataToRead)) {
$metadataToRead = array($metadataToRead);
}
$this->metadataToRead = $metadataToRead;
$this->metadataToAdd = $metadataToAdd;
$this->applyToSummaryRow = $applyToSummaryRow;
}
/**
* See {@link MetadataCallbackAddMetadata}.
*
* @param DataTable $table
*/
public function filter($table)
{
if ($this->applyToSummaryRow) {
$rows = $table->getRows();
} else {
$rows = $table->getRowsWithoutSummaryRow();
}
foreach ($rows as $key => $row) {
$params = array();
foreach ($this->metadataToRead as $name) {
$params[] = $row->getMetadata($name);
}
$newValue = call_user_func_array($this->functionToApply, $params);
if ($newValue !== false) {
$row->addMetadata($this->metadataToAdd, $newValue);
}
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\Row;
/**
* Execute a callback for each row of a {@link DataTable} passing certain column values and metadata
* as metadata, and replaces row metadata with the callback result.
*
* **Basic usage example**
*
* $dataTable->filter('MetadataCallbackReplace', array('url', function ($url) {
* return $url . '#index';
* }));
*
* @api
*/
class MetadataCallbackReplace extends ColumnCallbackReplace
{
/**
* Constructor.
*
* @param DataTable $table The DataTable that will eventually be filtered.
* @param array|string $metadataToFilter The metadata whose values should be passed to the callback
* and then replaced with the callback's result.
* @param callable $functionToApply The function to execute. Must take the metadata value as a parameter
* and return a value that will be used to replace the original.
* @param array|null $functionParameters deprecated - use an [anonymous function](http://php.net/manual/en/functions.anonymous.php)
* instead.
* @param array $extraColumnParameters Extra column values that should be passed to the callback, but
* shouldn't be replaced.
*/
public function __construct($table, $metadataToFilter, $functionToApply, $functionParameters = null,
$extraColumnParameters = array())
{
parent::__construct($table, $metadataToFilter, $functionToApply, $functionParameters, $extraColumnParameters);
}
/**
* @param Row $row
* @param string $metadataToFilter
* @param mixed $newValue
*/
protected function setElementToReplace($row, $metadataToFilter, $newValue)
{
$row->setMetadata($metadataToFilter, $newValue);
}
/**
* @param Row $row
* @param string $metadataToFilter
* @return array|bool|mixed
*/
protected function getElementToReplace($row, $metadataToFilter)
{
return $row->getMetadata($metadataToFilter);
}
}

View File

@@ -0,0 +1,125 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
/**
* Deletes every row for which a specific column does not match a supplied regex pattern.
*
* **Example**
*
* // filter out all rows whose labels doesn't start with piwik
* $dataTable->filter('Pattern', array('label', '^piwik'));
*
* @api
*/
class Pattern extends BaseFilter
{
/**
* @var string|array
*/
private $columnToFilter;
private $patternToSearch;
private $patternToSearchQuoted;
private $invertedMatch;
/**
* Constructor.
*
* @param DataTable $table The table to eventually filter.
* @param string $columnToFilter The column to match with the `$patternToSearch` pattern.
* @param string $patternToSearch The regex pattern to use.
* @param bool $invertedMatch Whether to invert the pattern or not. If true, will remove
* rows if they match the pattern.
*/
public function __construct($table, $columnToFilter, $patternToSearch, $invertedMatch = false)
{
parent::__construct($table);
$this->patternToSearch = $patternToSearch;
$this->patternToSearchQuoted = self::getPatternQuoted($patternToSearch);
$this->columnToFilter = $columnToFilter;
$this->invertedMatch = $invertedMatch;
}
/**
* Helper method to return the given pattern quoted
*
* @param string $pattern
* @return string
* @ignore
*/
public static function getPatternQuoted($pattern)
{
return '/' . str_replace('/', '\/', $pattern) . '/';
}
/**
* Performs case insensitive match
*
* @param string $patternQuoted
* @param string $string
* @param bool $invertedMatch
* @return int
* @ignore
*/
public static function match($patternQuoted, $string, $invertedMatch = false)
{
return preg_match($patternQuoted . "i", $string) == 1 ^ $invertedMatch;
}
/**
* See {@link Pattern}.
*
* @param DataTable $table
*/
public function filter($table)
{
foreach ($table->getRows() as $key => $row) {
//instead search must handle
// - negative search with -piwik
// - exact match with ""
// see (?!pattern) A subexpression that performs a negative lookahead search, which matches the search string at any point where a string not matching pattern begins.
$value = $row->getColumn($this->columnToFilter);
if ($value === false) {
$value = $row->getMetadata($this->columnToFilter);
}
if (!self::match($this->patternToSearchQuoted, $value, $this->invertedMatch)) {
$table->deleteRow($key);
}
}
}
/**
* See {@link Pattern}.
*
* @param array $array
* @return array
*/
public function filterArray($array)
{
$newArray = array();
foreach ($array as $key => $row) {
foreach ($this->columnToFilter as $column) {
if (!array_key_exists($column, $row)) {
continue;
}
if (self::match($this->patternToSearchQuoted, $row[$column], $this->invertedMatch)) {
$newArray[$key] = $row;
continue 2;
}
}
}
return $newArray;
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable;
/**
* Deletes rows that do not contain a column that matches a regex pattern and do not contain a
* subtable that contains a column that matches a regex pattern.
*
* **Example**
*
* // only display index pageviews in Actions.getPageUrls
* $dataTable->filter('PatternRecursive', array('label', 'index'));
*
* @api
*/
class PatternRecursive extends BaseFilter
{
private $columnToFilter;
private $patternToSearch;
private $patternToSearchQuoted;
/**
* Constructor.
*
* @param DataTable $table The table to eventually filter.
* @param string $columnToFilter The column to match with the `$patternToSearch` pattern.
* @param string $patternToSearch The regex pattern to use.
*/
public function __construct($table, $columnToFilter, $patternToSearch)
{
parent::__construct($table);
$this->patternToSearch = $patternToSearch;
$this->patternToSearchQuoted = Pattern::getPatternQuoted($patternToSearch);
$this->patternToSearch = $patternToSearch; //preg_quote($patternToSearch);
$this->columnToFilter = $columnToFilter;
}
/**
* See {@link PatternRecursive}.
*
* @param DataTable $table
* @return int The number of deleted rows.
*/
public function filter($table)
{
$rows = $table->getRows();
foreach ($rows as $key => $row) {
// A row is deleted if
// 1 - its label doesn't contain the pattern
// AND 2 - the label is not found in the children
$patternNotFoundInChildren = false;
$subTable = $row->getSubtable();
if (!$subTable) {
$patternNotFoundInChildren = true;
} else {
// we delete the row if we couldn't find the pattern in any row in the
// children hierarchy
if ($this->filter($subTable) == 0) {
$patternNotFoundInChildren = true;
}
}
if ($patternNotFoundInChildren
&& !Pattern::match($this->patternToSearchQuoted, $row->getColumn($this->columnToFilter), $invertedMatch = false)
) {
$table->deleteRow($key);
}
}
return $table->getRowsCount();
}
}

View File

@@ -0,0 +1,574 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Exception;
use Piwik\Columns\Dimension;
use Piwik\Columns\DimensionsProvider;
use Piwik\Common;
use Piwik\Config;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable\Row;
use Piwik\Log;
use Piwik\Metrics;
use Piwik\Period;
use Piwik\Piwik;
use Piwik\Plugin\Report;
use Piwik\Plugin\Segment;
use Piwik\Plugin\ReportsProvider;
use Piwik\Site;
/**
* DataTable filter that creates a pivot table from a report.
*
* A pivot table is a table that displays one metric value for two dimensions. The rows of
* the table represent one dimension and the columns another.
*
* This filter can pivot any report by any dimension as long as either:
*
* - the pivot-by dimension is the dimension of the report's subtable
* - or, the pivot-by dimension has an associated report, and the report to pivot has a dimension with
* a segment
*
* Reports are pivoted by iterating over the rows of the report, fetching the pivot-by report
* for the current row, and setting the columns of row to the rows of the pivot-by report. For example:
*
* to pivot Referrers.getKeywords by UserCountry.City, we first loop through the Referrers.getKeywords
* report's rows. For each row, we take the label (which is the referrer keyword), and get the
* UserCountry.getCity report using the referrerKeyword=... segment. If the row's label were 'abcdefg',
* we would use the 'referrerKeyword==abcdefg' segment.
*
* The UserCountry.getCity report we find is the report on visits by country, but only for the visits
* for the specific row. We take this report's row labels and add them as columns for the Referrers.getKeywords
* table.
*
* Implementation details:
*
* Fetching intersected table can be done by segment or subtable. If the requested pivot by
* dimension is the report's subtable dimension, then the subtable is used regardless, since it
* is much faster than fetching by segment.
*
* Also, by default, fetching by segment is disabled in the config (see the
* '[General] pivot_by_filter_enable_fetch_by_segment' option).
*/
class PivotByDimension extends BaseFilter
{
/**
* The pivot-by Dimension. The metadata in this class is used to determine if we can
* pivot the report and used to fetch intersected tables.
*
* @var Dimension
*/
private $pivotByDimension;
/**
* The report that reports on visits by the pivot dimension. The metadata in this class
* is used to determine if we can pivot the report and used to fetch intersected tables
* by segment.
*
* @var Report
*/
private $pivotDimensionReport;
/**
* The column that should be displayed in the pivot table. This should be a metric, eg,
* `'nb_visits'`, `'nb_actions'`, etc.
*
* @var string
*/
private $pivotColumn;
/**
* The number of columns to limit the pivot table to. Applying a pivot can result in
* tables with many, many columns. This can cause problems when displayed in web page.
*
* A default limit of 7 is imposed if no column limit is specified in construction.
* If a negative value is supplied, no limiting is performed.
*
* Columns are summed and sorted before being limited so the columns w/ the most
* visits will be displayed and the columns w/ the least will be cut off.
*
* @var int
*/
private $pivotByColumnLimit;
/**
* Metadata for the report being pivoted. The metadata in this class is used to
* determine if we can pivot the report and used to fetch intersected tables.
*
* @var Report
*/
private $thisReport;
/**
* Metadata for the segment of the dimension of the report being pivoted. When
* fetching intersected tables by segment, this is the segment used.
*
* @var Segment
*/
private $thisReportDimensionSegment;
/**
* Whether fetching by segment is enabled or not.
*
* @var bool
*/
private $isFetchingBySegmentEnabled;
/**
* The subtable dimension of the report being pivoted. Used to determine if and
* how intersected tables are fetched.
*
* @var Dimension|null
*/
private $subtableDimension;
/**
* The index value (if any) for the metric that should be displayed in the pivot
* table.
*
* @var int|null
*/
private $metricIndexValue;
/**
* Constructor.
*
* @param DataTable $table The table to pivot.
* @param string $report The ID of the report being pivoted, eg, `'Referrers.getKeywords'`.
* @param string $pivotByDimension The ID of the dimension to pivot by, eg, `'Referrers.Keyword'`.
* @param string|false $pivotColumn The metric that should be displayed in the pivot table, eg, `'nb_visits'`.
* If `false`, the first non-label column is used.
* @param false|int $pivotByColumnLimit The number of columns to limit the pivot table to.
* @param bool $isFetchingBySegmentEnabled Whether to allow fetching by segment.
* @throws Exception if pivoting the report by a dimension is unsupported.
*/
public function __construct($table, $report, $pivotByDimension, $pivotColumn, $pivotByColumnLimit = false,
$isFetchingBySegmentEnabled = true)
{
parent::__construct($table);
Log::debug("PivotByDimension::%s: creating with [report = %s, pivotByDimension = %s, pivotColumn = %s, "
. "pivotByColumnLimit = %s, isFetchingBySegmentEnabled = %s]", __FUNCTION__, $report, $pivotByDimension,
$pivotColumn, $pivotByColumnLimit, $isFetchingBySegmentEnabled);
$this->pivotColumn = $pivotColumn;
$this->pivotByColumnLimit = $pivotByColumnLimit ?: self::getDefaultColumnLimit();
$this->isFetchingBySegmentEnabled = $isFetchingBySegmentEnabled;
$namesToId = Metrics::getMappingFromNameToId();
$this->metricIndexValue = isset($namesToId[$this->pivotColumn]) ? $namesToId[$this->pivotColumn] : null;
$this->setPivotByDimension($pivotByDimension);
$this->setThisReportMetadata($report);
$this->checkSupportedPivot();
}
/**
* Pivots to table.
*
* @param DataTable $table The table to manipulate.
*/
public function filter($table)
{
// set of all column names in the pivoted table mapped with the sum of all column
// values. used later in truncating and ordering the pivoted table's columns.
$columnSet = array();
// if no pivot column was set, use the first one found in the row
if (empty($this->pivotColumn)) {
$this->pivotColumn = $this->getNameOfFirstNonLabelColumnInTable($table);
}
Log::debug("PivotByDimension::%s: pivoting table with pivot column = %s", __FUNCTION__, $this->pivotColumn);
foreach ($table->getRows() as $row) {
$row->setColumns(array('label' => $row->getColumn('label')));
$associatedTable = $this->getIntersectedTable($table, $row);
if (!empty($associatedTable)) {
foreach ($associatedTable->getRows() as $columnRow) {
$pivotTableColumn = $columnRow->getColumn('label');
$columnValue = $this->getColumnValue($columnRow, $this->pivotColumn);
if (isset($columnSet[$pivotTableColumn])) {
$columnSet[$pivotTableColumn] += $columnValue;
} else {
$columnSet[$pivotTableColumn] = $columnValue;
}
$row->setColumn($pivotTableColumn, $columnValue);
}
Common::destroy($associatedTable);
unset($associatedTable);
}
}
Log::debug("PivotByDimension::%s: pivoted columns set: %s", __FUNCTION__, $columnSet);
$others = Piwik::translate('General_Others');
$defaultRow = $this->getPivotTableDefaultRowFromColumnSummary($columnSet, $others);
Log::debug("PivotByDimension::%s: un-prepended default row: %s", __FUNCTION__, $defaultRow);
// post process pivoted datatable
foreach ($table->getRows() as $row) {
// remove subtables from rows
$row->removeSubtable();
$row->deleteMetadata('idsubdatatable_in_db');
// use default row to ensure column ordering and add missing columns/aggregate cut-off columns
$orderedColumns = $defaultRow;
foreach ($row->getColumns() as $name => $value) {
if (isset($orderedColumns[$name])) {
$orderedColumns[$name] = $value;
} else {
$orderedColumns[$others] += $value;
}
}
$row->setColumns($orderedColumns);
}
$table->clearQueuedFilters(); // TODO: shouldn't clear queued filters, but we can't wait for them to be run
// since generic filters are run before them. remove after refactoring
// processed metrics.
// prepend numerals to columns in a queued filter (this way, disable_queued_filters can be used
// to get machine readable data from the API if needed)
$prependedColumnNames = $this->getOrderedColumnsWithPrependedNumerals($defaultRow, $others);
Log::debug("PivotByDimension::%s: prepended column name mapping: %s", __FUNCTION__, $prependedColumnNames);
$table->queueFilter(function (DataTable $table) use ($prependedColumnNames) {
foreach ($table->getRows() as $row) {
$row->setColumns(array_combine($prependedColumnNames, $row->getColumns()));
}
});
}
/**
* An intersected table is a table that describes visits by a certain dimension for the visits
* represented by a row in another table. This method fetches intersected tables either via
* subtable or by using a segment. Read the class docs for more info.
*/
private function getIntersectedTable(DataTable $table, Row $row)
{
if ($this->isPivotDimensionSubtable()) {
return $this->loadSubtable($table, $row);
}
if ($this->isFetchingBySegmentEnabled) {
$segment = $row->getMetadata('segment');
if (empty($segment)) {
$segmentValue = $row->getMetadata('segmentValue');
if ($segmentValue === false) {
$segmentValue = $row->getColumn('label');
}
$segmentName = $this->thisReportDimensionSegment->getSegment();
if (empty($segmentName)) {
throw new \Exception("Invalid segment found when pivoting: " . $this->thisReportDimensionSegment->getName());
}
$segment = $segmentName . "==" . urlencode($segmentValue);
}
return $this->fetchIntersectedWithThisBySegment($table, $segment);
}
// should never occur, unless checkSupportedPivot() fails to catch an unsupported pivot
throw new Exception("Unexpected error, cannot fetch intersected table.");
}
private function isPivotDimensionSubtable()
{
return self::areDimensionsEqualAndNotNull($this->subtableDimension, $this->pivotByDimension);
}
private function loadSubtable(DataTable $table, Row $row)
{
$idSubtable = $row->getIdSubDataTable();
if ($idSubtable === null) {
return null;
}
$subtable = $row->getSubtable();
if (!$subtable) {
$subtable = $this->thisReport->fetchSubtable($idSubtable, $this->getRequestParamOverride($table));
}
if (!$subtable) { // sanity check
throw new Exception("Unexpected error: could not load subtable '$idSubtable'.");
}
return $subtable;
}
private function fetchIntersectedWithThisBySegment(DataTable $table, $segmentStr)
{
// TODO: segment + report API method query params should be stored in DataTable metadata so we don't have to access it here
$originalSegment = Common::getRequestVar('segment', false);
if (!empty($originalSegment)) {
$segmentStr = $originalSegment . ';' . $segmentStr;
}
Log::debug("PivotByDimension: Fetching intersected with segment '%s'", $segmentStr);
$params = array_merge($this->pivotDimensionReport->getParameters() ?: [], ['segment' => $segmentStr]);
$params = $params + $this->getRequestParamOverride($table);
return $this->pivotDimensionReport->fetch($params);
}
private function setPivotByDimension($pivotByDimension)
{
$factory = new DimensionsProvider();
$this->pivotByDimension = $factory->factory($pivotByDimension);
if (empty($this->pivotByDimension)) {
throw new Exception("Invalid dimension '$pivotByDimension'.");
}
$this->pivotDimensionReport = Report::getForDimension($this->pivotByDimension);
}
private function setThisReportMetadata($report)
{
if (is_string($report)) {
list($module, $action) = explode('.', $report);
$report = ReportsProvider::factory($module, $action);
if (empty($report)) {
throw new \Exception("Unable to find report '$module.$action'.");
}
}
$this->thisReport = $report;
$this->subtableDimension = $this->thisReport->getSubtableDimension();
$thisReportDimension = $this->thisReport->getDimension();
if ($thisReportDimension !== null) {
$segments = $thisReportDimension->getSegments();
$this->thisReportDimensionSegment = reset($segments);
}
}
private function checkSupportedPivot()
{
$reportId = $this->thisReport->getModule() . '.' . $this->thisReport->getAction();
if (!$this->isFetchingBySegmentEnabled) {
// if fetching by segment is disabled, then there must be a subtable for the current report and
// subtable's dimension must be the pivot dimension
if (empty($this->subtableDimension)) {
throw new Exception("Unsupported pivot: report '$reportId' has no subtable dimension.");
}
if (!$this->isPivotDimensionSubtable()) {
throw new Exception("Unsupported pivot: the subtable dimension for '$reportId' does not match the "
. "requested pivotBy dimension. [subtable dimension = {$this->subtableDimension->getId()}, "
. "pivot by dimension = {$this->pivotByDimension->getId()}]");
}
} else {
$canFetchBySubtable = !empty($this->subtableDimension)
&& $this->subtableDimension->getId() === $this->pivotByDimension->getId();
if ($canFetchBySubtable) {
return;
}
// if fetching by segment is enabled, and we cannot fetch by subtable, then there has to be a report
// for the pivot dimension (so we can fetch the report), and there has to be a segment for this report's
// dimension (so we can use it when fetching)
if (empty($this->pivotDimensionReport)) {
throw new Exception("Unsupported pivot: No report for pivot dimension '{$this->pivotByDimension->getId()}'"
. " (report required for fetching intersected tables by segment).");
}
if (empty($this->thisReportDimensionSegment)) {
throw new Exception("Unsupported pivot: No segment for dimension of report '$reportId'."
. " (segment required for fetching intersected tables by segment).");
}
}
}
/**
* @param $columnRow
* @param $pivotColumn
* @return false|mixed
*/
private function getColumnValue(Row $columnRow, $pivotColumn)
{
$value = $columnRow->getColumn($pivotColumn);
if (empty($value)
&& !empty($this->metricIndexValue)
) {
$value = $columnRow->getColumn($this->metricIndexValue);
}
return $value;
}
private function getNameOfFirstNonLabelColumnInTable(DataTable $table)
{
foreach ($table->getRows() as $row) {
foreach ($row->getColumns() as $columnName => $ignore) {
if ($columnName != 'label') {
return $columnName;
}
}
}
}
private function getRequestParamOverride(DataTable $table)
{
$params = array(
'pivotBy' => '',
'column' => '',
'flat' => 0,
'totals' => 0,
'disable_queued_filters' => 1,
'disable_generic_filters' => 1,
'showColumns' => '',
'hideColumns' => ''
);
/** @var Site $site */
$site = $table->getMetadata('site');
if (!empty($site)) {
$params['idSite'] = $site->getId();
}
/** @var Period $period */
$period = $table->getMetadata('period');
if (!empty($period)) {
$params['period'] = $period->getLabel();
if ($params['period'] == 'range') {
$params['date'] = $period->getRangeString();
} else {
$params['date'] = $period->getDateStart()->toString();
}
}
return $params;
}
private function getPivotTableDefaultRowFromColumnSummary($columnSet, $othersRowLabel)
{
// sort columns by sum (to ensure deterministic ordering)
uksort($columnSet, function ($key1, $key2) use ($columnSet) {
if ($columnSet[$key1] == $columnSet[$key2]) {
return strcmp($key1, $key2);
}
return $columnSet[$key2] > $columnSet[$key1] ? 1 : -1;
});
// limit columns if necessary (adding aggregate Others column at end)
if ($this->pivotByColumnLimit > 0
&& count($columnSet) > $this->pivotByColumnLimit
) {
$columnSet = array_slice($columnSet, 0, $this->pivotByColumnLimit - 1, $preserveKeys = true);
$columnSet[$othersRowLabel] = 0;
}
// remove column sums from array so it can be used as a default row
$columnSet = array_map(function () { return false; }, $columnSet);
// make sure label column is first
$columnSet = array('label' => false) + $columnSet;
return $columnSet;
}
private function getOrderedColumnsWithPrependedNumerals($defaultRow, $othersRowLabel)
{
$flags = ENT_COMPAT;
if (defined('ENT_HTML401')) {
$flags |= ENT_HTML401; // part of default flags for 5.4, but not 5.3
}
// must use decoded character otherwise sort later will fail
// (sort column will be set to decoded but columns will have &nbsp;)
$nbsp = html_entity_decode('&nbsp;', $flags, 'utf-8');
$result = array();
$currentIndex = 1;
foreach ($defaultRow as $columnName => $ignore) {
if ($columnName === $othersRowLabel
|| $columnName === 'label'
) {
$result[] = $columnName;
} else {
$modifiedColumnName = $currentIndex . '.' . $nbsp . $columnName;
$result[] = $modifiedColumnName;
++$currentIndex;
}
}
return $result;
}
/**
* Returns true if pivoting by subtable is supported for a report. Will return true if the report
* has a subtable dimension and if the subtable dimension is different than the report's dimension.
*
* @param Report $report
* @return bool
*/
public static function isPivotingReportBySubtableSupported(Report $report)
{
return self::areDimensionsNotEqualAndNotNull($report->getSubtableDimension(), $report->getDimension());
}
/**
* Returns true if fetching intersected tables by segment is enabled in the INI config, false if otherwise.
*
* @return bool
*/
public static function isSegmentFetchingEnabledInConfig()
{
return Config::getInstance()->General['pivot_by_filter_enable_fetch_by_segment'];
}
/**
* Returns the default maximum number of columns to allow in a pivot table from the INI config.
* Uses the **pivot_by_filter_default_column_limit** INI config option.
*
* @return int
*/
public static function getDefaultColumnLimit()
{
return Config::getInstance()->General['pivot_by_filter_default_column_limit'];
}
/**
* @param Dimension|null $lhs
* @param Dimension|null $rhs
* @return bool
*/
private static function areDimensionsEqualAndNotNull($lhs, $rhs)
{
return !empty($lhs) && !empty($rhs) && $lhs->getId() == $rhs->getId();
}
/**
* @param Dimension|null $lhs
* @param Dimension|null $rhs
* @return bool
*/
private static function areDimensionsNotEqualAndNotNull($lhs, $rhs)
{
return !empty($lhs) && !empty($rhs) && $lhs->getId() != $rhs->getId();
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
/**
* Executes a callback for each row of a {@link DataTable} and prepends each existing segment with the
* given segment.
*
* **Basic usage example**
*
* $dataTable->filter('PrependSegment', array('segmentName==segmentValue;'));
*
* @api
*/
class PrependSegment extends PrependValueToMetadata
{
/**
* @param DataTable $table
* @param string $prependSegment The segment to prepend if a segment is already defined. Make sure to include
* A condition, eg the segment should end with ';' or ','
*/
public function __construct($table, $prependSegment = '')
{
parent::__construct($table, 'segment', $prependSegment);
}
}

View File

@@ -0,0 +1,65 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
/**
* Executes a callback for each row of a {@link DataTable} and prepends the given value to each metadata entry
* but only if the given metadata entry exists.
*
* **Basic usage example**
*
* $dataTable->filter('PrependValueToMetadata', array('segment', 'segmentName==segmentValue'));
*
* @api
*/
class PrependValueToMetadata extends BaseFilter
{
private $metadataColumn;
private $valueToPrepend;
/**
* @param DataTable $table
* @param string $metadataName The name of the metadata that should be prepended
* @param string $valueToPrepend The value to prepend if the metadata entry exists
*/
public function __construct($table, $metadataName, $valueToPrepend)
{
parent::__construct($table);
$this->metadataColumn = $metadataName;
$this->valueToPrepend = $valueToPrepend;
}
/**
* See {@link PrependValueToMetadata}.
*
* @param DataTable $table
*/
public function filter($table)
{
if (empty($this->metadataColumn) || empty($this->valueToPrepend)) {
return;
}
$metadataColumn = $this->metadataColumn;
$valueToPrepend = $this->valueToPrepend;
$table->filter(function (DataTable $dataTable) use ($metadataColumn, $valueToPrepend) {
foreach ($dataTable->getRows() as $row) {
$filter = $row->getMetadata($metadataColumn);
if ($filter !== false) {
$row->setMetadata($metadataColumn, $valueToPrepend . $filter);
}
}
});
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
/**
* Check range
*
*/
class RangeCheck extends BaseFilter
{
public static $minimumValue = 0.00;
public static $maximumValue = 100.0;
/**
* @param DataTable $table
* @param string $columnToFilter name of the column to filter
* @param float $minimumValue minimum value for range
* @param float $maximumValue maximum value for range
*/
public function __construct($table, $columnToFilter, $minimumValue = 0.00, $maximumValue = 100.0)
{
parent::__construct($table);
$this->columnToFilter = $columnToFilter;
if ((float) $minimumValue < (float) $maximumValue) {
self::$minimumValue = $minimumValue;
self::$maximumValue = $maximumValue;
}
}
/**
* Executes the filter an adjusts all columns to fit the defined range
*
* @param DataTable $table
*/
public function filter($table)
{
foreach ($table->getRows() as $row) {
$value = $row->getColumn($this->columnToFilter);
if ($value === false) {
$value = $row->getMetadata($this->columnToFilter);
if ($value !== false) {
if ($value < (float) self::$minimumValue) {
$row->setMetadata($this->columnToFilter, self::$minimumValue);
} elseif ($value > (float) self::$maximumValue) {
$row->setMetadata($this->columnToFilter, self::$maximumValue);
}
}
continue;
}
if ($value !== false) {
if ($value < (float) self::$minimumValue) {
$row->setColumn($this->columnToFilter, self::$minimumValue);
} elseif ($value > (float) self::$maximumValue) {
$row->setColumn($this->columnToFilter, self::$maximumValue);
}
}
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
/**
* Delete all existing subtables from rows.
*
* **Basic example usage**
*
* $dataTable->filter('RemoveSubtables');
*
* @api
*/
class RemoveSubtables extends BaseFilter
{
/**
* Constructor.
*
* @param DataTable $table The DataTable that will be filtered eventually.
*/
public function __construct($table)
{
parent::__construct($table);
}
/**
* See {@link Limit}.
*
* @param DataTable $table
*/
public function filter($table)
{
$rows = $table->getRows();
foreach ($rows as $row) {
$row->removeSubtable();
}
}
}

View File

@@ -0,0 +1,170 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable\Simple;
use Piwik\DataTable;
use Piwik\Metrics;
use Piwik\Piwik;
use Piwik\Tracker\GoalManager;
/**
* Replaces column names in each row of a table using an array that maps old column
* names new ones.
*
* If no mapping is provided, this column will use one that maps index metric names
* (which are integers) with their string column names. In the database, reports are
* stored with integer metric names because it results in blobs that take up less space.
* When loading the reports, the column names must be replaced, which is handled by this
* class. (See {@link Piwik\Metrics} for more information about integer metric names.)
*
* **Basic example**
*
* // filter use in a plugin's API method
* public function getMyReport($idSite, $period, $date, $segment = false, $expanded = false)
* {
* $dataTable = Archive::createDataTableFromArchive('MyPlugin_MyReport', $idSite, $period, $date, $segment, $expanded);
* $dataTable->queueFilter('ReplaceColumnNames');
* return $dataTable;
* }
*
* @api
*/
class ReplaceColumnNames extends BaseFilter
{
protected $mappingToApply;
/**
* Constructor.
*
* @param DataTable $table The table that will be eventually filtered.
* @param array|null $mappingToApply The name mapping to apply. Must map old column names
* with new ones, eg,
*
* array('OLD_COLUMN_NAME' => 'NEW_COLUMN NAME',
* 'OLD_COLUMN_NAME2' => 'NEW_COLUMN NAME2')
*
* If null, {@link Piwik\Metrics::$mappingFromIdToName} is used.
*/
public function __construct($table, $mappingToApply = null)
{
parent::__construct($table);
$this->mappingToApply = Metrics::$mappingFromIdToName;
if (!is_null($mappingToApply)) {
$this->mappingToApply = $mappingToApply;
}
}
/**
* See {@link ReplaceColumnNames}.
*
* @param DataTable $table
*/
public function filter($table)
{
if ($table instanceof Simple) {
$this->filterSimple($table);
} else {
$this->filterTable($table);
}
}
/**
* @param DataTable $table
*/
protected function filterTable($table)
{
foreach ($table->getRows() as $row) {
$newColumns = $this->getRenamedColumns($row->getColumns());
$row->setColumns($newColumns);
$this->filterSubTable($row);
}
}
/**
* @param Simple $table
*/
protected function filterSimple(Simple $table)
{
foreach ($table->getRows() as $row) {
$columns = array_keys($row->getColumns());
foreach ($columns as $column) {
$newName = $this->getRenamedColumn($column);
if ($newName) {
$row->renameColumn($column, $newName);
}
}
}
}
protected function getRenamedColumn($column)
{
$newName = false;
if (isset($this->mappingToApply[$column])
&& $this->mappingToApply[$column] != $column
) {
$newName = $this->mappingToApply[$column];
}
return $newName;
}
/**
* Checks the given columns and renames them if required
*
* @param array $columns
* @return array
*/
protected function getRenamedColumns($columns)
{
$newColumns = array();
foreach ($columns as $columnName => $columnValue) {
$renamedColumn = $this->getRenamedColumn($columnName);
if ($renamedColumn) {
if ($renamedColumn == 'goals') {
$columnValue = $this->flattenGoalColumns($columnValue);
}
// If we happen to rename a column to a name that already exists,
// sum both values in the column. This should really not happen, but
// we introduced in 1.1 a new dataTable indexing scheme for Actions table, and
// could end up with both strings and their int indexes counterpart in a monthly/yearly dataTable
// built from DataTable with both formats
if (isset($newColumns[$renamedColumn])) {
$columnValue += $newColumns[$renamedColumn];
}
$columnName = $renamedColumn;
}
$newColumns[$columnName] = $columnValue;
}
return $newColumns;
}
/**
* @param $columnValue
* @return array
*/
protected function flattenGoalColumns($columnValue)
{
$newSubColumns = array();
foreach ($columnValue as $idGoal => $goalValues) {
$mapping = Metrics::$mappingFromIdToNameGoal;
if ($idGoal == GoalManager::IDGOAL_CART) {
$idGoal = Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART;
} elseif ($idGoal == GoalManager::IDGOAL_ORDER) {
$idGoal = Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER;
}
foreach ($goalValues as $id => $goalValue) {
$subColumnName = $mapping[$id];
$newSubColumns['idgoal=' . $idGoal][$subColumnName] = $goalValue;
}
}
return $newSubColumns;
}
}

View File

@@ -0,0 +1,82 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable;
use Piwik\Piwik;
/**
* Replaces the label of the summary row with a supplied label.
*
* This filter is only used to prettify the summary row label and so it should
* always be queued on a {@link DataTable}.
*
* This filter always recurses. In other words, this filter will always apply itself to
* all subtables in the given {@link DataTable}'s table hierarchy.
*
* **Basic example**
*
* $dataTable->queueFilter('ReplaceSummaryRowLabel', array(Piwik::translate('General_Others')));
*
* @api
*/
class ReplaceSummaryRowLabel extends BaseFilter
{
/**
* Constructor.
*
* @param DataTable $table The table that will eventually be filtered.
* @param string|null $newLabel The new label for summary row. If null, defaults to
* `Piwik::translate('General_Others')`.
*/
public function __construct($table, $newLabel = null)
{
parent::__construct($table);
if (is_null($newLabel)) {
$newLabel = Piwik::translate('General_Others');
}
$this->newLabel = $newLabel;
}
/**
* See {@link ReplaceSummaryRowLabel}.
*
* @param DataTable $table
*/
public function filter($table)
{
$row = $table->getRowFromId(DataTable::ID_SUMMARY_ROW);
if ($row) {
$row->setColumn('label', $this->newLabel);
} else {
$row = $table->getRowFromLabel(DataTable::LABEL_SUMMARY_ROW);
if ($row) {
$row->setColumn('label', $this->newLabel);
}
}
// recurse
foreach ($table->getRowsWithoutSummaryRow() as $row) {
$subTable = $row->getSubtable();
if ($subTable) {
$this->filter($subTable);
}
}
$summaryRow = $table->getRowFromId(DataTable::ID_SUMMARY_ROW);
if (!empty($summaryRow)) {
$subTable = $summaryRow->getSubtable();
if ($subTable) {
$this->filter($subTable);
}
}
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable;
use Piwik\DataTable\BaseFilter;
/**
* Sanitizes DataTable labels as an extra precaution. Called internally by Piwik.
*
*/
class SafeDecodeLabel extends BaseFilter
{
private $columnToDecode;
/**
* @param DataTable $table
*/
public function __construct($table)
{
parent::__construct($table);
$this->columnToDecode = 'label';
}
/**
* Decodes the given value
*
* @param string $value
* @return mixed|string
*/
public static function decodeLabelSafe($value)
{
if (empty($value)) {
return $value;
}
$raw = urldecode($value);
$value = htmlspecialchars_decode($raw, ENT_QUOTES);
// ENT_IGNORE so that if utf8 string has some errors, we simply discard invalid code unit sequences
$style = ENT_QUOTES | ENT_IGNORE;
// See changes in 5.4: http://nikic.github.com/2012/01/28/htmlspecialchars-improvements-in-PHP-5-4.html
// Note: at some point we should change ENT_IGNORE to ENT_SUBSTITUTE
$value = htmlspecialchars($value, $style, 'UTF-8');
return $value;
}
/**
* Decodes all columns of the given data table
*
* @param DataTable $table
*/
public function filter($table)
{
foreach ($table->getRows() as $row) {
$value = $row->getColumn($this->columnToDecode);
if ($value !== false) {
$value = self::decodeLabelSafe($value);
$row->setColumn($this->columnToDecode, $value);
$this->filterSubTable($row);
}
}
}
}

View File

@@ -0,0 +1,128 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable\Row;
use Piwik\DataTable\Simple;
use Piwik\DataTable;
use Piwik\Metrics;
use Piwik\Metrics\Sorter;
/**
* Sorts a {@link DataTable} based on the value of a specific column.
*
* It is possible to specify a natural sorting (see [php.net/natsort](http://php.net/natsort) for details).
*
* @api
*/
class Sort extends BaseFilter
{
protected $columnToSort;
protected $order;
protected $naturalSort;
protected $isSecondaryColumnSortEnabled;
protected $secondaryColumnSortCallback;
const ORDER_DESC = 'desc';
const ORDER_ASC = 'asc';
/**
* Constructor.
*
* @param DataTable $table The table to eventually filter.
* @param string $columnToSort The name of the column to sort by.
* @param string $order order `'asc'` or `'desc'`.
* @param bool $naturalSort Whether to use a natural sort or not (see {@link http://php.net/natsort}).
* @param bool $recursiveSort Whether to sort all subtables or not.
* @param bool|callback $doSortBySecondaryColumn If true will sort by a secondary column. The column is automatically
* detected and will be either nb_visits or label, if possible.
* If callback given it will sort by the column returned by the callback (if any)
* callback will be called with 2 parameters: primaryColumnToSort and table
*/
public function __construct($table, $columnToSort, $order = 'desc', $naturalSort = true, $recursiveSort = true, $doSortBySecondaryColumn = false)
{
parent::__construct($table);
if ($recursiveSort) {
$table->enableRecursiveSort();
}
$this->columnToSort = $columnToSort;
$this->naturalSort = $naturalSort;
$this->order = strtolower($order);
$this->isSecondaryColumnSortEnabled = !empty($doSortBySecondaryColumn);
$this->secondaryColumnSortCallback = is_callable($doSortBySecondaryColumn) ? $doSortBySecondaryColumn : null;
}
/**
* See {@link Sort}.
*
* @param DataTable $table
* @return mixed
*/
public function filter($table)
{
if ($table instanceof Simple) {
return;
}
if (empty($this->columnToSort)) {
return;
}
if (!$table->getRowsCountWithoutSummaryRow()) {
return;
}
$row = $table->getFirstRow();
if ($row === false) {
return;
}
$config = new Sorter\Config();
$sorter = new Sorter($config);
$config->naturalSort = $this->naturalSort;
$config->primaryColumnToSort = $sorter->getPrimaryColumnToSort($table, $this->columnToSort);
$config->primarySortOrder = $sorter->getPrimarySortOrder($this->order);
$config->primarySortFlags = $sorter->getBestSortFlags($table, $config->primaryColumnToSort);
if (!empty($this->secondaryColumnSortCallback)) {
$config->secondaryColumnToSort = call_user_func($this->secondaryColumnSortCallback, $config->primaryColumnToSort, $table);
} else {
$config->secondaryColumnToSort = $sorter->getSecondaryColumnToSort($row, $config->primaryColumnToSort);
}
$config->secondarySortOrder = $sorter->getSecondarySortOrder($this->order, $config->secondaryColumnToSort);
$config->secondarySortFlags = $sorter->getBestSortFlags($table, $config->secondaryColumnToSort);
// secondary sort should not be needed for all other sort flags (eg string/natural sort) as label is unique and would make it slower
$isSecondaryColumnSortNeeded = $config->primarySortFlags === SORT_NUMERIC;
$config->isSecondaryColumnSortEnabled = $this->isSecondaryColumnSortEnabled && $isSecondaryColumnSortNeeded;
$this->sort($sorter, $table);
}
private function sort(Sorter $sorter, DataTable $table)
{
$sorter->sort($table);
if ($table->isSortRecursiveEnabled()) {
foreach ($table->getRowsWithoutSummaryRow() as $row) {
$subTable = $row->getSubtable();
if ($subTable) {
$subTable->enableRecursiveSort();
$this->sort($sorter, $subTable);
}
}
}
}
}

View File

@@ -0,0 +1,123 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Filter;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\Piwik;
/**
* Truncates a {@link DataTable} by merging all rows after a certain index into a new summary
* row. If the count of rows is less than the index, nothing happens.
*
* The {@link ReplaceSummaryRowLabel} filter will be queued after the table is truncated.
*
* ### Examples
*
* **Basic usage**
*
* $dataTable->filter('Truncate', array($truncateAfter = 500));
*
* **Using a custom summary row label**
*
* $dataTable->filter('Truncate', array($truncateAfter = 500, $summaryRowLabel = Piwik::translate('General_Total')));
*
* @api
*/
class Truncate extends BaseFilter
{
/**
* Constructor.
*
* @param DataTable $table The table that will be filtered eventually.
* @param int $truncateAfter The row index to truncate at. All rows passed this index will
* be removed.
* @param string $labelSummaryRow The label to use for the summary row. Defaults to
* `Piwik::translate('General_Others')`.
* @param string $columnToSortByBeforeTruncating The column to sort by before truncation, eg,
* `'nb_visits'`.
* @param bool $filterRecursive If true executes this filter on all subtables descending from
* `$table`.
*/
public function __construct($table,
$truncateAfter,
$labelSummaryRow = null,
$columnToSortByBeforeTruncating = null,
$filterRecursive = true)
{
parent::__construct($table);
$this->truncateAfter = $truncateAfter;
if ($labelSummaryRow === null) {
$labelSummaryRow = Piwik::translate('General_Others');
}
$this->labelSummaryRow = $labelSummaryRow;
$this->columnToSortByBeforeTruncating = $columnToSortByBeforeTruncating;
$this->filterRecursive = $filterRecursive;
}
/**
* Executes the filter, see {@link Truncate}.
*
* @param DataTable $table
*/
public function filter($table)
{
if ($this->truncateAfter < 0) {
return;
}
$this->addSummaryRow($table);
$table->queueFilter('ReplaceSummaryRowLabel', array($this->labelSummaryRow));
if ($this->filterRecursive) {
foreach ($table->getRowsWithoutSummaryRow() as $row) {
if ($row->isSubtableLoaded()) {
$this->filter($row->getSubtable());
}
}
}
}
/**
* @param DataTable $table
*/
private function addSummaryRow($table)
{
if ($table->getRowsCount() <= $this->truncateAfter + 1) {
return;
}
$table->filter('Sort', array($this->columnToSortByBeforeTruncating, 'desc', $naturalSort = true, $recursiveSort = false));
$rows = array_values($table->getRows());
$count = $table->getRowsCount();
$newRow = new Row(array(Row::COLUMNS => array('label' => DataTable::LABEL_SUMMARY_ROW)));
$aggregationOps = $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME);
for ($i = $this->truncateAfter; $i < $count; $i++) {
if (!isset($rows[$i])) {
// case when the last row is a summary row, it is not indexed by $cout but by DataTable::ID_SUMMARY_ROW
$summaryRow = $table->getRowFromId(DataTable::ID_SUMMARY_ROW);
//FIXME: I'm not sure why it could return false, but it was reported in: http://forum.piwik.org/read.php?2,89324,page=1#msg-89442
if ($summaryRow) {
$newRow->sumRow($summaryRow, $enableCopyMetadata = false, $aggregationOps);
}
} else {
$newRow->sumRow($rows[$i], $enableCopyMetadata = false, $aggregationOps);
}
}
$table->filter('Limit', array(0, $this->truncateAfter));
$table->addSummaryRow($newRow);
unset($rows);
}
}

View File

@@ -0,0 +1,159 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable;
use Exception;
use Piwik\Common;
use Piwik\DataTable;
/**
* The DataTable_Manager registers all the instanciated DataTable and provides an
* easy way to access them. This is used to store all the DataTable during the archiving process.
* At the end of archiving, the ArchiveProcessor will read the stored datatable and record them in the DB.
*/
class Manager extends \ArrayObject
{
/**
* Id of the next inserted table id in the Manager
* @var int
*/
protected $nextTableId = 0;
private static $instance;
public static function getInstance()
{
if (!isset(self::$instance)) {
self::$instance = new Manager();
}
return self::$instance;
}
/**
* Add a DataTable to the registry
*
* @param DataTable $table
* @return int Index of the table in the manager array
*/
public function addTable($table)
{
$this->nextTableId++;
$this[$this->nextTableId] = $table;
return $this->nextTableId;
}
/**
* Returns the DataTable associated to the ID $idTable.
* NB: The datatable has to have been instanciated before!
* This method will not fetch the DataTable from the DB.
*
* @param int $idTable
* @throws Exception If the table can't be found
* @return DataTable The table
*/
public function getTable($idTable)
{
if (!isset($this[$idTable])) {
throw new TableNotFoundException(sprintf("Error: table id %s not found in memory. (If this error is causing you problems in production, please report it in Matomo issue tracker.)", $idTable));
}
return $this[$idTable];
}
/**
* Returns the latest used table ID
*
* @return int
*/
public function getMostRecentTableId()
{
return $this->nextTableId;
}
/**
* Delete all the registered DataTables from the manager
*/
public function deleteAll($deleteWhenIdTableGreaterThan = 0)
{
foreach ($this as $id => $table) {
if ($id > $deleteWhenIdTableGreaterThan) {
$this->deleteTable($id);
}
}
if ($deleteWhenIdTableGreaterThan == 0) {
$this->exchangeArray(array());
$this->nextTableId = 0;
}
}
/**
* Deletes (unsets) the datatable given its id and removes it from the manager
* Subsequent get for this table will fail
*
* @param int $id
*/
public function deleteTable($id)
{
if (isset($this[$id])) {
Common::destroy($this[$id]);
$this->setTableDeleted($id);
}
}
/**
* Deletes all tables starting from the $firstTableId to the most recent table id except the ones that are
* supposed to be ignored.
*
* @param int[] $idsToBeIgnored
* @param int $firstTableId
*/
public function deleteTablesExceptIgnored($idsToBeIgnored, $firstTableId = 0)
{
$lastTableId = $this->getMostRecentTableId();
for ($index = $firstTableId; $index <= $lastTableId; $index++) {
if (!in_array($index, $idsToBeIgnored)) {
$this->deleteTable($index);
}
}
}
/**
* Remove the table from the manager (table has already been unset)
*
* @param int $id
*/
public function setTableDeleted($id)
{
$this[$id] = null;
}
/**
* Debug only. Dumps all tables currently registered in the Manager
*/
public function dumpAllTables()
{
echo "<hr />Manager->dumpAllTables()<br />";
foreach ($this as $id => $table) {
if (!($table instanceof DataTable)) {
echo "Error table $id is not instance of datatable<br />";
var_export($table);
} else {
echo "<hr />";
echo "Table (index=$id) TableId = " . $table->getId() . "<br />";
echo $table;
echo "<br />";
}
}
echo "<br />-- End Manager->dumpAllTables()<hr />";
}
}

View File

@@ -0,0 +1,529 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable;
use Closure;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\DataTable\Renderer\Console;
/**
* Stores an array of {@link DataTable}s indexed by one type of {@link DataTable} metadata (such as site ID
* or period).
*
* DataTable Maps are returned on all queries that involve multiple sites and/or multiple
* periods. The Maps will contain a {@link DataTable} for each site and period combination.
*
* The Map implements some {@link DataTable} such as {@link queueFilter()} and {@link getRowsCount}.
*
*
* @api
*/
class Map implements DataTableInterface
{
/**
* Array containing the DataTable within this Set
*
* @var DataTable[]
*/
protected $array = array();
/**
* @see self::getKeyName()
* @var string
*/
protected $keyName = 'defaultKeyName';
/**
* Returns a string description of the data used to index the DataTables.
*
* This label is used by DataTable Renderers (it becomes a column name or the XML description tag).
*
* @return string eg, `'idSite'`, `'period'`
*/
public function getKeyName()
{
return $this->keyName;
}
/**
* Set the name of they metadata used to index {@link DataTable}s. See {@link getKeyName()}.
*
* @param string $name
*/
public function setKeyName($name)
{
$this->keyName = $name;
}
/**
* Returns the number of {@link DataTable}s in this DataTable\Map.
*
* @return int
*/
public function getRowsCount()
{
return count($this->getDataTables());
}
/**
* Queue a filter to {@link DataTable} child of contained by this instance.
*
* See {@link Piwik\DataTable::queueFilter()} for more information..
*
* @param string|Closure $className Filter name, eg. `'Limit'` or a Closure.
* @param array $parameters Filter parameters, eg. `array(50, 10)`.
*/
public function queueFilter($className, $parameters = array())
{
foreach ($this->getDataTables() as $table) {
$table->queueFilter($className, $parameters);
}
}
/**
* Apply the filters previously queued to each DataTable contained by this DataTable\Map.
*/
public function applyQueuedFilters()
{
foreach ($this->getDataTables() as $table) {
$table->applyQueuedFilters();
}
}
/**
* Apply a filter to all tables contained by this instance.
*
* @param string|Closure $className Name of filter class or a Closure.
* @param array $parameters Parameters to pass to the filter.
*/
public function filter($className, $parameters = array())
{
foreach ($this->getDataTables() as $table) {
$table->filter($className, $parameters);
}
}
/**
* Apply a filter to all subtables contained by this instance.
*
* @param string|Closure $className Name of filter class or a Closure.
* @param array $parameters Parameters to pass to the filter.
*/
public function filterSubtables($className, $parameters = array())
{
foreach ($this->getDataTables() as $table) {
$table->filterSubtables($className, $parameters);
}
}
/**
* Apply a queued filter to all subtables contained by this instance.
*
* @param string|Closure $className Name of filter class or a Closure.
* @param array $parameters Parameters to pass to the filter.
*/
public function queueFilterSubtables($className, $parameters = array())
{
foreach ($this->getDataTables() as $table) {
$table->queueFilterSubtables($className, $parameters);
}
}
/**
* Returns the array of DataTables contained by this class.
*
* @return DataTable[]|Map[]
*/
public function getDataTables()
{
return $this->array;
}
/**
* Returns the table with the specific label.
*
* @param string $label
* @return DataTable|Map
*/
public function getTable($label)
{
return $this->array[$label];
}
/**
* @param string $label
* @return bool
*/
public function hasTable($label)
{
return isset($this->array[$label]);
}
/**
* Returns the first element in the Map's array.
*
* @return DataTable|Map|false
*/
public function getFirstRow()
{
return reset($this->array);
}
/**
* Returns the last element in the Map's array.
*
* @return DataTable|Map|false
*/
public function getLastRow()
{
return end($this->array);
}
/**
* Adds a new {@link DataTable} or Map instance to this DataTable\Map.
*
* @param DataTable|Map $table
* @param string $label Label used to index this table in the array.
*/
public function addTable($table, $label)
{
$this->array[$label] = $table;
}
public function getRowFromIdSubDataTable($idSubtable)
{
$dataTables = $this->getDataTables();
// find first datatable containing data
foreach ($dataTables as $subTable) {
$subTableRow = $subTable->getRowFromIdSubDataTable($idSubtable);
if (!empty($subTableRow)) {
return $subTableRow;
}
}
return null;
}
/**
* Returns a string output of this DataTable\Map (applying the default renderer to every {@link DataTable}
* of this DataTable\Map).
*
* @return string
*/
public function __toString()
{
$renderer = new Console();
$renderer->setTable($this);
return (string)$renderer;
}
/**
* See {@link DataTable::enableRecursiveSort()}.
*/
public function enableRecursiveSort()
{
foreach ($this->getDataTables() as $table) {
$table->enableRecursiveSort();
}
}
/**
* See {@link DataTable::disableFilter()}.
*/
public function disableFilter($className)
{
foreach ($this->getDataTables() as $table) {
$table->disableFilter($className);
}
}
/**
* @ignore
*/
public function disableRecursiveFilters()
{
foreach ($this->getDataTables() as $table) {
$table->disableRecursiveFilters();
}
}
/**
* @ignore
*/
public function enableRecursiveFilters()
{
foreach ($this->getDataTables() as $table) {
$table->enableRecursiveFilters();
}
}
/**
* Renames the given column in each contained {@link DataTable}.
*
* See {@link DataTable::renameColumn()}.
*
* @param string $oldName
* @param string $newName
*/
public function renameColumn($oldName, $newName)
{
foreach ($this->getDataTables() as $table) {
$table->renameColumn($oldName, $newName);
}
}
/**
* Deletes the specified columns in each contained {@link DataTable}.
*
* See {@link DataTable::deleteColumns()}.
*
* @param array $columns The columns to delete.
* @param bool $deleteRecursiveInSubtables This param is currently not used.
*/
public function deleteColumns($columns, $deleteRecursiveInSubtables = false)
{
foreach ($this->getDataTables() as $table) {
$table->deleteColumns($columns);
}
}
/**
* Deletes a table from the array of DataTables.
*
* @param string $id The label associated with {@link DataTable}.
*/
public function deleteRow($id)
{
unset($this->array[$id]);
}
/**
* Deletes the given column in every contained {@link DataTable}.
*
* @see DataTable::deleteColumn
* @param string $name
*/
public function deleteColumn($name)
{
foreach ($this->getDataTables() as $table) {
$table->deleteColumn($name);
}
}
/**
* Returns the array containing all column values in all contained {@link DataTable}s for the requested column.
*
* @param string $name The column name.
* @return array
*/
public function getColumn($name)
{
$values = array();
foreach ($this->getDataTables() as $table) {
$moreValues = $table->getColumn($name);
foreach ($moreValues as &$value) {
$values[] = $value;
}
}
return $values;
}
/**
* Merges the rows of every child {@link DataTable} into a new one and
* returns it. This function will also set the label of the merged rows
* to the label of the {@link DataTable} they were originally from.
*
* The result of this function is determined by the type of DataTable
* this instance holds. If this DataTable\Map instance holds an array
* of DataTables, this function will transform it from:
*
* Label 0:
* DataTable(row1)
* Label 1:
* DataTable(row2)
*
* to:
*
* DataTable(row1[label = 'Label 0'], row2[label = 'Label 1'])
*
* If this instance holds an array of DataTable\Maps, this function will
* transform it from:
*
* Outer Label 0: // the outer DataTable\Map
* Inner Label 0: // one of the inner DataTable\Maps
* DataTable(row1)
* Inner Label 1:
* DataTable(row2)
* Outer Label 1:
* Inner Label 0:
* DataTable(row3)
* Inner Label 1:
* DataTable(row4)
*
* to:
*
* Inner Label 0:
* DataTable(row1[label = 'Outer Label 0'], row3[label = 'Outer Label 1'])
* Inner Label 1:
* DataTable(row2[label = 'Outer Label 0'], row4[label = 'Outer Label 1'])
*
* If this instance holds an array of DataTable\Maps, the
* metadata of the first child is used as the metadata of the result.
*
* This function can be used, for example, to smoosh IndexedBySite archive
* query results into one DataTable w/ different rows differentiated by site ID.
*
* Note: This DataTable/Map will be destroyed and will be no longer usable after the tables have been merged into
* the new dataTable to reduce memory usage. Destroying all DataTables witihn the Map also seems to fix a
* Segmentation Fault that occurred in the AllWebsitesDashboard when having > 16k sites.
*
* @return DataTable|Map
*/
public function mergeChildren()
{
$firstChild = reset($this->array);
if ($firstChild instanceof Map) {
$result = $firstChild->getEmptyClone();
/** @var $subDataTableMap Map */
foreach ($this->getDataTables() as $label => $subDataTableMap) {
foreach ($subDataTableMap->getDataTables() as $innerLabel => $subTable) {
if (!isset($result->array[$innerLabel])) {
$dataTable = new DataTable();
$dataTable->setMetadataValues($subTable->getAllTableMetadata());
$result->addTable($dataTable, $innerLabel);
}
$this->copyRowsAndSetLabel($result->array[$innerLabel], $subTable, $label);
}
}
} else {
$result = new DataTable();
foreach ($this->getDataTables() as $label => $subTable) {
$this->copyRowsAndSetLabel($result, $subTable, $label);
Common::destroy($subTable);
}
$this->array = array();
}
return $result;
}
/**
* Utility function used by mergeChildren. Copies the rows from one table,
* sets their 'label' columns to a value and adds them to another table.
*
* @param DataTable $toTable The table to copy rows to.
* @param DataTable $fromTable The table to copy rows from.
* @param string $label The value to set the 'label' column of every copied row.
*/
private function copyRowsAndSetLabel($toTable, $fromTable, $label)
{
foreach ($fromTable->getRows() as $fromRow) {
$oldColumns = $fromRow->getColumns();
unset($oldColumns['label']);
$columns = array_merge(array('label' => $label), $oldColumns);
$row = new Row(array(
Row::COLUMNS => $columns,
Row::METADATA => $fromRow->getMetadata(),
Row::DATATABLE_ASSOCIATED => $fromRow->getIdSubDataTable()
));
$toTable->addRow($row);
}
}
/**
* Sums a DataTable to all the tables in this array.
*
* _Note: Will only add `$tableToSum` if the childTable has some rows._
*
* See {@link Piwik\DataTable::addDataTable()}.
*
* @param DataTable $tableToSum
*/
public function addDataTable(DataTable $tableToSum)
{
foreach ($this->getDataTables() as $childTable) {
$childTable->addDataTable($tableToSum);
}
}
/**
* Returns a new DataTable\Map w/ child tables that have had their
* subtables merged.
*
* See {@link DataTable::mergeSubtables()}.
*
* @return Map
*/
public function mergeSubtables()
{
$result = $this->getEmptyClone();
foreach ($this->getDataTables() as $label => $childTable) {
$result->addTable($childTable->mergeSubtables(), $label);
}
return $result;
}
/**
* Returns a new DataTable\Map w/o any child DataTables, but with
* the same key name as this instance.
*
* @return Map
*/
public function getEmptyClone()
{
$dataTableMap = new Map;
$dataTableMap->setKeyName($this->getKeyName());
return $dataTableMap;
}
/**
* Returns the intersection of children's metadata arrays (what they all have in common).
*
* @param string $name The metadata name.
* @return mixed
*/
public function getMetadataIntersectArray($name)
{
$data = array();
foreach ($this->getDataTables() as $childTable) {
$childData = $childTable->getMetadata($name);
if (is_array($childData)) {
$data = array_intersect($data, $childData);
}
}
return array_values($data);
}
/**
* See {@link DataTable::getColumns()}.
*
* @return array
*/
public function getColumns()
{
foreach ($this->getDataTables() as $childTable) {
if ($childTable->getRowsCount() > 0) {
return $childTable->getColumns();
}
}
return array();
}
}

View File

@@ -0,0 +1,383 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable;
use Exception;
use Piwik\Columns\Dimension;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Metrics;
use Piwik\Piwik;
use Piwik\BaseFactory;
/**
* A DataTable Renderer can produce an output given a DataTable object.
* All new Renderers must be copied in DataTable/Renderer and added to the factory() method.
* To use a renderer, simply do:
* $render = new Xml();
* $render->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 DataTable|Simple|DataTable\Map $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',
'php'
);
/**
* 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("&nbsp;", "&iexcl;", "&cent;", "&pound;", "&curren;", "&yen;", "&brvbar;", "&sect;", "&uml;", "&copy;", "&ordf;", "&laquo;", "&not;", "&shy;", "&reg;", "&macr;", "&deg;", "&plusmn;", "&sup2;", "&sup3;", "&acute;", "&micro;", "&para;", "&middot;", "&cedil;", "&sup1;", "&ordm;", "&raquo;", "&frac14;", "&frac12;", "&frac34;", "&iquest;", "&Agrave;", "&Aacute;", "&Acirc;", "&Atilde;", "&Auml;", "&Aring;", "&AElig;", "&Ccedil;", "&Egrave;", "&Eacute;", "&Ecirc;", "&Euml;", "&Igrave;", "&Iacute;", "&Icirc;", "&Iuml;", "&ETH;", "&Ntilde;", "&Ograve;", "&Oacute;", "&Ocirc;", "&Otilde;", "&Ouml;", "&times;", "&Oslash;", "&Ugrave;", "&Uacute;", "&Ucirc;", "&Uuml;", "&Yacute;", "&THORN;", "&szlig;", "&agrave;", "&aacute;", "&acirc;", "&atilde;", "&auml;", "&aring;", "&aelig;", "&ccedil;", "&egrave;", "&eacute;", "&ecirc;", "&euml;", "&igrave;", "&iacute;", "&icirc;", "&iuml;", "&eth;", "&ntilde;", "&ograve;", "&oacute;", "&ocirc;", "&otilde;", "&ouml;", "&divide;", "&oslash;", "&ugrave;", "&uacute;", "&ucirc;", "&uuml;", "&yacute;", "&thorn;", "&yuml;", "&euro;");
$xmlentities = array("&#162;", "&#163;", "&#164;", "&#165;", "&#166;", "&#167;", "&#168;", "&#169;", "&#170;", "&#171;", "&#172;", "&#173;", "&#174;", "&#175;", "&#176;", "&#177;", "&#178;", "&#179;", "&#180;", "&#181;", "&#182;", "&#183;", "&#184;", "&#185;", "&#186;", "&#187;", "&#188;", "&#189;", "&#190;", "&#191;", "&#192;", "&#193;", "&#194;", "&#195;", "&#196;", "&#197;", "&#198;", "&#199;", "&#200;", "&#201;", "&#202;", "&#203;", "&#204;", "&#205;", "&#206;", "&#207;", "&#208;", "&#209;", "&#210;", "&#211;", "&#212;", "&#213;", "&#214;", "&#215;", "&#216;", "&#217;", "&#218;", "&#219;", "&#220;", "&#221;", "&#222;", "&#223;", "&#224;", "&#225;", "&#226;", "&#227;", "&#228;", "&#229;", "&#230;", "&#231;", "&#232;", "&#233;", "&#234;", "&#235;", "&#236;", "&#237;", "&#238;", "&#239;", "&#240;", "&#241;", "&#242;", "&#243;", "&#244;", "&#245;", "&#246;", "&#247;", "&#248;", "&#249;", "&#250;", "&#251;", "&#252;", "&#253;", "&#254;", "&#255;", "&#8364;");
$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 (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 <row> 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;
}
}

View File

@@ -0,0 +1,159 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Renderer;
use Piwik\DataTable;
use Piwik\DataTable\Renderer;
/**
* Simple output
*/
class Console extends Renderer
{
/**
* Prefix
*
* @var string
*/
protected $prefixRows = '#';
/**
* Computes the dataTable output and returns the string/binary
*
* @return string
*/
public function render()
{
return $this->renderTable($this->table);
}
/**
* Sets the prefix to be used
*
* @param string $str new prefix
*/
public function setPrefixRow($str)
{
$this->prefixRows = $str;
}
/**
* Computes the output of the given array of data tables
*
* @param DataTable\Map $map data tables to render
* @param string $prefix prefix to output before table data
* @return string
*/
protected function renderDataTableMap(DataTable\Map $map, $prefix)
{
$output = "Set<hr />";
$prefix = $prefix . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
foreach ($map->getDataTables() as $descTable => $table) {
$output .= $prefix . "<b>" . $descTable . "</b><br />";
$output .= $prefix . $this->renderTable($table, $prefix . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;');
$output .= "<hr />";
}
return $output;
}
/**
* Computes the given dataTable output and returns the string/binary
*
* @param DataTable $table data table to render
* @param string $prefix prefix to output before table data
* @return string
*/
protected function renderTable($table, $prefix = "")
{
if (is_array($table)) {
// convert array to DataTable
$table = DataTable::makeFromSimpleArray($table);
}
if ($table instanceof DataTable\Map) {
return $this->renderDataTableMap($table, $prefix);
}
if ($table->getRowsCount() == 0) {
return "Empty table<br />\n";
}
static $depth = 0;
$output = '';
$i = 1;
foreach ($table->getRows() as $row) {
$dataTableMapBreak = false;
$columns = array();
foreach ($row->getColumns() as $column => $value) {
if ($value instanceof DataTable\Map) {
$output .= $this->renderDataTableMap($value, $prefix);
$dataTableMapBreak = true;
break;
}
if (is_string($value)) {
$value = "'$value'";
} elseif (is_array($value)) {
$value = var_export($value, true);
}
$columns[] = "'$column' => $value";
}
if ($dataTableMapBreak === true) {
continue;
}
$columns = implode(", ", $columns);
$metadata = array();
foreach ($row->getMetadata() as $name => $value) {
if (is_string($value)) {
$value = "'$value'";
} elseif (is_array($value)) {
$value = var_export($value, true);
}
$metadata[] = "'$name' => $value";
}
$metadata = implode(", ", $metadata);
$output .= str_repeat($this->prefixRows, $depth)
. "- $i [" . $columns . "] [" . $metadata . "] [idsubtable = "
. $row->getIdSubDataTable() . "]<br />\n";
if (!is_null($row->getIdSubDataTable())) {
$subTable = $row->getSubtable();
if ($subTable) {
$depth++;
$output .= $this->renderTable($subTable, $prefix . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;');
$depth--;
} else {
$output .= "-- Sub DataTable not loaded<br />\n";
}
}
$i++;
}
$metadata = $table->getAllTableMetadata();
if (!empty($metadata)) {
$output .= "<hr />Metadata<br />";
foreach ($metadata as $id => $metadataIn) {
$output .= "<br />";
$output .= $prefix . " <b>$id</b><br />";
if (is_array($metadataIn)) {
foreach ($metadataIn as $name => $value) {
if (is_object($value) && !method_exists( $value, '__toString' )) {
$value = 'Object [' . get_class($value) . ']';
}
$output .= $prefix . $prefix . "$name => $value";
}
}
}
}
return $output;
}
}

View File

@@ -0,0 +1,495 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Renderer;
use Piwik\Common;
use Piwik\DataTable\Renderer;
use Piwik\DataTable\Simple;
use Piwik\DataTable;
use Piwik\Period;
use Piwik\Period\Range;
use Piwik\Piwik;
use Piwik\ProxyHttp;
/**
* CSV export
*
* When rendered using the default settings, a CSV report has the following characteristics:
* The first record contains headers for all the columns in the report.
* All rows have the same number of columns.
* The default field delimiter string is a comma (,).
* Formatting and layout are ignored.
*
*/
class Csv extends Renderer
{
/**
* Column separator
*
* @var string
*/
public $separator = ",";
/**
* Line end
*
* @var string
*/
public $lineEnd = "\n";
/**
* 'metadata' columns will be exported, prefixed by 'metadata_'
*
* @var bool
*/
public $exportMetadata = true;
/**
* Converts the content to unicode so that UTF8 characters (eg. chinese) can be imported in Excel
*
* @var bool
*/
public $convertToUnicode = true;
/**
* idSubtable will be exported in a column called 'idsubdatatable'
*
* @var bool
*/
public $exportIdSubtable = true;
/**
* This string is also hardcoded in archive,sh
*/
const NO_DATA_AVAILABLE = 'No data available';
private $unsupportedColumns = array();
/**
* Computes the dataTable output and returns the string/binary
*
* @return string
*/
public function render()
{
$str = $this->renderTable($this->table);
if (empty($str)) {
return self::NO_DATA_AVAILABLE;
}
$this->renderHeader();
$str = $this->convertToUnicode($str);
return $str;
}
/**
* Enables / Disables unicode converting
*
* @param $bool
*/
public function setConvertToUnicode($bool)
{
$this->convertToUnicode = $bool;
}
/**
* Sets the column separator
*
* @param $separator
*/
public function setSeparator($separator)
{
$this->separator = $separator;
}
/**
* Computes the output of the given data table
*
* @param DataTable|array $table
* @param array $allColumns
* @return string
*/
protected function renderTable($table, &$allColumns = array())
{
if (is_array($table)) {
// convert array to DataTable
$table = DataTable::makeFromSimpleArray($table);
}
if ($table instanceof DataTable\Map) {
$str = $this->renderDataTableMap($table, $allColumns);
} else {
$str = $this->renderDataTable($table, $allColumns);
}
return $str;
}
/**
* Computes the output of the given data table array
*
* @param DataTable\Map $table
* @param array $allColumns
* @return string
*/
protected function renderDataTableMap($table, &$allColumns = array())
{
$str = '';
foreach ($table->getDataTables() as $currentLinePrefix => $dataTable) {
$returned = explode("\n", $this->renderTable($dataTable, $allColumns));
// get rid of the columns names
$returned = array_slice($returned, 1);
// case empty datatable we don't print anything in the CSV export
// when in xml we would output <result date="2008-01-15" />
if (!empty($returned)) {
foreach ($returned as &$row) {
$row = $currentLinePrefix . $this->separator . $row;
}
$str .= "\n" . implode("\n", $returned);
}
}
// prepend table key to column list
$allColumns = array_merge(array($table->getKeyName() => true), $allColumns);
// add header to output string
$str = $this->getHeaderLine(array_keys($allColumns)) . $str;
return $str;
}
/**
* Converts the output of the given simple data table
*
* @param DataTable|Simple $table
* @param array $allColumns
* @return string
*/
protected function renderDataTable($table, &$allColumns = array())
{
if ($table instanceof Simple) {
$row = $table->getFirstRow();
if ($row !== false) {
$columnNameToValue = $row->getColumns();
if (count($columnNameToValue) == 1) {
// simple tables should only have one column, the value
$allColumns['value'] = true;
$value = array_values($columnNameToValue);
$str = 'value' . $this->lineEnd . $this->formatValue($value[0]);
return $str;
}
}
}
$csv = $this->makeArrayFromDataTable($table, $allColumns);
// now we make sure that all the rows in the CSV array have all the columns
foreach ($csv as &$row) {
foreach ($allColumns as $columnName => $true) {
if (!isset($row[$columnName])) {
$row[$columnName] = '';
}
}
}
$str = $this->buildCsvString($allColumns, $csv);
return $str;
}
/**
* Returns the CSV header line for a set of metrics. Will translate columns if desired.
*
* @param array $columnMetrics
* @return array
*/
private function getHeaderLine($columnMetrics)
{
foreach ($columnMetrics as $index => $value) {
if (in_array($value, $this->unsupportedColumns)) {
unset($columnMetrics[$index]);
}
}
if ($this->translateColumnNames) {
$columnMetrics = $this->translateColumnNames($columnMetrics);
}
foreach ($columnMetrics as &$value) {
$value = $this->formatValue($value);
}
return implode($this->separator, $columnMetrics);
}
/**
* Formats/Escapes the given value
*
* @param mixed $value
* @return string
*/
protected function formatValue($value)
{
if (is_string($value)
&& !is_numeric($value)
) {
$value = html_entity_decode($value, ENT_COMPAT, 'UTF-8');
} elseif ($value === false) {
$value = 0;
}
$value = $this->formatFormulas($value);
if (is_string($value)
&& (strpos($value, '"') !== false
|| strpos($value, $this->separator) !== false)
) {
$value = '"' . str_replace('"', '""', $value) . '"';
}
// in some number formats (e.g. German), the decimal separator is a comma
// we need to catch and replace this
if (is_numeric($value)) {
$value = (string)$value;
$value = str_replace(',', '.', $value);
}
return $value;
}
protected function formatFormulas($value)
{
// Excel / Libreoffice formulas may start with one of these characters
$formulaStartsWith = array('=', '+', '-', '@');
// remove first % sign and if string is still a number, return it as is
$valueWithoutFirstPercentSign = $this->removeFirstPercentSign($value);
if (empty($valueWithoutFirstPercentSign)
|| !is_string($value)
|| is_numeric($valueWithoutFirstPercentSign)) {
return $value;
}
$firstCharCellValue = $valueWithoutFirstPercentSign[0];
$isFormula = in_array($firstCharCellValue, $formulaStartsWith);
if($isFormula) {
return "'" . $value;
}
return $value;
}
/**
* Sends the http headers for csv file
*/
protected function renderHeader()
{
$fileName = Piwik::translate('General_Export');
$period = Common::getRequestVar('period', false);
$date = Common::getRequestVar('date', false);
if ($period || $date) {
// in test cases, there are no request params set
if ($period == 'range') {
$period = new Range($period, $date);
} elseif (strpos($date, ',') !== false) {
$period = new Range('range', $date);
} else {
$period = Period\Factory::build($period, $date);
}
$prettyDate = $period->getLocalizedLongString();
$meta = $this->getApiMetaData();
$fileName .= ' _ ' . $meta['name']
. ' _ ' . $prettyDate . '.csv';
}
// silent fail otherwise unit tests fail
Common::sendHeader('Content-Disposition: attachment; filename="' . $fileName . '"', true);
ProxyHttp::overrideCacheControlHeaders();
}
/**
* Flattens an array of column values so they can be outputted as CSV (which does not support
* nested structures).
*/
private function flattenColumnArray($columns, &$csvRow = array(), $csvColumnNameTemplate = '%s')
{
foreach ($columns as $name => $value) {
$csvName = sprintf($csvColumnNameTemplate, $this->getCsvColumnName($name));
if (is_array($value)) {
// if we're translating column names and this is an array of arrays, the column name
// format becomes a bit more complicated. also in this case, we assume $value is not
// nested beyond 2 levels (ie, array(0 => array(0 => 1, 1 => 2)), but not array(
// 0 => array(0 => array(), 1 => array())) )
if ($this->translateColumnNames
&& is_array(reset($value))
) {
foreach ($value as $level1Key => $level1Value) {
$inner = $name == 'goals' ? Piwik::translate('Goals_GoalX', $level1Key) : $name . ' ' . $level1Key;
$columnNameTemplate = '%s (' . $inner . ')';
$this->flattenColumnArray($level1Value, $csvRow, $columnNameTemplate);
}
} else {
$this->flattenColumnArray($value, $csvRow, $csvName . '_%s');
}
} else {
$csvRow[$csvName] = $value;
}
}
return $csvRow;
}
private function getCsvColumnName($name)
{
if ($this->translateColumnNames) {
return $this->translateColumnName($name);
} else {
return $name;
}
}
/**
* @param $allColumns
* @param $csv
* @return array
*/
private function buildCsvString($allColumns, $csv)
{
$str = '';
// specific case, we have only one column and this column wasn't named properly (indexed by a number)
// we don't print anything in the CSV file => an empty line
if (sizeof($allColumns) == 1
&& reset($allColumns)
&& !is_string(key($allColumns))
) {
$str .= '';
} else {
// render row names
$str .= $this->getHeaderLine(array_keys($allColumns)) . $this->lineEnd;
}
// we render the CSV
foreach ($csv as $theRow) {
$rowStr = '';
foreach ($allColumns as $columnName => $true) {
$rowStr .= $this->formatValue($theRow[$columnName]) . $this->separator;
}
// remove the last separator
$rowStr = substr_replace($rowStr, "", -strlen($this->separator));
$str .= $rowStr . $this->lineEnd;
}
$str = substr($str, 0, -strlen($this->lineEnd));
return $str;
}
/**
* @param $table
* @param $allColumns
* @return array of csv data
*/
private function makeArrayFromDataTable($table, &$allColumns)
{
$csv = array();
foreach ($table->getRows() as $row) {
$csvRow = $this->flattenColumnArray($row->getColumns());
if ($this->exportMetadata) {
$metadata = $row->getMetadata();
foreach ($metadata as $name => $value) {
if ($name == 'idsubdatatable_in_db') {
continue;
}
//if a metadata and a column have the same name make sure they don't overwrite
if ($this->translateColumnNames) {
$name = Piwik::translate('General_Metadata') . ': ' . $name;
} else {
$name = 'metadata_' . $name;
}
if (is_array($value)) {
if (!in_array($name, $this->unsupportedColumns)) {
$this->unsupportedColumns[] = $name;
}
} else {
$csvRow[$name] = $value;
}
}
}
foreach ($csvRow as $name => $value) {
if (in_array($name, $this->unsupportedColumns)) {
unset($allColumns[$name]);
} else {
$allColumns[$name] = true;
}
}
if ($this->exportIdSubtable) {
$idsubdatatable = $row->getIdSubDataTable();
if ($idsubdatatable !== false
&& $this->hideIdSubDatatable === false
) {
$csvRow['idsubdatatable'] = $idsubdatatable;
}
}
$csv[] = $csvRow;
}
if (!empty($this->unsupportedColumns)) {
foreach ($this->unsupportedColumns as $unsupportedColumn) {
foreach ($csv as $index => $row) {
unset($row[$index][$unsupportedColumn]);
}
}
}
return $csv;
}
/**
* @param $str
* @return string
*/
private function convertToUnicode($str)
{
if ($this->convertToUnicode
&& function_exists('mb_convert_encoding')
) {
$str = chr(255) . chr(254) . mb_convert_encoding($str, 'UTF-16LE', 'UTF-8');
}
return $str;
}
/**
* @param $value
* @return mixed
*/
protected function removeFirstPercentSign($value)
{
$needle = '%';
$posPercent = strpos($value, $needle);
if ($posPercent !== false) {
return substr_replace($value, '', $posPercent, strlen($needle));
}
return $value;
}
}

View File

@@ -0,0 +1,192 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Renderer;
use Exception;
use Piwik\DataTable;
use Piwik\DataTable\Renderer;
/**
* Simple HTML output
* Does not work with recursive DataTable (i.e., when a row can be associated with a subDataTable).
*
*/
class Html extends Renderer
{
protected $tableId;
protected $allColumns;
protected $tableStructure;
protected $i;
/**
* Sets the table id
*
* @param string $id
*/
public function setTableId($id)
{
$this->tableId = str_replace('.', '_', $id);
}
/**
* Computes the dataTable output and returns the string/binary
*
* @return string
*/
public function render()
{
$this->tableStructure = array();
$this->allColumns = array();
$this->i = 0;
return $this->renderTable($this->table);
}
/**
* Computes the output for the given data table
*
* @param DataTable $table
* @return string
*/
protected function renderTable($table)
{
if (is_array($table)) {
// convert array to DataTable
$table = DataTable::makeFromSimpleArray($table);
}
if ($table instanceof DataTable\Map) {
foreach ($table->getDataTables() as $date => $subtable) {
if ($subtable->getRowsCount()) {
$this->buildTableStructure($subtable, '_' . $table->getKeyName(), $date);
}
}
} else {
// Simple
if ($table->getRowsCount()) {
$this->buildTableStructure($table);
}
}
$out = $this->renderDataTable();
return $out;
}
/**
* Adds the given data table to the table structure array
*
* @param DataTable $table
* @param null|string $columnToAdd
* @param null|string $valueToAdd
* @throws Exception
*/
protected function buildTableStructure($table, $columnToAdd = null, $valueToAdd = null)
{
$i = $this->i;
$someMetadata = false;
$someIdSubTable = false;
/*
* table = array
* ROW1 = col1 | col2 | col3 | metadata | idSubTable
* ROW2 = col1 | col2 (no value but appears) | col3 | metadata | idSubTable
*/
if (!($table instanceof DataTable)) {
throw new Exception("HTML Renderer does not work with this combination of parameters");
}
foreach ($table->getRows() as $row) {
if (isset($columnToAdd) && isset($valueToAdd)) {
$this->allColumns[$columnToAdd] = true;
$this->tableStructure[$i][$columnToAdd] = $valueToAdd;
}
foreach ($row->getColumns() as $column => $value) {
$this->allColumns[$column] = true;
$this->tableStructure[$i][$column] = $value;
}
$metadata = array();
foreach ($row->getMetadata() as $name => $value) {
if (is_string($value)) {
$value = "'$value'";
} else if (is_array($value)) {
$value = var_export($value, true);
}
$metadata[] = "'$name' => $value";
}
if (count($metadata) != 0) {
$someMetadata = true;
$metadata = implode("<br />", $metadata);
$this->tableStructure[$i]['_metadata'] = $metadata;
}
$idSubtable = $row->getIdSubDataTable();
if (!is_null($idSubtable)) {
$someIdSubTable = true;
$this->tableStructure[$i]['_idSubtable'] = $idSubtable;
}
$i++;
}
$this->i = $i;
$this->allColumns['_metadata'] = $someMetadata;
$this->allColumns['_idSubtable'] = $someIdSubTable;
}
/**
* Computes the output for the table structure array
*
* @return string
*/
protected function renderDataTable()
{
$html = "<table " . ($this->tableId ? "id=\"{$this->tableId}\" " : "") . "border=\"1\">\n<thead>\n\t<tr>\n";
foreach ($this->allColumns as $name => $toDisplay) {
if ($toDisplay !== false) {
if ($name === 0) {
$name = 'value';
}
if ($this->translateColumnNames) {
$name = $this->translateColumnName($name);
}
$html .= "\t\t<th>$name</th>\n";
}
}
$html .= "\t</tr>\n</thead>\n<tbody>\n";
foreach ($this->tableStructure as $row) {
$html .= "\t<tr>\n";
foreach ($this->allColumns as $name => $toDisplay) {
if ($toDisplay !== false) {
$value = "-";
if (isset($row[$name])) {
if (is_array($row[$name])) {
$value = "<pre>" . self::formatValueXml(var_export($row[$name], true)) . "</pre>";
} else {
$value = self::formatValueXml($row[$name]);
}
}
$html .= "\t\t<td>$value</td>\n";
}
}
$html .= "\t</tr>\n";
}
$html .= "</tbody>\n</table>\n";
return $html;
}
}

View File

@@ -0,0 +1,120 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Renderer;
use Piwik\Common;
use Piwik\DataTable\Renderer;
use Piwik\DataTable;
/**
* JSON export.
* Works with recursive DataTable (when a row can be associated with a subDataTable).
*
*/
class Json extends Renderer
{
/**
* Computes the dataTable output and returns the string/binary
*
* @return string
*/
public function render()
{
return $this->renderTable($this->table);
}
/**
* Computes the output for the given data table
*
* @param DataTable $table
* @return string
*/
protected function renderTable($table)
{
if (is_array($table)) {
$array = $table;
if (self::shouldWrapArrayBeforeRendering($array, $wrapSingleValues = true)) {
$array = array($array);
}
foreach ($array as $key => $tab) {
if ($tab instanceof DataTable\Map
|| $tab instanceof DataTable
|| $tab instanceof DataTable\Simple) {
$array[$key] = $this->convertDataTableToArray($tab);
if (!is_array($array[$key])) {
$array[$key] = array('value' => $array[$key]);
}
}
}
} else {
$array = $this->convertDataTableToArray($table);
}
if (!is_array($array)) {
$array = array('value' => $array);
}
// decode all entities
$callback = function (&$value, $key) {
if (is_string($value)) {
$value = html_entity_decode($value, ENT_QUOTES, "UTF-8");
};
};
array_walk_recursive($array, $callback);
// silence "Warning: json_encode(): Invalid UTF-8 sequence in argument"
$str = @json_encode($array);
if ($str === false
&& json_last_error() === JSON_ERROR_UTF8
&& $this->canMakeArrayUtf8()) {
$array = $this->makeArrayUtf8($array);
$str = json_encode($array);
}
return $str;
}
private function canMakeArrayUtf8()
{
return function_exists('mb_convert_encoding');
}
private function makeArrayUtf8($array)
{
if (is_array($array)) {
foreach ($array as $key => $value) {
$array[$key] = self::makeArrayUtf8($value);
}
} elseif (is_string($array)) {
return mb_convert_encoding($array, 'UTF-8', 'auto');
}
return $array;
}
public static function sendHeaderJSON()
{
Common::sendHeader('Content-Type: application/json; charset=utf-8');
}
private function convertDataTableToArray($table)
{
$renderer = new Php();
$renderer->setTable($table);
$renderer->setRenderSubTables($this->isRenderSubtables());
$renderer->setSerialize(false);
$renderer->setHideIdSubDatableFromResponse($this->hideIdSubDatatable);
$array = $renderer->flatRender();
return $array;
}
}

View File

@@ -0,0 +1,250 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Renderer;
use Exception;
use Piwik\Common;
use Piwik\DataTable\Renderer;
use Piwik\DataTable\Simple;
use Piwik\DataTable;
use Piwik\Piwik;
/**
* Returns the equivalent PHP array for a given DataTable.
* You can specify in the constructor if you want the serialized version.
* Please note that by default it will produce a flat version of the array.
* See the method flatRender() for details. @see flatRender();
*
* Works with recursive DataTable (when a row can be associated with a subDataTable).
*
*/
class Php extends Renderer
{
protected $prettyDisplay = false;
protected $serialize = true;
/**
* Enables/Disables serialize
*
* @param bool $bool
*/
public function setSerialize($bool)
{
$this->serialize = (bool)$bool;
}
/**
* Enables/Disables pretty display
*
* @param bool $bool
*/
public function setPrettyDisplay($bool)
{
$this->prettyDisplay = (bool)$bool;
}
/**
* Converts current data table to string
*
* @return string
*/
public function __toString()
{
$data = $this->render();
if (!is_string($data)) {
$data = serialize($data);
}
return $data;
}
/**
* Computes the dataTable output and returns the string/binary
*
* @param null|DataTable|DataTable\Map|Simple $dataTable
* @return string
*/
public function render($dataTable = null)
{
if (is_null($dataTable)) {
$dataTable = $this->table;
}
$toReturn = $this->flatRender($dataTable);
if ($this->prettyDisplay) {
if (!is_array($toReturn)) {
$toReturn = Common::safe_unserialize($toReturn);
}
$toReturn = "<pre>" . var_export($toReturn, true) . "</pre>";
}
return $toReturn;
}
/**
* 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
*/
public function flatRender($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) {
$serializeSave = $this->serialize;
$this->serialize = false;
$flatArray[$keyName] = $this->flatRender($table);
$this->serialize = $serializeSave;
}
} elseif ($dataTable instanceof Simple) {
$flatArray = $this->renderSimpleTable($dataTable);
// if we return only one numeric value then we print out the result in a simple <result> tag
// keep it simple!
if (count($flatArray) == 1) {
$flatArray = current($flatArray);
}
} // A normal DataTable needs to be handled specifically
else {
$array = $this->renderTable($dataTable);
$flatArray = $this->flattenArray($array);
}
if ($this->serialize) {
$flatArray = serialize($flatArray);
}
return $flatArray;
}
/**
*
* @param array $array
* @return array
*/
protected function flattenArray($array)
{
$flatArray = array();
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;
}
/**
* Converts the current data table to an array
*
* @return array
* @throws Exception
*/
public function originalRender()
{
Piwik::checkObjectTypeIs($this->table, array('Simple', 'DataTable'));
if ($this->table instanceof Simple) {
$array = $this->renderSimpleTable($this->table);
} elseif ($this->table instanceof DataTable) {
$array = $this->renderTable($this->table);
}
if ($this->serialize) {
$array = serialize($array);
}
return $array;
}
/**
* Converts the given data table to an array
*
* @param DataTable $table
* @return array
*/
protected function renderTable($table)
{
$array = 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;
}
$subTable = $row->getSubtable();
if ($this->isRenderSubtables()
&& $subTable
) {
$subTable = $this->renderTable($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 renderSimpleTable($table)
{
$array = array();
$row = $table->getFirstRow();
if ($row === false) {
return $array;
}
foreach ($row->getColumns() as $columnName => $columnValue) {
$array[$columnName] = $columnValue;
}
return $array;
}
}

View File

@@ -0,0 +1,188 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Renderer;
use Exception;
use Piwik\Archive;
use Piwik\Common;
use Piwik\DataTable\Renderer;
use Piwik\DataTable;
use Piwik\Date;
use Piwik\SettingsPiwik;
/**
* RSS Feed.
* The RSS renderer can be used only on Set that are arrays of DataTable.
* A RSS feed contains one dataTable per element in the Set.
*
*/
class Rss extends Renderer
{
/**
* Computes the dataTable output and returns the string/binary
*
* @return string
*/
public function render()
{
return $this->renderTable($this->table);
}
/**
* Computes the output for the given data table
*
* @param DataTable $table
* @return string
* @throws Exception
*/
protected function renderTable($table)
{
if (!($table instanceof DataTable\Map)
|| $table->getKeyName() != 'date'
) {
throw new Exception("RSS feeds can be generated for one specific website &idSite=X." .
"\nPlease specify only one idSite or consider using &format=XML instead.");
}
$idSite = Common::getRequestVar('idSite', 1, 'int');
$period = Common::getRequestVar('period');
$piwikUrl = SettingsPiwik::getPiwikUrl()
. "?module=CoreHome&action=index&idSite=" . $idSite . "&period=" . $period;
$out = "";
$moreRecentFirst = array_reverse($table->getDataTables(), true);
foreach ($moreRecentFirst as $date => $subtable) {
/** @var DataTable $subtable */
$timestamp = $subtable->getMetadata(Archive\DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getDateStart()->getTimestamp();
$site = $subtable->getMetadata(Archive\DataTableFactory::TABLE_METADATA_SITE_INDEX);
$pudDate = date('r', $timestamp);
$dateInSiteTimezone = Date::factory($timestamp);
if($site) {
$dateInSiteTimezone = $dateInSiteTimezone->setTimezone($site->getTimezone());
}
$dateInSiteTimezone = $dateInSiteTimezone->toString('Y-m-d');
$thisPiwikUrl = Common::sanitizeInputValue($piwikUrl . "&date=$dateInSiteTimezone");
$siteName = $site ? $site->getName() : '';
$title = $siteName . " on " . $date;
$out .= "\t<item>
<pubDate>$pudDate</pubDate>
<guid>$thisPiwikUrl</guid>
<link>$thisPiwikUrl</link>
<title>$title</title>
<author>https://matomo.org</author>
<description>";
$out .= Common::sanitizeInputValue($this->renderDataTable($subtable));
$out .= "</description>\n\t</item>\n";
}
$header = $this->getRssHeader();
$footer = $this->getRssFooter();
return $header . $out . $footer;
}
/**
* Returns the RSS file footer
*
* @return string
*/
protected function getRssFooter()
{
return "\t</channel>\n</rss>";
}
/**
* Returns the RSS file header
*
* @return string
*/
protected function getRssHeader()
{
$generationDate = date('r');
$header = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<rss version=\"2.0\">
<channel>
<title>matomo statistics - RSS</title>
<link>https://matomo.org</link>
<description>Matomo RSS feed</description>
<pubDate>$generationDate</pubDate>
<generator>matomo</generator>
<language>en</language>
<lastBuildDate>$generationDate</lastBuildDate>";
return $header;
}
/**
* @param DataTable $table
*
* @return string
*/
protected function renderDataTable($table)
{
if ($table->getRowsCount() == 0) {
return "<strong><em>Empty table</em></strong><br />\n";
}
$i = 1;
$tableStructure = array();
/*
* table = array
* ROW1 = col1 | col2 | col3 | metadata | idSubTable
* ROW2 = col1 | col2 (no value but appears) | col3 | metadata | idSubTable
* subtable here
*/
$allColumns = array();
foreach ($table->getRows() as $row) {
foreach ($row->getColumns() as $column => $value) {
// for example, goals data is array: not supported in export RSS
// in the future we shall reuse ViewDataTable for html exports in RSS anyway
if (is_array($value)) {
continue;
}
$allColumns[$column] = true;
$tableStructure[$i][$column] = $value;
}
$i++;
}
$html = "\n";
$html .= "<table border=1 width=70%>";
$html .= "\n<tr>";
foreach ($allColumns as $name => $toDisplay) {
if ($toDisplay !== false) {
if ($this->translateColumnNames) {
$name = $this->translateColumnName($name);
}
$html .= "\n\t<td><strong>$name</strong></td>";
}
}
$html .= "\n</tr>";
foreach ($tableStructure as $row) {
$html .= "\n\n<tr>";
foreach ($allColumns as $columnName => $toDisplay) {
if ($toDisplay !== false) {
$value = "-";
if (isset($row[$columnName])) {
$value = urldecode($row[$columnName]);
}
$html .= "\n\t<td>$value</td>";
}
}
$html .= "</tr>";
}
$html .= "\n\n</table>";
return $html;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Renderer;
/**
* TSV export
*
* Excel doesn't import CSV properly, it expects TAB separated values by default.
* TSV is therefore the 'CSV' that is Excel compatible
*
*/
class Tsv extends Csv
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->setSeparator("\t");
}
/**
* Computes the dataTable output and returns the string/binary
*
* @return string
*/
public function render()
{
return parent::render();
}
}

View File

@@ -0,0 +1,465 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Renderer;
use Exception;
use Piwik\DataTable\Map;
use Piwik\DataTable\Renderer;
use Piwik\DataTable;
use Piwik\DataTable\Simple;
use Piwik\Piwik;
/**
* XML export of a given DataTable.
* See the tests cases for more information about the XML format (/tests/core/DataTable/Renderer.test.php)
* Or have a look at the API calls examples.
*
* Works with recursive DataTable (when a row can be associated with a subDataTable).
*
*/
class Xml extends Renderer
{
/**
* Computes the dataTable output and returns the string/binary
*
* @return string
*/
public function render()
{
return '<?xml version="1.0" encoding="utf-8" ?>' . "\n" . $this->renderTable($this->table);
}
/**
* Converts the given data table to an array
*
* @param DataTable|DataTable/Map $table data table to convert
* @return array
*/
protected function getArrayFromDataTable($table)
{
if (is_array($table)) {
return $table;
}
$renderer = new Php();
$renderer->setRenderSubTables($this->isRenderSubtables());
$renderer->setSerialize(false);
$renderer->setTable($table);
$renderer->setHideIdSubDatableFromResponse($this->hideIdSubDatatable);
return $renderer->flatRender();
}
/**
* Computes the output for the given data table
*
* @param DataTable|DataTable/Map $table
* @param bool $returnOnlyDataTableXml
* @param string $prefixLines
* @return array|string
* @throws Exception
*/
protected function renderTable($table, $returnOnlyDataTableXml = false, $prefixLines = '')
{
$array = $this->getArrayFromDataTable($table);
if ($table instanceof Map) {
$out = $this->renderDataTableMap($table, $array, $prefixLines);
if ($returnOnlyDataTableXml) {
return $out;
}
$out = "<results>\n$out</results>";
return $out;
}
// integer value of ZERO is a value we want to display
if ($array != 0 && empty($array)) {
if ($returnOnlyDataTableXml) {
throw new Exception("Illegal state, what xml shall we return?");
}
$out = "<result />";
return $out;
}
if ($table instanceof Simple) {
if (is_array($array)) {
$out = $this->renderDataTableSimple($array);
} else {
$out = $array;
}
if ($returnOnlyDataTableXml) {
return $out;
}
if (is_array($array)) {
$out = "<result>\n" . $out . "</result>";
} else {
$value = self::formatValueXml($out);
if ($value === '') {
$out = "<result />";
} else {
$out = "<result>" . $value . "</result>";
}
}
return $out;
}
if ($table instanceof DataTable) {
$out = $this->renderDataTable($array);
if ($returnOnlyDataTableXml) {
return $out;
}
$out = "<result>\n$out</result>";
return $out;
}
if (is_array($array)) {
$out = $this->renderArray($array, $prefixLines . "\t");
if ($returnOnlyDataTableXml) {
return $out;
}
return "<result>\n$out</result>";
}
}
/**
* Renders an array as XML.
*
* @param array $array The array to render.
* @param string $prefixLines The string to prefix each line in the output.
* @return string
*/
private function renderArray($array, $prefixLines)
{
$isAssociativeArray = Piwik::isAssociativeArray($array);
// check if array contains arrays, and if not wrap the result in an extra <row> element
// (only check if this is the root renderArray call)
// NOTE: this is for backwards compatibility. before, array's were added to a new DataTable.
// if the array had arrays, they were added as multiple rows, otherwise it was treated as
// one row. removing will change API output.
$wrapInRow = $prefixLines === "\t"
&& self::shouldWrapArrayBeforeRendering($array, $wrapSingleValues = false, $isAssociativeArray);
// render the array
$result = "";
if ($wrapInRow) {
$result .= "$prefixLines<row>\n";
$prefixLines .= "\t";
}
foreach ($array as $key => $value) {
// based on the type of array & the key, determine how this node will look
if ($isAssociativeArray) {
if (strpos($key, '=') !== false) {
list($keyAttributeName, $key) = explode('=', $key, 2);
$prefix = "<row $keyAttributeName=\"$key\">";
$suffix = "</row>";
$emptyNode = "<row $keyAttributeName=\"$key\">";
} elseif (!self::isValidXmlTagName($key)) {
$prefix = "<row key=\"$key\">";
$suffix = "</row>";
$emptyNode = "<row key=\"$key\"/>";
} else {
$prefix = "<$key>";
$suffix = "</$key>";
$emptyNode = "<$key />";
}
} else {
$prefix = "<row>";
$suffix = "</row>";
$emptyNode = "<row/>";
}
// render the array item
if (is_array($value) || $value instanceof \stdClass) {
$result .= $prefixLines . $prefix . "\n";
$result .= $this->renderArray((array) $value, $prefixLines . "\t");
$result .= $prefixLines . $suffix . "\n";
} elseif ($value instanceof DataTable
|| $value instanceof Map
) {
if ($value->getRowsCount() == 0) {
$result .= $prefixLines . $emptyNode . "\n";
} else {
$result .= $prefixLines . $prefix . "\n";
if ($value instanceof Map) {
$result .= $this->renderDataTableMap($value, $this->getArrayFromDataTable($value), $prefixLines);
} elseif ($value instanceof Simple) {
$result .= $this->renderDataTableSimple($this->getArrayFromDataTable($value), $prefixLines);
} else {
$result .= $this->renderDataTable($this->getArrayFromDataTable($value), $prefixLines);
}
$result .= $prefixLines . $suffix . "\n";
}
} else {
$xmlValue = self::formatValueXml($value);
if (strlen($xmlValue) != 0) {
$result .= $prefixLines . $prefix . $xmlValue . $suffix . "\n";
} else {
$result .= $prefixLines . $emptyNode . "\n";
}
}
}
if ($wrapInRow) {
$result .= substr($prefixLines, 0, strlen($prefixLines) - 1) . "</row>\n";
}
return $result;
}
/**
* Computes the output for the given data table array
*
* @param Map $table
* @param array $array
* @param string $prefixLines
* @return string
*/
protected function renderDataTableMap($table, $array, $prefixLines = "")
{
// CASE 1
//array
// 'day1' => string '14' (length=2)
// 'day2' => string '6' (length=1)
$firstTable = current($array);
if (!is_array($firstTable)) {
$xml = '';
$nameDescriptionAttribute = $table->getKeyName();
foreach ($array as $valueAttribute => $value) {
if (empty($value)) {
$xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\" />\n";
} elseif ($value instanceof Map) {
$out = $this->renderTable($value, true);
//TODO somehow this code is not tested, cover this case
$xml .= "\t<result $nameDescriptionAttribute=\"$valueAttribute\">\n$out</result>\n";
} else {
$xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">" . self::formatValueXml($value) . "</result>\n";
}
}
return $xml;
}
$subTables = $table->getDataTables();
$firstTable = current($subTables);
// CASE 2
//array
// 'day1' =>
// array
// 'nb_uniq_visitors' => string '18'
// 'nb_visits' => string '101'
// 'day2' =>
// array
// 'nb_uniq_visitors' => string '28'
// 'nb_visits' => string '11'
if ($firstTable instanceof Simple) {
$xml = '';
$nameDescriptionAttribute = $table->getKeyName();
foreach ($array as $valueAttribute => $dataTableSimple) {
if (count($dataTableSimple) == 0) {
$xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\" />\n";
} else {
if (is_array($dataTableSimple)) {
$dataTableSimple = "\n" . $this->renderDataTableSimple($dataTableSimple, $prefixLines . "\t") . $prefixLines . "\t";
}
$xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">" . $dataTableSimple . "</result>\n";
}
}
return $xml;
}
// CASE 3
//array
// 'day1' =>
// array
// 0 =>
// array
// 'label' => string 'phpmyvisites'
// 'nb_uniq_visitors' => int 11
// 'nb_visits' => int 13
// 1 =>
// array
// 'label' => string 'phpmyvisits'
// 'nb_uniq_visitors' => int 2
// 'nb_visits' => int 2
// 'day2' =>
// array
// 0 =>
// array
// 'label' => string 'piwik'
// 'nb_uniq_visitors' => int 121
// 'nb_visits' => int 130
// 1 =>
// array
// 'label' => string 'piwik bis'
// 'nb_uniq_visitors' => int 20
// 'nb_visits' => int 120
if ($firstTable instanceof DataTable) {
$xml = '';
$nameDescriptionAttribute = $table->getKeyName();
foreach ($array as $keyName => $arrayForSingleDate) {
$dataTableOut = $this->renderDataTable($arrayForSingleDate, $prefixLines . "\t");
if (empty($dataTableOut)) {
$xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$keyName\" />\n";
} else {
$xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$keyName\">\n";
$xml .= $dataTableOut;
$xml .= $prefixLines . "\t</result>\n";
}
}
return $xml;
}
if ($firstTable instanceof Map) {
$xml = '';
$tables = $table->getDataTables();
$nameDescriptionAttribute = $table->getKeyName();
foreach ($tables as $valueAttribute => $tableInArray) {
$out = $this->renderTable($tableInArray, true, $prefixLines . "\t");
$xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">\n" . $out . $prefixLines . "\t</result>\n";
}
return $xml;
}
return '';
}
/**
* Computes the output for the given data array
*
* @param array $array
* @param string $prefixLine
* @return string
*/
protected function renderDataTable($array, $prefixLine = "")
{
$columnsHaveInvalidChars = $this->areTableLabelsInvalidXmlTagNames(reset($array));
$out = '';
foreach ($array as $rowId => $row) {
if (!is_array($row)) {
$value = self::formatValueXml($row);
if (strlen($value) == 0) {
$out .= $prefixLine . "\t\t<$rowId />\n";
} else {
$out .= $prefixLine . "\t\t<$rowId>" . $value . "</$rowId>\n";
}
continue;
}
// Handing case idgoal=7, creating a new array for that one
$rowAttribute = '';
if (strstr($rowId, '=') !== false) {
$rowAttribute = explode('=', $rowId);
$rowAttribute = " " . $rowAttribute[0] . "='" . $rowAttribute[1] . "'";
}
$out .= $prefixLine . "\t<row$rowAttribute>";
if (count($row) === 1
&& key($row) === 0
) {
$value = self::formatValueXml(current($row));
$out .= $prefixLine . $value;
} else {
$out .= "\n";
foreach ($row as $name => $value) {
// handle the recursive dataTable case by XML outputting the recursive table
if (is_array($value)) {
$value = "\n" . $this->renderDataTable($value, $prefixLine . "\t\t");
$value .= $prefixLine . "\t\t";
} else {
$value = self::formatValueXml($value);
}
list($tagStart, $tagEnd) = $this->getTagStartAndEndFor($name, $columnsHaveInvalidChars);
if (strlen($value) == 0) {
$out .= $prefixLine . "\t\t<$tagStart />\n";
} else {
$out .= $prefixLine . "\t\t<$tagStart>" . $value . "</$tagEnd>\n";
}
}
$out .= "\t";
}
$out .= $prefixLine . "</row>\n";
}
return $out;
}
/**
* Computes the output for the given data array (representing a simple data table)
*
* @param $array
* @param string $prefixLine
* @return string
*/
protected function renderDataTableSimple($array, $prefixLine = "")
{
if (!is_array($array)) {
$array = array('value' => $array);
}
$columnsHaveInvalidChars = $this->areTableLabelsInvalidXmlTagNames($array);
$out = '';
foreach ($array as $keyName => $value) {
$xmlValue = self::formatValueXml($value);
list($tagStart, $tagEnd) = $this->getTagStartAndEndFor($keyName, $columnsHaveInvalidChars);
if (strlen($xmlValue) == 0) {
$out .= $prefixLine . "\t<$tagStart />\n";
} else {
$out .= $prefixLine . "\t<$tagStart>" . $xmlValue . "</$tagEnd>\n";
}
}
return $out;
}
/**
* Returns true if a string is a valid XML tag name, false if otherwise.
*
* @param string $str
* @return bool
*/
private static function isValidXmlTagName($str)
{
static $validTagRegex = null;
if ($validTagRegex === null) {
$invalidTagChars = "!\"#$%&'()*+,\\/;<=>?@[\\]\\\\^`{|}~";
$invalidTagStartChars = $invalidTagChars . "\\-.0123456789";
$validTagRegex = "/^[^" . $invalidTagStartChars . "][^" . $invalidTagChars . "]*$/";
}
$result = preg_match($validTagRegex, $str);
return !empty($result);
}
private function areTableLabelsInvalidXmlTagNames($rowArray)
{
if (!empty($rowArray)) {
foreach ($rowArray as $name => $value) {
if (!self::isValidXmlTagName($name)) {
return true;
}
}
}
return false;
}
private function getTagStartAndEndFor($keyName, $columnsHaveInvalidChars)
{
if ($columnsHaveInvalidChars) {
$tagStart = "col name=\"" . self::formatValueXml($keyName) . "\"";
$tagEnd = "col";
} else {
$tagStart = $tagEnd = $keyName;
}
return array($tagStart, $tagEnd);
}
}

View File

@@ -0,0 +1,744 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable;
use Exception;
use Piwik\DataTable;
use Piwik\Log;
use Piwik\Metrics;
/**
* This is what a {@link Piwik\DataTable} is composed of.
*
* DataTable rows contain columns, metadata and a subtable ID. Columns and metadata
* are stored as an array of name => value mappings.
*
* @api
*/
class Row extends \ArrayObject
{
/**
* List of columns that cannot be summed. An associative array for speed.
*
* @var array
*/
private static $unsummableColumns = array(
'label' => true,
'full_url' => true // column used w/ old Piwik versions,
);
// @see sumRow - implementation detail
public $maxVisitsSummed = 0;
private $metadata = array();
private $isSubtableLoaded = false;
/**
* @internal
*/
public $subtableId = null;
const COLUMNS = 0;
const METADATA = 1;
const DATATABLE_ASSOCIATED = 3;
/**
* Constructor.
*
* @param array $row An array with the following structure:
*
* array(
* Row::COLUMNS => array('label' => 'Matomo',
* 'column1' => 42,
* 'visits' => 657,
* 'time_spent' => 155744),
* Row::METADATA => array('logo' => 'test.png'),
* Row::DATATABLE_ASSOCIATED => $subtable // DataTable object
* // (but in the row only the ID will be stored)
* )
*/
public function __construct($row = array())
{
if (isset($row[self::COLUMNS])) {
$this->exchangeArray($row[self::COLUMNS]);
}
if (isset($row[self::METADATA])) {
$this->metadata = $row[self::METADATA];
}
if (isset($row[self::DATATABLE_ASSOCIATED])) {
if ($row[self::DATATABLE_ASSOCIATED] instanceof DataTable) {
$this->setSubtable($row[self::DATATABLE_ASSOCIATED]);
} else {
$this->subtableId = $row[self::DATATABLE_ASSOCIATED];
}
}
}
/**
* Used when archiving to serialize the Row's properties.
* @return array
* @ignore
*/
public function export()
{
return array(
self::COLUMNS => $this->getArrayCopy(),
self::METADATA => $this->metadata,
self::DATATABLE_ASSOCIATED => $this->subtableId,
);
}
/**
* When destroyed, a row destroys its associated subtable if there is one.
* @ignore
*/
public function __destruct()
{
if ($this->isSubtableLoaded) {
Manager::getInstance()->deleteTable($this->subtableId);
$this->subtableId = null;
$this->isSubtableLoaded = false;
}
}
/**
* Applies a basic rendering to the Row and returns the output.
*
* @return string describing the row. Example:
* "- 1 ['label' => 'piwik', 'nb_uniq_visitors' => 1685, 'nb_visits' => 1861] [] [idsubtable = 1375]"
*/
public function __toString()
{
$columns = array();
foreach ($this->getColumns() as $column => $value) {
if (is_string($value)) {
$value = "'$value'";
} elseif (is_array($value)) {
$value = var_export($value, true);
}
$columns[] = "'$column' => $value";
}
$columns = implode(", ", $columns);
$metadata = array();
foreach ($this->getMetadata() as $name => $value) {
if (is_string($value)) {
$value = "'$value'";
} elseif (is_array($value)) {
$value = var_export($value, true);
}
$metadata[] = "'$name' => $value";
}
$metadata = implode(", ", $metadata);
$output = "# [" . $columns . "] [" . $metadata . "] [idsubtable = " . $this->getIdSubDataTable() . "]<br />\n";
return $output;
}
/**
* Deletes the given column.
*
* @param string $name The column name.
* @return bool `true` on success, `false` if the column does not exist.
*/
public function deleteColumn($name)
{
if (!$this->offsetExists($name)) {
return false;
}
unset($this[$name]);
return true;
}
/**
* Renames a column.
*
* @param string $oldName The current name of the column.
* @param string $newName The new name of the column.
*/
public function renameColumn($oldName, $newName)
{
if (isset($this[$oldName])) {
$this[$newName] = $this[$oldName];
}
// outside the if () since we want to delete nulled columns
if ($this->offsetExists($oldName)) {
unset($this[$oldName]);
}
}
/**
* Returns a column by name.
*
* @param string $name The column name.
* @return mixed|false The column value or false if it doesn't exist.
*/
public function getColumn($name)
{
if (!isset($this[$name])) {
return false;
}
return $this[$name];
}
/**
* Returns the array of all metadata, or one requested metadata value.
*
* @param string|null $name The name of the metadata to return or null to return all metadata.
* @return mixed
*/
public function getMetadata($name = null)
{
if (is_null($name)) {
return $this->metadata;
}
if (!isset($this->metadata[$name])) {
return false;
}
return $this->metadata[$name];
}
/**
* Returns true if a column having the given name is already registered. The value will not be evaluated, it will
* just check whether a column exists independent of its value.
*
* @param string $name
* @return bool
*/
public function hasColumn($name)
{
return $this->offsetExists($name);
}
/**
* Returns the array containing all the columns.
*
* @return array Example:
*
* array(
* 'column1' => VALUE,
* 'label' => 'www.php.net'
* 'nb_visits' => 15894,
* )
*/
public function getColumns()
{
return $this->getArrayCopy();
}
/**
* Returns the ID of the subDataTable.
* If there is no such a table, returns null.
*
* @return int|null
*/
public function getIdSubDataTable()
{
return $this->subtableId;
}
/**
* Returns the associated subtable, if one exists. Returns `false` if none exists.
*
* @return DataTable|bool
*/
public function getSubtable()
{
if ($this->isSubtableLoaded) {
try {
return Manager::getInstance()->getTable($this->subtableId);
} catch (TableNotFoundException $e) {
// edge case
}
}
return false;
}
/**
* @param int $subtableId
* @ignore
*/
public function setNonLoadedSubtableId($subtableId)
{
$this->subtableId = $subtableId;
$this->isSubtableLoaded = false;
}
/**
* Sums a DataTable to this row's subtable. If this row has no subtable a new
* one is created.
*
* See {@link Piwik\DataTable::addDataTable()} to learn how DataTables are summed.
*
* @param DataTable $subTable Table to sum to this row's subtable.
*/
public function sumSubtable(DataTable $subTable)
{
if ($this->isSubtableLoaded) {
$thisSubTable = $this->getSubtable();
} else {
$this->warnIfSubtableAlreadyExists();
$thisSubTable = new DataTable();
$this->setSubtable($thisSubTable);
}
$columnOps = $subTable->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME);
$thisSubTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnOps);
$thisSubTable->addDataTable($subTable);
}
/**
* Attaches a subtable to this row, overwriting the existing subtable,
* if any.
*
* @param DataTable $subTable DataTable to associate to this row.
* @return DataTable Returns `$subTable`.
*/
public function setSubtable(DataTable $subTable)
{
$this->subtableId = $subTable->getId();
$this->isSubtableLoaded = true;
return $subTable;
}
/**
* Returns `true` if the subtable is currently loaded in memory via {@link Piwik\DataTable\Manager}.
*
* @return bool
*/
public function isSubtableLoaded()
{
// self::DATATABLE_ASSOCIATED are set as negative values,
// as a flag to signify that the subtable is loaded in memory
return $this->isSubtableLoaded;
}
/**
* Removes the subtable reference.
*/
public function removeSubtable()
{
$this->subtableId = null;
$this->isSubtableLoaded = false;
}
/**
* Set all the columns at once. Overwrites **all** previously set columns.
*
* @param array $columns eg, `array('label' => 'www.php.net', 'nb_visits' => 15894)`
*/
public function setColumns($columns)
{
$this->exchangeArray($columns);
}
/**
* Set the value `$value` to the column called `$name`.
*
* @param string $name name of the column to set.
* @param mixed $value value of the column to set.
*/
public function setColumn($name, $value)
{
$this[$name] = $value;
}
/**
* Set the value `$value` to the metadata called `$name`.
*
* @param string $name name of the metadata to set.
* @param mixed $value value of the metadata to set.
*/
public function setMetadata($name, $value)
{
$this->metadata[$name] = $value;
}
/**
* Deletes one metadata value or all metadata values.
*
* @param bool|string $name Metadata name (omit to delete entire metadata).
* @return bool `true` on success, `false` if the column didn't exist
*/
public function deleteMetadata($name = false)
{
if ($name === false) {
$this->metadata = array();
return true;
}
if (!isset($this->metadata[$name])) {
return false;
}
unset($this->metadata[$name]);
return true;
}
/**
* Add a new column to the row. If the column already exists, throws an exception.
*
* @param string $name name of the column to add.
* @param mixed $value value of the column to set or a PHP callable.
* @throws Exception if the column already exists.
*/
public function addColumn($name, $value)
{
if (isset($this[$name])) {
throw new Exception("Column $name already in the array!");
}
$this->setColumn($name, $value);
}
/**
* Add many columns to this row.
*
* @param array $columns Name/Value pairs, e.g., `array('name' => $value , ...)`
* @throws Exception if any column name does not exist.
* @return void
*/
public function addColumns($columns)
{
foreach ($columns as $name => $value) {
try {
$this->addColumn($name, $value);
} catch (Exception $e) {
}
}
if (!empty($e)) {
throw $e;
}
}
/**
* Add a new metadata to the row. If the metadata already exists, throws an exception.
*
* @param string $name name of the metadata to add.
* @param mixed $value value of the metadata to set.
* @throws Exception if the metadata already exists.
*/
public function addMetadata($name, $value)
{
if (isset($this->metadata[$name])) {
throw new Exception("Metadata $name already in the array!");
}
$this->setMetadata($name, $value);
}
private function isSummableColumn($columnName)
{
return empty(self::$unsummableColumns[$columnName]);
}
/**
* Sums the given `$rowToSum` columns values to the existing row column values.
* Only the int or float values will be summed. Label columns will be ignored
* even if they have a numeric value.
*
* Columns in `$rowToSum` that don't exist in `$this` are added to `$this`.
*
* @param \Piwik\DataTable\Row $rowToSum The row to sum to this row.
* @param bool $enableCopyMetadata Whether metadata should be copied or not.
* @param array|bool $aggregationOperations for columns that should not be summed, determine which
* aggregation should be used (min, max). format:
* `array('column name' => 'function name')`
* @throws Exception
*/
public function sumRow(Row $rowToSum, $enableCopyMetadata = true, $aggregationOperations = false)
{
foreach ($rowToSum as $columnToSumName => $columnToSumValue) {
if (!$this->isSummableColumn($columnToSumName)) {
continue;
}
$thisColumnValue = $this->getColumn($columnToSumName);
$operation = 'sum';
if (is_array($aggregationOperations) && isset($aggregationOperations[$columnToSumName])) {
if (is_string($aggregationOperations[$columnToSumName])) {
$operation = strtolower($aggregationOperations[$columnToSumName]);
} elseif (is_callable($aggregationOperations[$columnToSumName])) {
$operation = $aggregationOperations[$columnToSumName];
}
}
// max_actions is a core metric that is generated in ArchiveProcess_Day. Therefore, it can be
// present in any data table and is not part of the $aggregationOperations mechanism.
if ($columnToSumName == Metrics::INDEX_MAX_ACTIONS) {
$operation = 'max';
}
if (empty($operation)) {
throw new Exception("Unknown aggregation operation for column $columnToSumName.");
}
$newValue = $this->getColumnValuesMerged($operation, $thisColumnValue, $columnToSumValue, $this, $rowToSum, $columnToSumName);
$this->setColumn($columnToSumName, $newValue);
}
if ($enableCopyMetadata) {
$this->sumRowMetadata($rowToSum, $aggregationOperations);
}
}
/**
*/
private function getColumnValuesMerged($operation, $thisColumnValue, $columnToSumValue, $thisRow, $rowToSum, $columnName = null)
{
switch ($operation) {
case 'skip':
$newValue = null;
break;
case 'max':
$newValue = max($thisColumnValue, $columnToSumValue);
break;
case 'min':
if (!$thisColumnValue) {
$newValue = $columnToSumValue;
} elseif (!$columnToSumValue) {
$newValue = $thisColumnValue;
} else {
$newValue = min($thisColumnValue, $columnToSumValue);
}
break;
case 'sum':
$newValue = $this->sumRowArray($thisColumnValue, $columnToSumValue, $columnName);
break;
case 'uniquearraymerge':
if (is_array($thisColumnValue) && is_array($columnToSumValue)) {
foreach ($columnToSumValue as $columnSum) {
if (!in_array($columnSum, $thisColumnValue)) {
$thisColumnValue[] = $columnSum;
}
}
} elseif (!is_array($thisColumnValue) && is_array($columnToSumValue)) {
$thisColumnValue = $columnToSumValue;
}
$newValue = $thisColumnValue;
break;
default:
if (is_callable($operation)) {
return call_user_func($operation, $thisColumnValue, $columnToSumValue, $thisRow, $rowToSum);
}
throw new Exception("Unknown operation '$operation'.");
}
return $newValue;
}
/**
* Sums the metadata in `$rowToSum` with the metadata in `$this` row.
*
* @param Row $rowToSum
* @param array $aggregationOperations
*/
public function sumRowMetadata($rowToSum, $aggregationOperations = array())
{
if (!empty($rowToSum->metadata)
&& !$this->isSummaryRow()
) {
$aggregatedMetadata = array();
if (is_array($aggregationOperations)) {
// we need to aggregate value before value is overwritten by maybe another row
foreach ($aggregationOperations as $column => $operation) {
$thisMetadata = $this->getMetadata($column);
$sumMetadata = $rowToSum->getMetadata($column);
if ($thisMetadata === false && $sumMetadata === false) {
continue;
}
$aggregatedMetadata[$column] = $this->getColumnValuesMerged($operation, $thisMetadata, $sumMetadata, $this, $rowToSum, $column);
}
}
// We shall update metadata, and keep the metadata with the _most visits or pageviews_, rather than first or last seen
$visits = max($rowToSum->getColumn(Metrics::INDEX_PAGE_NB_HITS) || $rowToSum->getColumn(Metrics::INDEX_NB_VISITS),
// Old format pre-1.2, @see also method doSumVisitsMetrics()
$rowToSum->getColumn('nb_actions') || $rowToSum->getColumn('nb_visits'));
if (($visits && $visits > $this->maxVisitsSummed)
|| empty($this->metadata)
) {
$this->maxVisitsSummed = $visits;
$this->metadata = $rowToSum->metadata;
}
foreach ($aggregatedMetadata as $column => $value) {
// we need to make sure aggregated value is used, and not metadata from $rowToSum
$this->setMetadata($column, $value);
}
}
}
/**
* Returns `true` if this row is the summary row, `false` if otherwise. This function
* depends on the label of the row, and so, is not 100% accurate.
*
* @return bool
*/
public function isSummaryRow()
{
return $this->getColumn('label') === DataTable::LABEL_SUMMARY_ROW;
}
/**
* Helper function: sums 2 values
*
* @param number|bool $thisColumnValue
* @param number|array $columnToSumValue
* @param string|null $columnName for error reporting.
*
* @throws Exception
* @return array|int
*/
protected function sumRowArray($thisColumnValue, $columnToSumValue, $columnName = null)
{
if (is_numeric($columnToSumValue)) {
if ($thisColumnValue === false) {
$thisColumnValue = 0;
} else if (!is_numeric($thisColumnValue)) {
$label = $this->getColumn('label');
throw new \Exception(sprintf('Trying to sum unsupported operands for column %s in row with label = %s: %s + %s',
$columnName, $label, gettype($thisColumnValue), gettype($columnToSumValue)));
}
return $thisColumnValue + $columnToSumValue;
}
if ($columnToSumValue === false) {
return $thisColumnValue;
}
if ($thisColumnValue === false) {
return $columnToSumValue;
}
if (is_array($columnToSumValue)) {
$newValue = $thisColumnValue;
foreach ($columnToSumValue as $arrayIndex => $arrayValue) {
if (!isset($newValue[$arrayIndex])) {
$newValue[$arrayIndex] = false;
}
$newValue[$arrayIndex] = $this->sumRowArray($newValue[$arrayIndex], $arrayValue, $columnName);
}
return $newValue;
}
$this->warnWhenSummingTwoStrings($thisColumnValue, $columnToSumValue, $columnName);
return 0;
}
/**
* Helper function to compare array elements
*
* @param mixed $elem1
* @param mixed $elem2
* @return bool
* @ignore
*/
public static function compareElements($elem1, $elem2)
{
if (is_array($elem1)) {
if (is_array($elem2)) {
return strcmp(serialize($elem1), serialize($elem2));
}
return 1;
}
if (is_array($elem2)) {
return -1;
}
if ((string)$elem1 === (string)$elem2) {
return 0;
}
return ((string)$elem1 > (string)$elem2) ? 1 : -1;
}
/**
* Helper function that tests if two rows are equal.
*
* Two rows are equal if:
*
* - they have exactly the same columns / metadata
* - they have a subDataTable associated, then we check that both of them are the same.
*
* Column order is not important.
*
* @param \Piwik\DataTable\Row $row1 first to compare
* @param \Piwik\DataTable\Row $row2 second to compare
* @return bool
*/
public static function isEqual(Row $row1, Row $row2)
{
//same columns
$cols1 = $row1->getColumns();
$cols2 = $row2->getColumns();
$diff1 = array_udiff($cols1, $cols2, array(__CLASS__, 'compareElements'));
$diff2 = array_udiff($cols2, $cols1, array(__CLASS__, 'compareElements'));
if ($diff1 != $diff2) {
return false;
}
$dets1 = $row1->getMetadata();
$dets2 = $row2->getMetadata();
ksort($dets1);
ksort($dets2);
if ($dets1 != $dets2) {
return false;
}
// either both are null
// or both have a value
if (!(is_null($row1->getIdSubDataTable())
&& is_null($row2->getIdSubDataTable())
)
) {
$subtable1 = $row1->getSubtable();
$subtable2 = $row2->getSubtable();
if (!DataTable::isEqual($subtable1, $subtable2)) {
return false;
}
}
return true;
}
private function warnIfSubtableAlreadyExists()
{
if (!is_null($this->subtableId)) {
Log::warning(
"Row with label '%s' (columns = %s) has already a subtable id=%s but it was not loaded - overwriting the existing sub-table.",
$this->getColumn('label'),
implode(", ", $this->getColumns()),
$this->getIdSubDataTable()
);
}
}
protected function warnWhenSummingTwoStrings($thisColumnValue, $columnToSumValue, $columnName = null)
{
if (is_string($columnToSumValue)) {
Log::warning(
"Trying to add two strings in DataTable\Row::sumRowArray: %s + %s for column %s in row %s",
$thisColumnValue,
$columnToSumValue,
$columnName,
$this->__toString()
);
}
}
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable\Row;
use Piwik\DataTable;
use Piwik\DataTable\Row;
/**
* A special row whose column values are the aggregate of the row's subtable.
*
* This class creates sets its own columns to the sum of each row in the row's subtable.
*
* Non-numeric columns are bypassed during summation and do not appear in this
* rows columns.
*
* See {@link Piwik\DataTable\Row::sumRow()} for more information on the algorithm.
*
*/
class DataTableSummaryRow extends Row
{
/**
* Constructor.
*
* @param DataTable|null $subTable The subtable of this row. This parameter is mostly for
* convenience. If set, its rows will be summed to this one,
* but it will not be set as this row's subtable (so
* getSubtable() will return false).
*/
public function __construct($subTable = null)
{
if (isset($subTable)) {
$this->sumTable($subTable);
}
}
/**
* Reset this row to an empty one and sums the associated subtable again.
*/
public function recalculate()
{
$subTable = $this->getSubtable();
if ($subTable) {
$this->sumTable($subTable);
}
}
/**
* Sums a tables row with this one.
*
* @param DataTable $table
*/
private function sumTable($table)
{
$metadata = $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME);
$enableCopyMetadata = false;
foreach ($table->getRowsWithoutSummaryRow() as $row) {
$this->sumRow($row, $enableCopyMetadata, $metadata);
}
$summaryRow = $table->getRowFromId(DataTable::ID_SUMMARY_ROW);
if ($summaryRow) {
$this->sumRow($summaryRow, $enableCopyMetadata, $metadata);
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable;
use Piwik\DataTable;
/**
* A {@link Piwik\DataTable} where every row has two columns: **label** and **value**.
*
* Simple DataTables are only used to slightly alter the output of some renderers
* (notably the XML renderer).
*
* @api
*/
class Simple extends DataTable
{
/**
* Adds rows based on an array mapping label column values to value column
* values.
*
* @param array $array Array containing the rows, eg,
*
* array(
* 'Label row 1' => $value1,
* 'Label row 2' => $value2,
* )
*/
public function addRowsFromArray($array)
{
$this->addRowsFromSimpleArray(array($array));
}
}

View File

@@ -0,0 +1,13 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\DataTable;
class TableNotFoundException extends \Exception
{
}