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 */
9namespace Piwik\ViewDataTable;
10
11use Piwik\Cache;
12use Piwik\Common;
13use Piwik\Option;
14use Piwik\Piwik;
15use Piwik\Plugin\Report;
16use Piwik\Plugin\ViewDataTable;
17use Piwik\Plugins\CoreVisualizations\Visualizations\Cloud;
18use Piwik\Plugins\CoreVisualizations\Visualizations\HtmlTable;
19use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Bar;
20use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Pie;
21use Piwik\Plugins\Goals\Visualizations\Goals;
22use Piwik\Plugins\Insights\Visualizations\Insight;
23use Piwik\Plugin\Manager as PluginManager;
24
25/**
26 * ViewDataTable Manager.
27 *
28 */
29class Manager
30{
31    /**
32     * Returns the viewDataTable IDs of a visualization's class lineage.
33     *
34     * @see self::getVisualizationClassLineage
35     *
36     * @param string $klass The visualization class.
37     *
38     * @return array
39     */
40    public static function getIdsWithInheritance($klass)
41    {
42        $klasses = Common::getClassLineage($klass);
43
44        $result = array();
45        foreach ($klasses as $klass) {
46            try {
47                $result[] = $klass::getViewDataTableId();
48            } catch (\Exception $e) {
49                // in case $klass did not define an id: eg Plugin\ViewDataTable
50                continue;
51            }
52        }
53
54        return $result;
55    }
56
57    /**
58     * Returns all registered visualization classes. Uses the 'Visualization.getAvailable'
59     * event to retrieve visualizations.
60     *
61     * @return array Array mapping visualization IDs with their associated visualization classes.
62     * @throws \Exception If a visualization class does not exist or if a duplicate visualization ID
63     *                   is found.
64     * @return array
65     */
66    public static function getAvailableViewDataTables()
67    {
68        $cache = Cache::getTransientCache();
69        $cacheId = 'ViewDataTable.getAvailableViewDataTables';
70        $dataTables = $cache->fetch($cacheId);
71
72        if (!empty($dataTables)) {
73            return $dataTables;
74        }
75
76        $klassToExtend = '\\Piwik\\Plugin\\ViewDataTable';
77
78        /** @var string[] $visualizations */
79        $visualizations = PluginManager::getInstance()->findMultipleComponents('Visualizations', $klassToExtend);
80
81        $result = array();
82
83        foreach ($visualizations as $viz) {
84            if (!class_exists($viz)) {
85                throw new \Exception("Invalid visualization class '$viz' found in Visualization.getAvailableVisualizations.");
86            }
87
88            if (!is_subclass_of($viz, $klassToExtend)) {
89                throw new \Exception("ViewDataTable class '$viz' does not extend Plugin/ViewDataTable");
90            }
91
92            $vizId = $viz::getViewDataTableId();
93
94            if (isset($result[$vizId])) {
95                throw new \Exception("ViewDataTable ID '$vizId' is already in use!");
96            }
97
98            $result[$vizId] = $viz;
99        }
100
101        /**
102         * Triggered to filter available DataTable visualizations.
103         *
104         * Plugins that want to disable certain visualizations should subscribe to
105         * this event and remove visualizations from the incoming array.
106         *
107         * **Example**
108         *
109         *     public function filterViewDataTable(&$visualizations)
110         *     {
111         *         unset($visualizations[HtmlTable::ID]);
112         *     }
113         *
114         * @param array &$visualizations An array of all available visualizations indexed by visualization ID.
115         * @since Piwik 3.0.0
116         */
117        Piwik::postEvent('ViewDataTable.filterViewDataTable', array(&$result));
118
119        $cache->save($cacheId, $result);
120
121        return $result;
122    }
123
124    /**
125     * Returns all available visualizations that are not part of the CoreVisualizations plugin.
126     *
127     * @return array Array mapping visualization IDs with their associated visualization classes.
128     */
129    public static function getNonCoreViewDataTables()
130    {
131        $result = array();
132
133        foreach (static::getAvailableViewDataTables() as $vizId => $vizClass) {
134            if (false === strpos($vizClass, 'Piwik\\Plugins\\CoreVisualizations')
135                && false === strpos($vizClass, 'Piwik\\Plugins\\Goals\\Visualizations\\Goals')) {
136                $result[$vizId] = $vizClass;
137            }
138        }
139
140        return $result;
141    }
142
143    /**
144     * This method determines the default set of footer icons to display below a report.
145     *
146     * $result has the following format:
147     *
148     * ```
149     * array(
150     *     array( // footer icon group 1
151     *         'class' => 'footerIconGroup1CssClass',
152     *         'buttons' => array(
153     *             'id' => 'myid',
154     *             'title' => 'My Tooltip',
155     *             'icon' => 'path/to/my/icon.png'
156     *         )
157     *     ),
158     *     array( // footer icon group 2
159     *         'class' => 'footerIconGroup2CssClass',
160     *         'buttons' => array(...)
161     *     ),
162     *     ...
163     * )
164     * ```
165     */
166    public static function configureFooterIcons(ViewDataTable $view)
167    {
168        $result = array();
169
170        $normalViewIcons = self::getNormalViewIcons($view);
171
172        if (!empty($normalViewIcons['buttons'])) {
173            $result[] = $normalViewIcons;
174        }
175
176        // add insight views
177        $insightsViewIcons = array(
178            'class'   => 'tableInsightViews',
179            'buttons' => array(),
180        );
181
182        $graphViewIcons = self::getGraphViewIcons($view);
183
184        $nonCoreVisualizations = static::getNonCoreViewDataTables();
185
186        foreach ($nonCoreVisualizations as $id => $klass) {
187            if ($klass::canDisplayViewDataTable($view) || $view::ID == $id) {
188                $footerIcon = static::getFooterIconFor($id);
189                if (Insight::ID == $footerIcon['id']) {
190                    $insightsViewIcons['buttons'][] = static::getFooterIconFor($id);
191                } else {
192                    $graphViewIcons['buttons'][] = static::getFooterIconFor($id);
193                }
194            }
195        }
196
197        $graphViewIcons['buttons'] = array_filter($graphViewIcons['buttons']);
198
199        if (!empty($insightsViewIcons['buttons'])
200            && $view->config->show_insights
201        ) {
202            $result[] = $insightsViewIcons;
203        }
204
205        if (!empty($graphViewIcons['buttons'])) {
206            $result[] = $graphViewIcons;
207        }
208
209        return $result;
210    }
211
212    /**
213     * Returns an array with information necessary for adding the viewDataTable to the footer.
214     *
215     * @param string $viewDataTableId
216     *
217     * @return array
218     */
219    private static function getFooterIconFor($viewDataTableId)
220    {
221        $tables = static::getAvailableViewDataTables();
222
223        if (!array_key_exists($viewDataTableId, $tables)) {
224            return;
225        }
226
227        $klass = $tables[$viewDataTableId];
228
229        return array(
230            'id'    => $klass::getViewDataTableId(),
231            'title' => Piwik::translate($klass::FOOTER_ICON_TITLE),
232            'icon'  => $klass::FOOTER_ICON,
233        );
234    }
235
236    public static function clearAllViewDataTableParameters()
237    {
238        Option::deleteLike('viewDataTableParameters_%');
239    }
240
241    public static function clearUserViewDataTableParameters($userLogin)
242    {
243        Option::deleteLike('viewDataTableParameters_' . $userLogin . '_%');
244    }
245
246    public static function getViewDataTableParameters($login, $controllerAction, $containerId = null)
247    {
248        $paramsKey = self::buildViewDataTableParametersOptionKey($login, $controllerAction, $containerId);
249        $params    = Option::get($paramsKey);
250
251        if (empty($params)) {
252            return array();
253        }
254
255        $params = json_decode($params);
256        $params = (array) $params;
257
258        // when setting an invalid parameter, we silently ignore the invalid parameter and proceed
259        $params = self::removeNonOverridableParameters($controllerAction, $params);
260        self::unsetComparisonParams($params);
261
262        return $params;
263    }
264
265    /**
266     * Any parameter set here will be set into one of the following objects:
267     *
268     * - ViewDataTable.requestConfig[paramName]
269     * - ViewDataTable.config.custom_parameters[paramName]
270     * - ViewDataTable.config.custom_parameters[paramName]
271     *
272     * (see ViewDataTable::overrideViewPropertiesWithParams)
273
274     * @param $login
275     * @param $controllerAction
276     * @param $parametersToOverride
277     * @param string|null $containerId
278     * @throws \Exception
279     */
280    public static function saveViewDataTableParameters($login, $controllerAction, $parametersToOverride, $containerId = null)
281    {
282        $params = self::getViewDataTableParameters($login, $controllerAction);
283
284        self::unsetComparisonParams($params);
285
286        foreach ($parametersToOverride as $key => $value) {
287            if ($key === 'viewDataTable'
288                && !empty($params[$key])
289                && $params[$key] !== $value) {
290                if (!empty($params['columns'])) {
291                    unset($params['columns']);
292                }
293                if (!empty($params['columns_to_display'])) {
294                    unset($params['columns_to_display']);
295                }
296            }
297
298            $params[$key] = $value;
299        }
300
301        $paramsKey = self::buildViewDataTableParametersOptionKey($login, $controllerAction, $containerId);
302
303        // when setting an invalid parameter, we fail and let user know
304        self::errorWhenSettingNonOverridableParameter($controllerAction, $params);
305
306        Option::set($paramsKey, json_encode($params));
307    }
308
309    private static function buildViewDataTableParametersOptionKey($login, $controllerAction, $containerId)
310    {
311        $result = sprintf('viewDataTableParameters_%s_%s', $login, $controllerAction);
312        if (!empty($containerId)) {
313            $result .= '_' . $containerId;
314        }
315        return $result;
316    }
317
318    /**
319     * Display a meaningful error message when any invalid parameter is being set.
320     *
321     * @param $params
322     * @throws
323     */
324    private static function errorWhenSettingNonOverridableParameter($controllerAction, $params)
325    {
326        $viewDataTable = self::makeTemporaryViewDataTableInstance($controllerAction, $params);
327        $viewDataTable->throwWhenSettingNonOverridableParameter($params);
328    }
329
330    private static function removeNonOverridableParameters($controllerAction, $params)
331    {
332        $viewDataTable = self::makeTemporaryViewDataTableInstance($controllerAction, $params);
333        $nonOverridableParams = $viewDataTable->getNonOverridableParams($params);
334
335        foreach($params as $key => $value) {
336            if(in_array($key, $nonOverridableParams)) {
337                unset($params[$key]);
338            }
339        }
340        return $params;
341    }
342
343    /**
344     * @param $controllerAction
345     * @param $params
346     * @return ViewDataTable
347     * @throws \Exception
348     */
349    private static function makeTemporaryViewDataTableInstance($controllerAction, $params)
350    {
351        $report = new Report();
352        $viewDataTableType = isset($params['viewDataTable']) ? $params['viewDataTable'] : $report->getDefaultTypeViewDataTable();
353
354        $apiAction = $controllerAction;
355        $loadViewDataTableParametersForUser = false;
356        $viewDataTable = Factory::build($viewDataTableType, $apiAction, $controllerAction, $forceDefault = false, $loadViewDataTableParametersForUser);
357        return $viewDataTable;
358    }
359
360    private static function getNormalViewIcons(ViewDataTable $view)
361    {
362        // add normal view icons (eg, normal table, all columns, goals)
363        $normalViewIcons = array(
364            'class'   => 'tableAllColumnsSwitch',
365            'buttons' => array(),
366        );
367
368        if ($view->config->show_table) {
369            $normalViewIcons['buttons'][] = static::getFooterIconFor(HtmlTable::ID);
370        }
371
372        if ($view->config->show_table_all_columns) {
373            $normalViewIcons['buttons'][] = static::getFooterIconFor(HtmlTable\AllColumns::ID);
374        }
375
376        if ($view->config->show_goals) {
377            $goalButton = static::getFooterIconFor(Goals::ID);
378            if (Common::getRequestVar('idGoal', false) == 'ecommerceOrder') {
379                $goalButton['icon'] = 'icon-ecommerce-order';
380            }
381
382            $normalViewIcons['buttons'][] = $goalButton;
383        }
384
385        if ($view->config->show_ecommerce) {
386            $normalViewIcons['buttons'][] = array(
387                'id' => 'ecommerceOrder',
388                'title' => Piwik::translate('General_EcommerceOrders'),
389                'icon' => 'icon-ecommerce-order',
390                'text' => Piwik::translate('General_EcommerceOrders')
391            );
392
393            $normalViewIcons['buttons'][] = array(
394                'id' => 'ecommerceAbandonedCart',
395                'title' => Piwik::translate('General_AbandonedCarts'),
396                'icon' => 'icon-ecommerce-abandoned-cart',
397                'text' => Piwik::translate('General_AbandonedCarts')
398            );
399        }
400
401        $normalViewIcons['buttons'] = array_filter($normalViewIcons['buttons']);
402
403        return $normalViewIcons;
404    }
405
406    private static function getGraphViewIcons(ViewDataTable $view)
407    {
408        // add graph views
409        $graphViewIcons = array(
410            'class'   => 'tableGraphViews',
411            'buttons' => array(),
412        );
413
414        if ($view->config->show_all_views_icons) {
415            if ($view->config->show_bar_chart) {
416                $graphViewIcons['buttons'][] = static::getFooterIconFor(Bar::ID);
417            }
418
419            if ($view->config->show_pie_chart) {
420                $graphViewIcons['buttons'][] = static::getFooterIconFor(Pie::ID);
421            }
422
423            if ($view->config->show_tag_cloud) {
424                $graphViewIcons['buttons'][] = static::getFooterIconFor(Cloud::ID);
425            }
426        }
427
428        return $graphViewIcons;
429    }
430
431    private static function unsetComparisonParams(&$params)
432    {
433        unset($params['compareDates']);
434        unset($params['comparePeriods']);
435        unset($params['compareSegments']);
436        unset($params['compare']);
437    }
438}
439