update Piwik to version 2.16 (fixes #91)

This commit is contained in:
oliver 2016-04-10 18:55:57 +02:00
commit d885a4baa9
5833 changed files with 418860 additions and 226988 deletions

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -9,12 +9,11 @@
namespace Piwik\DataTable;
use Piwik\DataTable;
use Piwik\DataTable\Row;
/**
* A filter is set of logic that manipulates a DataTable. Existing filters do things like,
*
* - remove rows
*
* - 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
@ -22,10 +21,10 @@ use Piwik\DataTable\Row;
*
* 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
@ -37,7 +36,7 @@ abstract class BaseFilter
/**
* Constructor.
*
*
* @param DataTable $table
*/
public function __construct(DataTable $table)
@ -73,8 +72,8 @@ abstract class BaseFilter
if (!$this->enableRecursive) {
return;
}
if ($row->isSubtableLoaded()) {
$subTable = Manager::getInstance()->getTable($row->getIdSubDataTable());
$subTable = $row->getSubtable();
if ($subTable) {
$this->filter($subTable);
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -13,8 +13,8 @@
*/
namespace {
use Piwik\DataTable\Row\DataTableSummaryRow;
use Piwik\DataTable\Row;
use Piwik\DataTable\Row\DataTableSummaryRow;
class Piwik_DataTable_Row_DataTableSummary extends DataTableSummaryRow
{
@ -24,5 +24,11 @@ namespace {
{
}
}
// 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

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -9,30 +9,33 @@
namespace Piwik\DataTable\Filter;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable\Row;
use Piwik\DataTable;
use Piwik\Metrics;
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
@ -43,7 +46,7 @@ class AddColumnsProcessedMetrics extends BaseFilter
/**
* Constructor.
*
*
* @param DataTable $table The table to eventually filter.
* @param bool $deleteRowsWithNoVisit Whether to delete rows with no visits or not.
*/
@ -61,89 +64,32 @@ class AddColumnsProcessedMetrics extends BaseFilter
*/
public function filter($table)
{
$rowsIdToDelete = array();
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 = $this->getColumn($row, Metrics::INDEX_NB_VISITS);
$nbActions = $this->getColumn($row, Metrics::INDEX_NB_ACTIONS);
$nbVisits = Metric::getMetric($row, 'nb_visits');
$nbActions = Metric::getMetric($row, 'nb_actions');
if ($nbVisits == 0
&& $nbActions == 0
&& $this->deleteRowsWithNoVisit
) {
// case of keyword/website/campaign with a conversion for this day,
// but no visit, we don't show it
$rowsIdToDelete[] = $key;
continue;
// case of keyword/website/campaign with a conversion for this day, but no visit, we don't show it
$table->deleteRow($key);
}
$nbVisitsConverted = (int)$this->getColumn($row, Metrics::INDEX_NB_VISITS_CONVERTED);
if ($nbVisitsConverted > 0) {
$conversionRate = round(100 * $nbVisitsConverted / $nbVisits, $this->roundPrecision);
try {
$row->addColumn('conversion_rate', $conversionRate . "%");
} catch (\Exception $e) {
// conversion_rate can be defined upstream apparently? FIXME
}
}
if ($nbVisits == 0) {
$actionsPerVisit = $averageTimeOnSite = $bounceRate = $this->invalidDivision;
} else {
// nb_actions / nb_visits => Actions/visit
// sum_visit_length / nb_visits => Avg. Time on Site
// bounce_count / nb_visits => Bounce Rate
$actionsPerVisit = round($nbActions / $nbVisits, $this->roundPrecision);
$visitLength = $this->getColumn($row, Metrics::INDEX_SUM_VISIT_LENGTH);
$averageTimeOnSite = round($visitLength / $nbVisits, $rounding = 0);
$bounceRate = round(100 * $this->getColumn($row, Metrics::INDEX_BOUNCE_COUNT) / $nbVisits, $this->roundPrecision);
}
try {
$row->addColumn('nb_actions_per_visit', $actionsPerVisit);
$row->addColumn('avg_time_on_site', $averageTimeOnSite);
// It could be useful for API users to have raw sum length value.
//$row->addMetadata('sum_visit_length', $visitLength);
} catch (\Exception $e) {
}
try {
$row->addColumn('bounce_rate', $bounceRate . "%");
} catch (\Exception $e) {
}
$this->filterSubTable($row);
}
$table->deleteRows($rowsIdToDelete);
}
/**
* Returns column from a given row.
* Will work with 2 types of datatable
* - raw datatables coming from the archive DB, which columns are int indexed
* - datatables processed resulting of API calls, which columns have human readable english names
*
* @param Row|array $row
* @param int $columnIdRaw see consts in Archive::
* @param bool|array $mappingIdToName
* @return mixed Value of column, false if not found
*/
protected function getColumn($row, $columnIdRaw, $mappingIdToName = false)
{
if (empty($mappingIdToName)) {
$mappingIdToName = Metrics::$mappingFromIdToName;
}
$columnIdReadable = $mappingIdToName[$columnIdRaw];
if ($row instanceof Row) {
$raw = $row->getColumn($columnIdRaw);
if ($raw !== false) {
return $raw;
}
return $row->getColumn($columnIdReadable);
}
if (isset($row[$columnIdRaw])) {
return $row[$columnIdRaw];
}
if (isset($row[$columnIdReadable])) {
return $row[$columnIdReadable];
}
return false;
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -8,16 +8,23 @@
*/
namespace Piwik\DataTable\Filter;
use Exception;
use Piwik\Archive\DataTableFactory;
use Piwik\DataTable;
use Piwik\Metrics;
use Piwik\Piwik;
use Piwik\Tracker\GoalManager;
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
@ -36,17 +43,17 @@ use Piwik\Tracker\GoalManager;
* 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
@ -68,7 +75,7 @@ class AddColumnsProcessedMetricsGoal extends AddColumnsProcessedMetrics
/**
* 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).
@ -76,13 +83,14 @@ class AddColumnsProcessedMetricsGoal extends AddColumnsProcessedMetrics
* 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)
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;
}
/**
@ -95,132 +103,63 @@ class AddColumnsProcessedMetricsGoal extends AddColumnsProcessedMetrics
{
// Add standard processed metrics
parent::filter($table);
$roundingPrecision = GoalManager::REVENUE_PRECISION;
$expectedColumns = array();
foreach ($table->getRows() as $key => $row) {
$currentColumns = $row->getColumns();
$newColumns = array();
// visits could be undefined when there is a conversion but no visit
$nbVisits = (int)$this->getColumn($row, Metrics::INDEX_NB_VISITS);
$conversions = (int)$this->getColumn($row, Metrics::INDEX_NB_CONVERSIONS);
$goals = $this->getColumn($currentColumns, Metrics::INDEX_GOALS);
if ($goals) {
$revenue = 0;
foreach ($goals as $goalId => $goalMetrics) {
if ($goalId == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART) {
continue;
}
if ($goalId >= GoalManager::IDGOAL_ORDER
|| $goalId == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER
) {
$revenue += (int)$this->getColumn($goalMetrics, Metrics::INDEX_GOAL_REVENUE, Metrics::$mappingFromIdToNameGoal);
}
}
$goals = $this->getGoalsInTable($table);
if (!empty($this->goalsToProcess)) {
$goals = array_unique(array_merge($goals, $this->goalsToProcess));
sort($goals);
}
if ($revenue == 0) {
$revenue = (int)$this->getColumn($currentColumns, Metrics::INDEX_REVENUE);
}
if (!isset($currentColumns['revenue_per_visit'])) {
// If no visit for this metric, but some conversions, we still want to display some kind of "revenue per visit"
// even though it will actually be in this edge case "Revenue per conversion"
$revenuePerVisit = $this->invalidDivision;
if ($nbVisits > 0
|| $conversions > 0
) {
$revenuePerVisit = round($revenue / ($nbVisits == 0 ? $conversions : $nbVisits), $roundingPrecision);
}
$newColumns['revenue_per_visit'] = $revenuePerVisit;
}
if ($this->processOnlyIdGoal == self::GOALS_MINIMAL_REPORT) {
$row->addColumns($newColumns);
$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;
}
// Display per goal metrics
// - conversion rate
// - conversions
// - revenue per visit
foreach ($goals as $goalId => $goalMetrics) {
$goalId = str_replace("idgoal=", "", $goalId);
if (($this->processOnlyIdGoal > self::GOALS_FULL_TABLE
|| $this->isEcommerce)
&& $this->processOnlyIdGoal != $goalId
) {
continue;
}
$conversions = (int)$this->getColumn($goalMetrics, Metrics::INDEX_GOAL_NB_CONVERSIONS, Metrics::$mappingFromIdToNameGoal);
// Goal Conversion rate
$name = 'goal_' . $goalId . '_conversion_rate';
if ($nbVisits == 0) {
$value = $this->invalidDivision;
} else {
$value = round(100 * $conversions / $nbVisits, $roundingPrecision);
}
$newColumns[$name] = $value . "%";
$expectedColumns[$name] = true;
$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;
}
// Goal Conversions
$name = 'goal_' . $goalId . '_nb_conversions';
$newColumns[$name] = $conversions;
$expectedColumns[$name] = true;
// Goal Revenue per visit
$name = 'goal_' . $goalId . '_revenue_per_visit';
// See comment above for $revenuePerVisit
$goalRevenue = (float)$this->getColumn($goalMetrics, Metrics::INDEX_GOAL_REVENUE, Metrics::$mappingFromIdToNameGoal);
$revenuePerVisit = round($goalRevenue / ($nbVisits == 0 ? $conversions : $nbVisits), $roundingPrecision);
$newColumns[$name] = $revenuePerVisit;
$expectedColumns[$name] = true;
// Total revenue
$name = 'goal_' . $goalId . '_revenue';
$newColumns[$name] = $goalRevenue;
$expectedColumns[$name] = true;
if ($this->isEcommerce) {
// AOV Average Order Value
$name = 'goal_' . $goalId . '_avg_order_revenue';
$newColumns[$name] = $goalRevenue / $conversions;
$expectedColumns[$name] = true;
// Items qty
$name = 'goal_' . $goalId . '_items';
$newColumns[$name] = $this->getColumn($goalMetrics, Metrics::INDEX_GOAL_ECOMMERCE_ITEMS, Metrics::$mappingFromIdToNameGoal);
$expectedColumns[$name] = true;
}
// 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;
}
}
// conversion_rate can be defined upstream apparently? FIXME
try {
$row->addColumns($newColumns);
} catch (Exception $e) {
}
}
$expectedColumns['revenue_per_visit'] = true;
$extraProcessedMetrics[] = new Conversions($idSite, $idGoal); // PerGoal\Conversions or GoalSpecific\
$extraProcessedMetrics[] = new GoalSpecificRevenuePerVisit($idSite, $idGoal); // PerGoal\Revenue
$extraProcessedMetrics[] = new Revenue($idSite, $idGoal); // PerGoal\Revenue
// make sure all goals values are set, 0 by default
// if no value then sorting would put at the end
$expectedColumns = array_keys($expectedColumns);
$rows = $table->getRows();
foreach ($rows as &$row) {
foreach ($expectedColumns as $name) {
if (false === $row->getColumn($name)) {
$value = 0;
if (strpos($name, 'conversion_rate') !== false) {
$value = '0%';
}
$row->addColumn($name, $value);
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,78 @@
<?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;
}
/** @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)));
}
}
}
}

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

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -16,12 +16,12 @@ 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

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -28,11 +28,11 @@ use Piwik\Piwik;
* 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
@ -65,7 +65,7 @@ class BeautifyRangeLabels extends ColumnCallbackReplace
parent::__construct($table, 'label', array($this, 'beautify'), array());
$this->labelSingular = $labelSingular;
$this->labelPlural = $labelPlural;
$this->labelPlural = $labelPlural;
}
/**

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -17,11 +17,11 @@ use Piwik\DataTable;
* 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
@ -70,7 +70,7 @@ class BeautifyTimeRangeLabels extends BeautifyRangeLabels
{
if ($lowerBound < 60) {
return sprintf($this->labelSecondsPlural, $lowerBound, $lowerBound);
} else if ($lowerBound == 60) {
} elseif ($lowerBound == 60) {
return $this->labelSingular;
} else {
return sprintf($this->labelPlural, ceil($lowerBound / 60));

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -8,8 +8,10 @@
*/
namespace Piwik\DataTable\Filter;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\NumberFormatter;
use Piwik\Site;
/**
@ -17,15 +19,16 @@ use Piwik\Site;
* 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,
* 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
*/
class CalculateEvolutionFilter extends ColumnCallbackAddColumnPercentage
{
@ -100,7 +103,9 @@ class CalculateEvolutionFilter extends ColumnCallbackAddColumnPercentage
protected function getDivisor($row)
{
$pastRow = $this->getPastRowFromCurrent($row);
if (!$pastRow) return 0;
if (!$pastRow) {
return 0;
}
return $pastRow->getColumn($this->columnNameUsedAsDivisor);
}
@ -121,6 +126,9 @@ class CalculateEvolutionFilter extends ColumnCallbackAddColumnPercentage
{
$value = self::getPercentageValue($value, $divisor, $this->quotientPrecision);
$value = self::appendPercentSign($value);
$value = Common::forceDotAsSeparatorForDecimalPoint($value);
return $value;
}
@ -150,9 +158,10 @@ class CalculateEvolutionFilter extends ColumnCallbackAddColumnPercentage
{
$number = self::getPercentageValue($currentValue - $pastValue, $pastValue, $quotientPrecision);
if ($appendPercentSign) {
$number = self::appendPercentSign($number);
return NumberFormatter::getInstance()->formatPercent($number, $quotientPrecision);
}
return $number;
return NumberFormatter::getInstance()->format($number, $quotientPrecision);
}
public static function appendPercentSign($number)
@ -165,6 +174,7 @@ class CalculateEvolutionFilter extends ColumnCallbackAddColumnPercentage
if ($number > 0) {
$number = '+' . $number;
}
return $number;
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -10,16 +10,17 @@ 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
@ -79,17 +80,33 @@ class ColumnCallbackAddColumn extends BaseFilter
*/
public function filter($table)
{
foreach ($table->getRows() as $row) {
$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 ($this->columns as $column) {
foreach ($columns as $column) {
$columnValues[] = $row->getColumn($column);
}
$parameters = array_merge($columnValues, $this->functionParameters);
$value = call_user_func_array($this->functionToApply, $parameters);
$parameters = array_merge($columnValues, $functionParams);
$row->setColumn($this->columnToAdd, $value);
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

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -13,11 +13,11 @@ 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));
*

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -15,14 +15,14 @@ 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
@ -38,7 +38,7 @@ class ColumnCallbackAddColumnQuotient extends BaseFilter
/**
* 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.
@ -75,7 +75,7 @@ class ColumnCallbackAddColumnQuotient extends BaseFilter
*/
public function filter($table)
{
foreach ($table->getRows() as $key => $row) {
foreach ($table->getRows() as $row) {
$value = $this->getDividend($row);
if ($value === false && $this->shouldSkipRows) {
continue;
@ -109,6 +109,7 @@ class ColumnCallbackAddColumnQuotient extends BaseFilter
if ($divisor > 0 && $value > 0) {
$quotient = round($value / $divisor, $this->quotientPrecision);
}
return $quotient;
}
@ -135,7 +136,7 @@ class ColumnCallbackAddColumnQuotient extends BaseFilter
{
if (!is_null($this->totalValueUsedAsDivisor)) {
return $this->totalValueUsedAsDivisor;
} else if ($this->getDivisorFromSummaryRow) {
} elseif ($this->getDivisorFromSummaryRow) {
$summaryRow = $this->table->getRowFromId(DataTable::ID_SUMMARY_ROW);
return $summaryRow->getColumn($this->columnNameUsedAsDivisor);
} else {

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -14,11 +14,11 @@ 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
@ -31,7 +31,7 @@ class ColumnCallbackAddMetadata extends BaseFilter
/**
* 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.
@ -48,12 +48,12 @@ class ColumnCallbackAddMetadata extends BaseFilter
if (!is_array($columnsToRead)) {
$columnsToRead = array($columnsToRead);
}
$this->columnsToRead = $columnsToRead;
$this->functionToApply = $functionToApply;
$this->columnsToRead = $columnsToRead;
$this->functionToApply = $functionToApply;
$this->functionParameters = $functionParameters;
$this->metadataToAdd = $metadataToAdd;
$this->applyToSummaryRow = $applyToSummaryRow;
$this->metadataToAdd = $metadataToAdd;
$this->applyToSummaryRow = $applyToSummaryRow;
}
/**
@ -63,11 +63,13 @@ class ColumnCallbackAddMetadata extends BaseFilter
*/
public function filter($table)
{
foreach ($table->getRows() as $key => $row) {
if (!$this->applyToSummaryRow && $key == DataTable::ID_SUMMARY_ROW) {
continue;
}
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);

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

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -15,23 +15,22 @@ 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 $columnToFilter;
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.

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -15,9 +15,9 @@ 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);
@ -25,10 +25,11 @@ use Piwik\DataTable\Row;
* 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
{
@ -39,7 +40,7 @@ class ColumnCallbackReplace extends BaseFilter
/**
* 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.
@ -54,14 +55,14 @@ class ColumnCallbackReplace extends BaseFilter
$extraColumnParameters = array())
{
parent::__construct($table);
$this->functionToApply = $functionToApply;
$this->functionToApply = $functionToApply;
$this->functionParameters = $functionParameters;
if (!is_array($columnsToFilter)) {
$columnsToFilter = array($columnsToFilter);
}
$this->columnsToFilter = $columnsToFilter;
$this->columnsToFilter = $columnsToFilter;
$this->extraColumnParameters = $extraColumnParameters;
}
@ -72,13 +73,14 @@ class ColumnCallbackReplace extends BaseFilter
*/
public function filter($table)
{
foreach ($table->getRows() as $key => $row) {
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) {
@ -86,14 +88,21 @@ class ColumnCallbackReplace extends BaseFilter
}
$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();
}
}
/**

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -16,15 +16,15 @@ use Piwik\DataTable\BaseFilter;
* 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
@ -91,6 +91,7 @@ class ColumnDelete extends BaseFilter
* See {@link ColumnDelete}.
*
* @param DataTable $table
* @return DataTable
*/
public function filter($table)
{
@ -100,15 +101,16 @@ class ColumnDelete extends BaseFilter
// remove columns specified in $this->columnsToRemove
if (!empty($this->columnsToRemove)) {
foreach ($table->getRows() as $row) {
foreach ($table as $index => $row) {
foreach ($this->columnsToRemove as $column) {
if ($this->deleteIfZeroOnly) {
$value = $row->getColumn($column);
$value = $row[$column];
if ($value === false || !empty($value)) {
continue;
}
}
$row->deleteColumn($column);
unset($table[$index][$column]);
}
}
@ -117,9 +119,8 @@ class ColumnDelete extends BaseFilter
// remove columns not specified in $columnsToKeep
if (!empty($this->columnsToKeep)) {
foreach ($table->getRows() as $row) {
foreach ($row->getColumns() as $name => $value) {
foreach ($table as $index => $row) {
foreach ($row as $name => $value) {
$keep = false;
// @see self::APPEND_TO_COLUMN_NAME_TO_KEEP
foreach ($this->columnsToKeep as $nameKeep => $true) {
@ -132,7 +133,7 @@ class ColumnDelete extends BaseFilter
&& $name != 'label' // label cannot be removed via whitelisting
&& !isset($this->columnsToKeep[$name])
) {
$row->deleteColumn($name);
unset($table[$index][$name]);
}
}
}
@ -141,10 +142,12 @@ class ColumnDelete extends BaseFilter
}
// recurse
if ($recurse) {
foreach ($table->getRows() as $row) {
if ($recurse && !is_array($table)) {
foreach ($table as $row) {
$this->filterSubTable($row);
}
}
return $table;
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -10,21 +10,22 @@ 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));
@ -50,7 +51,7 @@ class ExcludeLowPopulation extends BaseFilter
* @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,
* 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
@ -59,7 +60,13 @@ class ExcludeLowPopulation extends BaseFilter
public function __construct($table, $columnToFilter, $minimumValue, $minimumPercentageThreshold = false)
{
parent::__construct($table);
$this->columnToFilter = $columnToFilter;
$row = $table->getFirstRow();
if ($row === false) {
return;
}
$this->columnToFilter = $this->selectColumnToExclude($columnToFilter, $row);
if ($minimumValue == 0) {
if ($minimumPercentageThreshold === false) {
@ -80,6 +87,9 @@ class ExcludeLowPopulation extends BaseFilter
*/
public function filter($table)
{
if(empty($this->columnToFilter)) {
return;
}
$minimumValue = $this->minimumValue;
$isValueLowPopulation = function ($value) use ($minimumValue) {
return $value < $minimumValue;
@ -87,4 +97,29 @@ class ExcludeLowPopulation extends BaseFilter
$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

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -10,6 +10,7 @@ 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
@ -18,12 +19,12 @@ use Piwik\DataTable\BaseFilter;
* _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
@ -51,17 +52,17 @@ class GroupBy extends BaseFilter
* @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.
* 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, $parameters = array())
public function __construct($table, $groupByColumn, $reduceFunction = null, $parameters = array())
{
parent::__construct($table);
$this->groupByColumn = $groupByColumn;
$this->groupByColumn = $groupByColumn;
$this->reduceFunction = $reduceFunction;
$this->parameters = $parameters;
$this->parameters = $parameters;
}
/**
@ -71,19 +72,19 @@ class GroupBy extends BaseFilter
*/
public function filter($table)
{
/** @var Row[] $groupByRows */
$groupByRows = array();
$nonGroupByRowIds = array();
foreach ($table->getRows() as $rowId => $row) {
// skip the summary row
if ($rowId == DataTable::ID_SUMMARY_ROW) {
continue;
}
foreach ($table->getRowsWithoutSummaryRow() as $rowId => $row) {
$groupByColumnValue = $row->getColumn($this->groupByColumn);
$groupByValue = $groupByColumnValue;
// reduce the group by column of this row
$groupByColumnValue = $row->getColumn($this->groupByColumn);
$parameters = array_merge(array($groupByColumnValue), $this->parameters);
$groupByValue = call_user_func_array($this->reduceFunction, $parameters);
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
@ -98,6 +99,10 @@ class GroupBy extends BaseFilter
}
}
if ($this->groupByColumn === 'label') {
$table->setLabelsHaveChanged();
}
// delete the unneeded rows.
$table->deleteRows($nonGroupByRowIds);
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -13,9 +13,9 @@ 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));
*
@ -34,9 +34,9 @@ class Limit extends BaseFilter
public function __construct($table, $offset, $limit = -1, $keepSummaryRow = false)
{
parent::__construct($table);
$this->offset = $offset;
$this->limit = $limit;
$this->offset = $offset;
$this->limit = $limit;
$this->keepSummaryRow = $keepSummaryRow;
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -16,7 +16,7 @@ use Piwik\DataTable\BaseFilter;
* 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'));
*
@ -31,7 +31,7 @@ class MetadataCallbackAddMetadata extends BaseFilter
/**
* 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.
@ -57,16 +57,18 @@ class MetadataCallbackAddMetadata extends BaseFilter
/**
* See {@link MetadataCallbackAddMetadata}.
*
*
* @param DataTable $table
*/
public function filter($table)
{
foreach ($table->getRows() as $key => $row) {
if (!$this->applyToSummaryRow && $key == DataTable::ID_SUMMARY_ROW) {
continue;
}
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);

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -14,9 +14,9 @@ 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';
* }));
@ -27,7 +27,7 @@ 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.

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -13,9 +13,9 @@ 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'));
*
@ -23,6 +23,9 @@ use Piwik\DataTable\BaseFilter;
*/
class Pattern extends BaseFilter
{
/**
* @var string|array
*/
private $columnToFilter;
private $patternToSearch;
private $patternToSearchQuoted;
@ -30,7 +33,7 @@ class Pattern extends BaseFilter
/**
* 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.
@ -53,7 +56,7 @@ class Pattern extends BaseFilter
* @return string
* @ignore
*/
static public function getPatternQuoted($pattern)
public static function getPatternQuoted($pattern)
{
return '/' . str_replace('/', '\/', $pattern) . '/';
}
@ -67,14 +70,14 @@ class Pattern extends BaseFilter
* @return int
* @ignore
*/
static public function match($patternQuoted, $string, $invertedMatch = false)
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)
@ -93,4 +96,30 @@ class Pattern extends BaseFilter
}
}
}
/**
* 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

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -8,17 +8,15 @@
*/
namespace Piwik\DataTable\Filter;
use Exception;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable;
use Piwik\DataTable\Manager;
/**
* 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'));
*
@ -32,7 +30,7 @@ class PatternRecursive extends BaseFilter
/**
* 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.
@ -48,7 +46,7 @@ class PatternRecursive extends BaseFilter
/**
* See {@link PatternRecursive}.
*
*
* @param DataTable $table
* @return int The number of deleted rows.
*/
@ -62,18 +60,15 @@ class PatternRecursive extends BaseFilter
// AND 2 - the label is not found in the children
$patternNotFoundInChildren = false;
try {
$idSubTable = $row->getIdSubDataTable();
$subTable = Manager::getInstance()->getTable($idSubTable);
$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;
}
} catch (Exception $e) {
// there is no subtable loaded for example
$patternNotFoundInChildren = true;
}
if ($patternNotFoundInChildren

View file

@ -0,0 +1,550 @@
<?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\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\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) {
$segmentValue = $row->getColumn('label');
return $this->fetchIntersectedWithThisBySegment($table, $segmentValue);
}
// 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, $segmentValue)
{
$segmentStr = $this->thisReportDimensionSegment->getSegment() . "==" . urlencode($segmentValue);
// 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('segment' => $segmentStr) + $this->getRequestParamOverride($table);
return $this->pivotDimensionReport->fetch($params);
}
private function setPivotByDimension($pivotByDimension)
{
$this->pivotByDimension = Dimension::factory($pivotByDimension);
if (empty($this->pivotByDimension)) {
throw new Exception("Invalid dimension '$pivotByDimension'.");
}
$this->pivotDimensionReport = Report::getForDimension($this->pivotByDimension);
}
private function setThisReportMetadata($report)
{
list($module, $method) = explode('.', $report);
$this->thisReport = Report::factory($module, $method);
if (empty($this->thisReport)) {
throw new Exception("Unable to find report '$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->getName();
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)
arsort($columnSet);
// 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

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -17,8 +17,8 @@ use Piwik\DataTable\BaseFilter;
*/
class RangeCheck extends BaseFilter
{
static public $minimumValue = 0.00;
static public $maximumValue = 100.0;
public static $minimumValue = 0.00;
public static $maximumValue = 100.0;
/**
* @param DataTable $table
@ -32,7 +32,7 @@ class RangeCheck extends BaseFilter
$this->columnToFilter = $columnToFilter;
if ($minimumValue < $maximumValue) {
if ((float) $minimumValue < (float) $maximumValue) {
self::$minimumValue = $minimumValue;
self::$maximumValue = $maximumValue;
}
@ -47,10 +47,23 @@ class RangeCheck extends BaseFilter
{
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 < self::$minimumValue) {
if ($value < (float) self::$minimumValue) {
$row->setColumn($this->columnToFilter, self::$minimumValue);
} elseif ($value > self::$maximumValue) {
} elseif ($value > (float) self::$maximumValue) {
$row->setColumn($this->columnToFilter, self::$maximumValue);
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -18,15 +18,15 @@ 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)
* {
@ -34,7 +34,7 @@ use Piwik\Tracker\GoalManager;
* $dataTable->queueFilter('ReplaceColumnNames');
* return $dataTable;
* }
*
*
* @api
*/
class ReplaceColumnNames extends BaseFilter
@ -43,14 +43,14 @@ class ReplaceColumnNames extends BaseFilter
/**
* 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)
@ -81,9 +81,8 @@ class ReplaceColumnNames extends BaseFilter
*/
protected function filterTable($table)
{
foreach ($table->getRows() as $key => $row) {
$oldColumns = $row->getColumns();
$newColumns = $this->getRenamedColumns($oldColumns);
foreach ($table->getRows() as $row) {
$newColumns = $this->getRenamedColumns($row->getColumns());
$row->setColumns($newColumns);
$this->filterSubTable($row);
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -10,29 +10,28 @@ namespace Piwik\DataTable\Filter;
use Piwik\DataTable\BaseFilter;
use Piwik\DataTable;
use Piwik\DataTable\Manager;
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')`.
@ -53,20 +52,20 @@ class ReplaceSummaryRowLabel extends BaseFilter
*/
public function filter($table)
{
$rows = $table->getRows();
foreach ($rows as $id => $row) {
if ($row->getColumn('label') == DataTable::LABEL_SUMMARY_ROW
|| $id == DataTable::ID_SUMMARY_ROW
) {
$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);
break;
}
}
// recurse
foreach ($rows as $row) {
if ($row->isSubtableLoaded()) {
$subTable = Manager::getInstance()->getTable($row->getIdSubDataTable());
foreach ($table->getRowsWithoutSummaryRow() as $row) {
$subTable = $row->getSubtable();
if ($subTable) {
$this->filter($subTable);
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -13,7 +13,7 @@ use Piwik\DataTable\BaseFilter;
/**
* Sanitizes DataTable labels as an extra precaution. Called internally by Piwik.
*
*
*/
class SafeDecodeLabel extends BaseFilter
{

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -16,7 +16,7 @@ use Piwik\Metrics;
/**
* 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
@ -25,24 +25,30 @@ class Sort extends BaseFilter
{
protected $columnToSort;
protected $order;
protected $sign;
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.
*/
public function __construct($table, $columnToSort, $order = 'desc', $naturalSort = true, $recursiveSort = false)
public function __construct($table, $columnToSort, $order = 'desc', $naturalSort = true, $recursiveSort = true)
{
parent::__construct($table);
if ($recursiveSort) {
$table->enableRecursiveSort();
}
$this->columnToSort = $columnToSort;
$this->naturalSort = $naturalSort;
$this->naturalSort = $naturalSort;
$this->setOrder($order);
}
@ -55,67 +61,56 @@ class Sort extends BaseFilter
{
if ($order == 'asc') {
$this->order = 'asc';
$this->sign = 1;
$this->sign = 1;
} else {
$this->order = 'desc';
$this->sign = -1;
$this->sign = -1;
}
}
/**
* Sorting method used for sorting numbers
*
* @param number $a
* @param number $b
* @param array $rowA array[0 => value of column to sort, 1 => label]
* @param array $rowB array[0 => value of column to sort, 1 => label]
* @return int
*/
public function numberSort($a, $b)
public function numberSort($rowA, $rowB)
{
return !isset($a->c[Row::COLUMNS][$this->columnToSort])
&& !isset($b->c[Row::COLUMNS][$this->columnToSort])
if (isset($rowA[0]) && isset($rowB[0])) {
if ($rowA[0] != $rowB[0] || !isset($rowA[1])) {
return $this->sign * ($rowA[0] < $rowB[0] ? -1 : 1);
} else {
return -1 * $this->sign * strnatcasecmp($rowA[1], $rowB[1]);
}
} elseif (!isset($rowB[0]) && !isset($rowA[0])) {
return -1 * $this->sign * strnatcasecmp($rowA[1], $rowB[1]);
} elseif (!isset($rowA[0])) {
return 1;
}
? 0
: (
!isset($a->c[Row::COLUMNS][$this->columnToSort])
? 1
: (
!isset($b->c[Row::COLUMNS][$this->columnToSort])
? -1
: (($a->c[Row::COLUMNS][$this->columnToSort] != $b->c[Row::COLUMNS][$this->columnToSort]
|| !isset($a->c[Row::COLUMNS]['label']))
? ($this->sign * (
$a->c[Row::COLUMNS][$this->columnToSort]
< $b->c[Row::COLUMNS][$this->columnToSort]
? -1
: 1)
)
: -1 * $this->sign * strnatcasecmp(
$a->c[Row::COLUMNS]['label'],
$b->c[Row::COLUMNS]['label'])
)
)
);
return -1;
}
/**
* Sorting method used for sorting values natural
*
* @param mixed $a
* @param mixed $b
* @param mixed $valA
* @param mixed $valB
* @return int
*/
function naturalSort($a, $b)
public function naturalSort($valA, $valB)
{
return !isset($a->c[Row::COLUMNS][$this->columnToSort])
&& !isset($b->c[Row::COLUMNS][$this->columnToSort])
return !isset($valA)
&& !isset($valB)
? 0
: (!isset($a->c[Row::COLUMNS][$this->columnToSort])
: (!isset($valA)
? 1
: (!isset($b->c[Row::COLUMNS][$this->columnToSort])
: (!isset($valB)
? -1
: $this->sign * strnatcasecmp(
$a->c[Row::COLUMNS][$this->columnToSort],
$b->c[Row::COLUMNS][$this->columnToSort]
$valA,
$valB
)
)
);
@ -124,27 +119,38 @@ class Sort extends BaseFilter
/**
* Sorting method used for sorting values
*
* @param mixed $a
* @param mixed $b
* @param mixed $valA
* @param mixed $valB
* @return int
*/
function sortString($a, $b)
public function sortString($valA, $valB)
{
return !isset($a->c[Row::COLUMNS][$this->columnToSort])
&& !isset($b->c[Row::COLUMNS][$this->columnToSort])
return !isset($valA)
&& !isset($valB)
? 0
: (!isset($a->c[Row::COLUMNS][$this->columnToSort])
: (!isset($valA)
? 1
: (!isset($b->c[Row::COLUMNS][$this->columnToSort])
: (!isset($valB)
? -1
: $this->sign *
strcasecmp($a->c[Row::COLUMNS][$this->columnToSort],
$b->c[Row::COLUMNS][$this->columnToSort]
strcasecmp($valA,
$valB
)
)
);
}
protected function getColumnValue(Row $row)
{
$value = $row->getColumn($this->columnToSort);
if ($value === false || is_array($value)) {
return null;
}
return $value;
}
/**
* Sets the column to be used for sorting
*
@ -153,18 +159,18 @@ class Sort extends BaseFilter
*/
protected function selectColumnToSort($row)
{
$value = $row->getColumn($this->columnToSort);
if ($value !== false) {
$value = $row->hasColumn($this->columnToSort);
if ($value) {
return $this->columnToSort;
}
$columnIdToName = Metrics::getMappingFromIdToName();
$columnIdToName = Metrics::getMappingFromNameToId();
// sorting by "nb_visits" but the index is Metrics::INDEX_NB_VISITS in the table
if (isset($columnIdToName[$this->columnToSort])) {
$column = $columnIdToName[$this->columnToSort];
$value = $row->getColumn($column);
$value = $row->hasColumn($column);
if ($value !== false) {
if ($value) {
return $column;
}
}
@ -172,8 +178,8 @@ class Sort extends BaseFilter
// eg. was previously sorted by revenue_per_visit, but this table
// doesn't have this column; defaults with nb_visits
$column = Metrics::INDEX_NB_VISITS;
$value = $row->getColumn($column);
if ($value !== false) {
$value = $row->hasColumn($column);
if ($value) {
return $column;
}
@ -193,21 +199,25 @@ class Sort extends BaseFilter
if ($table instanceof Simple) {
return;
}
if (empty($this->columnToSort)) {
return;
}
$rows = $table->getRows();
if (count($rows) == 0) {
if (!$table->getRowsCount()) {
return;
}
$row = current($rows);
$row = $table->getFirstRow();
if ($row === false) {
return;
}
$this->columnToSort = $this->selectColumnToSort($row);
$value = $row->getColumn($this->columnToSort);
if (is_numeric($value)) {
$value = $this->getFirstValueFromDataTable($table);
if (is_numeric($value) && $this->columnToSort !== 'label') {
$methodToUse = "numberSort";
} else {
if ($this->naturalSort) {
@ -216,6 +226,65 @@ class Sort extends BaseFilter
$methodToUse = "sortString";
}
}
$table->sort(array($this, $methodToUse), $this->columnToSort);
$this->sort($table, $methodToUse);
}
private function getFirstValueFromDataTable($table)
{
foreach ($table->getRowsWithoutSummaryRow() as $row) {
$value = $this->getColumnValue($row);
if (!is_null($value)) {
return $value;
}
}
}
/**
* Sorts the DataTable rows using the supplied callback function.
*
* @param string $functionCallback A comparison callback compatible with {@link usort}.
* @param string $columnSortedBy The column name `$functionCallback` sorts by. This is stored
* so we can determine how the DataTable was sorted in the future.
*/
private function sort(DataTable $table, $functionCallback)
{
$table->setTableSortedBy($this->columnToSort);
$rows = $table->getRowsWithoutSummaryRow();
// get column value and label only once for performance tweak
$values = array();
if ($functionCallback === 'numberSort') {
foreach ($rows as $key => $row) {
$values[$key] = array($this->getColumnValue($row), $row->getColumn('label'));
}
} else {
foreach ($rows as $key => $row) {
$values[$key] = $this->getColumnValue($row);
}
}
uasort($values, array($this, $functionCallback));
$sortedRows = array();
foreach ($values as $key => $value) {
$sortedRows[] = $rows[$key];
}
$table->setRows($sortedRows);
unset($rows);
unset($sortedRows);
if ($table->isSortRecursiveEnabled()) {
foreach ($table->getRowsWithoutSummaryRow() as $row) {
$subTable = $row->getSubtable();
if ($subTable) {
$subTable->enableRecursiveSort();
$this->sort($subTable, $functionCallback);
}
}
}
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -16,26 +16,26 @@ 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.
@ -69,11 +69,15 @@ class Truncate extends BaseFilter
*/
public function filter($table)
{
if ($this->truncateAfter < 0) {
return;
}
$this->addSummaryRow($table);
$table->queueFilter('ReplaceSummaryRowLabel', array($this->labelSummaryRow));
if ($this->filterRecursive) {
foreach ($table->getRows() as $row) {
foreach ($table->getRowsWithoutSummaryRow() as $row) {
if ($row->isSubtableLoaded()) {
$this->filter($row->getSubtable());
}
@ -81,17 +85,23 @@ class Truncate extends BaseFilter
}
}
/**
* @param DataTable $table
*/
private function addSummaryRow($table)
{
$table->filter('Sort', array($this->columnToSortByBeforeTruncating, 'desc'));
if ($table->getRowsCount() <= $this->truncateAfter + 1) {
return;
}
$rows = $table->getRows();
$count = $table->getRowsCount();
$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
@ -99,10 +109,10 @@ class Truncate extends BaseFilter
//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, $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME));
$newRow->sumRow($summaryRow, $enableCopyMetadata = false, $aggregationOps);
}
} else {
$newRow->sumRow($rows[$i], $enableCopyMetadata = false, $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME));
$newRow->sumRow($rows[$i], $enableCopyMetadata = false, $aggregationOps);
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -12,29 +12,30 @@ namespace Piwik\DataTable;
use Exception;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Singleton;
/**
* 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.
*
* @method static \Piwik\DataTable\Manager getInstance()
*/
class Manager extends Singleton
class Manager extends \ArrayObject
{
/**
* Array used to store the DataTable
*
* @var array
*/
private $tables = array();
/**
* Id of the next inserted table id in the Manager
* @var int
*/
protected $nextTableId = 1;
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
@ -44,9 +45,9 @@ class Manager extends Singleton
*/
public function addTable($table)
{
$this->tables[$this->nextTableId] = $table;
$this->nextTableId++;
return $this->nextTableId - 1;
$this[$this->nextTableId] = $table;
return $this->nextTableId;
}
/**
@ -60,10 +61,11 @@ class Manager extends Singleton
*/
public function getTable($idTable)
{
if (!isset($this->tables[$idTable])) {
throw new TableNotFoundException(sprintf("This report has been reprocessed since your last click. To see this error less often, please increase the timeout value in seconds in Settings > General Settings. (error: id %s not found).", $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 Piwik issue tracker.)", $idTable));
}
return $this->tables[$idTable];
return $this[$idTable];
}
/**
@ -73,7 +75,7 @@ class Manager extends Singleton
*/
public function getMostRecentTableId()
{
return $this->nextTableId - 1;
return $this->nextTableId;
}
/**
@ -81,14 +83,15 @@ class Manager extends Singleton
*/
public function deleteAll($deleteWhenIdTableGreaterThan = 0)
{
foreach ($this->tables as $id => $table) {
foreach ($this as $id => $table) {
if ($id > $deleteWhenIdTableGreaterThan) {
$this->deleteTable($id);
}
}
if ($deleteWhenIdTableGreaterThan == 0) {
$this->tables = array();
$this->nextTableId = 1;
$this->exchangeArray(array());
$this->nextTableId = 0;
}
}
@ -100,8 +103,8 @@ class Manager extends Singleton
*/
public function deleteTable($id)
{
if (isset($this->tables[$id])) {
Common::destroy($this->tables[$id]);
if (isset($this[$id])) {
Common::destroy($this[$id]);
$this->setTableDeleted($id);
}
}
@ -131,7 +134,7 @@ class Manager extends Singleton
*/
public function setTableDeleted($id)
{
$this->tables[$id] = null;
$this[$id] = null;
}
/**
@ -140,7 +143,7 @@ class Manager extends Singleton
public function dumpAllTables()
{
echo "<hr />Manager->dumpAllTables()<br />";
foreach ($this->tables as $id => $table) {
foreach ($this as $id => $table) {
if (!($table instanceof DataTable)) {
echo "Error table $id is not instance of datatable<br />";
var_export($table);

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -8,6 +8,7 @@
*/
namespace Piwik\DataTable;
use Closure;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\DataTable\Renderer\Console;
@ -15,10 +16,10 @@ 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}.
*
*
@ -73,7 +74,7 @@ class Map implements DataTableInterface
/**
* 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.
@ -104,11 +105,37 @@ class Map implements DataTableInterface
*/
public function filter($className, $parameters = array())
{
foreach ($this->getDataTables() as $id => $table) {
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.
*
@ -142,7 +169,7 @@ class Map implements DataTableInterface
/**
* Returns the last element in the Map's array.
*
*
* @return DataTable|Map|false
*/
public function getLastRow()
@ -161,6 +188,22 @@ class Map implements DataTableInterface
$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).
@ -184,11 +227,31 @@ class Map implements DataTableInterface
}
}
/**
* @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
*/
@ -203,7 +266,7 @@ class Map implements DataTableInterface
* 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.
*/
@ -216,7 +279,7 @@ class Map implements DataTableInterface
/**
* Deletes a table from the array of DataTables.
*
*
* @param string $id The label associated with {@link DataTable}.
*/
public function deleteRow($id)
@ -246,12 +309,14 @@ class Map implements DataTableInterface
public function getColumn($name)
{
$values = array();
foreach ($this->getDataTables() as $table) {
$moreValues = $table->getColumn($name);
foreach ($moreValues as &$value) {
$values[] = $value;
}
}
return $values;
}
@ -263,19 +328,19 @@ class Map implements DataTableInterface
* 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)
@ -286,9 +351,9 @@ class Map implements DataTableInterface
* 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:
@ -366,11 +431,11 @@ class Map implements DataTableInterface
/**
* 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)

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -9,10 +9,11 @@
namespace Piwik\DataTable;
use Exception;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Loader;
use Piwik\Metrics;
use Piwik\Piwik;
use Piwik\BaseFactory;
/**
* A DataTable Renderer can produce an output given a DataTable object.
@ -22,7 +23,7 @@ use Piwik\Piwik;
* $render->setTable($dataTable);
* echo $render;
*/
abstract class Renderer
abstract class Renderer extends BaseFactory
{
protected $table;
@ -100,7 +101,7 @@ abstract class Renderer
*/
protected function renderHeader()
{
@header('Content-Type: text/plain; charset=utf-8');
Common::sendHeader('Content-Type: text/plain; charset=utf-8');
}
/**
@ -110,22 +111,6 @@ abstract class Renderer
*/
abstract public function render();
/**
* Computes the exception output and returns the string/binary
*
* @return string
*/
abstract public function renderException();
protected function getExceptionMessage()
{
$message = $this->exception->getMessage();
if (\Piwik_ShouldPrintBackTraceWithMessage()) {
$message .= "\n" . $this->exception->getTraceAsString();
}
return self::renderHtmlEntities($message);
}
/**
* @see render()
* @return string
@ -144,32 +129,17 @@ abstract class Renderer
public function setTable($table)
{
if (!is_array($table)
&& !($table instanceof DataTable)
&& !($table instanceof DataTable\Map)
&& !($table instanceof DataTableInterface)
) {
throw new Exception("DataTable renderers renderer accepts only DataTable and Map instances, and arrays.");
throw new Exception("DataTable renderers renderer accepts only DataTable, Simple and Map instances, and arrays.");
}
$this->table = $table;
}
/**
* Set the Exception to be rendered
*
* @param Exception $exception to be rendered
* @throws Exception
*/
public function setException($exception)
{
if (!($exception instanceof Exception)) {
throw new Exception("The exception renderer accepts only an Exception object.");
}
$this->exception = $exception;
}
/**
* @var array
*/
static protected $availableRenderers = array('xml',
protected static $availableRenderers = array('xml',
'json',
'csv',
'tsv',
@ -182,41 +152,25 @@ abstract class Renderer
*
* @return array
*/
static public function getRenderers()
public static function getRenderers()
{
return self::$availableRenderers;
}
/**
* Returns the DataTable associated to the output format $name
*
* @param string $name
* @throws Exception If the renderer is unknown
* @return \Piwik\DataTable\Renderer
*/
static public function factory($name)
protected static function getClassNameFromClassId($id)
{
$className = ucfirst(strtolower($name));
$className = ucfirst(strtolower($id));
$className = 'Piwik\DataTable\Renderer\\' . $className;
try {
Loader::loadClass($className);
return new $className;
} catch (Exception $e) {
$availableRenderers = implode(', ', self::getRenderers());
@header('Content-Type: text/plain; charset=utf-8');
throw new Exception(Piwik::translate('General_ExceptionInvalidRendererFormat', array($className, $availableRenderers)));
}
return $className;
}
/**
* Returns $rawData after all applicable characters have been converted to HTML entities.
*
* @param String $rawData data to be converted
* @return String
*/
static protected function renderHtmlEntities($rawData)
protected static function getInvalidClassIdExceptionMessage($id)
{
return self::formatValueXml($rawData);
$availableRenderers = implode(', ', self::getRenderers());
$klassName = self::getClassNameFromClassId($id);
return Piwik::translate('General_ExceptionInvalidRendererFormat', array($klassName, $availableRenderers));
}
/**
@ -236,12 +190,14 @@ abstract class Renderer
$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);
$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;
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -8,7 +8,6 @@
*/
namespace Piwik\DataTable\Renderer;
use Piwik\DataTable\Manager;
use Piwik\DataTable;
use Piwik\DataTable\Renderer;
@ -31,22 +30,9 @@ class Console extends Renderer
*/
public function render()
{
$this->renderHeader();
return $this->renderTable($this->table);
}
/**
* Computes the exception output and returns the string/binary
*
* @return string
*/
public function renderException()
{
$this->renderHeader();
$exceptionMessage = $this->getExceptionMessage();
return 'Error: ' . $exceptionMessage;
}
/**
* Sets the prefix to be used
*
@ -85,8 +71,9 @@ class Console extends Renderer
*/
protected function renderTable($table, $prefix = "")
{
if (is_array($table)) // convert array to DataTable
{
if (is_array($table)) {
// convert array to DataTable
$table = DataTable::makeFromSimpleArray($table);
}
@ -110,8 +97,11 @@ class Console extends Renderer
$dataTableMapBreak = true;
break;
}
if (is_string($value)) $value = "'$value'";
elseif (is_array($value)) $value = var_export($value, true);
if (is_string($value)) {
$value = "'$value'";
} elseif (is_array($value)) {
$value = var_export($value, true);
}
$columns[] = "'$column' => $value";
}
@ -122,8 +112,11 @@ class Console extends Renderer
$metadata = array();
foreach ($row->getMetadata() as $name => $value) {
if (is_string($value)) $value = "'$value'";
elseif (is_array($value)) $value = var_export($value, true);
if (is_string($value)) {
$value = "'$value'";
} elseif (is_array($value)) {
$value = var_export($value, true);
}
$metadata[] = "'$name' => $value";
}
$metadata = implode(", ", $metadata);
@ -133,14 +126,10 @@ class Console extends Renderer
. $row->getIdSubDataTable() . "]<br />\n";
if (!is_null($row->getIdSubDataTable())) {
if ($row->isSubtableLoaded()) {
$subTable = $row->getSubtable();
if ($subTable) {
$depth++;
$output .= $this->renderTable(
Manager::getInstance()->getTable(
$row->getIdSubDataTable()
),
$prefix . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'
);
$output .= $this->renderTable($subTable, $prefix . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;');
$depth--;
} else {
$output .= "-- Sub DataTable not loaded<br />\n";
@ -155,7 +144,7 @@ class Console extends Renderer
foreach ($metadata as $id => $metadataIn) {
$output .= "<br />";
$output .= $prefix . " <b>$id</b><br />";
if(is_array($metadataIn)) {
if (is_array($metadataIn)) {
foreach ($metadataIn as $name => $value) {
$output .= $prefix . $prefix . "$name => $value";
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -70,6 +70,8 @@ class Csv extends Renderer
*/
const NO_DATA_AVAILABLE = 'No data available';
private $unsupportedColumns = array();
/**
* Computes the dataTable output and returns the string/binary
*
@ -84,26 +86,10 @@ class Csv extends Renderer
$this->renderHeader();
if ($this->convertToUnicode
&& function_exists('mb_convert_encoding')
) {
$str = chr(255) . chr(254) . mb_convert_encoding($str, 'UTF-16LE', 'UTF-8');
}
$str = $this->convertToUnicode($str);
return $str;
}
/**
* Computes the exception output and returns the string/binary
*
* @return string
*/
function renderException()
{
@header('Content-Type: text/html; charset=utf-8');
$exceptionMessage = $this->getExceptionMessage();
return 'Error: ' . $exceptionMessage;
}
/**
* Enables / Disables unicode converting
*
@ -133,8 +119,9 @@ class Csv extends Renderer
*/
protected function renderTable($table, &$allColumns = array())
{
if (is_array($table)) // convert array to DataTable
{
if (is_array($table)) {
// convert array to DataTable
$table = DataTable::makeFromSimpleArray($table);
}
@ -205,42 +192,7 @@ class Csv extends Renderer
}
}
$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 dont overwrite
if ($this->translateColumnNames) {
$name = Piwik::translate('General_Metadata') . ': ' . $name;
} else {
$name = 'metadata_' . $name;
}
$csvRow[$name] = $value;
}
}
foreach ($csvRow as $name => $value) {
$allColumns[$name] = true;
}
if ($this->exportIdSubtable) {
$idsubdatatable = $row->getIdSubDataTable();
if ($idsubdatatable !== false
&& $this->hideIdSubDatatable === false
) {
$csvRow['idsubdatatable'] = $idsubdatatable;
}
}
$csv[] = $csvRow;
}
$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) {
@ -251,31 +203,7 @@ class Csv extends Renderer
}
}
$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));
$str = $this->buildCsvString($allColumns, $csv);
return $str;
}
@ -287,9 +215,20 @@ class Csv extends Renderer
*/
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);
}
@ -334,14 +273,15 @@ class Csv extends Renderer
$period = Common::getRequestVar('period', false);
$date = Common::getRequestVar('date', false);
if ($period || $date) // in test cases, there are no request params set
{
if ($period || $date) {
// in test cases, there are no request params set
if ($period == 'range') {
$period = new Range($period, $date);
} else if (strpos($date, ',') !== false) {
} elseif (strpos($date, ',') !== false) {
$period = new Range('range', $date);
} else {
$period = Period::factory($period, Date::factory($date));
$period = Period\Factory::build($period, Date::factory($date));
}
$prettyDate = $period->getLocalizedLongString();
@ -353,8 +293,7 @@ class Csv extends Renderer
}
// silent fail otherwise unit tests fail
@header('Content-Type: application/vnd.ms-excel');
@header('Content-Disposition: attachment; filename="' . $fileName . '"');
Common::sendHeader('Content-Disposition: attachment; filename="' . $fileName . '"', true);
ProxyHttp::overrideCacheControlHeaders();
}
@ -400,4 +339,119 @@ class Csv extends Renderer
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 dont 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;
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -29,27 +29,18 @@ class Html extends Renderer
*
* @param string $id
*/
function setTableId($id)
public function setTableId($id)
{
$this->tableId = str_replace('.', '_', $id);
}
/**
* Output HTTP Content-Type header
*/
protected function renderHeader()
{
@header('Content-Type: text/html; charset=utf-8');
}
/**
* Computes the dataTable output and returns the string/binary
*
* @return string
*/
function render()
public function render()
{
$this->renderHeader();
$this->tableStructure = array();
$this->allColumns = array();
$this->i = 0;
@ -57,18 +48,6 @@ class Html extends Renderer
return $this->renderTable($this->table);
}
/**
* Computes the exception output and returns the string/binary
*
* @return string
*/
function renderException()
{
$this->renderHeader();
$exceptionMessage = $this->getExceptionMessage();
return nl2br($exceptionMessage);
}
/**
* Computes the output for the given data table
*
@ -77,8 +56,9 @@ class Html extends Renderer
*/
protected function renderTable($table)
{
if (is_array($table)) // convert array to DataTable
{
if (is_array($table)) {
// convert array to DataTable
$table = DataTable::makeFromSimpleArray($table);
}
@ -88,8 +68,9 @@ class Html extends Renderer
$this->buildTableStructure($subtable, '_' . $table->getKeyName(), $date);
}
}
} else // Simple
{
} else {
// Simple
if ($table->getRowsCount()) {
$this->buildTableStructure($table);
}
@ -134,7 +115,9 @@ class Html extends Renderer
$metadata = array();
foreach ($row->getMetadata() as $name => $value) {
if (is_string($value)) $value = "'$value'";
if (is_string($value)) {
$value = "'$value'";
}
$metadata[] = "'$name' => $value";
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -11,7 +11,6 @@ namespace Piwik\DataTable\Renderer;
use Piwik\Common;
use Piwik\DataTable\Renderer;
use Piwik\DataTable;
use Piwik\ProxyHttp;
/**
* JSON export.
@ -27,27 +26,9 @@ class Json extends Renderer
*/
public function render()
{
$this->renderHeader();
return $this->renderTable($this->table);
}
/**
* Computes the exception output and returns the string/binary
*
* @return string
*/
function renderException()
{
$this->renderHeader();
$exceptionMessage = $this->getExceptionMessage();
$exceptionMessage = str_replace(array("\r\n", "\n"), "", $exceptionMessage);
$result = json_encode(array('result' => 'error', 'message' => $exceptionMessage));
return $this->jsonpWrap($result);
}
/**
* Computes the output for the given data table
*
@ -73,7 +54,6 @@ class Json extends Renderer
}
}
}
} else {
$array = $this->convertDataTableToArray($table);
}
@ -90,40 +70,15 @@ class Json extends Renderer
};
array_walk_recursive($array, $callback);
$str = json_encode($array);
return $this->jsonpWrap($str);
}
/**
* @param $str
* @return string
*/
protected function jsonpWrap($str)
{
if (($jsonCallback = Common::getRequestVar('callback', false)) === false)
$jsonCallback = Common::getRequestVar('jsoncallback', false);
if ($jsonCallback !== false) {
if (preg_match('/^[0-9a-zA-Z_.]*$/D', $jsonCallback) > 0) {
$str = $jsonCallback . "(" . $str . ")";
}
}
// silence "Warning: json_encode(): Invalid UTF-8 sequence in argument"
$str = @json_encode($array);
return $str;
}
/**
* Sends the http header for json file
*/
protected function renderHeader()
{
self::sendHeaderJSON();
ProxyHttp::overrideCacheControlHeaders();
}
public static function sendHeaderJSON()
{
@header('Content-Type: application/json; charset=utf-8');
Common::sendHeader('Content-Type: application/json; charset=utf-8');
}
private function convertDataTableToArray($table)

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -9,7 +9,6 @@
namespace Piwik\DataTable\Renderer;
use Exception;
use Piwik\DataTable\Manager;
use Piwik\DataTable\Renderer;
use Piwik\DataTable\Simple;
use Piwik\DataTable;
@ -71,8 +70,6 @@ class Php extends Renderer
*/
public function render($dataTable = null)
{
$this->renderHeader();
if (is_null($dataTable)) {
$dataTable = $this->table;
}
@ -87,26 +84,6 @@ class Php extends Renderer
return $toReturn;
}
/**
* Computes the exception output and returns the string/binary
*
* @return string
*/
public function renderException()
{
$this->renderHeader();
$exceptionMessage = $this->getExceptionMessage();
$return = array('result' => 'error', 'message' => $exceptionMessage);
if ($this->serialize) {
$return = serialize($return);
}
return $return;
}
/**
* Produces a flat php array from the DataTable, putting "columns" and "metadata" on the same level.
*
@ -133,7 +110,7 @@ class Php extends Renderer
if (self::shouldWrapArrayBeforeRendering($flatArray)) {
$flatArray = array($flatArray);
}
} else if ($dataTable instanceof DataTable\Map) {
} elseif ($dataTable instanceof DataTable\Map) {
$flatArray = array();
foreach ($dataTable->getDataTables() as $keyName => $table) {
$serializeSave = $this->serialize;
@ -141,7 +118,7 @@ class Php extends Renderer
$flatArray[$keyName] = $this->flatRender($table);
$this->serialize = $serializeSave;
}
} else if ($dataTable instanceof Simple) {
} 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
@ -228,10 +205,11 @@ class Php extends Renderer
$newRow['issummaryrow'] = true;
}
$subTable = $row->getSubtable();
if ($this->isRenderSubtables()
&& $row->isSubtableLoaded()
&& $subTable
) {
$subTable = $this->renderTable(Manager::getInstance()->getTable($row->getIdSubDataTable()));
$subTable = $this->renderTable($subTable);
$newRow['subtable'] = $subTable;
if ($this->hideIdSubDatatable === false
&& isset($newRow['metadata']['idsubdatatable_in_db'])

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -15,7 +15,6 @@ use Piwik\DataTable\Renderer;
use Piwik\DataTable;
use Piwik\Date;
use Piwik\SettingsPiwik;
use Piwik\Url;
/**
* RSS Feed.
@ -30,24 +29,11 @@ class Rss extends Renderer
*
* @return string
*/
function render()
public function render()
{
$this->renderHeader();
return $this->renderTable($this->table);
}
/**
* Computes the exception output and returns the string/binary
*
* @return string
*/
function renderException()
{
header('Content-type: text/plain');
$exceptionMessage = $this->getExceptionMessage();
return 'Error: ' . $exceptionMessage;
}
/**
* Computes the output for the given data table
*
@ -101,14 +87,6 @@ class Rss extends Renderer
return $header . $out . $footer;
}
/**
* Sends the xml file http header
*/
protected function renderHeader()
{
@header('Content-Type: text/xml; charset=utf-8');
}
/**
* Returns the RSS file footer
*
@ -185,7 +163,6 @@ class Rss extends Renderer
}
}
$html .= "\n</tr>";
$colspan = count($allColumns);
foreach ($tableStructure as $row) {
$html .= "\n\n<tr>";

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -8,7 +8,6 @@
*/
namespace Piwik\DataTable\Renderer;
/**
* TSV export
*
@ -21,7 +20,7 @@ class Tsv extends Csv
/**
* Constructor
*/
function __construct()
public function __construct()
{
parent::__construct();
$this->setSeparator("\t");
@ -32,7 +31,7 @@ class Tsv extends Csv
*
* @return string
*/
function render()
public function render()
{
return parent::render();
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -30,31 +30,11 @@ class Xml extends Renderer
*
* @return string
*/
function render()
public function render()
{
$this->renderHeader();
return '<?xml version="1.0" encoding="utf-8" ?>' . "\n" . $this->renderTable($this->table);
}
/**
* Computes the exception output and returns the string/binary
*
* @return string
*/
function renderException()
{
$this->renderHeader();
$exceptionMessage = $this->getExceptionMessage();
$return = '<?xml version="1.0" encoding="utf-8" ?>' . "\n" .
"<result>\n" .
"\t<error message=\"" . $exceptionMessage . "\" />\n" .
"</result>";
return $return;
}
/**
* Converts the given data table to an array
*
@ -174,17 +154,16 @@ class Xml extends Renderer
foreach ($array as $key => $value) {
// based on the type of array & the key, determine how this node will look
if ($isAssociativeArray) {
$keyIsInvalidXmlElement = is_numeric($key) || is_numeric($key[0]);
if ($keyIsInvalidXmlElement) {
$prefix = "<row key=\"$key\">";
$suffix = "</row>";
$emptyNode = "<row key=\"$key\"/>";
} else if (strpos($key, '=') !== false) {
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>";
@ -201,7 +180,7 @@ class Xml extends Renderer
$result .= $prefixLines . $prefix . "\n";
$result .= $this->renderArray($value, $prefixLines . "\t");
$result .= $prefixLines . $suffix . "\n";
} else if ($value instanceof DataTable
} elseif ($value instanceof DataTable
|| $value instanceof Map
) {
if ($value->getRowsCount() == 0) {
@ -210,7 +189,7 @@ class Xml extends Renderer
$result .= $prefixLines . $prefix . "\n";
if ($value instanceof Map) {
$result .= $this->renderDataTableMap($value, $this->getArrayFromDataTable($value), $prefixLines);
} else if ($value instanceof Simple) {
} elseif ($value instanceof Simple) {
$result .= $this->renderDataTableSimple($this->getArrayFromDataTable($value), $prefixLines);
} else {
$result .= $this->renderDataTable($this->getArrayFromDataTable($value), $prefixLines);
@ -358,6 +337,8 @@ class Xml extends Renderer
*/
protected function renderDataTable($array, $prefixLine = "")
{
$columnsHaveInvalidChars = $this->areTableLabelsInvalidXmlTagNames(reset($array));
$out = '';
foreach ($array as $rowId => $row) {
if (!is_array($row)) {
@ -370,10 +351,9 @@ class Xml extends Renderer
continue;
}
// Handing case idgoal=7, creating a new array for that one
$rowAttribute = '';
if (($equalFound = strstr($rowId, '=')) !== false) {
if (strstr($rowId, '=') !== false) {
$rowAttribute = explode('=', $rowId);
$rowAttribute = " " . $rowAttribute[0] . "='" . $rowAttribute[1] . "'";
}
@ -394,10 +374,13 @@ class Xml extends Renderer
} else {
$value = self::formatValueXml($value);
}
list($tagStart, $tagEnd) = $this->getTagStartAndEndFor($name, $columnsHaveInvalidChars);
if (strlen($value) == 0) {
$out .= $prefixLine . "\t\t<$name />\n";
$out .= $prefixLine . "\t\t<$tagStart />\n";
} else {
$out .= $prefixLine . "\t\t<$name>" . $value . "</$name>\n";
$out .= $prefixLine . "\t\t<$tagStart>" . $value . "</$tagEnd>\n";
}
}
$out .= "\t";
@ -420,24 +403,62 @@ class Xml extends Renderer
$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<$keyName />\n";
$out .= $prefixLine . "\t<$tagStart />\n";
} else {
$out .= $prefixLine . "\t<$keyName>" . $xmlValue . "</$keyName>\n";
$out .= $prefixLine . "\t<$tagStart>" . $xmlValue . "</$tagEnd>\n";
}
}
return $out;
}
/**
* Sends the XML headers
* Returns true if a string is a valid XML tag name, false if otherwise.
*
* @param string $str
* @return bool
*/
protected function renderHeader()
private static function isValidXmlTagName($str)
{
// silent fail because otherwise it throws an exception in the unit tests
@header('Content-Type: text/xml; charset=utf-8');
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

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -10,18 +10,18 @@ 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
class Row implements \ArrayAccess, \IteratorAggregate
{
/**
* List of columns that cannot be summed. An associative array for speed.
@ -30,27 +30,21 @@ class Row
*/
private static $unsummableColumns = array(
'label' => true,
'full_url' => true // column used w/ old Piwik versions
'full_url' => true // column used w/ old Piwik versions,
);
/**
* This array contains the row information:
* - array indexed by self::COLUMNS contains the columns, pairs of (column names, value)
* - (optional) array indexed by self::METADATA contains the metadata, pairs of (metadata name, value)
* - (optional) integer indexed by self::DATATABLE_ASSOCIATED contains the ID of the DataTable associated to this row.
* This ID can be used to read the DataTable from the DataTable_Manager.
*
* @var array
* @see constructor for more information
* @ignore
*/
public $c = array();
private $subtableIdWasNegativeBeforeSerialize = false;
// @see sumRow - implementation detail
public $maxVisitsSummed = 0;
private $columns = array();
private $metadata = array();
private $isSubtableLoaded = false;
/**
* @internal
*/
public $subtableId = null;
const COLUMNS = 0;
const METADATA = 1;
const DATATABLE_ASSOCIATED = 3;
@ -59,7 +53,7 @@ class Row
* Constructor.
*
* @param array $row An array with the following structure:
*
*
* array(
* Row::COLUMNS => array('label' => 'Piwik',
* 'column1' => 42,
@ -72,51 +66,33 @@ class Row
*/
public function __construct($row = array())
{
$this->c[self::COLUMNS] = array();
$this->c[self::METADATA] = array();
$this->c[self::DATATABLE_ASSOCIATED] = null;
if (isset($row[self::COLUMNS])) {
$this->c[self::COLUMNS] = $row[self::COLUMNS];
$this->columns = $row[self::COLUMNS];
}
if (isset($row[self::METADATA])) {
$this->c[self::METADATA] = $row[self::METADATA];
$this->metadata = $row[self::METADATA];
}
if (isset($row[self::DATATABLE_ASSOCIATED])
&& $row[self::DATATABLE_ASSOCIATED] instanceof DataTable
) {
$this->setSubtable($row[self::DATATABLE_ASSOCIATED]);
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];
}
}
}
/**
* Because $this->c[self::DATATABLE_ASSOCIATED] is negative when the table is in memory,
* we must prior to serialize() call, make sure the ID is saved as positive integer
*
* Only serialize the "c" member
* Used when archiving to serialize the Row's properties.
* @return array
* @ignore
*/
public function __sleep()
public function export()
{
if (!empty($this->c[self::DATATABLE_ASSOCIATED])
&& $this->c[self::DATATABLE_ASSOCIATED] < 0
) {
$this->c[self::DATATABLE_ASSOCIATED] = -1 * $this->c[self::DATATABLE_ASSOCIATED];
$this->subtableIdWasNegativeBeforeSerialize = true;
}
return array('c');
}
/**
* Must be called after the row was serialized and __sleep was called.
* @ignore
*/
public function cleanPostSerialize()
{
if ($this->subtableIdWasNegativeBeforeSerialize) {
$this->c[self::DATATABLE_ASSOCIATED] = -1 * $this->c[self::DATATABLE_ASSOCIATED];
$this->subtableIdWasNegativeBeforeSerialize = false;
}
return array(
self::COLUMNS => $this->columns,
self::METADATA => $this->metadata,
self::DATATABLE_ASSOCIATED => $this->subtableId,
);
}
/**
@ -125,9 +101,10 @@ class Row
*/
public function __destruct()
{
if ($this->isSubtableLoaded()) {
Manager::getInstance()->deleteTable($this->getIdSubDataTable());
$this->c[self::DATATABLE_ASSOCIATED] = null;
if ($this->isSubtableLoaded) {
Manager::getInstance()->deleteTable($this->subtableId);
$this->subtableId = null;
$this->isSubtableLoaded = false;
}
}
@ -141,15 +118,21 @@ class Row
{
$columns = array();
foreach ($this->getColumns() as $column => $value) {
if (is_string($value)) $value = "'$value'";
elseif (is_array($value)) $value = var_export($value, true);
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);
if (is_string($value)) {
$value = "'$value'";
} elseif (is_array($value)) {
$value = var_export($value, true);
}
$metadata[] = "'$name' => $value";
}
$metadata = implode(", ", $metadata);
@ -165,10 +148,11 @@ class Row
*/
public function deleteColumn($name)
{
if (!array_key_exists($name, $this->c[self::COLUMNS])) {
if (!array_key_exists($name, $this->columns)) {
return false;
}
unset($this->c[self::COLUMNS][$name]);
unset($this->columns[$name]);
return true;
}
@ -180,11 +164,12 @@ class Row
*/
public function renameColumn($oldName, $newName)
{
if (isset($this->c[self::COLUMNS][$oldName])) {
$this->c[self::COLUMNS][$newName] = $this->c[self::COLUMNS][$oldName];
if (isset($this->columns[$oldName])) {
$this->columns[$newName] = $this->columns[$oldName];
}
// outside the if() since we want to delete nulled columns
unset($this->c[self::COLUMNS][$oldName]);
// outside the if () since we want to delete nulled columns
unset($this->columns[$oldName]);
}
/**
@ -195,10 +180,11 @@ class Row
*/
public function getColumn($name)
{
if (!isset($this->c[self::COLUMNS][$name])) {
if (!isset($this->columns[$name])) {
return false;
}
return $this->c[self::COLUMNS][$name];
return $this->columns[$name];
}
/**
@ -210,19 +196,31 @@ class Row
public function getMetadata($name = null)
{
if (is_null($name)) {
return $this->c[self::METADATA];
return $this->metadata;
}
if (!isset($this->c[self::METADATA][$name])) {
if (!isset($this->metadata[$name])) {
return false;
}
return $this->c[self::METADATA][$name];
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 array_key_exists($name, $this->columns);
}
/**
* Returns the array containing all the columns.
*
* @return array Example:
*
*
* array(
* 'column1' => VALUE,
* 'label' => 'www.php.net'
@ -231,7 +229,7 @@ class Row
*/
public function getColumns()
{
return $this->c[self::COLUMNS];
return $this->columns;
}
/**
@ -242,10 +240,7 @@ class Row
*/
public function getIdSubDataTable()
{
return !is_null($this->c[self::DATATABLE_ASSOCIATED])
// abs() is to ensure we return a positive int, @see isSubtableLoaded()
? abs($this->c[self::DATATABLE_ASSOCIATED])
: null;
return $this->subtableId;
}
/**
@ -255,48 +250,49 @@ class Row
*/
public function getSubtable()
{
if ($this->isSubtableLoaded()) {
return Manager::getInstance()->getTable($this->getIdSubDataTable());
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()) {
if ($this->isSubtableLoaded) {
$thisSubTable = $this->getSubtable();
} else {
$this->warnIfSubtableAlreadyExists();
$thisSubTable = new DataTable();
$this->addSubtable($thisSubTable);
$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.
*
* @param DataTable $subTable DataTable to associate to this row.
* @return DataTable Returns `$subTable`.
* @throws Exception if a subtable already exists for this row.
*/
public function addSubtable(DataTable $subTable)
{
if (!is_null($this->c[self::DATATABLE_ASSOCIATED])) {
throw new Exception("Adding a subtable to the row, but it already has a subtable associated.");
}
return $this->setSubtable($subTable);
}
/**
* Attaches a subtable to this row, overwriting the existing subtable,
* if any.
@ -306,9 +302,9 @@ class Row
*/
public function setSubtable(DataTable $subTable)
{
// Hacking -1 to ensure value is negative, so we know the table was loaded
// @see isSubtableLoaded()
$this->c[self::DATATABLE_ASSOCIATED] = -1 * $subTable->getId();
$this->subtableId = $subTable->getId();
$this->isSubtableLoaded = true;
return $subTable;
}
@ -321,8 +317,7 @@ class Row
{
// self::DATATABLE_ASSOCIATED are set as negative values,
// as a flag to signify that the subtable is loaded in memory
return !is_null($this->c[self::DATATABLE_ASSOCIATED])
&& $this->c[self::DATATABLE_ASSOCIATED] < 0;
return $this->isSubtableLoaded;
}
/**
@ -330,17 +325,18 @@ class Row
*/
public function removeSubtable()
{
$this->c[self::DATATABLE_ASSOCIATED] = null;
$this->subtableId = null;
$this->isSubtableLoaded = false;
}
/**
* Set all the columns at once. Overwrites **all** previously set columns.
*
* @param array eg, `array('label' => 'www.php.net', 'nb_visits' => 15894)`
* @param array $columns eg, `array('label' => 'www.php.net', 'nb_visits' => 15894)`
*/
public function setColumns($columns)
{
$this->c[self::COLUMNS] = $columns;
$this->columns = $columns;
}
/**
@ -351,7 +347,7 @@ class Row
*/
public function setColumn($name, $value)
{
$this->c[self::COLUMNS][$name] = $value;
$this->columns[$name] = $value;
}
/**
@ -362,7 +358,7 @@ class Row
*/
public function setMetadata($name, $value)
{
$this->c[self::METADATA][$name] = $value;
$this->metadata[$name] = $value;
}
/**
@ -374,13 +370,13 @@ class Row
public function deleteMetadata($name = false)
{
if ($name === false) {
$this->c[self::METADATA] = array();
$this->metadata = array();
return true;
}
if (!isset($this->c[self::METADATA][$name])) {
if (!isset($this->metadata[$name])) {
return false;
}
unset($this->c[self::METADATA][$name]);
unset($this->metadata[$name]);
return true;
}
@ -388,15 +384,15 @@ class Row
* 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.
* @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->c[self::COLUMNS][$name])) {
if (isset($this->columns[$name])) {
throw new Exception("Column $name already in the array!");
}
$this->c[self::COLUMNS][$name] = $value;
$this->setColumn($name, $value);
}
/**
@ -429,51 +425,61 @@ class Row
*/
public function addMetadata($name, $value)
{
if (isset($this->c[self::METADATA][$name])) {
if (isset($this->metadata[$name])) {
throw new Exception("Metadata $name already in the array!");
}
$this->c[self::METADATA][$name] = $value;
$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 $aggregationOperations for columns that should not be summed, determine which
* @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->getColumns() as $columnToSumName => $columnToSumValue) {
if (!isset(self::$unsummableColumns[$columnToSumName])) // make sure we can add this column
{
$thisColumnValue = $this->getColumn($columnToSumName);
$operation = (is_array($aggregationOperations) && isset($aggregationOperations[$columnToSumName]) ?
strtolower($aggregationOperations[$columnToSumName]) : 'sum');
// 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->setColumn($columnToSumName, $newValue);
if (!$this->isSummableColumn($columnToSumName)) {
continue;
}
$thisColumnValue = $this->getColumn($columnToSumName);
$operation = 'sum';
if (is_array($aggregationOperations) && isset($aggregationOperations[$columnToSumName])) {
$operation = strtolower($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->setColumn($columnToSumName, $newValue);
}
if ($enableCopyMetadata) {
$this->sumRowMetadata($rowToSum);
$this->sumRowMetadata($rowToSum, $aggregationOperations);
}
}
@ -491,7 +497,7 @@ class Row
case 'min':
if (!$thisColumnValue) {
$newValue = $columnToSumValue;
} else if (!$columnToSumValue) {
} elseif (!$columnToSumValue) {
$newValue = $thisColumnValue;
} else {
$newValue = min($thisColumnValue, $columnToSumValue);
@ -500,6 +506,19 @@ class Row
case 'sum':
$newValue = $this->sumRowArray($thisColumnValue, $columnToSumValue);
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:
throw new Exception("Unknown operation '$operation'.");
}
@ -508,23 +527,45 @@ class Row
/**
* Sums the metadata in `$rowToSum` with the metadata in `$this` row.
*
*
* @param Row $rowToSum
* @param array $aggregationOperations
*/
public function sumRowMetadata($rowToSum)
public function sumRowMetadata($rowToSum, $aggregationOperations = array())
{
if (!empty($rowToSum->c[self::METADATA])
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 $columnn => $operation) {
$thisMetadata = $this->getMetadata($columnn);
$sumMetadata = $rowToSum->getMetadata($columnn);
if ($thisMetadata === false && $sumMetadata === false) {
continue;
}
$aggregatedMetadata[$columnn] = $this->getColumnValuesMerged($operation, $thisMetadata, $sumMetadata);
}
}
// 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->c[self::METADATA])
|| empty($this->metadata)
) {
$this->maxVisitsSummed = $visits;
$this->c[self::METADATA] = $rowToSum->c[self::METADATA];
$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);
}
}
}
@ -532,7 +573,7 @@ class Row
/**
* 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()
@ -558,10 +599,15 @@ class Row
return $thisColumnValue + $columnToSumValue;
}
if ($columnToSumValue === false) {
return $thisColumnValue;
}
if ($thisColumnValue === false) {
return $columnToSumValue;
}
if (is_array($columnToSumValue)) {
if ($thisColumnValue == false) {
return $columnToSumValue;
}
$newValue = $thisColumnValue;
foreach ($columnToSumValue as $arrayIndex => $arrayValue) {
if (!isset($newValue[$arrayIndex])) {
@ -572,16 +618,7 @@ class Row
return $newValue;
}
if (is_string($columnToSumValue)) {
if ($thisColumnValue === false) {
return $columnToSumValue;
} else if ($columnToSumValue === false) {
return $thisColumnValue;
} else {
throw new Exception("Trying to add two strings values in DataTable\Row::sumRowArray: "
. "'$thisColumnValue' + '$columnToSumValue'");
}
}
$this->warnWhenSummingTwoStrings($thisColumnValue, $columnToSumValue);
return 0;
}
@ -594,7 +631,7 @@ class Row
* @return bool
* @ignore
*/
static public function compareElements($elem1, $elem2)
public static function compareElements($elem1, $elem2)
{
if (is_array($elem1)) {
if (is_array($elem2)) {
@ -602,11 +639,13 @@ class Row
}
return 1;
}
if (is_array($elem2))
if (is_array($elem2)) {
return -1;
}
if ((string)$elem1 === (string)$elem2)
if ((string)$elem1 === (string)$elem2) {
return 0;
}
return ((string)$elem1 > (string)$elem2) ? 1 : -1;
}
@ -615,17 +654,17 @@ class Row
* 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
*/
static public function isEqual(Row $row1, Row $row2)
public static function isEqual(Row $row1, Row $row2)
{
//same columns
$cols1 = $row1->getColumns();
@ -662,4 +701,53 @@ class Row
}
return true;
}
public function offsetExists($offset)
{
return $this->hasColumn($offset);
}
public function offsetGet($offset)
{
return $this->getColumn($offset);
}
public function offsetSet($offset, $value)
{
$this->setColumn($offset, $value);
}
public function offsetUnset($offset)
{
$this->deleteColumn($offset);
}
public function getIterator()
{
return new \ArrayIterator($this->columns);
}
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)
{
if (is_string($columnToSumValue)) {
Log::warning(
"Trying to add two strings in DataTable\Row::sumRowArray: %s + %s for row %s",
$thisColumnValue,
$columnToSumValue,
$this->__toString()
);
}
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -8,15 +8,14 @@
*/
namespace Piwik\DataTable\Row;
use Piwik\DataTable\Manager;
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.
*
@ -27,7 +26,7 @@ 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
@ -35,9 +34,7 @@ class DataTableSummaryRow extends Row
*/
public function __construct($subTable = null)
{
parent::__construct();
if ($subTable !== null) {
if (isset($subTable)) {
$this->sumTable($subTable);
}
}
@ -47,9 +44,8 @@ class DataTableSummaryRow extends Row
*/
public function recalculate()
{
$id = $this->getIdSubDataTable();
if ($id !== null) {
$subTable = Manager::getInstance()->getTable($id);
$subTable = $this->getSubtable();
if ($subTable) {
$this->sumTable($subTable);
}
}
@ -61,8 +57,17 @@ class DataTableSummaryRow extends Row
*/
private function sumTable($table)
{
foreach ($table->getRows() as $row) {
$this->sumRow($row, $enableCopyMetadata = false, $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME));
$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

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -12,7 +12,7 @@ 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).
*
@ -25,7 +25,7 @@ class Simple extends DataTable
* values.
*
* @param array $array Array containing the rows, eg,
*
*
* array(
* 'Label row 1' => $value1,
* 'Label row 2' => $value2,

View file

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