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\Plugin;
10
11use Exception;
12use Piwik\Access;
13use Piwik\API\Proxy;
14use Piwik\API\Request;
15use Piwik\Common;
16use Piwik\Config as PiwikConfig;
17use Piwik\Container\StaticContainer;
18use Piwik\Date;
19use Piwik\Exception\NoPrivilegesException;
20use Piwik\Exception\NoWebsiteFoundException;
21use Piwik\FrontController;
22use Piwik\Menu\MenuAdmin;
23use Piwik\Menu\MenuTop;
24use Piwik\NoAccessException;
25use Piwik\Notification\Manager as NotificationManager;
26use Piwik\Period\Month;
27use Piwik\Period;
28use Piwik\Period\PeriodValidator;
29use Piwik\Period\Range;
30use Piwik\Piwik;
31use Piwik\Plugins\CoreAdminHome\CustomLogo;
32use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution;
33use Piwik\Plugins\LanguagesManager\LanguagesManager;
34use Piwik\SettingsPiwik;
35use Piwik\Site;
36use Piwik\Url;
37use Piwik\Plugin;
38use Piwik\View;
39use Piwik\View\ViewInterface;
40use Piwik\ViewDataTable\Factory as ViewDataTableFactory;
41
42/**
43 * Base class of all plugin Controllers.
44 *
45 * Plugins that wish to add display HTML should create a Controller that either
46 * extends from this class or from {@link ControllerAdmin}. Every public method in
47 * the controller will be exposed as a controller method and can be invoked via
48 * an HTTP request.
49 *
50 * Learn more about Piwik's MVC system [here](/guides/mvc-in-piwik).
51 *
52 * ### Examples
53 *
54 * **Defining a controller**
55 *
56 *     class Controller extends \Piwik\Plugin\Controller
57 *     {
58 *         public function index()
59 *         {
60 *             $view = new View("@MyPlugin/index.twig");
61 *             // ... setup view ...
62 *             return $view->render();
63 *         }
64 *     }
65 *
66 * **Linking to a controller action**
67 *
68 *     <a href="?module=MyPlugin&action=index&idSite=1&period=day&date=2013-10-10">Link</a>
69 *
70 */
71abstract class Controller
72{
73    /**
74     * The plugin name, eg. `'Referrers'`.
75     *
76     * @var string
77     * @api
78     */
79    protected $pluginName;
80
81    /**
82     * The value of the **date** query parameter.
83     *
84     * @var string
85     * @api
86     */
87    protected $strDate;
88
89    /**
90     * The Date object created with ($strDate)[#strDate] or null if the requested date is a range.
91     *
92     * @var Date|null
93     * @api
94     */
95    protected $date;
96
97    /**
98     * The value of the **idSite** query parameter.
99     *
100     * @var int
101     * @api
102     */
103    protected $idSite;
104
105    /**
106     * The Site object created with {@link $idSite}.
107     *
108     * @var Site
109     * @api
110     */
111    protected $site = null;
112
113    /**
114     * The SecurityPolicy object.
115     *
116     * @var \Piwik\View\SecurityPolicy
117     * @api
118     */
119    protected $securityPolicy = null;
120
121    /**
122     * Constructor.
123     *
124     * @api
125     */
126    public function __construct()
127    {
128        $this->init();
129    }
130
131    protected function init()
132    {
133        $aPluginName = explode('\\', get_class($this));
134        $this->pluginName = $aPluginName[2];
135
136        $this->securityPolicy = StaticContainer::get(View\SecurityPolicy::class);
137
138        $date = Common::getRequestVar('date', 'yesterday', 'string');
139        try {
140            $this->idSite = Common::getRequestVar('idSite', false, 'int');
141            $this->site = new Site($this->idSite);
142            $date = $this->getDateParameterInTimezone($date, $this->site->getTimezone());
143            $this->setDate($date);
144        } catch (Exception $e) {
145            // the date looks like YYYY-MM-DD,YYYY-MM-DD or other format
146            $this->date = null;
147        }
148    }
149
150    /**
151     * Helper method that converts `"today"` or `"yesterday"` to the specified timezone.
152     * If the date is absolute, ie. YYYY-MM-DD, it will not be converted to the timezone.
153     *
154     * @param string $date `'today'`, `'yesterday'`, `'YYYY-MM-DD'`
155     * @param string $timezone The timezone to use.
156     * @return Date
157     * @api
158     */
159    protected function getDateParameterInTimezone($date, $timezone)
160    {
161        $timezoneToUse = null;
162        // if the requested date is not YYYY-MM-DD, we need to ensure
163        //  it is relative to the website's timezone
164        if (in_array($date, array('today', 'yesterday'))) {
165            // today is at midnight; we really want to get the time now, so that
166            // * if the website is UTC+12 and it is 5PM now in UTC, the calendar will allow to select the UTC "tomorrow"
167            // * if the website is UTC-12 and it is 5AM now in UTC, the calendar will allow to select the UTC "yesterday"
168            if ($date === 'today') {
169                $date = 'now';
170            } elseif ($date === 'yesterday') {
171                $date = 'yesterdaySameTime';
172            }
173            $timezoneToUse = $timezone;
174        }
175        return Date::factory($date, $timezoneToUse);
176    }
177
178    /**
179     * Sets the date to be used by all other methods in the controller.
180     * If the date has to be modified, this method should be called just after
181     * construction.
182     *
183     * @param Date $date The new Date.
184     * @return void
185     * @api
186     */
187    protected function setDate(Date $date)
188    {
189        $this->date = $date;
190        $this->strDate = $date->toString();
191    }
192
193    /**
194     * Returns values that are enabled for the parameter &period=
195     * @return array eg. array('day', 'week', 'month', 'year', 'range')
196     */
197    protected static function getEnabledPeriodsInUI()
198    {
199        $periodValidator = new PeriodValidator();
200        return $periodValidator->getPeriodsAllowedForUI();
201    }
202
203    /**
204     * @return array
205     */
206    private static function getEnabledPeriodsNames()
207    {
208        $availablePeriods = self::getEnabledPeriodsInUI();
209        $periodNames = array(
210            'day'   => array(
211                'singular' => Piwik::translate('Intl_PeriodDay'),
212                'plural' => Piwik::translate('Intl_PeriodDays')
213            ),
214            'week'  => array(
215                'singular' => Piwik::translate('Intl_PeriodWeek'),
216                'plural' => Piwik::translate('Intl_PeriodWeeks')
217            ),
218            'month' => array(
219                'singular' => Piwik::translate('Intl_PeriodMonth'),
220                'plural' => Piwik::translate('Intl_PeriodMonths')
221            ),
222            'year'  => array(
223                'singular' => Piwik::translate('Intl_PeriodYear'),
224                'plural' => Piwik::translate('Intl_PeriodYears')
225            ),
226            // Note: plural is not used for date range
227            'range' => array(
228                'singular' => Piwik::translate('General_DateRangeInPeriodList'),
229                'plural' => Piwik::translate('General_DateRangeInPeriodList')
230            ),
231        );
232
233        $periodNames = array_intersect_key($periodNames, array_fill_keys($availablePeriods, true));
234        return $periodNames;
235    }
236
237    /**
238     * Returns the name of the default method that will be called
239     * when visiting: index.php?module=PluginName without the action parameter.
240     *
241     * @return string
242     * @api
243     */
244    public function getDefaultAction()
245    {
246        return 'index';
247    }
248
249    /**
250     * A helper method that renders a view either to the screen or to a string.
251     *
252     * @param ViewInterface $view The view to render.
253     * @return string|void
254     */
255    protected function renderView(ViewInterface $view)
256    {
257        return $view->render();
258    }
259
260    /**
261     * Assigns the given variables to the template and renders it.
262     *
263     * Example:
264     *
265     *     public function myControllerAction () {
266     *        return $this->renderTemplate('index', array(
267     *            'answerToLife' => '42'
268     *        ));
269     *     }
270     *
271     * This will render the 'index.twig' file within the plugin templates folder and assign the view variable
272     * `answerToLife` to `42`.
273     *
274     * @param string $template   The name of the template file. If only a name is given it will automatically use
275     *                           the template within the plugin folder. For instance 'myTemplate' will result in
276     *                           '@$pluginName/myTemplate.twig'. Alternatively you can include the full path:
277     *                           '@anyOtherFolder/otherTemplate'. The trailing '.twig' is not needed.
278     * @param array $variables   For instance array('myViewVar' => 'myValue'). In template you can use {{ myViewVar }}
279     * @return string
280     * @since 2.5.0
281     * @api
282     */
283    protected function renderTemplate($template, array $variables = [])
284    {
285        return $this->renderTemplateAs($template, $variables);
286    }
287
288    /**
289     * @see {self::renderTemplate()}
290     *
291     * @param $template
292     * @param array $variables
293     * @param string|null $viewType 'basic' or 'admin'. If null, determined based on the controller instance type.
294     * @return string
295     * @throws Exception
296     */
297    protected function renderTemplateAs($template, array $variables = array(), $viewType = null)
298    {
299        if (false === strpos($template, '@') || false === strpos($template, '/')) {
300            $template = '@' . $this->pluginName . '/' . $template;
301        }
302
303        $view = new View($template);
304
305        $this->checkViewType($viewType);
306
307        if (empty($viewType)) {
308            $viewType = $this instanceof ControllerAdmin ? 'admin' : 'basic';
309        }
310
311        // alternatively we could check whether the templates extends either admin.twig or dashboard.twig and based on
312        // that call the correct method. This will be needed once we unify Controller and ControllerAdmin see
313        // https://github.com/piwik/piwik/issues/6151
314        if ($this instanceof ControllerAdmin && $viewType === 'admin') {
315            $this->setBasicVariablesViewAs($view, $viewType);
316        } elseif (empty($this->site) || empty($this->idSite)) {
317            $this->setBasicVariablesViewAs($view, $viewType);
318        } else {
319            $this->setGeneralVariablesViewAs($view, $viewType);
320        }
321
322        foreach ($variables as $key => $value) {
323            $view->$key = $value;
324        }
325
326        if (isset($view->siteName)) {
327            $view->siteNameDecoded = Common::unsanitizeInputValue($view->siteName);
328        }
329
330        return $view->render();
331    }
332
333    /**
334     * Convenience method that creates and renders a ViewDataTable for a API method.
335     *
336     * @param string|\Piwik\Plugin\Report $apiAction The name of the API action (eg, `'getResolution'`) or
337     *                                      an instance of an report.
338     * @param bool $controllerAction The name of the Controller action name  that is rendering the report. Defaults
339     *                               to the `$apiAction`.
340     * @param bool $fetch If `true`, the rendered string is returned, if `false` it is `echo`'d.
341     * @throws \Exception if `$pluginName` is not an existing plugin or if `$apiAction` is not an
342     *                    existing method of the plugin's API.
343     * @return string|void See `$fetch`.
344     * @api
345     */
346    protected function renderReport($apiAction, $controllerAction = false)
347    {
348        if (empty($controllerAction) && is_string($apiAction)) {
349            $report = ReportsProvider::factory($this->pluginName, $apiAction);
350
351            if (!empty($report)) {
352                $apiAction = $report;
353            }
354        }
355
356        if ($apiAction instanceof Report) {
357            $this->checkSitePermission();
358            $apiAction->checkIsEnabled();
359
360            return $apiAction->render();
361        }
362
363        $pluginName = $this->pluginName;
364
365        /** @var Proxy $apiProxy */
366        $apiProxy = Proxy::getInstance();
367
368        if (!$apiProxy->isExistingApiAction($pluginName, $apiAction)) {
369            throw new \Exception("Invalid action name '$apiAction' for '$pluginName' plugin.");
370        }
371
372        $apiAction = $apiProxy->buildApiActionName($pluginName, $apiAction);
373
374        if ($controllerAction !== false) {
375            $controllerAction = $pluginName . '.' . $controllerAction;
376        }
377
378        $view      = ViewDataTableFactory::build(null, $apiAction, $controllerAction);
379        $rendered  = $view->render();
380
381        return $rendered;
382    }
383
384    /**
385     * Returns a ViewDataTable object that will render a jqPlot evolution graph
386     * for the last30 days/weeks/etc. of the current period, relative to the current date.
387     *
388     * @param string $currentModuleName The name of the current plugin.
389     * @param string $currentControllerAction The name of the action that renders the desired
390     *                                        report.
391     * @param string $apiMethod The API method that the ViewDataTable will use to get
392     *                          graph data.
393     * @return ViewDataTable
394     * @api
395     */
396    protected function getLastUnitGraph($currentModuleName, $currentControllerAction, $apiMethod)
397    {
398        $view = ViewDataTableFactory::build(
399            Evolution::ID, $apiMethod, $currentModuleName . '.' . $currentControllerAction, $forceDefault = true);
400        $view->config->show_goals = false;
401        return $view;
402    }
403
404    /**
405     * Same as {@link getLastUnitGraph()}, but will set some properties of the ViewDataTable
406     * object based on the arguments supplied.
407     *
408     * @param string $currentModuleName The name of the current plugin.
409     * @param string $currentControllerAction The name of the action that renders the desired
410     *                                        report.
411     * @param array $columnsToDisplay The value to use for the ViewDataTable's columns_to_display config
412     *                                property.
413     * @param array $selectableColumns The value to use for the ViewDataTable's selectable_columns config
414     *                                 property.
415     * @param bool|string $reportDocumentation The value to use for the ViewDataTable's documentation config
416     *                                         property.
417     * @param string $apiMethod The API method that the ViewDataTable will use to get graph data.
418     * @return ViewDataTable
419     * @api
420     */
421    protected function getLastUnitGraphAcrossPlugins($currentModuleName, $currentControllerAction, $columnsToDisplay = false,
422                                                     $selectableColumns = array(), $reportDocumentation = false,
423                                                     $apiMethod = 'API.get')
424    {
425        // load translations from meta data
426        $idSite = Common::getRequestVar('idSite');
427        $period = Common::getRequestVar('period');
428        $date = Common::getRequestVar('date');
429        $meta = \Piwik\Plugins\API\API::getInstance()->getReportMetadata($idSite, $period, $date);
430
431        $columns = array_merge($columnsToDisplay ? $columnsToDisplay : array(), $selectableColumns);
432        $translations = array_combine($columns, $columns);
433        foreach ($meta as $reportMeta) {
434            if ($reportMeta['action'] === 'get' && !isset($reportMeta['parameters'])) {
435                foreach ($columns as $column) {
436                    if (isset($reportMeta['metrics'][$column])) {
437                        $translations[$column] = $reportMeta['metrics'][$column];
438                    }
439                }
440            }
441        }
442
443        // initialize the graph and load the data
444        $view = $this->getLastUnitGraph($currentModuleName, $currentControllerAction, $apiMethod);
445
446        if ($columnsToDisplay !== false) {
447            $view->config->columns_to_display = $columnsToDisplay;
448        }
449
450        if (property_exists($view->config, 'selectable_columns')) {
451            $view->config->selectable_columns = array_merge($view->config->selectable_columns ? : array(), $selectableColumns);
452        }
453
454        $view->config->translations += $translations;
455
456        if ($reportDocumentation) {
457            $view->config->documentation = $reportDocumentation;
458        }
459
460        return $view;
461    }
462
463    /**
464     * Returns the array of new processed parameters once the parameters are applied.
465     * For example: if you set range=last30 and date=2008-03-10,
466     *  the date element of the returned array will be "2008-02-10,2008-03-10"
467     *
468     * Parameters you can set:
469     * - range: last30, previous10, etc.
470     * - date: YYYY-MM-DD, today, yesterday
471     * - period: day, week, month, year
472     *
473     * @param array $paramsToSet array( 'date' => 'last50', 'viewDataTable' =>'sparkline' )
474     * @throws \Piwik\NoAccessException
475     * @return array
476     */
477    protected function getGraphParamsModified($paramsToSet = array())
478    {
479        if (!isset($paramsToSet['period'])) {
480            $period = Common::getRequestVar('period');
481        } else {
482            $period = $paramsToSet['period'];
483        }
484        if ($period === 'range') {
485            return $paramsToSet;
486        }
487        if (!isset($paramsToSet['range'])) {
488            $range = 'last30';
489        } else {
490            $range = $paramsToSet['range'];
491        }
492
493        if (!isset($paramsToSet['date'])) {
494            $endDate = $this->strDate;
495        } else {
496            $endDate = $paramsToSet['date'];
497        }
498
499        if (is_null($this->site)) {
500            throw new NoAccessException("Website not initialized, check that you are logged in and/or using the correct token_auth.");
501        }
502        $paramDate = Range::getRelativeToEndDate($period, $range, $endDate, $this->site);
503
504        $params = array_merge($paramsToSet, array('date' => $paramDate));
505        return $params;
506    }
507
508    /**
509     * Returns a numeric value from the API.
510     * Works only for API methods that originally returns numeric values (there is no cast here)
511     *
512     * @param string $methodToCall Name of method to call, eg. Referrers.getNumberOfDistinctSearchEngines
513     * @param bool|string $date A custom date to use when getting the value. If false, the 'date' query
514     *                                          parameter is used.
515     *
516     * @return int|float
517     */
518    protected function getNumericValue($methodToCall, $date = false)
519    {
520        $params = $date === false ? array() : array('date' => $date);
521
522        $return = Request::processRequest($methodToCall, $params);
523        $columns = $return->getFirstRow()->getColumns();
524        return reset($columns);
525    }
526
527    /**
528     * Returns a URL to a sparkline image for a report served by the current plugin.
529     *
530     * The result of this URL should be used with the [sparkline()](/api-reference/Piwik/View#twig) twig function.
531     *
532     * The current site ID and period will be used.
533     *
534     * @param string $action Method name of the controller that serves the report.
535     * @param array $customParameters The array of query parameter name/value pairs that
536     *                                should be set in result URL.
537     * @return string The generated URL.
538     * @api
539     */
540    protected function getUrlSparkline($action, $customParameters = array())
541    {
542        $params = $this->getGraphParamsModified(
543            array('viewDataTable' => 'sparkline',
544                  'action'        => $action,
545                  'module'        => $this->pluginName)
546            + $customParameters
547        );
548        // convert array values to comma separated
549        foreach ($params as &$value) {
550            if (is_array($value)) {
551                $value = rawurlencode(implode(',', $value));
552            }
553        }
554        $url = Url::getCurrentQueryStringWithParametersModified($params);
555        return $url;
556    }
557
558    /**
559     * Sets the first date available in the period selector's calendar.
560     *
561     * @param Date $minDate The min date.
562     * @param View $view The view that contains the period selector.
563     * @api
564     */
565    protected function setMinDateView(Date $minDate, $view)
566    {
567        $view->minDateYear = $minDate->toString('Y');
568        $view->minDateMonth = $minDate->toString('m');
569        $view->minDateDay = $minDate->toString('d');
570    }
571
572    /**
573     * Sets the last date available in the period selector's calendar. Usually this is just the "today" date
574     * for a site (which varies based on the timezone of a site).
575     *
576     * @param Date $maxDate The max date.
577     * @param View $view The view that contains the period selector.
578     * @api
579     */
580    protected function setMaxDateView(Date $maxDate, $view)
581    {
582        $view->maxDateYear = $maxDate->toString('Y');
583        $view->maxDateMonth = $maxDate->toString('m');
584        $view->maxDateDay = $maxDate->toString('d');
585    }
586
587    /**
588     * Assigns variables to {@link Piwik\View} instances that display an entire page.
589     *
590     * The following variables assigned:
591     *
592     * **date** - The value of the **date** query parameter.
593     * **idSite** - The value of the **idSite** query parameter.
594     * **rawDate** - The value of the **date** query parameter.
595     * **prettyDate** - A pretty string description of the current period.
596     * **siteName** - The current site's name.
597     * **siteMainUrl** - The URL of the current site.
598     * **startDate** - The start date of the current period. A {@link Piwik\Date} instance.
599     * **endDate** - The end date of the current period. A {@link Piwik\Date} instance.
600     * **language** - The current language's language code.
601     * **config_action_url_category_delimiter** - The value of the `[General] action_url_category_delimiter`
602     *                                            INI config option.
603     * **topMenu** - The result of `MenuTop::getInstance()->getMenu()`.
604     *
605     * As well as the variables set by {@link setPeriodVariablesView()}.
606     *
607     * Will exit on error.
608     *
609     * @param View $view
610     * @param string|null $viewType 'basic' or 'admin'. If null, set based on the type of controller.
611     * @return void
612     * @api
613     */
614    protected function setGeneralVariablesView($view)
615    {
616        $this->setGeneralVariablesViewAs($view, $viewType = null);
617    }
618
619    protected function setGeneralVariablesViewAs($view, $viewType)
620    {
621        $this->checkViewType($viewType);
622
623        if ($viewType === null) {
624            $viewType = $this instanceof ControllerAdmin ? 'admin' : 'basic';
625        }
626
627        $view->idSite = $this->idSite;
628        $this->checkSitePermission();
629        $this->setPeriodVariablesView($view);
630
631        $view->siteName = $this->site->getName();
632        $view->siteMainUrl = $this->site->getMainUrl();
633
634        $siteTimezone = $this->site->getTimezone();
635
636        $datetimeMinDate = $this->site->getCreationDate()->getDatetime();
637        $minDate = Date::factory($datetimeMinDate, $siteTimezone);
638        $this->setMinDateView($minDate, $view);
639
640        $maxDate = Date::factory('now', $siteTimezone);
641        $this->setMaxDateView($maxDate, $view);
642
643        $rawDate = Common::getRequestVar('date');
644        Period::checkDateFormat($rawDate);
645
646        $periodStr = Common::getRequestVar('period');
647
648        if ($periodStr !== 'range') {
649            $date      = Date::factory($this->strDate);
650            $validDate = $this->getValidDate($date, $minDate, $maxDate);
651            $period    = Period\Factory::build($periodStr, $validDate);
652
653            if ($date->toString() !== $validDate->toString()) {
654                // we to not always change date since it could convert a strDate "today" to "YYYY-MM-DD"
655                // only change $this->strDate if it was not valid before
656                $this->setDate($validDate);
657            }
658        } else {
659            $period = new Range($periodStr, $rawDate, $siteTimezone);
660        }
661
662        // Setting current period start & end dates, for pre-setting the calendar when "Date Range" is selected
663        $dateStart = $period->getDateStart();
664        $dateStart = $this->getValidDate($dateStart, $minDate, $maxDate);
665
666        $dateEnd   = $period->getDateEnd();
667        $dateEnd   = $this->getValidDate($dateEnd, $minDate, $maxDate);
668
669        if ($periodStr === 'range') {
670            // make sure we actually display the correct calendar pretty date
671            $newRawDate = $dateStart->toString() . ',' . $dateEnd->toString();
672            $period = new Range($periodStr, $newRawDate, $siteTimezone);
673        }
674
675        $view->date = $this->strDate;
676        $view->prettyDate = self::getCalendarPrettyDate($period);
677        // prettyDateLong is not used by core, leaving in case plugins may be using it
678        $view->prettyDateLong = $period->getLocalizedLongString();
679        $view->rawDate = $rawDate;
680        $view->startDate = $dateStart;
681        $view->endDate = $dateEnd;
682
683        $timezoneOffsetInSeconds = Date::getUtcOffset($siteTimezone);
684        $view->timezoneOffset = $timezoneOffsetInSeconds;
685
686        $language = LanguagesManager::getLanguageForSession();
687        $view->language = !empty($language) ? $language : LanguagesManager::getLanguageCodeForCurrentUser();
688
689        $this->setBasicVariablesViewAs($view, $viewType);
690
691        $view->topMenu = MenuTop::getInstance()->getMenu();
692        $view->adminMenu = MenuAdmin::getInstance()->getMenu();
693
694        $notifications = $view->notifications;
695        if (empty($notifications)) {
696            $view->notifications = NotificationManager::getAllNotificationsToDisplay();
697            NotificationManager::cancelAllNonPersistent();
698        }
699    }
700
701    private function getValidDate(Date $date, Date $minDate, Date $maxDate)
702    {
703        if ($date->isEarlier($minDate)) {
704            $date = $minDate;
705        }
706
707        if ($date->isLater($maxDate)) {
708            $date = $maxDate;
709        }
710
711        return $date;
712    }
713
714    /**
715     * Needed when a controller extends ControllerAdmin but you don't want to call the controller admin basic variables
716     * view. Solves a problem when a controller has regular controller and admin controller views.
717     * @param View $view
718     */
719    protected function setBasicVariablesNoneAdminView($view)
720    {
721        $view->clientSideConfig = PiwikConfig::getInstance()->getClientSideOptions();
722        $view->isSuperUser = Access::getInstance()->hasSuperUserAccess();
723        $view->hasSomeAdminAccess = Piwik::isUserHasSomeAdminAccess();
724        $view->hasSomeViewAccess  = Piwik::isUserHasSomeViewAccess();
725        $view->isUserIsAnonymous  = Piwik::isUserIsAnonymous();
726        $view->hasSuperUserAccess = Piwik::hasUserSuperUserAccess();
727
728        if (!Piwik::isUserIsAnonymous()) {
729            $view->contactEmail = implode(',', Piwik::getContactEmailAddresses());
730
731            // for BC only. Use contactEmail instead
732            $view->emailSuperUser = implode(',', Piwik::getAllSuperUserAccessEmailAddresses());
733        }
734
735        $capabilities = array();
736        if ($this->idSite && $this->site) {
737            $capabilityProvider = StaticContainer::get(Access\CapabilitiesProvider::class);
738            foreach ($capabilityProvider->getAllCapabilities() as $capability) {
739                if (Piwik::isUserHasCapability($this->idSite, $capability->getId())) {
740                    $capabilities[] = $capability->getId();
741                }
742            }
743        }
744
745        $view->userCapabilities = $capabilities;
746
747        $this->addCustomLogoInfo($view);
748
749        $customLogo = new CustomLogo();
750        $view->logoHeader = $customLogo->getHeaderLogoUrl();
751        $view->logoLarge = $customLogo->getLogoUrl();
752        $view->logoSVG = $customLogo->getSVGLogoUrl();
753        $view->hasSVGLogo = $customLogo->hasSVGLogo();
754        $view->contactEmail = implode(',', Piwik::getContactEmailAddresses());
755        $view->themeStyles = ThemeStyles::get();
756
757        $general = PiwikConfig::getInstance()->General;
758        $view->enableFrames = $general['enable_framed_pages']
759            || (isset($general['enable_framed_logins']) && $general['enable_framed_logins']);
760        $embeddedAsIframe = (Common::getRequestVar('module', '', 'string') === 'Widgetize');
761        if (!$view->enableFrames && !$embeddedAsIframe) {
762            $view->setXFrameOptions('sameorigin');
763        }
764
765        $pluginManager = Plugin\Manager::getInstance();
766        $view->relativePluginWebDirs = (object) $pluginManager->getWebRootDirectoriesForCustomPluginDirs();
767        $view->isMultiSitesEnabled = $pluginManager->isPluginActivated('MultiSites');
768        $view->isSingleSite = Access::doAsSuperUser(function() {
769            $allSites = Request::processRequest('SitesManager.getAllSitesId', [], []);
770            return count($allSites) === 1;
771        });
772
773        if (isset($this->site) && is_object($this->site) && $this->site instanceof Site) {
774            $view->siteName = $this->site->getName();
775        }
776
777        self::setHostValidationVariablesView($view);
778    }
779
780    /**
781     * Assigns a set of generally useful variables to a {@link Piwik\View} instance.
782     *
783     * The following variables assigned:
784     *
785     * **isSuperUser** - True if the current user is the Super User, false if otherwise.
786     * **hasSomeAdminAccess** - True if the current user has admin access to at least one site,
787     *                          false if otherwise.
788     * **isCustomLogo** - The value of the `branding_use_custom_logo` option.
789     * **logoHeader** - The header logo URL to use.
790     * **logoLarge** - The large logo URL to use.
791     * **logoSVG** - The SVG logo URL to use.
792     * **hasSVGLogo** - True if there is a SVG logo, false if otherwise.
793     * **enableFrames** - The value of the `[General] enable_framed_pages` INI config option. If
794     *                    true, {@link Piwik\View::setXFrameOptions()} is called on the view.
795     *
796     * Also calls {@link setHostValidationVariablesView()}.
797     *
798     * @param View $view
799     * @param string $viewType 'basic' or 'admin'. Used by ControllerAdmin.
800     * @api
801     */
802    protected function setBasicVariablesView($view)
803    {
804        $this->setBasicVariablesViewAs($view);
805    }
806
807    protected function setBasicVariablesViewAs($view, $viewType = null)
808    {
809        $this->checkViewType($viewType); // param is not used here, but the check can be useful for a developer
810
811        $this->setBasicVariablesNoneAdminView($view);
812    }
813
814    protected function addCustomLogoInfo($view)
815    {
816        $customLogo = new CustomLogo();
817        $view->isCustomLogo  = $customLogo->isEnabled();
818        $view->customFavicon = $customLogo->getPathUserFavicon();
819    }
820
821    /**
822     * Checks if the current host is valid and sets variables on the given view, including:
823     *
824     * - **isValidHost** - true if host is valid, false if otherwise
825     * - **invalidHostMessage** - message to display if host is invalid (only set if host is invalid)
826     * - **invalidHost** - the invalid hostname (only set if host is invalid)
827     * - **mailLinkStart** - the open tag of a link to email the Super User of this problem (only set
828     *                       if host is invalid)
829     *
830     * @param View $view
831     * @api
832     */
833    public static function setHostValidationVariablesView($view)
834    {
835        // check if host is valid
836        $view->isValidHost = Url::isValidHost();
837        if (!$view->isValidHost) {
838            // invalid host, so display warning to user
839            $validHosts = Url::getTrustedHostsFromConfig();
840            $validHost = $validHosts[0];
841            $invalidHost = Common::sanitizeInputValue(Url::getHost(false));
842
843            $emailSubject = rawurlencode(Piwik::translate('CoreHome_InjectedHostEmailSubject', $invalidHost));
844            $emailBody = rawurlencode(Piwik::translate('CoreHome_InjectedHostEmailBody'));
845            $superUserEmail = implode(',', Piwik::getContactEmailAddresses());
846
847            $mailToUrl = "mailto:$superUserEmail?subject=$emailSubject&body=$emailBody";
848            $mailLinkStart = "<a href=\"$mailToUrl\">";
849
850            $invalidUrl = Url::getCurrentUrlWithoutQueryString($checkIfTrusted = false);
851            $validUrl = Url::getCurrentScheme() . '://' . $validHost
852                . Url::getCurrentScriptName();
853            $invalidUrl = Common::sanitizeInputValue($invalidUrl);
854            $validUrl = Common::sanitizeInputValue($validUrl);
855
856            $changeTrustedHostsUrl = "index.php"
857                . Url::getCurrentQueryStringWithParametersModified(array(
858                                                                        'module' => 'CoreAdminHome',
859                                                                        'action' => 'generalSettings'
860                                                                   ))
861                . "#trustedHostsSection";
862
863            $warningStart = Piwik::translate('CoreHome_InjectedHostWarningIntro', array(
864                                                                                      '<strong>' . $invalidUrl . '</strong>',
865                                                                                      '<strong>' . $validUrl . '</strong>'
866                                                                                 )) . ' <br/>';
867
868            if (Piwik::hasUserSuperUserAccess()) {
869                $view->invalidHostMessage = $warningStart . ' '
870                    . Piwik::translate('CoreHome_InjectedHostSuperUserWarning', array(
871                                                                                    "<a href=\"$changeTrustedHostsUrl\">",
872                                                                                    $invalidHost,
873                                                                                    '</a>',
874                                                                                    "<br/><a href=\"$validUrl\">",
875                                                                                    $validHost,
876                                                                                    '</a>'
877                                                                               ));
878            } elseif (Piwik::isUserIsAnonymous()) {
879                $view->invalidHostMessage = $warningStart . ' '
880                    . Piwik::translate('CoreHome_InjectedHostNonSuperUserWarning', array(
881                        "<br/><a href=\"$validUrl\">",
882                        '</a>',
883                        '<span style="display:none">',
884                        '</span>'
885                    ));
886            } else {
887                $view->invalidHostMessage = $warningStart . ' '
888                    . Piwik::translate('CoreHome_InjectedHostNonSuperUserWarning', array(
889                                                                                       "<br/><a href=\"$validUrl\">",
890                                                                                       '</a>',
891                                                                                       $mailLinkStart,
892                                                                                       '</a>'
893                                                                                  ));
894            }
895            $view->invalidHostMessageHowToFix = '<p><b>How do I fix this problem and how do I login again?</b><br/> The Matomo Super User can manually edit the file /path/to/matomo/config/config.ini.php
896						and add the following lines: <pre>[General]' . "\n" . 'trusted_hosts[] = "' . $invalidHost . '"</pre>After making the change, you will be able to login again.</p>
897						<p>You may also <i>disable this security feature (not recommended)</i>. To do so edit config/config.ini.php and add:
898						<pre>[General]' . "\n" . 'enable_trusted_host_check=0</pre>';
899
900            $view->invalidHost = $invalidHost; // for UserSettings warning
901            $view->invalidHostMailLinkStart = $mailLinkStart;
902        }
903    }
904
905    /**
906     * Sets general period variables on a view, including:
907     *
908     * - **displayUniqueVisitors** - Whether unique visitors should be displayed for the current
909     *                               period.
910     * - **period** - The value of the **period** query parameter.
911     * - **otherPeriods** - `array('day', 'week', 'month', 'year', 'range')`
912     * - **periodsNames** - List of available periods mapped to their singular and plural translations.
913     *
914     * @param View $view
915     * @throws Exception if the current period is invalid.
916     * @api
917     */
918    public static function setPeriodVariablesView($view)
919    {
920        if (isset($view->period)) {
921            return;
922        }
923
924        $periodValidator = new PeriodValidator();
925
926        $currentPeriod = Common::getRequestVar('period');
927        $view->displayUniqueVisitors = SettingsPiwik::isUniqueVisitorsEnabled($currentPeriod);
928        $availablePeriods = $periodValidator->getPeriodsAllowedForUI();
929
930        if (! $periodValidator->isPeriodAllowedForUI($currentPeriod)) {
931            throw new Exception("Period must be one of: " . implode(", ", $availablePeriods));
932        }
933
934        $found = array_search($currentPeriod, $availablePeriods);
935        unset($availablePeriods[$found]);
936
937        $view->period = $currentPeriod;
938        $view->otherPeriods = $availablePeriods;
939        $view->enabledPeriods = self::getEnabledPeriodsInUI();
940        $view->periodsNames = self::getEnabledPeriodsNames();
941    }
942
943    /**
944     * Helper method used to redirect the current HTTP request to another module/action.
945     *
946     * This function will exit immediately after executing.
947     *
948     * @param string $moduleToRedirect The plugin to redirect to, eg. `"MultiSites"`.
949     * @param string $actionToRedirect Action, eg. `"index"`.
950     * @param int|null $websiteId The new idSite query parameter, eg, `1`.
951     * @param string|null $defaultPeriod The new period query parameter, eg, `'day'`.
952     * @param string|null $defaultDate The new date query parameter, eg, `'today'`.
953     * @param array $parameters Other query parameters to append to the URL.
954     * @api
955     */
956    public function redirectToIndex($moduleToRedirect, $actionToRedirect, $websiteId = null, $defaultPeriod = null,
957                                    $defaultDate = null, $parameters = array())
958    {
959        try {
960            $this->doRedirectToUrl($moduleToRedirect, $actionToRedirect, $websiteId, $defaultPeriod, $defaultDate, $parameters);
961        } catch (Exception $e) {
962            // no website ID to default to, so could not redirect
963        }
964
965        if (Piwik::hasUserSuperUserAccess()) {
966            $siteTableName = Common::prefixTable('site');
967            $message = "Error: no website was found in this Matomo installation.
968			<br />Check the table '$siteTableName' in your database, it should contain your Matomo websites.";
969
970            $ex = new NoWebsiteFoundException($message);
971            $ex->setIsHtmlMessage();
972
973            throw $ex;
974        }
975
976        if (!Piwik::isUserIsAnonymous()) {
977            $currentLogin = Piwik::getCurrentUserLogin();
978            $emails = implode(',', Piwik::getContactEmailAddresses());
979            $errorMessage  = sprintf(Piwik::translate('CoreHome_NoPrivilegesAskPiwikAdmin'), $currentLogin, "<br/><a href='mailto:" . $emails . "?subject=Access to Matomo for user $currentLogin'>", "</a>");
980            $errorMessage .= "<br /><br />&nbsp;&nbsp;&nbsp;<b><a href='index.php?module=" . Piwik::getLoginPluginName() . "&amp;action=logout'>&rsaquo; " . Piwik::translate('General_Logout') . "</a></b><br />";
981
982            $ex = new NoPrivilegesException($errorMessage);
983            $ex->setIsHtmlMessage();
984
985            throw $ex;
986        }
987
988        echo FrontController::getInstance()->dispatch(Piwik::getLoginPluginName(), false);
989        exit;
990    }
991
992
993    /**
994     * Checks that the token_auth in the URL matches the currently logged-in user's token_auth.
995     *
996     * This is a protection against CSRF and should be used in all controller
997     * methods that modify Piwik or any user settings.
998     *
999     * If called from JavaScript by using the `ajaxHelper` you have to call `ajaxHelper.withTokenInUrl();` before
1000     * `ajaxHandler.send();` to send the token along with the request.
1001     *
1002     * **The token_auth should never appear in the browser's address bar.**
1003     *
1004     * @throws \Piwik\NoAccessException If the token doesn't match.
1005     * @api
1006     */
1007    protected function checkTokenInUrl()
1008    {
1009        $tokenRequest = Common::getRequestVar('token_auth', false);
1010        $tokenUser = Piwik::getCurrentUserTokenAuth();
1011
1012        if (empty($tokenRequest) && empty($tokenUser)) {
1013            return; // UI tests
1014        }
1015
1016        if ($tokenRequest !== $tokenUser) {
1017            throw new NoAccessException(Piwik::translate('General_ExceptionInvalidToken'));
1018        }
1019    }
1020
1021    /**
1022     * Returns a prettified date string for use in period selector widget.
1023     *
1024     * @param Period $period The period to return a pretty string for.
1025     * @return string
1026     * @api
1027     */
1028    public static function getCalendarPrettyDate($period)
1029    {
1030        if ($period instanceof Month) {
1031            // show month name when period is for a month
1032
1033            return $period->getLocalizedLongString();
1034        } else {
1035            return $period->getPrettyString();
1036        }
1037    }
1038
1039    /**
1040     * Returns the pretty date representation
1041     *
1042     * @param $date string
1043     * @param $period string
1044     * @return string Pretty date
1045     */
1046    public static function getPrettyDate($date, $period)
1047    {
1048        return self::getCalendarPrettyDate(Period\Factory::build($period, Date::factory($date)));
1049    }
1050
1051    protected function checkSitePermission()
1052    {
1053        if (!empty($this->idSite)) {
1054            Access::getInstance()->checkUserHasViewAccess($this->idSite);
1055            new Site($this->idSite);
1056        } elseif (empty($this->site) || empty($this->idSite)) {
1057            throw new Exception("The requested website idSite is not found in the request, or is invalid.
1058				Please check that you are logged in Matomo and have permission to access the specified website.");
1059        }
1060    }
1061
1062    /**
1063     * @param $moduleToRedirect
1064     * @param $actionToRedirect
1065     * @param $websiteId
1066     * @param $defaultPeriod
1067     * @param $defaultDate
1068     * @param $parameters
1069     * @throws Exception
1070     */
1071    private function doRedirectToUrl($moduleToRedirect, $actionToRedirect, $websiteId, $defaultPeriod, $defaultDate, $parameters)
1072    {
1073        $menu = new Menu();
1074
1075        $parameters = array_merge(
1076            $menu->urlForDefaultUserParams($websiteId, $defaultPeriod, $defaultDate),
1077            $parameters
1078        );
1079        $queryParams = !empty($parameters) ? '&' . Url::getQueryStringFromParameters($parameters) : '';
1080        $url = "index.php?module=%s&action=%s";
1081        $url = sprintf($url, $moduleToRedirect, $actionToRedirect);
1082        $url = $url . $queryParams;
1083        Url::redirectToUrl($url);
1084    }
1085
1086    private function checkViewType($viewType)
1087    {
1088        if ($viewType === 'admin' && !($this instanceof ControllerAdmin)) {
1089            throw new Exception("'admin' view type is only allowed with ControllerAdmin class.");
1090        }
1091    }
1092}
1093
1094