1<?php
2/**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9
10namespace Piwik;
11
12use Closure;
13use Exception;
14use Piwik\Archive\DataTableFactory;
15use Piwik\DataTable\DataTableInterface;
16use Piwik\DataTable\Manager;
17use Piwik\DataTable\Renderer\Html;
18use Piwik\DataTable\Row;
19use Piwik\DataTable\Row\DataTableSummaryRow;
20use Piwik\DataTable\Simple;
21use ReflectionClass;
22
23/**
24 * @see Common::destroy()
25 */
26require_once PIWIK_INCLUDE_PATH . '/core/Common.php';
27require_once PIWIK_INCLUDE_PATH . "/core/DataTable/Bridges.php";
28
29/**
30 * The primary data structure used to store analytics data in Piwik.
31 *
32 * <a name="class-desc-the-basics"></a>
33 * ### The Basics
34 *
35 * DataTables consist of rows and each row consists of columns. A column value can be
36 * a numeric, a string or an array.
37 *
38 * Every row has an ID. The ID is either the index of the row or {@link ID_SUMMARY_ROW}.
39 *
40 * DataTables are hierarchical data structures. Each row can also contain an additional
41 * nested sub-DataTable (commonly referred to as a 'subtable').
42 *
43 * Both DataTables and DataTable rows can hold **metadata**. _DataTable metadata_ is information
44 * regarding all the data, such as the site or period that the data is for. _Row metadata_
45 * is information regarding that row, such as a browser logo or website URL.
46 *
47 * Finally, all DataTables contain a special _summary_ row. This row, if it exists, is
48 * always at the end of the DataTable.
49 *
50 * ### Populating DataTables
51 *
52 * Data can be added to DataTables in three different ways. You can either:
53 *
54 * 1. create rows one by one and add them through {@link addRow()} then truncate if desired,
55 * 2. create an array of DataTable\Row instances or an array of arrays and add them using
56 *    {@link addRowsFromArray()} or {@link addRowsFromSimpleArray()}
57 *    then truncate if desired,
58 * 3. or set the maximum number of allowed rows (with {@link setMaximumAllowedRows()})
59 *    and add rows one by one.
60 *
61 * If you want to eventually truncate your data (standard practice for all Piwik plugins),
62 * the third method is the most memory efficient. It is, unfortunately, not always possible
63 * to use since it requires that the data be sorted before adding.
64 *
65 * ### Manipulating DataTables
66 *
67 * There are two ways to manipulate a DataTable. You can either:
68 *
69 * 1. manually iterate through each row and manipulate the data,
70 * 2. or you can use predefined filters.
71 *
72 * A filter is a class that has a 'filter' method which will manipulate a DataTable in
73 * some way. There are several predefined Filters that allow you to do common things,
74 * such as,
75 *
76 * - add a new column to each row,
77 * - add new metadata to each row,
78 * - modify an existing column value for each row,
79 * - sort an entire DataTable,
80 * - and more.
81 *
82 * Using these filters instead of writing your own code will increase code clarity and
83 * reduce code redundancy. Additionally, filters have the advantage that they can be
84 * applied to DataTable\Map instances. So you can visit every DataTable in a {@link DataTable\Map}
85 * without having to write a recursive visiting function.
86 *
87 * All predefined filters exist in the **Piwik\DataTable\BaseFilter** namespace.
88 *
89 * _Note: For convenience, [anonymous functions](http://www.php.net/manual/en/functions.anonymous.php)
90 * can be used as DataTable filters._
91 *
92 * ### Applying Filters
93 *
94 * Filters can be applied now (via {@link filter()}), or they can be applied later (via
95 * {@link queueFilter()}).
96 *
97 * Filters that sort rows or manipulate the number of rows should be applied right away.
98 * Non-essential, presentation filters should be queued.
99 *
100 * ### Learn more
101 *
102 * - See **{@link ArchiveProcessor}** to learn how DataTables are persisted.
103 *
104 * ### Examples
105 *
106 * **Populating a DataTable**
107 *
108 *     // adding one row at a time
109 *     $dataTable = new DataTable();
110 *     $dataTable->addRow(new Row(array(
111 *         Row::COLUMNS => array('label' => 'thing1', 'nb_visits' => 1, 'nb_actions' => 1),
112 *         Row::METADATA => array('url' => 'http://thing1.com')
113 *     )));
114 *     $dataTable->addRow(new Row(array(
115 *         Row::COLUMNS => array('label' => 'thing2', 'nb_visits' => 2, 'nb_actions' => 2),
116 *         Row::METADATA => array('url' => 'http://thing2.com')
117 *     )));
118 *
119 *     // using an array of rows
120 *     $dataTable = new DataTable();
121 *     $dataTable->addRowsFromArray(array(
122 *         array(
123 *             Row::COLUMNS => array('label' => 'thing1', 'nb_visits' => 1, 'nb_actions' => 1),
124 *             Row::METADATA => array('url' => 'http://thing1.com')
125 *         ),
126 *         array(
127 *             Row::COLUMNS => array('label' => 'thing2', 'nb_visits' => 2, 'nb_actions' => 2),
128 *             Row::METADATA => array('url' => 'http://thing2.com')
129 *         )
130 *     ));
131 *
132 *     // using a "simple" array
133 *     $dataTable->addRowsFromSimpleArray(array(
134 *         array('label' => 'thing1', 'nb_visits' => 1, 'nb_actions' => 1),
135 *         array('label' => 'thing2', 'nb_visits' => 2, 'nb_actions' => 2)
136 *     ));
137 *
138 * **Getting & setting metadata**
139 *
140 *     $dataTable = \Piwik\Plugins\Referrers\API::getInstance()->getSearchEngines($idSite = 1, $period = 'day', $date = '2007-07-24');
141 *     $oldPeriod = $dataTable->metadata['period'];
142 *     $dataTable->metadata['period'] = Period\Factory::build('week', Date::factory('2013-10-18'));
143 *
144 * **Serializing & unserializing**
145 *
146 *     $maxRowsInTable = Config::getInstance()->General['datatable_archiving_maximum_rows_standard'];j
147 *
148 *     $dataTable = // ... build by aggregating visits ...
149 *     $serializedData = $dataTable->getSerialized($maxRowsInTable, $maxRowsInSubtable = $maxRowsInTable,
150 *                                                 $columnToSortBy = Metrics::INDEX_NB_VISITS);
151 *
152 *     $serializedDataTable = $serializedData[0];
153 *     $serailizedSubTable = $serializedData[$idSubtable];
154 *
155 * **Filtering for an API method**
156 *
157 *     public function getMyReport($idSite, $period, $date, $segment = false, $expanded = false)
158 *     {
159 *         $dataTable = Archive::createDataTableFromArchive('MyPlugin_MyReport', $idSite, $period, $date, $segment, $expanded);
160 *         $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS, 'desc', $naturalSort = false, $expanded));
161 *         $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', __NAMESPACE__ . '\getUrlFromLabelForMyReport'));
162 *         return $dataTable;
163 *     }
164 *
165 *
166 * @api
167 */
168class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
169{
170    const MAX_DEPTH_DEFAULT = 15;
171
172    /** Name for metadata that describes when a report was archived. */
173    const ARCHIVED_DATE_METADATA_NAME = 'ts_archived';
174
175    /** Name for metadata that describes which columns are empty and should not be shown. */
176    const EMPTY_COLUMNS_METADATA_NAME = 'empty_column';
177
178    /** Name for metadata that describes the number of rows that existed before the Limit filter was applied. */
179    const TOTAL_ROWS_BEFORE_LIMIT_METADATA_NAME = 'total_rows_before_limit';
180
181    /**
182     * Name for metadata that describes how individual columns should be aggregated when {@link addDataTable()}
183     * or {@link Piwik\DataTable\Row::sumRow()} is called.
184     *
185     * This metadata value must be an array that maps column names with valid operations. Valid aggregation operations are:
186     *
187     * - `'skip'`: do nothing
188     * - `'max'`: does `max($column1, $column2)`
189     * - `'min'`: does `min($column1, $column2)`
190     * - `'sum'`: does `$column1 + $column2`
191     *
192     * See {@link addDataTable()} and {@link DataTable\Row::sumRow()} for more information.
193     */
194    const COLUMN_AGGREGATION_OPS_METADATA_NAME = 'column_aggregation_ops';
195
196    /**
197     * Name for metadata that stores array of generic filters that should not be run on the table.
198     */
199    const GENERIC_FILTERS_TO_DISABLE_METADATA_NAME = 'generic_filters_to_disable';
200
201    /** The ID of the Summary Row. */
202    const ID_SUMMARY_ROW = -1;
203
204    /**
205     * The ID of the special metadata row. This row only exists in the serialized row data and stores the datatable metadata.
206     *
207     * This allows us to save datatable metadata in archive data.
208     */
209    const ID_ARCHIVED_METADATA_ROW = -3;
210
211    /** The original label of the Summary Row. */
212    const LABEL_SUMMARY_ROW = -1;
213    const LABEL_TOTALS_ROW = -2;
214    const LABEL_ARCHIVED_METADATA_ROW = '__datatable_metadata__';
215
216    /**
217     * Name for metadata that contains extra {@link Piwik\Plugin\ProcessedMetric}s for a DataTable.
218     * These metrics will be added in addition to the ones specified in the table's associated
219     * {@link Piwik\Plugin\Report} class.
220     */
221    const EXTRA_PROCESSED_METRICS_METADATA_NAME = 'extra_processed_metrics';
222
223    /**
224     * Maximum nesting level.
225     */
226    private static $maximumDepthLevelAllowed = self::MAX_DEPTH_DEFAULT;
227
228    /**
229     * Array of Row
230     *
231     * @var Row[]
232     */
233    protected $rows = array();
234
235    /**
236     * Id assigned to the DataTable, used to lookup the table using the DataTable_Manager
237     *
238     * @var int
239     */
240    protected $currentId;
241
242    /**
243     * Current depth level of this data table
244     * 0 is the parent data table
245     *
246     * @var int
247     */
248    protected $depthLevel = 0;
249
250    /**
251     * This flag is set to false once we modify the table in a way that outdates the index
252     *
253     * @var bool
254     */
255    protected $indexNotUpToDate = true;
256
257    /**
258     * This flag sets the index to be rebuild whenever a new row is added,
259     * as opposed to re-building the full index when getRowFromLabel is called.
260     * This is to optimize and not rebuild the full Index in the case where we
261     * add row, getRowFromLabel, addRow, getRowFromLabel thousands of times.
262     *
263     * @var bool
264     */
265    protected $rebuildIndexContinuously = false;
266
267    /**
268     * Column name of last time the table was sorted
269     *
270     * @var string
271     */
272    protected $tableSortedBy = false;
273
274    /**
275     * List of BaseFilter queued to this table
276     *
277     * @var array
278     */
279    protected $queuedFilters = array();
280
281    /**
282     * List of disabled filter names eg 'Limit' or 'Sort'
283     *
284     * @var array
285     */
286    protected $disabledFilters = array();
287
288    /**
289     * We keep track of the number of rows before applying the LIMIT filter that deletes some rows
290     *
291     * @var int
292     */
293    protected $rowsCountBeforeLimitFilter = 0;
294
295    /**
296     * Defaults to false for performance reasons (most of the time we don't need recursive sorting so we save a looping over the dataTable)
297     *
298     * @var bool
299     */
300    protected $enableRecursiveSort = false;
301
302    /**
303     * When the table and all subtables are loaded, this flag will be set to true to ensure filters are applied to all subtables
304     *
305     * @var bool
306     */
307    protected $enableRecursiveFilters = false;
308
309    /**
310     * @var array
311     */
312    protected $rowsIndexByLabel = array();
313
314    /**
315     * @var \Piwik\DataTable\Row
316     */
317    protected $summaryRow = null;
318
319    /**
320     * @var \Piwik\DataTable\Row
321     */
322    protected $totalsRow = null;
323
324    /**
325     * Table metadata. Read [this](#class-desc-the-basics) to learn more.
326     *
327     * Any data that describes the data held in the table's rows should go here.
328     *
329     * Note: this field is protected so derived classes will serialize it.
330     *
331     * @var array
332     */
333    protected $metadata = array();
334
335    /**
336     * Maximum number of rows allowed in this datatable (including the summary row).
337     * If adding more rows is attempted, the extra rows get summed to the summary row.
338     *
339     * @var int
340     */
341    protected $maximumAllowedRows = 0;
342
343    /**
344     * Constructor. Creates an empty DataTable.
345     */
346    public function __construct()
347    {
348        // registers this instance to the manager
349        $this->currentId = Manager::getInstance()->addTable($this);
350    }
351
352    /**
353     * Destructor. Makes sure DataTable memory will be cleaned up.
354     */
355    public function __destruct()
356    {
357        static $depth = 0;
358        // destruct can be called several times
359        if ($depth < self::$maximumDepthLevelAllowed
360            && isset($this->rows)
361        ) {
362            $depth++;
363            foreach ($this->rows as $row) {
364                Common::destroy($row);
365            }
366            if (isset($this->summaryRow)) {
367                Common::destroy($this->summaryRow);
368            }
369            unset($this->rows);
370            Manager::getInstance()->setTableDeleted($this->currentId);
371            $depth--;
372        }
373    }
374
375    /**
376     * Clone. Called when cloning the datatable. We need to make sure to create a new datatableId.
377     * If we do not increase tableId it can result in segmentation faults when destructing a datatable.
378     */
379    public function __clone()
380    {
381        // registers this instance to the manager
382        $this->currentId = Manager::getInstance()->addTable($this);
383    }
384
385    public function setLabelsHaveChanged()
386    {
387        $this->indexNotUpToDate = true;
388    }
389
390    /**
391     * @ignore
392     * does not update the summary row!
393     */
394    public function setRows($rows)
395    {
396        unset($this->rows);
397        $this->rows = $rows;
398        $this->indexNotUpToDate = true;
399    }
400
401    /**
402     * Sorts the DataTable rows using the supplied callback function.
403     *
404     * @param string $functionCallback A comparison callback compatible with {@link usort}.
405     * @param string $columnSortedBy The column name `$functionCallback` sorts by. This is stored
406     *                               so we can determine how the DataTable was sorted in the future.
407     */
408    public function sort($functionCallback, $columnSortedBy)
409    {
410        $this->setTableSortedBy($columnSortedBy);
411
412        usort($this->rows, $functionCallback);
413
414        if ($this->isSortRecursiveEnabled()) {
415            foreach ($this->getRowsWithoutSummaryRow() as $row) {
416                $subTable = $row->getSubtable();
417                if ($subTable) {
418                    $subTable->enableRecursiveSort();
419                    $subTable->sort($functionCallback, $columnSortedBy);
420                }
421            }
422        }
423    }
424
425    public function setTotalsRow(Row $totalsRow)
426    {
427        $this->totalsRow = $totalsRow;
428    }
429
430    public function getTotalsRow()
431    {
432        return $this->totalsRow;
433    }
434
435    public function getSummaryRow()
436    {
437        return $this->summaryRow;
438    }
439
440    /**
441     * Returns the name of the column this table was sorted by (if any).
442     *
443     * See {@link sort()}.
444     *
445     * @return false|string The sorted column name or false if none.
446     */
447    public function getSortedByColumnName()
448    {
449        return $this->tableSortedBy;
450    }
451
452    /**
453     * Enables recursive sorting. If this method is called {@link sort()} will also sort all
454     * subtables.
455     */
456    public function enableRecursiveSort()
457    {
458        $this->enableRecursiveSort = true;
459    }
460
461    /**
462     * @ignore
463     */
464    public function isSortRecursiveEnabled()
465    {
466        return $this->enableRecursiveSort === true;
467    }
468
469    /**
470     * @ignore
471     */
472    public function setTableSortedBy($column)
473    {
474        $this->indexNotUpToDate = true;
475        $this->tableSortedBy = $column;
476    }
477
478    /**
479     * Enables recursive filtering. If this method is called then the {@link filter()} method
480     * will apply filters to every subtable in addition to this instance.
481     */
482    public function enableRecursiveFilters()
483    {
484        $this->enableRecursiveFilters = true;
485    }
486
487    /**
488     * @ignore
489     */
490    public function disableRecursiveFilters()
491    {
492        $this->enableRecursiveFilters = false;
493    }
494
495    /**
496     * Applies a filter to this datatable.
497     *
498     * If {@link enableRecursiveFilters()} was called, the filter will be applied
499     * to all subtables as well.
500     *
501     * @param string|Closure $className Class name, eg. `"Sort"` or "Piwik\DataTable\Filters\Sort"`. If no
502     *                                  namespace is supplied, `Piwik\DataTable\BaseFilter` is assumed. This parameter
503     *                                  can also be a closure that takes a DataTable as its first parameter.
504     * @param array $parameters Array of extra parameters to pass to the filter.
505     */
506    public function filter($className, $parameters = array())
507    {
508        if ($className instanceof \Closure
509            || is_array($className)
510        ) {
511            array_unshift($parameters, $this);
512            call_user_func_array($className, $parameters);
513            return;
514        }
515
516        if (in_array($className, $this->disabledFilters)) {
517            return;
518        }
519
520        if (!class_exists($className, true)) {
521            $className = 'Piwik\DataTable\Filter\\' . $className;
522        }
523        $reflectionObj = new ReflectionClass($className);
524
525        // the first parameter of a filter is the DataTable
526        // we add the current datatable as the parameter
527        $parameters = array_merge(array($this), $parameters);
528
529        $filter = $reflectionObj->newInstanceArgs($parameters);
530
531        $filter->enableRecursive($this->enableRecursiveFilters);
532
533        $filter->filter($this);
534    }
535
536    /**
537     * Applies a filter to all subtables but not to this datatable.
538     *
539     * @param string|Closure $className Class name, eg. `"Sort"` or "Piwik\DataTable\Filters\Sort"`. If no
540     *                                  namespace is supplied, `Piwik\DataTable\BaseFilter` is assumed. This parameter
541     *                                  can also be a closure that takes a DataTable as its first parameter.
542     * @param array $parameters Array of extra parameters to pass to the filter.
543     */
544    public function filterSubtables($className, $parameters = array())
545    {
546        foreach ($this->getRowsWithoutSummaryRow() as $row) {
547            $subtable = $row->getSubtable();
548            if ($subtable) {
549                $subtable->filter($className, $parameters);
550                $subtable->filterSubtables($className, $parameters);
551            }
552        }
553    }
554
555    /**
556     * Adds a filter and a list of parameters to the list of queued filters of all subtables. These filters will be
557     * executed when {@link applyQueuedFilters()} is called.
558     *
559     * Filters that prettify the column values or don't need the full set of rows should be queued. This
560     * way they will be run after the table is truncated which will result in better performance.
561     *
562     * @param string|Closure $className The class name of the filter, eg. `'Limit'`.
563     * @param array $parameters The parameters to give to the filter, eg. `array($offset, $limit)` for the Limit filter.
564     */
565    public function queueFilterSubtables($className, $parameters = array())
566    {
567        foreach ($this->getRowsWithoutSummaryRow() as $row) {
568            $subtable = $row->getSubtable();
569            if ($subtable) {
570                $subtable->queueFilter($className, $parameters);
571                $subtable->queueFilterSubtables($className, $parameters);
572            }
573        }
574    }
575
576    /**
577     * Adds a filter and a list of parameters to the list of queued filters. These filters will be
578     * executed when {@link applyQueuedFilters()} is called.
579     *
580     * Filters that prettify the column values or don't need the full set of rows should be queued. This
581     * way they will be run after the table is truncated which will result in better performance.
582     *
583     * @param string|Closure $className The class name of the filter, eg. `'Limit'`.
584     * @param array $parameters The parameters to give to the filter, eg. `array($offset, $limit)` for the Limit filter.
585     */
586    public function queueFilter($className, $parameters = array())
587    {
588        if (!is_array($parameters)) {
589            $parameters = array($parameters);
590        }
591        $this->queuedFilters[] = array('className' => $className, 'parameters' => $parameters);
592    }
593
594    /**
595     * Disable a specific filter to run on this DataTable in case you have already applied this filter or if you will
596     * handle this filter manually by using a custom filter. Be aware if you disable a given filter, that filter won't
597     * be ever executed. Even if another filter calls this filter on the DataTable.
598     *
599     * @param string $className  eg 'Limit' or 'Sort'. Passing a `Closure` or an `array($class, $methodName)` is not
600     *                           supported yet. We check for exact match. So if you disable 'Limit' and
601     *                           call `->filter('Limit')` this filter won't be executed. If you call
602     *                           `->filter('Piwik\DataTable\Filter\Limit')` that filter will be executed. See it as a
603     *                           feature.
604     * @ignore
605     */
606    public function disableFilter($className)
607    {
608        $this->disabledFilters[] = $className;
609    }
610
611    /**
612     * Applies all filters that were previously queued to the table. See {@link queueFilter()}
613     * for more information.
614     */
615    public function applyQueuedFilters()
616    {
617        foreach ($this->queuedFilters as $filter) {
618            $this->filter($filter['className'], $filter['parameters']);
619        }
620        $this->clearQueuedFilters();
621    }
622
623    /**
624     * Sums a DataTable to this one.
625     *
626     * This method will sum rows that have the same label. If a row is found in `$tableToSum` whose
627     * label is not found in `$this`, the row will be added to `$this`.
628     *
629     * If the subtables for this table are loaded, they will be summed as well.
630     *
631     * Rows are summed together by summing individual columns. By default columns are summed by
632     * adding one column value to another. Some columns cannot be aggregated this way. In these
633     * cases, the {@link COLUMN_AGGREGATION_OPS_METADATA_NAME}
634     * metadata can be used to specify a different type of operation.
635     *
636     * @param \Piwik\DataTable $tableToSum
637     * @throws Exception
638     */
639    public function addDataTable(DataTable $tableToSum)
640    {
641        if ($tableToSum instanceof Simple) {
642            if ($tableToSum->getRowsCount() > 1) {
643                throw new Exception("Did not expect a Simple table with more than one row in addDataTable()");
644            }
645            $row = $tableToSum->getFirstRow();
646            $this->aggregateRowFromSimpleTable($row);
647        } else {
648            $columnAggregationOps = $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME);
649            foreach ($tableToSum->getRowsWithoutSummaryRow() as $row) {
650                $this->aggregateRowWithLabel($row, $columnAggregationOps);
651            }
652            // we do not use getRows() as this method might get called 100k times when aggregating many datatables and
653            // this takes a lot of time.
654            $row = $tableToSum->getRowFromId(DataTable::ID_SUMMARY_ROW);
655            if ($row) {
656                $this->aggregateRow($this->summaryRow, $row, $columnAggregationOps, true);
657            }
658        }
659    }
660
661    /**
662     * Returns the Row whose `'label'` column is equal to `$label`.
663     *
664     * This method executes in constant time except for the first call which caches row
665     * label => row ID mappings.
666     *
667     * @param string $label `'label'` column value to look for.
668     * @return Row|false The row if found, `false` if otherwise.
669     */
670    public function getRowFromLabel($label)
671    {
672        $rowId = $this->getRowIdFromLabel($label);
673        if (is_int($rowId) && isset($this->rows[$rowId])) {
674            return $this->rows[$rowId];
675        }
676        if ($rowId == self::ID_SUMMARY_ROW
677            && !empty($this->summaryRow)
678        ) {
679            return $this->summaryRow;
680        }
681        if (empty($rowId)
682            && !empty($this->totalsRow)
683            && $label == $this->totalsRow->getColumn('label')
684        ) {
685            return $this->totalsRow;
686        }
687        if ($rowId instanceof Row) {
688            return $rowId;
689        }
690        return false;
691    }
692
693    /**
694     * Returns the row id for the row whose `'label'` column is equal to `$label`.
695     *
696     * This method executes in constant time except for the first call which caches row
697     * label => row ID mappings.
698     *
699     * @param string $label `'label'` column value to look for.
700     * @return int The row ID.
701     */
702    public function getRowIdFromLabel($label)
703    {
704        if ($this->indexNotUpToDate) {
705            $this->rebuildIndex();
706        }
707
708        $label = (string) $label;
709
710        if (!isset($this->rowsIndexByLabel[$label])) {
711            // in case label is '-1' and there is no normal row w/ that label. Note: this is for BC since
712            // in the past, it was possible to get the summary row by searching for the label '-1'
713            if ($label == self::LABEL_SUMMARY_ROW
714                && !is_null($this->summaryRow)
715            ) {
716                return self::ID_SUMMARY_ROW;
717            }
718
719            return false;
720        }
721
722        return $this->rowsIndexByLabel[$label];
723    }
724
725    /**
726     * Returns an empty DataTable with the same metadata and queued filters as `$this` one.
727     *
728     * @param bool $keepFilters Whether to pass the queued filter list to the new DataTable or not.
729     * @return DataTable
730     */
731    public function getEmptyClone($keepFilters = true)
732    {
733        $clone = new DataTable;
734        if ($keepFilters) {
735            $clone->queuedFilters = $this->queuedFilters;
736        }
737        $clone->metadata = $this->metadata;
738        return $clone;
739    }
740
741    /**
742     * Rebuilds the index used to lookup a row by label
743     * @internal
744     */
745    public function rebuildIndex()
746    {
747        $this->rowsIndexByLabel = array();
748        $this->rebuildIndexContinuously = true;
749
750        foreach ($this->rows as $id => $row) {
751            $label = $row->getColumn('label');
752            if ($label !== false) {
753                $this->rowsIndexByLabel[$label] = $id;
754            }
755        }
756
757        $this->indexNotUpToDate = false;
758    }
759
760    /**
761     * Returns a row by ID. The ID is either the index of the row or {@link ID_SUMMARY_ROW}.
762     *
763     * @param int $id The row ID.
764     * @return Row|false The Row or false if not found.
765     */
766    public function getRowFromId($id)
767    {
768        if ($id == self::ID_SUMMARY_ROW
769            && !is_null($this->summaryRow)
770        ) {
771            return $this->summaryRow;
772        }
773
774        if (!isset($this->rows[$id])) {
775            return false;
776        }
777        return $this->rows[$id];
778    }
779
780    /**
781     * Returns the row that has a subtable with ID matching `$idSubtable`.
782     *
783     * @param int $idSubTable The subtable ID.
784     * @return Row|false The row or false if not found
785     */
786    public function getRowFromIdSubDataTable($idSubTable)
787    {
788        $idSubTable = (int)$idSubTable;
789        foreach ($this->rows as $row) {
790            if ($row->getIdSubDataTable() === $idSubTable) {
791                return $row;
792            }
793        }
794        return false;
795    }
796
797    /**
798     * Adds a row to this table.
799     *
800     * If {@link setMaximumAllowedRows()} was called and the current row count is
801     * at the maximum, the new row will be summed to the summary row. If there is no summary row,
802     * this row is set as the summary row.
803     *
804     * @param Row $row
805     * @return Row `$row` or the summary row if we're at the maximum number of rows.
806     */
807    public function addRow(Row $row)
808    {
809        // if there is a upper limit on the number of allowed rows and the table is full,
810        // add the new row to the summary row
811        if ($this->maximumAllowedRows > 0
812            && $this->getRowsCount() >= $this->maximumAllowedRows - 1
813        ) {
814            if ($this->summaryRow === null) {
815                // create the summary row if necessary
816
817                $columns = array('label' => self::LABEL_SUMMARY_ROW) + $row->getColumns();
818                $this->addSummaryRow(new Row(array(Row::COLUMNS => $columns)));
819            } else {
820                $this->summaryRow->sumRow(
821                    $row, $enableCopyMetadata = false, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME));
822            }
823            return $this->summaryRow;
824        }
825
826        $this->rows[] = $row;
827        if (!$this->indexNotUpToDate
828            && $this->rebuildIndexContinuously
829        ) {
830            $label = $row->getColumn('label');
831            if ($label !== false) {
832                $this->rowsIndexByLabel[$label] = count($this->rows) - 1;
833            }
834        }
835        return $row;
836    }
837
838    /**
839     * Sets the summary row.
840     *
841     * _Note: A DataTable can have only one summary row._
842     *
843     * @param Row $row
844     * @return Row Returns `$row`.
845     */
846    public function addSummaryRow(Row $row)
847    {
848        $this->summaryRow = $row;
849        $row->setIsSummaryRow();
850
851        // NOTE: the summary row does not go in the index, since it will overwrite rows w/ label == -1
852
853        return $row;
854    }
855
856    /**
857     * Returns the DataTable ID.
858     *
859     * @return int
860     */
861    public function getId()
862    {
863        return $this->currentId;
864    }
865
866    /**
867     * Adds a new row from an array.
868     *
869     * You can add row metadata with this method.
870     *
871     * @param array $row eg. `array(Row::COLUMNS => array('visits' => 13, 'test' => 'toto'),
872     *                              Row::METADATA => array('mymetadata' => 'myvalue'))`
873     */
874    public function addRowFromArray($row)
875    {
876        $this->addRowsFromArray(array($row));
877    }
878
879    /**
880     * Adds a new row a from an array of column values.
881     *
882     * Row metadata cannot be added with this method.
883     *
884     * @param array $row eg. `array('name' => 'google analytics', 'license' => 'commercial')`
885     */
886    public function addRowFromSimpleArray($row)
887    {
888        $this->addRowsFromSimpleArray(array($row));
889    }
890
891    /**
892     * Returns the array of Rows.
893     * Internal logic in Matomo core should avoid using this method as it is time and memory consuming when being
894     * executed thousands of times. The alternative is to use {@link getRowsWithoutSummaryRow()} + get the summary
895     * row manually.
896     *
897     * @return Row[]
898     */
899    public function getRows()
900    {
901        if (is_null($this->summaryRow)) {
902            return $this->rows;
903        } else {
904            return $this->rows + array(self::ID_SUMMARY_ROW => $this->summaryRow);
905        }
906    }
907
908    /**
909     * @ignore
910     */
911    public function getRowsWithoutSummaryRow()
912    {
913        return $this->rows;
914    }
915
916    /**
917     * @ignore
918     */
919    public function getRowsCountWithoutSummaryRow()
920    {
921        return count($this->rows);
922    }
923
924    /**
925     * Returns an array containing all column values for the requested column.
926     *
927     * @param string $name The column name.
928     * @return array The array of column values.
929     */
930    public function getColumn($name)
931    {
932        $columnValues = array();
933        foreach ($this->getRows() as $row) {
934            $columnValues[] = $row->getColumn($name);
935        }
936        return $columnValues;
937    }
938
939    /**
940     * Returns an array containing all column values of columns whose name starts with `$name`.
941     *
942     * @param string $namePrefix The column name prefix.
943     * @return array The array of column values.
944     */
945    public function getColumnsStartingWith($namePrefix)
946    {
947        $columnValues = array();
948        foreach ($this->getRows() as $row) {
949            $columns = $row->getColumns();
950            foreach ($columns as $column => $value) {
951                if (strpos($column, $namePrefix) === 0) {
952                    $columnValues[] = $row->getColumn($column);
953                }
954            }
955        }
956        return $columnValues;
957    }
958
959    /**
960     * Returns the names of every column this DataTable contains. This method will return the
961     * columns of the first row with data and will assume they occur in every other row as well.
962     *
963     *_ Note: If column names still use their in-database INDEX values (@see Metrics), they
964     *        will be converted to their string name in the array result._
965     *
966     * @return array Array of string column names.
967     */
968    public function getColumns()
969    {
970        $result = array();
971        foreach ($this->getRows() as $row) {
972            $columns = $row->getColumns();
973            if (!empty($columns)) {
974                $result = array_keys($columns);
975                break;
976            }
977        }
978
979        // make sure column names are not DB index values
980        foreach ($result as &$column) {
981            if (isset(Metrics::$mappingFromIdToName[$column])) {
982                $column = Metrics::$mappingFromIdToName[$column];
983            }
984        }
985
986        return $result;
987    }
988
989    /**
990     * Returns an array containing the requested metadata value of each row.
991     *
992     * @param string $name The metadata column to return.
993     * @return array
994     */
995    public function getRowsMetadata($name)
996    {
997        $metadataValues = array();
998        foreach ($this->getRows() as $row) {
999            $metadataValues[] = $row->getMetadata($name);
1000        }
1001        return $metadataValues;
1002    }
1003
1004    /**
1005     * Delete row metadata by name in every row.
1006     *
1007     * @param       $name
1008     * @param bool $deleteRecursiveInSubtables
1009     */
1010    public function deleteRowsMetadata($name, $deleteRecursiveInSubtables = false)
1011    {
1012        foreach ($this->rows as $row) {
1013            $row->deleteMetadata($name);
1014
1015            $subTable = $row->getSubtable();
1016            if ($subTable) {
1017                $subTable->deleteRowsMetadata($name, $deleteRecursiveInSubtables);
1018            }
1019        }
1020        if (!is_null($this->summaryRow)) {
1021            $this->summaryRow->deleteMetadata($name);
1022        }
1023        if (!is_null($this->totalsRow)) {
1024            $this->totalsRow->deleteMetadata($name);
1025        }
1026
1027    }
1028
1029    /**
1030     * Returns the number of rows in the table including the summary row.
1031     *
1032     * @return int
1033     */
1034    public function getRowsCount()
1035    {
1036        if (is_null($this->summaryRow)) {
1037            return count($this->rows);
1038        } else {
1039            return count($this->rows) + 1;
1040        }
1041    }
1042
1043    /**
1044     * Returns the first row of the DataTable.
1045     *
1046     * @return Row|false The first row or `false` if it cannot be found.
1047     */
1048    public function getFirstRow()
1049    {
1050        if (count($this->rows) == 0) {
1051            if (!is_null($this->summaryRow)) {
1052                return $this->summaryRow;
1053            }
1054            return false;
1055        }
1056        return reset($this->rows);
1057    }
1058
1059    /**
1060     * Returns the last row of the DataTable. If there is a summary row, it
1061     * will always be considered the last row.
1062     *
1063     * @return Row|false The last row or `false` if it cannot be found.
1064     */
1065    public function getLastRow()
1066    {
1067        if (!is_null($this->summaryRow)) {
1068            return $this->summaryRow;
1069        }
1070
1071        if (count($this->rows) == 0) {
1072            return false;
1073        }
1074
1075        return end($this->rows);
1076    }
1077
1078    /**
1079     * Returns the number of rows in the entire DataTable hierarchy. This is the number of rows in this DataTable
1080     * summed with the row count of each descendant subtable.
1081     *
1082     * @return int
1083     */
1084    public function getRowsCountRecursive()
1085    {
1086        $totalCount = 0;
1087        foreach ($this->rows as $row) {
1088            $subTable = $row->getSubtable();
1089            if ($subTable) {
1090                $count = $subTable->getRowsCountRecursive();
1091                $totalCount += $count;
1092            }
1093        }
1094
1095        $totalCount += $this->getRowsCount();
1096        return $totalCount;
1097    }
1098
1099    /**
1100     * Delete a column by name in every row. This change is NOT applied recursively to all
1101     * subtables.
1102     *
1103     * @param string $name Column name to delete.
1104     */
1105    public function deleteColumn($name)
1106    {
1107        $this->deleteColumns(array($name));
1108    }
1109
1110    public function __sleep()
1111    {
1112        return array('rows', 'summaryRow', 'metadata', 'totalsRow');
1113    }
1114
1115    /**
1116     * Rename a column in every row. This change is applied recursively to all subtables.
1117     *
1118     * @param string $oldName Old column name.
1119     * @param string $newName New column name.
1120     */
1121    public function renameColumn($oldName, $newName)
1122    {
1123        foreach ($this->rows as $row) {
1124            $row->renameColumn($oldName, $newName);
1125
1126            $subTable = $row->getSubtable();
1127            if ($subTable) {
1128                $subTable->renameColumn($oldName, $newName);
1129            }
1130        }
1131        if (!is_null($this->summaryRow)) {
1132            $this->summaryRow->renameColumn($oldName, $newName);
1133        }
1134        if (!is_null($this->totalsRow)) {
1135            $this->totalsRow->renameColumn($oldName, $newName);
1136        }
1137    }
1138
1139    /**
1140     * Deletes several columns by name in every row.
1141     *
1142     * @param array $names List of column names to delete.
1143     * @param bool $deleteRecursiveInSubtables Whether to apply this change to all subtables or not.
1144     */
1145    public function deleteColumns($names, $deleteRecursiveInSubtables = false)
1146    {
1147        foreach ($this->rows as $row) {
1148            foreach ($names as $name) {
1149                $row->deleteColumn($name);
1150            }
1151            $subTable = $row->getSubtable();
1152            if ($subTable) {
1153                $subTable->deleteColumns($names, $deleteRecursiveInSubtables);
1154            }
1155        }
1156        if (!is_null($this->summaryRow)) {
1157            foreach ($names as $name) {
1158                $this->summaryRow->deleteColumn($name);
1159            }
1160        }
1161        if (!is_null($this->totalsRow)) {
1162            foreach ($names as $name) {
1163                $this->totalsRow->deleteColumn($name);
1164            }
1165        }
1166    }
1167
1168    /**
1169     * Deletes a row by ID.
1170     *
1171     * @param int $id The row ID.
1172     * @throws Exception If the row `$id` cannot be found.
1173     */
1174    public function deleteRow($id)
1175    {
1176        if ($id === self::ID_SUMMARY_ROW) {
1177            $this->summaryRow = null;
1178            return;
1179        }
1180        if (!isset($this->rows[$id])) {
1181            throw new Exception("Trying to delete unknown row with idkey = $id");
1182        }
1183        unset($this->rows[$id]);
1184    }
1185
1186    /**
1187     * Deletes rows from `$offset` to `$offset + $limit`.
1188     *
1189     * @param int $offset The offset to start deleting rows from.
1190     * @param int|null $limit The number of rows to delete. If `null` all rows after the offset
1191     *                        will be removed.
1192     * @return int The number of rows deleted.
1193     */
1194    public function deleteRowsOffset($offset, $limit = null)
1195    {
1196        if ($limit === 0) {
1197            return 0;
1198        }
1199
1200        $count = $this->getRowsCount();
1201        if ($offset >= $count) {
1202            return 0;
1203        }
1204
1205        // if we delete until the end, we delete the summary row as well
1206        if (is_null($limit)
1207            || $limit >= $count
1208        ) {
1209            $this->summaryRow = null;
1210        }
1211
1212        if (is_null($limit)) {
1213            array_splice($this->rows, $offset);
1214        } else {
1215            array_splice($this->rows, $offset, $limit);
1216        }
1217
1218        return $count - $this->getRowsCount();
1219    }
1220
1221    /**
1222     * Deletes a set of rows by ID.
1223     *
1224     * @param array $rowIds The list of row IDs to delete.
1225     * @throws Exception If a row ID cannot be found.
1226     */
1227    public function deleteRows(array $rowIds)
1228    {
1229        foreach ($rowIds as $key) {
1230            $this->deleteRow($key);
1231        }
1232    }
1233
1234    /**
1235     * Returns a string representation of this DataTable for convenient viewing.
1236     *
1237     * _Note: This uses the **html** DataTable renderer._
1238     *
1239     * @return string
1240     */
1241    public function __toString()
1242    {
1243        $renderer = new Html();
1244        $renderer->setTable($this);
1245        return (string)$renderer;
1246    }
1247
1248    /**
1249     * Returns true if both DataTable instances are exactly the same.
1250     *
1251     * DataTables are equal if they have the same number of rows, if
1252     * each row has a label that exists in the other table, and if each row
1253     * is equal to the row in the other table with the same label. The order
1254     * of rows is not important.
1255     *
1256     * @param \Piwik\DataTable $table1
1257     * @param \Piwik\DataTable $table2
1258     * @return bool
1259     */
1260    public static function isEqual(DataTable $table1, DataTable $table2)
1261    {
1262        $table1->rebuildIndex();
1263        $table2->rebuildIndex();
1264
1265        if ($table1->getRowsCount() != $table2->getRowsCount()) {
1266            return false;
1267        }
1268
1269        $rows1 = $table1->getRows();
1270
1271        foreach ($rows1 as $row1) {
1272            $row2 = $table2->getRowFromLabel($row1->getColumn('label'));
1273            if ($row2 === false
1274                || !Row::isEqual($row1, $row2)
1275            ) {
1276                return false;
1277            }
1278        }
1279
1280        return true;
1281    }
1282
1283    /**
1284     * Serializes an entire DataTable hierarchy and returns the array of serialized DataTables.
1285     *
1286     * The first element in the returned array will be the serialized representation of this DataTable.
1287     * Every subsequent element will be a serialized subtable.
1288     *
1289     * This DataTable and subtables can optionally be truncated before being serialized. In most
1290     * cases where DataTables can become quite large, they should be truncated before being persisted
1291     * in an archive.
1292     *
1293     * The result of this method is intended for use with the {@link ArchiveProcessor::insertBlobRecord()} method.
1294     *
1295     * @throws Exception If infinite recursion detected. This will occur if a table's subtable is one of its parent tables.
1296     * @param int $maximumRowsInDataTable If not null, defines the maximum number of rows allowed in the serialized DataTable.
1297     * @param int $maximumRowsInSubDataTable If not null, defines the maximum number of rows allowed in serialized subtables.
1298     * @param string $columnToSortByBeforeTruncation The column to sort by before truncating, eg, `Metrics::INDEX_NB_VISITS`.
1299     * @param array $aSerializedDataTable Will contain all the output arrays
1300     * @return array The array of serialized DataTables:
1301     *
1302     *                   array(
1303     *                       // this DataTable (the root)
1304     *                       0 => 'eghuighahgaueytae78yaet7yaetae',
1305     *
1306     *                       // a subtable
1307     *                       1 => 'gaegae gh gwrh guiwh uigwhuige',
1308     *
1309     *                       // another subtable
1310     *                       2 => 'gqegJHUIGHEQjkgneqjgnqeugUGEQHGUHQE',
1311     *
1312     *                       // etc.
1313     *                   );
1314     */
1315    public function getSerialized($maximumRowsInDataTable = null,
1316                                  $maximumRowsInSubDataTable = null,
1317                                  $columnToSortByBeforeTruncation = null,
1318                                  &$aSerializedDataTable = array())
1319    {
1320        static $depth = 0;
1321        // make sure subtableIds are consecutive from 1 to N
1322        static $subtableId = 0;
1323
1324        if ($depth > self::$maximumDepthLevelAllowed) {
1325            $depth = 0;
1326            $subtableId = 0;
1327            throw new Exception("Maximum recursion level of " . self::$maximumDepthLevelAllowed . " reached. Maybe you have set a DataTable\Row with an associated DataTable belonging already to one of its parent tables?");
1328        }
1329
1330        // gather metadata before filters are called, so their metadata is not stored in serialized form
1331        $metadata = $this->getAllTableMetadata();
1332        foreach ($metadata as $key => $value) {
1333            if (!is_scalar($value) && !is_string($value)) {
1334                unset($metadata[$key]);
1335            }
1336        }
1337
1338        if (!is_null($maximumRowsInDataTable)) {
1339            $this->filter('Truncate',
1340                array($maximumRowsInDataTable - 1,
1341                      DataTable::LABEL_SUMMARY_ROW,
1342                      $columnToSortByBeforeTruncation,
1343                      $filterRecursive = false)
1344            );
1345        }
1346
1347        $consecutiveSubtableIds = array();
1348        $forcedId = $subtableId;
1349
1350        // For each row (including the summary row), get the serialized row
1351        // If it is associated to a sub table, get the serialized table recursively ;
1352        // but returns all serialized tables and subtable in an array of 1 dimension
1353        foreach ($this->getRows() as $id => $row) {
1354            $subTable = $row->getSubtable();
1355            if ($subTable) {
1356                $consecutiveSubtableIds[$id] = ++$subtableId;
1357                $depth++;
1358                $subTable->getSerialized($maximumRowsInSubDataTable, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation, $aSerializedDataTable);
1359                $depth--;
1360            } else {
1361                $row->removeSubtable();
1362            }
1363        }
1364
1365        // if the datatable is the parent we force the Id at 0 (this is part of the specification)
1366        if ($depth == 0) {
1367            $forcedId = 0;
1368            $subtableId = 0;
1369        }
1370
1371        // we then serialize the rows and store them in the serialized dataTable
1372        $rows = array();
1373        foreach ($this->rows as $id => $row) {
1374            if (isset($consecutiveSubtableIds[$id])) {
1375                $backup = $row->subtableId;
1376                $row->subtableId = $consecutiveSubtableIds[$id];
1377                $rows[$id] = $row->export();
1378                $row->subtableId = $backup;
1379            } else {
1380                $rows[$id] = $row->export();
1381            }
1382        }
1383
1384        if (isset($this->summaryRow)) {
1385            $id = self::ID_SUMMARY_ROW;
1386            $row = $this->summaryRow;
1387
1388            // duplicating code above so we don't create a new array w/ getRows() above in this function which is
1389            // used heavily in matomo.
1390            if (isset($consecutiveSubtableIds[$id])) {
1391                $backup = $row->subtableId;
1392                $row->subtableId = $consecutiveSubtableIds[$id];
1393                $rows[$id] = $row->export();
1394                $row->subtableId = $backup;
1395            } else {
1396                $rows[$id] = $row->export();
1397            }
1398        }
1399
1400        if (!empty($metadata)) {
1401            $metadataRow = new Row();
1402            $metadataRow->setColumns($metadata);
1403
1404            // set the label so the row will be indexed correctly internally
1405            $metadataRow->setColumn('label', self::LABEL_ARCHIVED_METADATA_ROW);
1406
1407            $rows[self::ID_ARCHIVED_METADATA_ROW] = $metadataRow->export();
1408        }
1409
1410        $aSerializedDataTable[$forcedId] = serialize($rows);
1411        unset($rows);
1412
1413        return $aSerializedDataTable;
1414    }
1415
1416    private static $previousRowClasses = array('O:39:"Piwik\DataTable\Row\DataTableSummaryRow"', 'O:19:"Piwik\DataTable\Row"', 'O:36:"Piwik_DataTable_Row_DataTableSummary"', 'O:19:"Piwik_DataTable_Row"');
1417    private static $rowClassToUseForUnserialize = 'O:29:"Piwik_DataTable_SerializedRow"';
1418
1419    /**
1420     * It is faster to unserialize existing serialized Row instances to "Piwik_DataTable_SerializedRow" and access the
1421     * `$row->c` property than implementing a "__wakeup" method in the Row instance to map the "$row->c" to $row->columns
1422     * etc. We're talking here about 15% faster reports aggregation in some cases. To be concrete: We have a test where
1423     * Archiving a year takes 1700 seconds with "__wakeup" and 1400 seconds with this method. Yes, it takes 300 seconds
1424     * to wake up millions of rows. We should be able to remove this code here end 2015 and use the "__wakeup" way by then.
1425     * Why? By then most new archives will have only arrays serialized anyway and therefore this mapping is rather an overhead.
1426     *
1427     * @param string $serialized
1428     * @return array
1429     * @throws Exception In case the unserialize fails
1430     */
1431    private function unserializeRows($serialized)
1432    {
1433        $serialized = str_replace(self::$previousRowClasses, self::$rowClassToUseForUnserialize, $serialized);
1434        $rows = Common::safe_unserialize($serialized, [
1435            Row::class,
1436            DataTableSummaryRow::class,
1437            \Piwik_DataTable_SerializedRow::class
1438        ]);
1439
1440        if ($rows === false) {
1441            throw new Exception("The unserialization has failed!");
1442        }
1443
1444        return $rows;
1445    }
1446
1447    /**
1448     * Adds a set of rows from a serialized DataTable string.
1449     *
1450     * See {@link serialize()}.
1451     *
1452     * _Note: This function will successfully load DataTables serialized by Piwik 1.X._
1453     *
1454     * @param string $serialized A string with the format of a string in the array returned by
1455     *                                 {@link serialize()}.
1456     * @throws Exception if `$serialized` is invalid.
1457     */
1458    public function addRowsFromSerializedArray($serialized)
1459    {
1460        $rows = $this->unserializeRows($serialized);
1461
1462        if (array_key_exists(self::ID_SUMMARY_ROW, $rows)) {
1463            if (is_array($rows[self::ID_SUMMARY_ROW])) {
1464                $this->summaryRow = new Row($rows[self::ID_SUMMARY_ROW]);
1465                $this->summaryRow->setIsSummaryRow();
1466            } elseif (isset($rows[self::ID_SUMMARY_ROW]->c)) {
1467                $this->summaryRow = new Row($rows[self::ID_SUMMARY_ROW]->c); // Pre Piwik 2.13
1468                $this->summaryRow->setIsSummaryRow();
1469            }
1470            unset($rows[self::ID_SUMMARY_ROW]);
1471        }
1472
1473        if (array_key_exists(self::ID_ARCHIVED_METADATA_ROW, $rows)) {
1474            $metadata = $rows[self::ID_ARCHIVED_METADATA_ROW][Row::COLUMNS];
1475            unset($metadata['label']);
1476            $this->setAllTableMetadata($metadata);
1477            unset($rows[self::ID_ARCHIVED_METADATA_ROW]);
1478        }
1479
1480        foreach ($rows as $id => $row) {
1481            if (isset($row->c)) {
1482                $this->addRow(new Row($row->c)); // Pre Piwik 2.13
1483            } else {
1484                $this->addRow(new Row($row));
1485            }
1486        }
1487    }
1488
1489    /**
1490     * Adds multiple rows from an array.
1491     *
1492     * You can add row metadata with this method.
1493     *
1494     * @param array $array Array with the following structure
1495     *
1496     *                         array(
1497     *                             // row1
1498     *                             array(
1499     *                                 Row::COLUMNS => array( col1_name => value1, col2_name => value2, ...),
1500     *                                 Row::METADATA => array( metadata1_name => value1,  ...), // see Row
1501     *                             ),
1502     *                             // row2
1503     *                             array( ... ),
1504     *                         )
1505     */
1506    public function addRowsFromArray($array)
1507    {
1508        foreach ($array as $id => $row) {
1509            if (is_array($row)) {
1510                $row = new Row($row);
1511            }
1512
1513            if ($id == self::ID_SUMMARY_ROW) {
1514                $this->summaryRow = $row;
1515                $this->summaryRow->setIsSummaryRow();
1516            } else {
1517                $this->addRow($row);
1518            }
1519        }
1520    }
1521
1522    /**
1523     * Adds multiple rows from an array containing arrays of column values.
1524     *
1525     * Row metadata cannot be added with this method.
1526     *
1527     * @param array $array Array with the following structure:
1528     *
1529     *                       array(
1530     *                             array( col1_name => valueA, col2_name => valueC, ...),
1531     *                             array( col1_name => valueB, col2_name => valueD, ...),
1532     *                       )
1533     * @throws Exception if `$array` is in an incorrect format.
1534     */
1535    public function addRowsFromSimpleArray($array)
1536    {
1537        if (count($array) === 0) {
1538            return;
1539        }
1540
1541        $exceptionText = " Data structure returned is not convertible in the requested format." .
1542            " Try to call this method with the parameters '&format=original&serialize=1'" .
1543            "; you will get the original php data structure serialized." .
1544            " The data structure looks like this: \n \$data = %s; ";
1545
1546        // first pass to see if the array has the structure
1547        // array(col1_name => val1, col2_name => val2, etc.)
1548        // with val* that are never arrays (only strings/numbers/bool/etc.)
1549        // if we detect such a "simple" data structure we convert it to a row with the correct columns' names
1550        $thisIsNotThatSimple = false;
1551
1552        foreach ($array as $columnValue) {
1553            if (is_array($columnValue) || is_object($columnValue)) {
1554                $thisIsNotThatSimple = true;
1555                break;
1556            }
1557        }
1558        if ($thisIsNotThatSimple === false) {
1559            // case when the array is indexed by the default numeric index
1560            if (array_keys($array) === array_keys(array_fill(0, count($array), true))) {
1561                foreach ($array as $row) {
1562                    $this->addRow(new Row(array(Row::COLUMNS => array($row))));
1563                }
1564            } else {
1565                $this->addRow(new Row(array(Row::COLUMNS => $array)));
1566            }
1567            // we have converted our simple array to one single row
1568            // => we exit the method as the job is now finished
1569            return;
1570        }
1571
1572        foreach ($array as $key => $row) {
1573            // stuff that looks like a line
1574            if (is_array($row)) {
1575                /**
1576                 * We make sure we can convert this PHP array without losing information.
1577                 * We are able to convert only simple php array (no strings keys, no sub arrays, etc.)
1578                 *
1579                 */
1580
1581                // if the key is a string it means that some information was contained in this key.
1582                // it cannot be lost during the conversion. Because we are not able to handle properly
1583                // this key, we throw an explicit exception.
1584                if (is_string($key)) {
1585                    // we define an exception we may throw if at one point we notice that we cannot handle the data structure
1586                    throw new Exception(sprintf($exceptionText, var_export($array, true)));
1587                }
1588                // if any of the sub elements of row is an array we cannot handle this data structure...
1589                foreach ($row as $subRow) {
1590                    if (is_array($subRow)) {
1591                        throw new Exception(sprintf($exceptionText, var_export($array, true)));
1592                    }
1593                }
1594                $row = new Row(array(Row::COLUMNS => $row));
1595            } // other (string, numbers...) => we build a line from this value
1596            else {
1597                $row = new Row(array(Row::COLUMNS => array($key => $row)));
1598            }
1599            $this->addRow($row);
1600        }
1601    }
1602
1603    /**
1604     * Rewrites the input `$array`
1605     *
1606     *     array (
1607     *         LABEL => array(col1 => X, col2 => Y),
1608     *         LABEL2 => array(col1 => X, col2 => Y),
1609     *     )
1610     *
1611     * to a DataTable with rows that look like:
1612     *
1613     *     array (
1614     *         array( Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y)),
1615     *         array( Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)),
1616     *     )
1617     *
1618     * Will also convert arrays like:
1619     *
1620     *     array (
1621     *         LABEL => X,
1622     *         LABEL2 => Y,
1623     *     )
1624     *
1625     * to:
1626     *
1627     *     array (
1628     *         array( Row::COLUMNS => array('label' => LABEL, 'value' => X)),
1629     *         array( Row::COLUMNS => array('label' => LABEL2, 'value' => Y)),
1630     *     )
1631     *
1632     * @param array $array Indexed array, two formats supported, see above.
1633     * @param array|null $subtablePerLabel An array mapping label values with DataTable instances to associate as a subtable.
1634     * @return \Piwik\DataTable
1635     */
1636    public static function makeFromIndexedArray($array, $subtablePerLabel = null)
1637    {
1638        $table = new DataTable();
1639        foreach ($array as $label => $row) {
1640            $cleanRow = array();
1641
1642            // Support the case of an $array of single values
1643            if (!is_array($row)) {
1644                $row = array('value' => $row);
1645            }
1646            // Put the 'label' column first
1647            $cleanRow[Row::COLUMNS] = array('label' => $label) + $row;
1648            // Assign subtable if specified
1649            if (isset($subtablePerLabel[$label])) {
1650                $cleanRow[Row::DATATABLE_ASSOCIATED] = $subtablePerLabel[$label];
1651            }
1652
1653            if ($label === RankingQuery::LABEL_SUMMARY_ROW) {
1654                $table->addSummaryRow(new Row($cleanRow));
1655            } else {
1656                $table->addRow(new Row($cleanRow));
1657            }
1658        }
1659        return $table;
1660    }
1661
1662    /**
1663     * Sets the maximum depth level to at least a certain value. If the current value is
1664     * greater than `$atLeastLevel`, the maximum nesting level is not changed.
1665     *
1666     * The maximum depth level determines the maximum number of subtable levels in the
1667     * DataTable tree. For example, if it is set to `2`, this DataTable is allowed to
1668     * have subtables, but the subtables are not.
1669     *
1670     * @param int $atLeastLevel
1671     */
1672    public static function setMaximumDepthLevelAllowedAtLeast($atLeastLevel)
1673    {
1674        self::$maximumDepthLevelAllowed = max($atLeastLevel, self::$maximumDepthLevelAllowed);
1675        if (self::$maximumDepthLevelAllowed < 1) {
1676            self::$maximumDepthLevelAllowed = 1;
1677        }
1678    }
1679
1680    /**
1681     * Returns metadata by name.
1682     *
1683     * @param string $name The metadata name.
1684     * @return mixed|false The metadata value or `false` if it cannot be found.
1685     */
1686    public function getMetadata($name)
1687    {
1688        if (!isset($this->metadata[$name])) {
1689            return false;
1690        }
1691        return $this->metadata[$name];
1692    }
1693
1694    /**
1695     * Sets a metadata value by name.
1696     *
1697     * @param string $name The metadata name.
1698     * @param mixed $value
1699     */
1700    public function setMetadata($name, $value)
1701    {
1702        $this->metadata[$name] = $value;
1703    }
1704
1705    /**
1706     * Returns all table metadata.
1707     *
1708     * @return array
1709     */
1710    public function getAllTableMetadata()
1711    {
1712        return $this->metadata;
1713    }
1714
1715    /**
1716     * Sets several metadata values by name.
1717     *
1718     * @param array $values Array mapping metadata names with metadata values.
1719     */
1720    public function setMetadataValues($values)
1721    {
1722        foreach ($values as $name => $value) {
1723            $this->metadata[$name] = $value;
1724        }
1725    }
1726
1727    /**
1728     * Sets metadata, erasing existing values.
1729     *
1730     * @param array $values Array mapping metadata names with metadata values.
1731     */
1732    public function setAllTableMetadata($metadata)
1733    {
1734        $this->metadata = $metadata;
1735    }
1736
1737    /**
1738     * Sets the maximum number of rows allowed in this datatable (including the summary
1739     * row). If adding more then the allowed number of rows is attempted, the extra
1740     * rows are summed to the summary row.
1741     *
1742     * @param int $maximumAllowedRows If `0`, the maximum number of rows is unset.
1743     */
1744    public function setMaximumAllowedRows($maximumAllowedRows)
1745    {
1746        $this->maximumAllowedRows = $maximumAllowedRows;
1747    }
1748
1749    /**
1750     * Traverses a DataTable tree using an array of labels and returns the row
1751     * it finds or `false` if it cannot find one. The number of path segments that
1752     * were successfully walked is also returned.
1753     *
1754     * If `$missingRowColumns` is supplied, the specified path is created. When
1755     * a subtable is encountered w/o the required label, a new row is created
1756     * with the label, and a new subtable is added to the row.
1757     *
1758     * Read [http://en.wikipedia.org/wiki/Tree_(data_structure)#Traversal_methods](http://en.wikipedia.org/wiki/Tree_(data_structure)#Traversal_methods)
1759     * for more information about tree walking.
1760     *
1761     * @param array $path The path to walk. An array of label values. The first element
1762     *                    refers to a row in this DataTable, the second in a subtable of
1763     *                    the first row, the third a subtable of the second row, etc.
1764     * @param array|bool $missingRowColumns The default columns to use when creating new rows.
1765     *                                      If this parameter is supplied, new rows will be
1766     *                                      created for path labels that cannot be found.
1767     * @param int $maxSubtableRows The maximum number of allowed rows in new subtables. New
1768     *                             subtables are only created if `$missingRowColumns` is provided.
1769     * @return array First element is the found row or `false`. Second element is
1770     *               the number of path segments walked. If a row is found, this
1771     *               will be == to `count($path)`. Otherwise, it will be the index
1772     *               of the path segment that we could not find.
1773     */
1774    public function walkPath($path, $missingRowColumns = false, $maxSubtableRows = 0)
1775    {
1776        $pathLength = count($path);
1777
1778        $table = $this;
1779        $next = false;
1780        for ($i = 0; $i < $pathLength; ++$i) {
1781            $segment = $path[$i];
1782
1783            $next = $table->getRowFromLabel($segment);
1784            if ($next === false) {
1785                // if there is no table to advance to, and we're not adding missing rows, return false
1786                if ($missingRowColumns === false) {
1787                    return array(false, $i);
1788                } else {
1789                    // if we're adding missing rows, add a new row
1790
1791                    $row = new DataTableSummaryRow();
1792                    $row->setColumns(array('label' => $segment) + $missingRowColumns);
1793
1794                    $next = $table->addRow($row);
1795
1796                    if ($next !== $row) {
1797                        // if the row wasn't added, the table is full
1798
1799                        // Summary row, has no metadata
1800                        $next->deleteMetadata();
1801                        return array($next, $i);
1802                    }
1803                }
1804            }
1805
1806            $table = $next->getSubtable();
1807            if ($table === false) {
1808                // if the row has no table (and thus no child rows), and we're not adding
1809                // missing rows, return false
1810                if ($missingRowColumns === false) {
1811                    return array(false, $i);
1812                } elseif ($i != $pathLength - 1) {
1813                    // create subtable if missing, but only if not on the last segment
1814
1815                    $table = new DataTable();
1816                    $table->setMaximumAllowedRows($maxSubtableRows);
1817                    $table->metadata[self::COLUMN_AGGREGATION_OPS_METADATA_NAME]
1818                        = $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME);
1819                    $next->setSubtable($table);
1820                    // Summary row, has no metadata
1821                    $next->deleteMetadata();
1822                }
1823            }
1824        }
1825
1826        return array($next, $i);
1827    }
1828
1829    /**
1830     * Returns a new DataTable in which the rows of this table are replaced with the aggregatated rows of all its subtables.
1831     *
1832     * @param string|bool $labelColumn If supplied the label of the parent row will be added to
1833     *                                 a new column in each subtable row.
1834     *
1835     *                                 If set to, `'label'` each subtable row's label will be prepended
1836     *                                 w/ the parent row's label. So `'child_label'` becomes
1837     *                                 `'parent_label - child_label'`.
1838     * @param bool $useMetadataColumn If true and if `$labelColumn` is supplied, the parent row's
1839     *                                label will be added as metadata and not a new column.
1840     * @return \Piwik\DataTable
1841     */
1842    public function mergeSubtables($labelColumn = false, $useMetadataColumn = false)
1843    {
1844        $result = new DataTable();
1845        $result->setAllTableMetadata($this->getAllTableMetadata());
1846        foreach ($this->getRowsWithoutSummaryRow() as $row) {
1847            $subtable = $row->getSubtable();
1848            if ($subtable !== false) {
1849                $parentLabel = $row->getColumn('label');
1850
1851                // add a copy of each subtable row to the new datatable
1852                foreach ($subtable->getRows() as $id => $subRow) {
1853                    $copy = clone $subRow;
1854
1855                    // if the summary row, add it to the existing summary row (or add a new one)
1856                    if ($id == self::ID_SUMMARY_ROW) {
1857                        $existing = $result->getRowFromId(self::ID_SUMMARY_ROW);
1858                        if ($existing === false) {
1859                            $result->addSummaryRow($copy);
1860                        } else {
1861                            $existing->sumRow($copy, $copyMeta = true, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME));
1862                        }
1863                    } else {
1864                        if ($labelColumn !== false) {
1865                            // if we're modifying the subtable's rows' label column, then we make
1866                            // sure to prepend the existing label w/ the parent row's label. otherwise
1867                            // we're just adding the parent row's label as a new column/metadata.
1868                            $newLabel = $parentLabel;
1869                            if ($labelColumn == 'label') {
1870                                $newLabel .= ' - ' . $copy->getColumn('label');
1871                            }
1872
1873                            // modify the child row's label or add new column/metadata
1874                            if ($useMetadataColumn) {
1875                                $copy->setMetadata($labelColumn, $newLabel);
1876                            } else {
1877                                $copy->setColumn($labelColumn, $newLabel);
1878                            }
1879                        }
1880
1881                        $result->addRow($copy);
1882                    }
1883                }
1884            }
1885        }
1886        return $result;
1887    }
1888
1889    /**
1890     * Returns a new DataTable created with data from a 'simple' array.
1891     *
1892     * See {@link addRowsFromSimpleArray()}.
1893     *
1894     * @param array $array
1895     * @return \Piwik\DataTable
1896     */
1897    public static function makeFromSimpleArray($array)
1898    {
1899        $dataTable = new DataTable();
1900        $dataTable->addRowsFromSimpleArray($array);
1901        return $dataTable;
1902    }
1903
1904    /**
1905     * Creates a new DataTable instance from a serialized DataTable string.
1906     *
1907     * See {@link getSerialized()} and {@link addRowsFromSerializedArray()}
1908     * for more information on DataTable serialization.
1909     *
1910     * @param string $data
1911     * @return \Piwik\DataTable
1912     */
1913    public static function fromSerializedArray($data)
1914    {
1915        $result = new DataTable();
1916        $result->addRowsFromSerializedArray($data);
1917        return $result;
1918    }
1919
1920    /**
1921     * Aggregates the $row columns to this table.
1922     *
1923     * $row must have a column "label". The $row will be summed to this table's row with the same label.
1924     *
1925     * @param $row
1926     * @params null|array $columnAggregationOps
1927     * @throws \Exception
1928     */
1929    protected function aggregateRowWithLabel(Row $row, $columnAggregationOps)
1930    {
1931        $labelToLookFor = $row->getColumn('label');
1932        if ($labelToLookFor === false) {
1933            $message = sprintf("Label column not found in the table to add in addDataTable(). Row: %s",
1934                var_export($row->getColumns(), 1)
1935            );
1936            throw new Exception($message);
1937        }
1938        $rowFound = $this->getRowFromLabel($labelToLookFor);
1939        // if we find the summary row in the other table, ignore it, since we're aggregating normal rows in this method.
1940        // the summary row is aggregated explicitly after this method is called.
1941        if (!empty($rowFound)
1942            && $rowFound->isSummaryRow()
1943        ) {
1944            $rowFound = false;
1945        }
1946        $this->aggregateRow($rowFound, $row, $columnAggregationOps, $isSummaryRow = false);
1947    }
1948
1949    private function aggregateRow($thisRow, Row $otherRow, $columnAggregationOps, $isSummaryRow)
1950    {
1951        if (empty($thisRow)) {
1952            $thisRow = new Row();
1953            $otherRowLabel = $otherRow->getColumn('label');
1954            if ($otherRowLabel !== false) {
1955                $thisRow->addColumn('label', $otherRowLabel);
1956            }
1957            $thisRow->setAllMetadata($otherRow->getMetadata());
1958
1959            if ($isSummaryRow) {
1960                $this->addSummaryRow($thisRow);
1961            } else {
1962                $this->addRow($thisRow);
1963            }
1964        }
1965
1966        $thisRow->sumRow($otherRow, $copyMeta = true, $columnAggregationOps);
1967
1968        // if the row to add has a subtable whereas the current row doesn't
1969        // we simply add it (cloning the subtable)
1970        // if the row has the subtable already
1971        // then we have to recursively sum the subtables
1972        $subTable = $otherRow->getSubtable();
1973        if ($subTable) {
1974            $subTable->metadata[self::COLUMN_AGGREGATION_OPS_METADATA_NAME] = $columnAggregationOps;
1975            $thisRow->sumSubtable($subTable);
1976        }
1977    }
1978
1979    /**
1980     * @param $row
1981     */
1982    protected function aggregateRowFromSimpleTable($row)
1983    {
1984        if ($row === false) {
1985            return;
1986        }
1987        $thisRow = $this->getFirstRow();
1988        if ($thisRow === false) {
1989            $thisRow = new Row;
1990            $this->addRow($thisRow);
1991        }
1992        $thisRow->sumRow($row, $copyMeta = true, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME));
1993    }
1994
1995    /**
1996     * Unsets all queued filters.
1997     */
1998    public function clearQueuedFilters()
1999    {
2000        $this->queuedFilters = array();
2001    }
2002
2003    public function getQueuedFilters()
2004    {
2005        return $this->queuedFilters;
2006    }
2007
2008    /**
2009     * @return \ArrayIterator|Row[]
2010     */
2011    public function getIterator(): \ArrayIterator
2012    {
2013        return new \ArrayIterator($this->getRows());
2014    }
2015
2016    public function offsetExists($offset): bool
2017    {
2018        $row = $this->getRowFromId($offset);
2019
2020        return false !== $row;
2021    }
2022
2023    public function offsetGet($offset): Row
2024    {
2025        return $this->getRowFromId($offset);
2026    }
2027
2028    public function offsetSet($offset, $value): void
2029    {
2030        $this->rows[$offset] = $value;
2031    }
2032
2033    public function offsetUnset($offset): void
2034    {
2035        $this->deleteRow($offset);
2036    }
2037}
2038