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 /> <b><a href='index.php?module=" . Piwik::getLoginPluginName() . "&action=logout'>› " . 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