<?php /** * Matomo - free/libre analytics platform * * @link https://matomo.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * */ namespace Piwik; use Exception; use Piwik\AssetManager\UIAssetCacheBuster; use Piwik\Container\StaticContainer; use Piwik\Plugins\CoreAdminHome\Controller; use Piwik\Plugins\CorePluginsAdmin\CorePluginsAdmin; use Piwik\View\ViewInterface; use Piwik\View\SecurityPolicy; use Twig\Environment; /** * Transition for pre-Piwik 0.4.4 */ if (!defined('PIWIK_USER_PATH')) { define('PIWIK_USER_PATH', PIWIK_INCLUDE_PATH); } /** * Encapsulates and manages a [Twig](http://twig.sensiolabs.org/) template. * * View lets you set properties that will be passed on to a Twig template. * View will also set several properties that will be available in all Twig * templates, including: * * - **currentModule**: The value of the **module** query parameter. * - **currentAction**: The value of the **action** query parameter. * - **userLogin**: The current user login name. * - **sites**: List of site data for every site the current user has at least * view access for. * - **url**: The current URL (sanitized). * - **token_auth**: The current user's token auth. * - **userHasSomeAdminAccess**: True if the user has admin access to at least * one site, false if otherwise. * - **userIsSuperUser**: True if the user is the superuser, false if otherwise. * - **latest_version_available**: The latest version of Piwik available. * - **isWidget**: The value of the 'widget' query parameter. * - **show_autocompleter**: Whether the site selector should be shown or not. * - **loginModule**: The name of the currently used authentication module. * - **isInternetEnabled**: Whether the matomo server is allowed to connect to * external networks. * * ### Template Naming Convention * * Template files should be named after the controller method they are used in. * If they are used in more than one controller method or are included by another * template, they should describe the output they generate and be prefixed with * an underscore, eg, **_dataTable.twig**. * * ### Twig * * Twig templates must exist in the **templates** folder in a plugin's root * folder. * * The following filters are available to twig templates: * * - **translate**: Outputs internationalized text using a translation token, eg, * `{{ 'General_Date'|translate }}`. sprintf parameters can be passed * to the filter. * - **urlRewriteWithParameters**: Modifies the current query string with the given * set of parameters, eg, * * {{ {'module':'MyPlugin', 'action':'index'} | urlRewriteWithParameters }} * * - **sumTime**: Pretty formats an number of seconds. * - **money**: Formats a numerical value as a monetary value using the currency * of the supplied site (second arg is site ID). * eg, `{{ 23|money(site.idsite)|raw }} * - **truncate**: Truncates the text to certain length (determined by first arg.) * eg, `{{ myReallyLongText|truncate(80) }}` * - **implode**: Calls `implode`. * - **ucwords**: Calls `ucwords`. * * The following functions are available to twig templates: * * - **linkTo**: Modifies the current query string with the given set of parameters, * eg `{{ linkTo({'module':'MyPlugin', 'action':'index'}) }}`. * - **sparkline**: Outputs a sparkline image HTML element using the sparkline image * src link. eg, `{{ sparkline(sparklineUrl) }}`. * - **postEvent**: Posts an event that allows event observers to add text to a string * which is outputted in the template, eg, `{{ postEvent('MyPlugin.event') }}` * - **isPluginLoaded**: Returns true if the supplied plugin is loaded, false if otherwise. * `{% if isPluginLoaded('Goals') %}...{% endif %}` * - **areAdsForProfessionalServicesEnabled**: Returns true if it is ok to show some advertising in the UI for providers of Professional Support for Piwik (from Piwik 2.16.0) * - **isMultiServerEnvironment**: Returns true if Piwik is used on more than one server (since Piwik 2.16.1) * * ### Examples * * **Basic usage** * * // a controller method * public function myView() * { * $view = new View("@MyPlugin/myView"); * $view->property1 = "a view property"; * $view->property2 = "another view property"; * return $view->render(); * } * * * @api */ class View implements ViewInterface { private $template = ''; /** * Instance * @var Environment */ private $twig; protected $templateVars = array(); private $contentType = 'text/html; charset=utf-8'; private $xFrameOptions = null; private $enableCacheBuster = true; private $useStrictReferrerPolicy = true; /** * Can be disabled to not send headers when rendering a view. This can be useful if heaps of views are being * rendered during one request to possibly prevent a segmentation fault see eg #15307 . It should not be disabled * for a main view, but could be disabled for views that are being rendered eg during a twig event as a "subview" which * is part of the "main view". * @var bool */ public $sendHeadersWhenRendering = true; /** * Constructor. * * @param string $templateFile The template file to load. Must be in the following format: * `"@MyPlugin/templateFileName"`. Note the absence of .twig * from the end of the name. */ public function __construct($templateFile) { $templateExt = '.twig'; if (substr($templateFile, -strlen($templateExt)) !== $templateExt) { $templateFile .= $templateExt; } $this->template = $templateFile; $this->initializeTwig(); $this->piwik_version = Version::VERSION; $this->userLogin = Piwik::getCurrentUserLogin(); $this->isSuperUser = Access::getInstance()->hasSuperUserAccess(); // following is used in ajaxMacros called macro (showMoreHelp as passed in other templates) - requestErrorDiv $isGeneralSettingsAdminEnabled = Controller::isGeneralSettingsAdminEnabled(); $isPluginsAdminEnabled = CorePluginsAdmin::isPluginsAdminEnabled(); // simplify template usage $this->showMoreFaqInfo = $this->isSuperUser && ($isGeneralSettingsAdminEnabled || $isPluginsAdminEnabled); try { $this->piwikUrl = SettingsPiwik::getPiwikUrl(); } catch (Exception $ex) { // pass (occurs when DB cannot be connected to, perhaps piwik URL cache should be stored in config file...) } $this->userRequiresPasswordConfirmation = Piwik::doesUserRequirePasswordConfirmation(Piwik::getCurrentUserLogin()); } /** * Disables the cache buster (adding of ?cb=...) to JavaScript and stylesheet files */ public function disableCacheBuster() { $this->enableCacheBuster = false; } /** * Returns the template filename. * * @return string */ public function getTemplateFile() { return $this->template; } /** * Returns the variables to bind to the template when rendering. * * @param array $override Template variable override values. Mainly useful * when including View templates in other templates. * @return array */ public function getTemplateVars($override = array()) { return $override + $this->templateVars; } /** * Directly assigns a variable to the view script. * Variable names may not be prefixed with '_'. * * @param string $key The variable name. * @param mixed $val The variable value. */ public function __set($key, $val) { $this->templateVars[$key] = $val; } /** * Retrieves an assigned variable. * Variable names may not be prefixed with '_'. * * @param string $key The variable name. * @return mixed The variable value. */ public function &__get($key) { return $this->templateVars[$key]; } /** * Returns true if a template variable has been set or not. * * @param string $name The name of the template variable. * @return bool */ public function __isset($name) { return isset($this->templateVars[$name]); } /** * Unsets a template variable. * * @param string $name The name of the template variable. */ public function __unset($name) { unset($this->templateVars[$name]); } private function initializeTwig() { $this->twig = StaticContainer::get(Twig::class)->getTwigEnvironment(); } /** * Renders the current view. Also sends the stored 'Content-Type' HTML header. * See {@link setContentType()}. * * @return string Generated template. */ public function render() { try { $this->currentModule = Piwik::getModule(); $this->currentAction = Piwik::getAction(); $this->url = Common::sanitizeInputValue(Url::getCurrentUrl()); $this->token_auth = Piwik::getCurrentUserTokenAuth(); $this->userHasSomeAdminAccess = Piwik::isUserHasSomeAdminAccess(); $this->userIsAnonymous = Piwik::isUserIsAnonymous(); $this->userIsSuperUser = Piwik::hasUserSuperUserAccess(); $this->latest_version_available = UpdateCheck::isNewestVersionAvailable(); $this->showUpdateNotificationToUser = !SettingsPiwik::isShowUpdateNotificationToSuperUsersOnlyEnabled() || Piwik::hasUserSuperUserAccess(); $this->disableLink = Common::getRequestVar('disableLink', 0, 'int'); $this->isWidget = Common::getRequestVar('widget', 0, 'int'); $this->isMultiServerEnvironment = SettingsPiwik::isMultiServerEnvironment(); $this->isInternetEnabled = SettingsPiwik::isInternetEnabled(); $this->shouldPropagateTokenAuth = $this->shouldPropagateTokenAuthInAjaxRequests(); $piwikAds = StaticContainer::get('Piwik\ProfessionalServices\Advertising'); $this->areAdsForProfessionalServicesEnabled = $piwikAds->areAdsForProfessionalServicesEnabled(); if (Development::isEnabled()) { $cacheBuster = rand(0, 10000); } else { $cacheBuster = UIAssetCacheBuster::getInstance()->piwikVersionBasedCacheBuster(); } $this->cacheBuster = $cacheBuster; $this->loginModule = Piwik::getLoginPluginName(); } catch (Exception $e) { Log::debug($e); // can fail, for example at installation (no plugin loaded yet) } if ($this->sendHeadersWhenRendering) { ProxyHttp::overrideCacheControlHeaders('no-store'); Common::sendHeader('Content-Type: ' . $this->contentType); // always sending this header, sometimes empty, to ensure that Dashboard embed loads // - when calling sendHeader() multiple times, the last one prevails if(!empty($this->xFrameOptions)) { Common::sendHeader('X-Frame-Options: ' . (string)$this->xFrameOptions); } // don't send Referer-Header for outgoing links if (!empty($this->useStrictReferrerPolicy)) { Common::sendHeader('Referrer-Policy: same-origin'); } else { // always send explicit default header Common::sendHeader('Referrer-Policy: no-referrer-when-downgrade'); } // this will be an empty string if CSP is disabled $cspHeader = StaticContainer::get(SecurityPolicy::class)->createHeaderString(); if ('' !== $cspHeader) { Common::sendHeader($cspHeader); } } return $this->renderTwigTemplate(); } /** * @internal * @ignore * @return Environment */ public function getTwig() { return $this->twig; } protected function renderTwigTemplate() { $output = $this->twig->render($this->getTemplateFile(), $this->getTemplateVars()); if ($this->enableCacheBuster) { $output = $this->applyFilter_cacheBuster($output); } $helper = new Theme; $output = $helper->rewriteAssetsPathToTheme($output); return $output; } protected function applyFilter_cacheBuster($output) { $cacheBuster = UIAssetCacheBuster::getInstance(); $cache = Cache::getTransientCache(); $cssCacheBusterId = $cache->fetch('cssCacheBusterId'); if (empty($cssCacheBusterId)) { $assetManager = AssetManager::getInstance(); $stylesheet = $assetManager->getMergedStylesheetAsset(); if ($stylesheet->exists()) { $content = $stylesheet->getContent(); } else { $content = $assetManager->getMergedStylesheet()->getContent(); } $cssCacheBusterId = $cacheBuster->md5BasedCacheBuster($content); $cache->save('cssCacheBusterId', $cssCacheBusterId); } $tagJs = 'cb=' . ($this->cacheBuster ?? $cacheBuster->piwikVersionBasedCacheBuster()); $tagCss = 'cb=' . $cssCacheBusterId; $pattern = array( '~<script type=[\'"]text/javascript[\'"] src=[\'"]([^\'"]+)[\'"]>~', '~<script src=[\'"]([^\'"]+)[\'"] type=[\'"]text/javascript[\'"]>~', '~<script type=[\'"]text/javascript[\'"] src=[\'"]([^\'"]+?chunk=[^\'"]+)[\'"] defer>~', '~<link rel=[\'"]stylesheet[\'"] type=[\'"]text/css[\'"] href=[\'"]([^\'"]+)[\'"] ?/?>~', // removes the double ?cb= tag '~(src|href)=\"index.php\?module=([A-Za-z0-9_]+)&action=([A-Za-z0-9_]+)\?cb=~', ); $replace = array( '<script type="text/javascript" src="$1?' . $tagJs . '">', '<script type="text/javascript" src="$1?' . $tagJs . '">', '<script type="text/javascript" src="$1&' . $tagJs . '" defer>', '<link rel="stylesheet" type="text/css" href="$1?' . $tagCss . '" />', '$1="index.php?module=$2&action=$3&cb=', ); return preg_replace($pattern, $replace, $output); } /** * Set stored value used in the Content-Type HTTP header field. The header is * set just before rendering. * * @param string $contentType */ public function setContentType($contentType) { $this->contentType = $contentType; } /** * Set X-Frame-Options field in the HTTP response. The header is set just * before rendering. * * _Note: setting this allows you to make sure the View **cannot** be * embedded in iframes. Learn more [here](https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options)._ * * @param string $option ('deny' or 'sameorigin') */ public function setXFrameOptions($option = 'deny') { if ($option === 'deny' || $option === 'sameorigin') { $this->xFrameOptions = $option; } if ($option == 'allow') { $this->xFrameOptions = null; } } /** * Add form to view * * @param QuickForm2 $form * @ignore */ public function addForm(QuickForm2 $form) { // assign array with form data $this->assign('form_data', $form->getFormData()); $this->assign('element_list', $form->getElementList()); } /** * Assign value to a variable for use in a template * @param string|array $var * @param mixed $value * @ignore */ public function assign($var, $value = null) { if (is_string($var)) { $this->$var = $value; } elseif (is_array($var)) { foreach ($var as $key => $value) { $this->$key = $value; } } } /** * Clear compiled Twig templates * @ignore */ public static function clearCompiledTemplates() { $enable = StaticContainer::get('view.clearcompiledtemplates.enable'); if ($enable) { // some high performance systems that run many Matomo instances may never want to clear this template cache // if they use eg a blue/green deployment $templatesCompiledPath = StaticContainer::get('path.tmp.templates'); Filesystem::unlinkRecursive($templatesCompiledPath, false); } } /** * Creates a View for and then renders the single report template. * * Can be used for pages that display only one report to avoid having to create * a new template. * * @param string $title The report title. * @param string $reportHtml The report body HTML. * @return string|void The report contents if `$fetch` is true. */ public static function singleReport($title, $reportHtml) { $view = new View('@CoreHome/_singleReport'); $view->title = $title; $view->report = $reportHtml; return $view->render(); } private function shouldPropagateTokenAuthInAjaxRequests() { $generalConfig = Config::getInstance()->General; return Common::getRequestVar('module', false) == 'Widgetize' || $generalConfig['enable_framed_pages'] == '1' || $this->validTokenAuthInUrl(); } /** * @return bool * @throws Exception */ private function validTokenAuthInUrl() { $tokenAuth = Common::getRequestVar('token_auth', '', 'string', $_GET); return ($tokenAuth && $tokenAuth === Piwik::getCurrentUserTokenAuth()); } /** * Returns whether a strict Referrer-Policy header will be sent. Generally this should be set to 'true'. * * @return bool */ public function getUseStrictReferrerPolicy() { return $this->useStrictReferrerPolicy; } /** * Sets whether a strict Referrer-Policy header will be sent (if not, nothing is sent). * * @param bool $useStrictReferrerPolicy */ public function setUseStrictReferrerPolicy($useStrictReferrerPolicy) { $this->useStrictReferrerPolicy = $useStrictReferrerPolicy; } }