add piwik installation

This commit is contained in:
coderkun 2014-04-25 03:56:02 +02:00
commit 8c5d4f0c31
3197 changed files with 563902 additions and 0 deletions

View file

@ -0,0 +1,45 @@
<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin;
use Piwik\Singleton;
/**
* The base class of all API singletons.
*
* Plugins that want to expose functionality through the Reporting API should create a class
* that extends this one. Every public method in that class that is not annotated with **@ignore**
* will be callable through Piwik's Web API.
*
* _Note: If your plugin calculates and stores reports, they should be made available through the API._
*
* ### Examples
*
* **Defining an API for a plugin**
*
* class API extends \Piwik\Plugin\API
* {
* public function myMethod($idSite, $period, $date, $segment = false)
* {
* $dataTable = // ... get some data ...
* return $dataTable;
* }
* }
*
* **Linking to an API method**
*
* <a href="?module=API&method=MyPlugin.myMethod&idSite=1&period=day&date=2013-10-23">Link</a>
*
* @api
*/
abstract class API extends Singleton
{
}

View file

@ -0,0 +1,133 @@
<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin;
use Piwik\ArchiveProcessor;
use Piwik\Config as PiwikConfig;
/**
* The base class that should be extended by plugins that compute their own
* analytics data.
*
* Descendants should implement the {@link aggregateDayReport()} and {@link aggregateMultipleReports()}
* methods.
*
* Both of these methods should persist analytics data using the {@link \Piwik\ArchiveProcessor}
* instance returned by {@link getProcessor()}. The {@link aggregateDayReport()} method should
* compute analytics data using the {@link \Piwik\DataAccess\LogAggregator} instance
* returned by {@link getLogAggregator()}.
*
* ### Examples
*
* **Extending Archiver**
*
* class MyArchiver extends Archiver
* {
* public function aggregateDayReport()
* {
* $logAggregator = $this->getLogAggregator();
*
* $data = $logAggregator->queryVisitsByDimension(...);
*
* $dataTable = new DataTable();
* $dataTable->addRowsFromSimpleArray($data);
*
* $archiveProcessor = $this->getProcessor();
* $archiveProcessor->insertBlobRecords('MyPlugin_myReport', $dataTable->getSerialized(500));
* }
*
* public function aggregateMultipleReports()
* {
* $archiveProcessor = $this->getProcessor();
* $archiveProcessor->aggregateDataTableRecords('MyPlugin_myReport', 500);
* }
* }
*
* @api
*/
abstract class Archiver
{
/**
* @var \Piwik\ArchiveProcessor
*/
private $processor;
/**
* Constructor.
*
* @param ArchiveProcessor $processor The ArchiveProcessor instance to use when persisting archive
* data.
*/
public function __construct(ArchiveProcessor $processor)
{
$this->maximumRows = PiwikConfig::getInstance()->General['datatable_archiving_maximum_rows_standard'];
$this->processor = $processor;
}
/**
* Archives data for a day period.
*
* Implementations of this method should do more computation intensive activities such
* as aggregating data across log tables. Since this method only deals w/ data logged for a day,
* aggregating individual log table rows isn't a problem. Doing this for any larger period,
* however, would cause performance degradation.
*
* Aggregate log table rows using a {@link Piwik\DataAccess\LogAggregator} instance. Get a
* {@link Piwik\DataAccess\LogAggregator} instance using the {@link getLogAggregator()} method.
*/
abstract public function aggregateDayReport();
/**
* Archives data for a non-day period.
*
* Implementations of this method should only aggregate existing reports of subperiods of the
* current period. For example, it is more efficient to aggregate reports for each day of a
* week than to aggregate each log entry of the week.
*
* Use {@link Piwik\ArchiveProcessor::aggregateNumericMetrics()} and {@link Piwik\ArchiveProcessor::aggregateDataTableRecords()}
* to aggregate archived reports. Get the {@link Piwik\ArchiveProcessor} instance using the {@link getProcessor()}
* method.
*/
abstract public function aggregateMultipleReports();
/**
* Returns a {@link Piwik\ArchiveProcessor} instance that can be used to insert archive data for
* the period, segment and site we are archiving data for.
*
* @return \Piwik\ArchiveProcessor
* @api
*/
protected function getProcessor()
{
return $this->processor;
}
/**
* Returns a {@link Piwik\DataAccess\LogAggregator} instance that can be used to aggregate log table rows
* for this period, segment and site.
*
* @return \Piwik\DataAccess\LogAggregator
* @api
*/
protected function getLogAggregator()
{
return $this->getProcessor()->getLogAggregator();
}
/**
* Whether this Archiver should be used or not.
*
* @return bool
*/
public function isEnabled()
{
return true;
}
}

View file

@ -0,0 +1,68 @@
<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin;
use Piwik\Common;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputInterface;
/**
* The base class for console commands.
*
* @api
*/
class ConsoleCommand extends SymfonyCommand
{
/**
* Constructor.
*
* @param string|null $name The name of the command, eg, `'generate:api'`.
*/
public function __construct($name = null)
{
if (!Common::isPhpCliMode()) {
throw new \RuntimeException('Only executable in CLI mode');
}
parent::__construct($name);
}
public function writeSuccessMessage(OutputInterface $output, $messages)
{
$lengths = array_map('strlen', $messages);
$maxLen = max($lengths) + 4;
$separator = str_pad('', $maxLen, '*');
$output->writeln('');
$output->writeln('<info>' . $separator . '</info>');
foreach ($messages as $message) {
$output->writeln(' ' . $message . ' ');
}
$output->writeln('<info>' . $separator . '</info>');
$output->writeln('');
}
protected function checkAllRequiredOptionsAreNotEmpty(InputInterface $input)
{
$options = $this->getDefinition()->getOptions();
foreach ($options as $option) {
$name = $option->getName();
$value = $input->getOption($name);
if ($option->isValueRequired() && empty($value)) {
throw new \InvalidArgumentException(sprintf('The required option %s is not set', $name));
}
}
}
}

View file

@ -0,0 +1,944 @@
<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin;
use Exception;
use Piwik\Access;
use Piwik\API\Proxy;
use Piwik\API\Request;
use Piwik\Common;
use Piwik\Config as PiwikConfig;
use Piwik\DataTable\Filter\CalculateEvolutionFilter;
use Piwik\Date;
use Piwik\FrontController;
use Piwik\Menu\MenuTop;
use Piwik\NoAccessException;
use Piwik\Notification\Manager as NotificationManager;
use Piwik\Period\Month;
use Piwik\Period;
use Piwik\Period\Range;
use Piwik\Piwik;
use Piwik\Plugins\CoreAdminHome\CustomLogo;
use Piwik\Plugins\LanguagesManager\LanguagesManager;
use Piwik\Plugins\SitesManager\API as APISitesManager;
use Piwik\Plugins\UsersManager\API as APIUsersManager;
use Piwik\Registry;
use Piwik\SettingsPiwik;
use Piwik\Site;
use Piwik\Url;
use Piwik\View;
use Piwik\View\ViewInterface;
use Piwik\ViewDataTable\Factory as ViewDataTableFactory;
/**
* Base class of all plugin Controllers.
*
* Plugins that wish to add display HTML should create a Controller that either
* extends from this class or from {@link ControllerAdmin}. Every public method in
* the controller will be exposed as a controller method and can be invoked via
* an HTTP request.
*
* Learn more about Piwik's MVC system [here](/guides/mvc-in-piwik).
*
* ### Examples
*
* **Defining a controller**
*
* class Controller extends \Piwik\Plugin\Controller
* {
* public function index()
* {
* $view = new View("@MyPlugin/index.twig");
* // ... setup view ...
* return $view->render();
* }
* }
*
* **Linking to a controller action**
*
* <a href="?module=MyPlugin&action=index&idSite=1&period=day&date=2013-10-10">Link</a>
*
*/
abstract class Controller
{
/**
* The plugin name, eg. `'Referrers'`.
*
* @var string
* @api
*/
protected $pluginName;
/**
* The value of the **date** query parameter.
*
* @var string
* @api
*/
protected $strDate;
/**
* The Date object created with ($strDate)[#strDate] or null if the requested date is a range.
*
* @var Date|null
* @api
*/
protected $date;
/**
* The value of the **idSite** query parameter.
*
* @var int
* @api
*/
protected $idSite;
/**
* The Site object created with {@link $idSite}.
*
* @var Site
* @api
*/
protected $site = null;
/**
* Constructor.
*
* @api
*/
public function __construct()
{
$this->init();
}
protected function init()
{
$aPluginName = explode('\\', get_class($this));
$this->pluginName = $aPluginName[2];
$date = Common::getRequestVar('date', 'yesterday', 'string');
try {
$this->idSite = Common::getRequestVar('idSite', false, 'int');
$this->site = new Site($this->idSite);
$date = $this->getDateParameterInTimezone($date, $this->site->getTimezone());
$this->setDate($date);
} catch (Exception $e) {
// the date looks like YYYY-MM-DD,YYYY-MM-DD or other format
$this->date = null;
}
}
/**
* Helper method that converts `"today"` or `"yesterday"` to the specified timezone.
* If the date is absolute, ie. YYYY-MM-DD, it will not be converted to the timezone.
*
* @param string $date `'today'`, `'yesterday'`, `'YYYY-MM-DD'`
* @param string $timezone The timezone to use.
* @return Date
* @api
*/
protected function getDateParameterInTimezone($date, $timezone)
{
$timezoneToUse = null;
// if the requested date is not YYYY-MM-DD, we need to ensure
// it is relative to the website's timezone
if (in_array($date, array('today', 'yesterday'))) {
// today is at midnight; we really want to get the time now, so that
// * if the website is UTC+12 and it is 5PM now in UTC, the calendar will allow to select the UTC "tomorrow"
// * if the website is UTC-12 and it is 5AM now in UTC, the calendar will allow to select the UTC "yesterday"
if ($date == 'today') {
$date = 'now';
} elseif ($date == 'yesterday') {
$date = 'yesterdaySameTime';
}
$timezoneToUse = $timezone;
}
return Date::factory($date, $timezoneToUse);
}
/**
* Sets the date to be used by all other methods in the controller.
* If the date has to be modified, this method should be called just after
* construction.
*
* @param Date $date The new Date.
* @return void
* @api
*/
protected function setDate(Date $date)
{
$this->date = $date;
$this->strDate = $date->toString();
}
/**
* Returns the name of the default method that will be called
* when visiting: index.php?module=PluginName without the action parameter.
*
* @return string
* @api
*/
public function getDefaultAction()
{
return 'index';
}
/**
* A helper method that renders a view either to the screen or to a string.
*
* @param ViewInterface $view The view to render.
* @return string|void
*/
protected function renderView(ViewInterface $view)
{
return $view->render();
}
/**
* Convenience method that creates and renders a ViewDataTable for a API method.
*
* @param string $apiAction The name of the API action (eg, `'getResolution'`).
* @param bool $controllerAction The name of the Controller action name that is rendering the report. Defaults
* to the `$apiAction`.
* @param bool $fetch If `true`, the rendered string is returned, if `false` it is `echo`'d.
* @throws \Exception if `$pluginName` is not an existing plugin or if `$apiAction` is not an
* existing method of the plugin's API.
* @return string|void See `$fetch`.
* @api
*/
protected function renderReport($apiAction, $controllerAction = false)
{
$pluginName = $this->pluginName;
/** @var Proxy $apiProxy */
$apiProxy = Proxy::getInstance();
if (!$apiProxy->isExistingApiAction($pluginName, $apiAction)) {
throw new \Exception("Invalid action name '$apiAction' for '$pluginName' plugin.");
}
$apiAction = $apiProxy->buildApiActionName($pluginName, $apiAction);
if ($controllerAction !== false) {
$controllerAction = $pluginName . '.' . $controllerAction;
}
$view = ViewDataTableFactory::build(null, $apiAction, $controllerAction);
$rendered = $view->render();
return $rendered;
}
/**
* Returns a ViewDataTable object that will render a jqPlot evolution graph
* for the last30 days/weeks/etc. of the current period, relative to the current date.
*
* @param string $currentModuleName The name of the current plugin.
* @param string $currentControllerAction The name of the action that renders the desired
* report.
* @param string $apiMethod The API method that the ViewDataTable will use to get
* graph data.
* @return ViewDataTable
* @api
*/
protected function getLastUnitGraph($currentModuleName, $currentControllerAction, $apiMethod)
{
$view = ViewDataTableFactory::build(
'graphEvolution', $apiMethod, $currentModuleName . '.' . $currentControllerAction, $forceDefault = true);
$view->config->show_goals = false;
return $view;
}
/**
* Same as {@link getLastUnitGraph()}, but will set some properties of the ViewDataTable
* object based on the arguments supplied.
*
* @param string $currentModuleName The name of the current plugin.
* @param string $currentControllerAction The name of the action that renders the desired
* report.
* @param array $columnsToDisplay The value to use for the ViewDataTable's columns_to_display config
* property.
* @param array $selectableColumns The value to use for the ViewDataTable's selectable_columns config
* property.
* @param bool|string $reportDocumentation The value to use for the ViewDataTable's documentation config
* property.
* @param string $apiMethod The API method that the ViewDataTable will use to get graph data.
* @return ViewDataTable
* @api
*/
protected function getLastUnitGraphAcrossPlugins($currentModuleName, $currentControllerAction, $columnsToDisplay = false,
$selectableColumns = array(), $reportDocumentation = false,
$apiMethod = 'API.get')
{
// load translations from meta data
$idSite = Common::getRequestVar('idSite');
$period = Common::getRequestVar('period');
$date = Common::getRequestVar('date');
$meta = \Piwik\Plugins\API\API::getInstance()->getReportMetadata($idSite, $period, $date);
$columns = array_merge($columnsToDisplay, $selectableColumns);
$translations = array_combine($columns, $columns);
foreach ($meta as $reportMeta) {
if ($reportMeta['action'] == 'get' && !isset($reportMeta['parameters'])) {
foreach ($columns as $column) {
if (isset($reportMeta['metrics'][$column])) {
$translations[$column] = $reportMeta['metrics'][$column];
}
}
}
}
// initialize the graph and load the data
$view = $this->getLastUnitGraph($currentModuleName, $currentControllerAction, $apiMethod);
if ($columnsToDisplay !== false) {
$view->config->columns_to_display = $columnsToDisplay;
}
if (property_exists($view->config, 'selectable_columns')) {
$view->config->selectable_columns = array_merge($view->config->selectable_columns ? : array(), $selectableColumns);
}
$view->config->translations += $translations;
if ($reportDocumentation) {
$view->config->documentation = $reportDocumentation;
}
return $view;
}
/**
* Returns the array of new processed parameters once the parameters are applied.
* For example: if you set range=last30 and date=2008-03-10,
* the date element of the returned array will be "2008-02-10,2008-03-10"
*
* Parameters you can set:
* - range: last30, previous10, etc.
* - date: YYYY-MM-DD, today, yesterday
* - period: day, week, month, year
*
* @param array $paramsToSet array( 'date' => 'last50', 'viewDataTable' =>'sparkline' )
* @throws \Piwik\NoAccessException
* @return array
*/
protected function getGraphParamsModified($paramsToSet = array())
{
if (!isset($paramsToSet['period'])) {
$period = Common::getRequestVar('period');
} else {
$period = $paramsToSet['period'];
}
if ($period == 'range') {
return $paramsToSet;
}
if (!isset($paramsToSet['range'])) {
$range = 'last30';
} else {
$range = $paramsToSet['range'];
}
if (!isset($paramsToSet['date'])) {
$endDate = $this->strDate;
} else {
$endDate = $paramsToSet['date'];
}
if (is_null($this->site)) {
throw new NoAccessException("Website not initialized, check that you are logged in and/or using the correct token_auth.");
}
$paramDate = Range::getRelativeToEndDate($period, $range, $endDate, $this->site);
$params = array_merge($paramsToSet, array('date' => $paramDate));
return $params;
}
/**
* Returns a numeric value from the API.
* Works only for API methods that originally returns numeric values (there is no cast here)
*
* @param string $methodToCall Name of method to call, eg. Referrers.getNumberOfDistinctSearchEngines
* @param bool|string $date A custom date to use when getting the value. If false, the 'date' query
* parameter is used.
*
* @return int|float
*/
protected function getNumericValue($methodToCall, $date = false)
{
$params = $date === false ? array() : array('date' => $date);
$return = Request::processRequest($methodToCall, $params);
$columns = $return->getFirstRow()->getColumns();
return reset($columns);
}
/**
* Returns a URL to a sparkline image for a report served by the current plugin.
*
* The result of this URL should be used with the [sparkline()](/api-reference/Piwik/View#twig) twig function.
*
* The current site ID and period will be used.
*
* @param string $action Method name of the controller that serves the report.
* @param array $customParameters The array of query parameter name/value pairs that
* should be set in result URL.
* @return string The generated URL.
* @api
*/
protected function getUrlSparkline($action, $customParameters = array())
{
$params = $this->getGraphParamsModified(
array('viewDataTable' => 'sparkline',
'action' => $action,
'module' => $this->pluginName)
+ $customParameters
);
// convert array values to comma separated
foreach ($params as &$value) {
if (is_array($value)) {
$value = rawurlencode(implode(',', $value));
}
}
$url = Url::getCurrentQueryStringWithParametersModified($params);
return $url;
}
/**
* Sets the first date available in the period selector's calendar.
*
* @param Date $minDate The min date.
* @param View $view The view that contains the period selector.
* @api
*/
protected function setMinDateView(Date $minDate, $view)
{
$view->minDateYear = $minDate->toString('Y');
$view->minDateMonth = $minDate->toString('m');
$view->minDateDay = $minDate->toString('d');
}
/**
* Sets the last date available in the period selector's calendar. Usually this is just the "today" date
* for a site (which varies based on the timezone of a site).
*
* @param Date $maxDate The max date.
* @param View $view The view that contains the period selector.
* @api
*/
protected function setMaxDateView(Date $maxDate, $view)
{
$view->maxDateYear = $maxDate->toString('Y');
$view->maxDateMonth = $maxDate->toString('m');
$view->maxDateDay = $maxDate->toString('d');
}
/**
* Assigns variables to {@link Piwik\View} instances that display an entire page.
*
* The following variables assigned:
*
* **date** - The value of the **date** query parameter.
* **idSite** - The value of the **idSite** query parameter.
* **rawDate** - The value of the **date** query parameter.
* **prettyDate** - A pretty string description of the current period.
* **siteName** - The current site's name.
* **siteMainUrl** - The URL of the current site.
* **startDate** - The start date of the current period. A {@link Piwik\Date} instance.
* **endDate** - The end date of the current period. A {@link Piwik\Date} instance.
* **language** - The current language's language code.
* **config_action_url_category_delimiter** - The value of the `[General] action_url_category_delimiter`
* INI config option.
* **topMenu** - The result of `MenuTop::getInstance()->getMenu()`.
*
* As well as the variables set by {@link setPeriodVariablesView()}.
*
* Will exit on error.
*
* @param View $view
* @return void
* @api
*/
protected function setGeneralVariablesView($view)
{
$view->date = $this->strDate;
try {
$view->idSite = $this->idSite;
if (empty($this->site) || empty($this->idSite)) {
throw new Exception("The requested website idSite is not found in the request, or is invalid.
Please check that you are logged in Piwik and have permission to access the specified website.");
}
$this->setPeriodVariablesView($view);
$rawDate = Common::getRequestVar('date');
$periodStr = Common::getRequestVar('period');
if ($periodStr != 'range') {
$date = Date::factory($this->strDate);
$period = Period::factory($periodStr, $date);
} else {
$period = new Range($periodStr, $rawDate, $this->site->getTimezone());
}
$view->rawDate = $rawDate;
$view->prettyDate = self::getCalendarPrettyDate($period);
$view->siteName = $this->site->getName();
$view->siteMainUrl = $this->site->getMainUrl();
$datetimeMinDate = $this->site->getCreationDate()->getDatetime();
$minDate = Date::factory($datetimeMinDate, $this->site->getTimezone());
$this->setMinDateView($minDate, $view);
$maxDate = Date::factory('now', $this->site->getTimezone());
$this->setMaxDateView($maxDate, $view);
// Setting current period start & end dates, for pre-setting the calendar when "Date Range" is selected
$dateStart = $period->getDateStart();
if ($dateStart->isEarlier($minDate)) {
$dateStart = $minDate;
}
$dateEnd = $period->getDateEnd();
if ($dateEnd->isLater($maxDate)) {
$dateEnd = $maxDate;
}
$view->startDate = $dateStart;
$view->endDate = $dateEnd;
$language = LanguagesManager::getLanguageForSession();
$view->language = !empty($language) ? $language : LanguagesManager::getLanguageCodeForCurrentUser();
$this->setBasicVariablesView($view);
$view->topMenu = MenuTop::getInstance()->getMenu();
$view->notifications = NotificationManager::getAllNotificationsToDisplay();
NotificationManager::cancelAllNonPersistent();
} catch (Exception $e) {
Piwik_ExitWithMessage($e->getMessage(), $e->getTraceAsString());
}
}
/**
* Assigns a set of generally useful variables to a {@link Piwik\View} instance.
*
* The following variables assigned:
*
* **debugTrackVisitsInsidePiwikUI** - The value of the `[Debug] track_visits_inside_piwik_ui`
* INI config option.
* **isSuperUser** - True if the current user is the Super User, false if otherwise.
* **hasSomeAdminAccess** - True if the current user has admin access to at least one site,
* false if otherwise.
* **isCustomLogo** - The value of the `branding_use_custom_logo` option.
* **logoHeader** - The header logo URL to use.
* **logoLarge** - The large logo URL to use.
* **logoSVG** - The SVG logo URL to use.
* **hasSVGLogo** - True if there is a SVG logo, false if otherwise.
* **enableFrames** - The value of the `[General] enable_framed_pages` INI config option. If
* true, {@link Piwik\View::setXFrameOptions()} is called on the view.
*
* Also calls {@link setHostValidationVariablesView()}.
*
* @param View $view
* @api
*/
protected function setBasicVariablesView($view)
{
$view->clientSideConfig = PiwikConfig::getInstance()->getClientSideOptions();
$view->debugTrackVisitsInsidePiwikUI = PiwikConfig::getInstance()->Debug['track_visits_inside_piwik_ui'];
$view->isSuperUser = Access::getInstance()->hasSuperUserAccess();
$view->hasSomeAdminAccess = Piwik::isUserHasSomeAdminAccess();
$view->hasSomeViewAccess = Piwik::isUserHasSomeViewAccess();
$view->isUserIsAnonymous = Piwik::isUserIsAnonymous();
$view->hasSuperUserAccess = Piwik::hasUserSuperUserAccess();
$customLogo = new CustomLogo();
$view->isCustomLogo = $customLogo->isEnabled();
$view->logoHeader = \Piwik\Plugins\API\API::getInstance()->getHeaderLogoUrl();
$view->logoLarge = \Piwik\Plugins\API\API::getInstance()->getLogoUrl();
$view->logoSVG = \Piwik\Plugins\API\API::getInstance()->getSVGLogoUrl();
$view->hasSVGLogo = \Piwik\Plugins\API\API::getInstance()->hasSVGLogo();
$view->superUserEmails = implode(',', Piwik::getAllSuperUserAccessEmailAddresses());
$general = PiwikConfig::getInstance()->General;
$view->enableFrames = $general['enable_framed_pages']
|| (isset($general['enable_framed_logins']) && $general['enable_framed_logins']);
if (!$view->enableFrames) {
$view->setXFrameOptions('sameorigin');
}
self::setHostValidationVariablesView($view);
}
/**
* Checks if the current host is valid and sets variables on the given view, including:
*
* - **isValidHost** - true if host is valid, false if otherwise
* - **invalidHostMessage** - message to display if host is invalid (only set if host is invalid)
* - **invalidHost** - the invalid hostname (only set if host is invalid)
* - **mailLinkStart** - the open tag of a link to email the Super User of this problem (only set
* if host is invalid)
*
* @param View $view
* @api
*/
public static function setHostValidationVariablesView($view)
{
// check if host is valid
$view->isValidHost = Url::isValidHost();
if (!$view->isValidHost) {
// invalid host, so display warning to user
$validHosts = Url::getTrustedHostsFromConfig();
$validHost = $validHosts[0];
$invalidHost = Common::sanitizeInputValue($_SERVER['HTTP_HOST']);
$emailSubject = rawurlencode(Piwik::translate('CoreHome_InjectedHostEmailSubject', $invalidHost));
$emailBody = rawurlencode(Piwik::translate('CoreHome_InjectedHostEmailBody'));
$superUserEmail = implode(',', Piwik::getAllSuperUserAccessEmailAddresses());
$mailToUrl = "mailto:$superUserEmail?subject=$emailSubject&body=$emailBody";
$mailLinkStart = "<a href=\"$mailToUrl\">";
$invalidUrl = Url::getCurrentUrlWithoutQueryString($checkIfTrusted = false);
$validUrl = Url::getCurrentScheme() . '://' . $validHost
. Url::getCurrentScriptName();
$invalidUrl = Common::sanitizeInputValue($invalidUrl);
$validUrl = Common::sanitizeInputValue($validUrl);
$changeTrustedHostsUrl = "index.php"
. Url::getCurrentQueryStringWithParametersModified(array(
'module' => 'CoreAdminHome',
'action' => 'generalSettings'
))
. "#trustedHostsSection";
$warningStart = Piwik::translate('CoreHome_InjectedHostWarningIntro', array(
'<strong>' . $invalidUrl . '</strong>',
'<strong>' . $validUrl . '</strong>'
)) . ' <br/>';
if (Piwik::hasUserSuperUserAccess()) {
$view->invalidHostMessage = $warningStart . ' '
. Piwik::translate('CoreHome_InjectedHostSuperUserWarning', array(
"<a href=\"$changeTrustedHostsUrl\">",
$invalidHost,
'</a>',
"<br/><a href=\"$validUrl\">",
$validHost,
'</a>'
));
} else if (Piwik::isUserIsAnonymous()) {
$view->invalidHostMessage = $warningStart . ' '
. Piwik::translate('CoreHome_InjectedHostNonSuperUserWarning', array(
"<br/><a href=\"$validUrl\">",
'</a>',
'<span style="display:none">',
'</span>'
));
} else {
$view->invalidHostMessage = $warningStart . ' '
. Piwik::translate('CoreHome_InjectedHostNonSuperUserWarning', array(
"<br/><a href=\"$validUrl\">",
'</a>',
$mailLinkStart,
'</a>'
));
}
$view->invalidHostMessageHowToFix = '<p><b>How do I fix this problem and how do I login again?</b><br/> The Piwik Super User can manually edit the file piwik/config/config.ini.php
and add the following lines: <pre>[General]' . "\n" . 'trusted_hosts[] = "' . $invalidHost . '"</pre>After making the change, you will be able to login again.</p>
<p>You may also <i>disable this security feature (not recommended)</i>. To do so edit config/config.ini.php and add:
<pre>[General]' . "\n" . 'enable_trusted_host_check=0</pre>';
$view->invalidHost = $invalidHost; // for UserSettings warning
$view->invalidHostMailLinkStart = $mailLinkStart;
}
}
/**
* Sets general period variables on a view, including:
*
* - **displayUniqueVisitors** - Whether unique visitors should be displayed for the current
* period.
* - **period** - The value of the **period** query parameter.
* - **otherPeriods** - `array('day', 'week', 'month', 'year', 'range')`
* - **periodsNames** - List of available periods mapped to their singular and plural translations.
*
* @param View $view
* @throws Exception if the current period is invalid.
* @api
*/
public static function setPeriodVariablesView($view)
{
if (isset($view->period)) {
return;
}
$currentPeriod = Common::getRequestVar('period');
$view->displayUniqueVisitors = SettingsPiwik::isUniqueVisitorsEnabled($currentPeriod);
$availablePeriods = array('day', 'week', 'month', 'year', 'range');
if (!in_array($currentPeriod, $availablePeriods)) {
throw new Exception("Period must be one of: " . implode(",", $availablePeriods));
}
$periodNames = array(
'day' => array('singular' => Piwik::translate('CoreHome_PeriodDay'), 'plural' => Piwik::translate('CoreHome_PeriodDays')),
'week' => array('singular' => Piwik::translate('CoreHome_PeriodWeek'), 'plural' => Piwik::translate('CoreHome_PeriodWeeks')),
'month' => array('singular' => Piwik::translate('CoreHome_PeriodMonth'), 'plural' => Piwik::translate('CoreHome_PeriodMonths')),
'year' => array('singular' => Piwik::translate('CoreHome_PeriodYear'), 'plural' => Piwik::translate('CoreHome_PeriodYears')),
// Note: plural is not used for date range
'range' => array('singular' => Piwik::translate('General_DateRangeInPeriodList'), 'plural' => Piwik::translate('General_DateRangeInPeriodList')),
);
$found = array_search($currentPeriod, $availablePeriods);
if ($found !== false) {
unset($availablePeriods[$found]);
}
$view->period = $currentPeriod;
$view->otherPeriods = $availablePeriods;
$view->periodsNames = $periodNames;
}
/**
* Helper method used to redirect the current HTTP request to another module/action.
*
* This function will exit immediately after executing.
*
* @param string $moduleToRedirect The plugin to redirect to, eg. `"MultiSites"`.
* @param string $actionToRedirect Action, eg. `"index"`.
* @param int|null $websiteId The new idSite query parameter, eg, `1`.
* @param string|null $defaultPeriod The new period query parameter, eg, `'day'`.
* @param string|null $defaultDate The new date query parameter, eg, `'today'`.
* @param array $parameters Other query parameters to append to the URL.
* @api
*/
public function redirectToIndex($moduleToRedirect, $actionToRedirect, $websiteId = null, $defaultPeriod = null,
$defaultDate = null, $parameters = array())
{
if (empty($websiteId)) {
$websiteId = $this->getDefaultWebsiteId();
}
if (empty($defaultDate)) {
$defaultDate = $this->getDefaultDate();
}
if (empty($defaultPeriod)) {
$defaultPeriod = $this->getDefaultPeriod();
}
$parametersString = '';
if (!empty($parameters)) {
$parametersString = '&' . Url::getQueryStringFromParameters($parameters);
}
if ($websiteId) {
$url = "Location: index.php?module=" . $moduleToRedirect
. "&action=" . $actionToRedirect
. "&idSite=" . $websiteId
. "&period=" . $defaultPeriod
. "&date=" . $defaultDate
. $parametersString;
header($url);
exit;
}
if (Piwik::hasUserSuperUserAccess()) {
Piwik_ExitWithMessage("Error: no website was found in this Piwik installation.
<br />Check the table '" . Common::prefixTable('site') . "' in your database, it should contain your Piwik websites.", false, true);
}
$currentLogin = Piwik::getCurrentUserLogin();
if (!empty($currentLogin)
&& $currentLogin != 'anonymous'
) {
$emails = implode(',', Piwik::getAllSuperUserAccessEmailAddresses());
$errorMessage = sprintf(Piwik::translate('CoreHome_NoPrivilegesAskPiwikAdmin'), $currentLogin, "<br/><a href='mailto:" . $emails . "?subject=Access to Piwik for user $currentLogin'>", "</a>");
$errorMessage .= "<br /><br />&nbsp;&nbsp;&nbsp;<b><a href='index.php?module=" . Registry::get('auth')->getName() . "&amp;action=logout'>&rsaquo; " . Piwik::translate('General_Logout') . "</a></b><br />";
Piwik_ExitWithMessage($errorMessage, false, true);
}
echo FrontController::getInstance()->dispatch(Piwik::getLoginPluginName(), false);
exit;
}
/**
* Returns default site ID that Piwik should load.
*
* _Note: This value is a Piwik setting set by each user._
*
* @return bool|int
* @api
*/
protected function getDefaultWebsiteId()
{
$defaultWebsiteId = false;
// User preference: default website ID to load
$defaultReport = APIUsersManager::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), APIUsersManager::PREFERENCE_DEFAULT_REPORT);
if (is_numeric($defaultReport)) {
$defaultWebsiteId = $defaultReport;
}
if ($defaultWebsiteId && Piwik::isUserHasViewAccess($defaultWebsiteId)) {
return $defaultWebsiteId;
}
$sitesId = APISitesManager::getInstance()->getSitesIdWithAtLeastViewAccess();
if (!empty($sitesId)) {
return $sitesId[0];
}
return false;
}
/**
* Returns default date for Piwik reports.
*
* _Note: This value is a Piwik setting set by each user._
*
* @return string `'today'`, `'2010-01-01'`, etc.
* @api
*/
protected function getDefaultDate()
{
// NOTE: a change in this function might mean a change in plugins/UsersManager/javascripts/usersSettings.js as well
$userSettingsDate = APIUsersManager::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), APIUsersManager::PREFERENCE_DEFAULT_REPORT_DATE);
if ($userSettingsDate == 'yesterday') {
return $userSettingsDate;
}
// if last7, last30, etc.
if (strpos($userSettingsDate, 'last') === 0
|| strpos($userSettingsDate, 'previous') === 0
) {
return $userSettingsDate;
}
return 'today';
}
/**
* Returns default period type for Piwik reports.
*
* @return string `'day'`, `'week'`, `'month'`, `'year'` or `'range'`
* @api
*/
protected function getDefaultPeriod()
{
$userSettingsDate = APIUsersManager::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), APIUsersManager::PREFERENCE_DEFAULT_REPORT_DATE);
if ($userSettingsDate === false) {
return PiwikConfig::getInstance()->General['default_period'];
}
if (in_array($userSettingsDate, array('today', 'yesterday'))) {
return 'day';
}
if (strpos($userSettingsDate, 'last') === 0
|| strpos($userSettingsDate, 'previous') === 0
) {
return 'range';
}
return $userSettingsDate;
}
/**
* Checks that the token_auth in the URL matches the currently logged-in user's token_auth.
*
* This is a protection against CSRF and should be used in all controller
* methods that modify Piwik or any user settings.
*
* **The token_auth should never appear in the browser's address bar.**
*
* @throws \Piwik\NoAccessException If the token doesn't match.
* @api
*/
protected function checkTokenInUrl()
{
if (Common::getRequestVar('token_auth', false) != Piwik::getCurrentUserTokenAuth()) {
throw new NoAccessException(Piwik::translate('General_ExceptionInvalidToken'));
}
}
/**
* Returns a prettified date string for use in period selector widget.
*
* @param Period $period The period to return a pretty string for.
* @return string
* @api
*/
public static function getCalendarPrettyDate($period)
{
if ($period instanceof Month) // show month name when period is for a month
{
return $period->getLocalizedLongString();
} else {
return $period->getPrettyString();
}
}
/**
* Returns the pretty date representation
*
* @param $date string
* @param $period string
* @return string Pretty date
*/
public static function getPrettyDate($date, $period)
{
return self::getCalendarPrettyDate(Period::factory($period, Date::factory($date)));
}
/**
* Calculates the evolution from one value to another and returns HTML displaying
* the evolution percent. The HTML includes an up/down arrow and is colored red, black or
* green depending on whether the evolution is negative, 0 or positive.
*
* No HTML is returned if the current value and evolution percent are both 0.
*
* @param string $date The date of the current value.
* @param int $currentValue The value to calculate evolution to.
* @param string $pastDate The date of past value.
* @param int $pastValue The value in the past to calculate evolution from.
* @return string|false The HTML or `false` if the evolution is 0 and the current value is 0.
* @api
*/
protected function getEvolutionHtml($date, $currentValue, $pastDate, $pastValue)
{
$evolutionPercent = CalculateEvolutionFilter::calculate(
$currentValue, $pastValue, $precision = 1);
// do not display evolution if evolution percent is 0 and current value is 0
if ($evolutionPercent == 0
&& $currentValue == 0
) {
return false;
}
$titleEvolutionPercent = $evolutionPercent;
if ($evolutionPercent < 0) {
$class = "negative-evolution";
$img = "arrow_down.png";
} else if ($evolutionPercent == 0) {
$class = "neutral-evolution";
$img = "stop.png";
} else {
$class = "positive-evolution";
$img = "arrow_up.png";
$titleEvolutionPercent = '+' . $titleEvolutionPercent;
}
$title = Piwik::translate('General_EvolutionSummaryGeneric', array(
Piwik::translate('General_NVisits', $currentValue),
$date,
Piwik::translate('General_NVisits', $pastValue),
$pastDate,
$titleEvolutionPercent
));
$result = '<span class="metricEvolution" title="' . $title
. '"><img style="padding-right:4px" src="plugins/MultiSites/images/' . $img . '"/><strong';
if (isset($class)) {
$result .= ' class="' . $class . '"';
}
$result .= '>' . $evolutionPercent . '</strong></span>';
return $result;
}
}

View file

@ -0,0 +1,213 @@
<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin;
use Piwik\Config as PiwikConfig;
use Piwik\Config;
use Piwik\Menu\MenuAdmin;
use Piwik\Menu\MenuTop;
use Piwik\Notification;
use Piwik\Notification\Manager as NotificationManager;
use Piwik\Piwik;
use Piwik\Url;
use Piwik\Version;
use Piwik\View;
/**
* Base class of plugin controllers that provide administrative functionality.
*
* See {@link Controller} to learn more about Piwik controllers.
*
*/
abstract class ControllerAdmin extends Controller
{
private static $isEacceleratorUsed = false;
private static function notifyWhenTrackingStatisticsDisabled()
{
$statsEnabled = PiwikConfig::getInstance()->Tracker['record_statistics'];
if ($statsEnabled == "0") {
$notification = new Notification(Piwik::translate('General_StatisticsAreNotRecorded'));
$notification->context = Notification::CONTEXT_INFO;
Notification\Manager::notify('ControllerAdmin_StatsAreNotRecorded', $notification);
}
}
private static function notifyAnyInvalidPlugin()
{
$missingPlugins = \Piwik\Plugin\Manager::getInstance()->getMissingPlugins();
if (empty($missingPlugins)) {
return;
}
if (!Piwik::hasUserSuperUserAccess()) {
return;
}
$pluginsLink = Url::getCurrentQueryStringWithParametersModified(array(
'module' => 'CorePluginsAdmin', 'action' => 'plugins'
));
$invalidPluginsWarning = Piwik::translate('CoreAdminHome_InvalidPluginsWarning', array(
self::getPiwikVersion(),
'<strong>' . implode('</strong>,&nbsp;<strong>', $missingPlugins) . '</strong>'))
. Piwik::translate('CoreAdminHome_InvalidPluginsYouCanUninstall', array(
'<a href="' . $pluginsLink . '"/>',
'</a>'
));
$notification = new Notification($invalidPluginsWarning);
$notification->raw = true;
$notification->context = Notification::CONTEXT_WARNING;
$notification->title = Piwik::translate('General_Warning') . ':';
Notification\Manager::notify('ControllerAdmin_InvalidPluginsWarning', $notification);
}
/**
* Calls {@link setBasicVariablesView()} and {@link setBasicVariablesAdminView()}
* using the supplied view.
*
* @param View $view
* @api
*/
protected function setBasicVariablesView($view)
{
parent::setBasicVariablesView($view);
self::setBasicVariablesAdminView($view);
}
/**
* @ignore
*/
static public function displayWarningIfConfigFileNotWritable()
{
$isConfigFileWritable = PiwikConfig::getInstance()->isFileWritable();
if (!$isConfigFileWritable) {
$exception = PiwikConfig::getInstance()->getConfigNotWritableException();
$message = $exception->getMessage();
$notification = new Notification($message);
$notification->raw = true;
$notification->context = Notification::CONTEXT_WARNING;
Notification\Manager::notify('ControllerAdmin_ConfigNotWriteable', $notification);
}
}
/**
* See http://dev.piwik.org/trac/ticket/4439#comment:8 and https://github.com/eaccelerator/eaccelerator/issues/12
*
* Eaccelerator does not support closures and is known to be not comptabile with Piwik. Therefore we are disabling
* it automatically. At this point it looks like Eaccelerator is no longer under development and the bug has not
* been fixed within a year.
*/
public static function disableEacceleratorIfEnabled()
{
$isEacceleratorUsed = ini_get('eaccelerator.enable');
if (!empty($isEacceleratorUsed)) {
self::$isEacceleratorUsed = true;
@ini_set('eaccelerator.enable', 0);
}
}
private static function notifyIfEAcceleratorIsUsed()
{
if (self::$isEacceleratorUsed) {
$message = sprintf("You are using the PHP accelerator & optimizer eAccelerator which is known to be not compatible with Piwik.
We have disabled eAccelerator, which might affect the performance of Piwik.
Read the %srelated ticket%s for more information and how to fix this problem.",
'<a target="_blank" href="http://dev.piwik.org/trac/ticket/4439">', '</a>');
$notification = new Notification($message);
$notification->context = Notification::CONTEXT_WARNING;
$notification->raw = true;
Notification\Manager::notify('ControllerAdmin_EacceleratorIsUsed', $notification);
}
}
/**
* Assigns view properties that would be useful to views that render admin pages.
*
* Assigns the following variables:
*
* - **statisticsNotRecorded** - Set to true if the `[Tracker] record_statistics` INI
* config is `0`. If not `0`, this variable will not be defined.
* - **topMenu** - The result of `MenuTop::getInstance()->getMenu()`.
* - **currentAdminMenuName** - The currently selected admin menu name.
* - **enableFrames** - The value of the `[General] enable_framed_pages` INI config option. If
* true, {@link Piwik\View::setXFrameOptions()} is called on the view.
* - **isSuperUser** - Whether the current user is a superuser or not.
* - **usingOldGeoIPPlugin** - Whether this Piwik install is currently using the old GeoIP
* plugin or not.
* - **invalidPluginsWarning** - Set if some of the plugins to load (determined by INI configuration)
* are invalid or missing.
* - **phpVersion** - The current PHP version.
* - **phpIsNewEnough** - Whether the current PHP version is new enough to run Piwik.
* - **adminMenu** - The result of `MenuAdmin::getInstance()->getMenu()`.
*
* @param View $view
* @api
*/
static public function setBasicVariablesAdminView(View $view)
{
self::notifyWhenTrackingStatisticsDisabled();
self::notifyIfEAcceleratorIsUsed();
$view->topMenu = MenuTop::getInstance()->getMenu();
$view->currentAdminMenuName = MenuAdmin::getInstance()->getCurrentAdminMenuName();
$view->isDataPurgeSettingsEnabled = self::isDataPurgeSettingsEnabled();
$view->enableFrames = PiwikConfig::getInstance()->General['enable_framed_settings'];
if (!$view->enableFrames) {
$view->setXFrameOptions('sameorigin');
}
$view->isSuperUser = Piwik::hasUserSuperUserAccess();
self::notifyAnyInvalidPlugin();
self::checkPhpVersion($view);
$adminMenu = MenuAdmin::getInstance()->getMenu();
$view->adminMenu = $adminMenu;
$view->notifications = NotificationManager::getAllNotificationsToDisplay();
NotificationManager::cancelAllNonPersistent();
}
static public function isDataPurgeSettingsEnabled()
{
return (bool) Config::getInstance()->General['enable_delete_old_data_settings_admin'];
}
static protected function getPiwikVersion()
{
return "Piwik " . Version::VERSION;
}
/**
* Check if the current PHP version is >= 5.3. If not, a warning is displayed
* to the user.
*/
private static function checkPhpVersion($view)
{
$view->phpVersion = PHP_VERSION;
$view->phpIsNewEnough = version_compare($view->phpVersion, '5.3.0', '>=');
}
protected function getDefaultWebsiteId()
{
$sitesId = \Piwik\Plugins\SitesManager\API::getInstance()->getSitesIdWithAdminAccess();
if (!empty($sitesId)) {
return $sitesId[0];
}
return parent::getDefaultWebsiteId();
}
}

View file

@ -0,0 +1,105 @@
<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin;
use Piwik\Version;
use Piwik\Plugin\Manager as PluginManager;
/**
*
*/
class Dependency
{
private $piwikVersion;
public function __construct()
{
$this->setPiwikVersion(Version::VERSION);
}
public function getMissingDependencies($requires)
{
$missingRequirements = array();
if (empty($requires)) {
return $missingRequirements;
}
foreach ($requires as $name => $requiredVersion) {
$currentVersion = $this->getCurrentVersion($name);
$missingVersions = $this->getMissingVersions($currentVersion, $requiredVersion);
if (!empty($missingVersions)) {
$missingRequirements[] = array(
'requirement' => $name,
'actualVersion' => $currentVersion,
'requiredVersion' => $requiredVersion,
'causedBy' => implode(', ', $missingVersions)
);
}
}
return $missingRequirements;
}
public function getMissingVersions($currentVersion, $requiredVersion)
{
$currentVersion = trim($currentVersion);
$requiredVersions = explode(',' , (string) $requiredVersion);
$missingVersions = array();
foreach ($requiredVersions as $required) {
$comparison = '>=';
$required = trim($required);
if (preg_match('{^(<>|!=|>=?|<=?|==?)\s*(.*)}', $required, $matches)) {
$required = $matches[2];
$comparison = trim($matches[1]);
}
if (false === version_compare($currentVersion, $required, $comparison)) {
$missingVersions[] = $comparison . $required;
}
}
return $missingVersions;
}
public function setPiwikVersion($piwikVersion)
{
$this->piwikVersion = $piwikVersion;
}
private function getCurrentVersion($name)
{
switch (strtolower($name)) {
case 'piwik':
return $this->piwikVersion;
case 'php':
return PHP_VERSION;
default:
try {
$pluginNames = PluginManager::getAllPluginsNames();
if (!in_array($name, $pluginNames) || !PluginManager::getInstance()->isPluginLoaded($name)) {
return '';
}
$plugin = PluginManager::getInstance()->loadPlugin(ucfirst($name));
if (!empty($plugin)) {
return $plugin->getVersion();
}
} catch (\Exception $e) {}
}
return '';
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,106 @@
<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin;
use Exception;
use Piwik\Common;
use Piwik\Piwik;
use Piwik\Version;
/**
* @see core/Version.php
*/
require_once PIWIK_INCLUDE_PATH . '/core/Version.php';
/**
* Loads plugin metadata found in the following files:
* - piwik.json
*/
class MetadataLoader
{
const PLUGIN_JSON_FILENAME = 'plugin.json';
/**
* The name of the plugin whose metadata will be loaded.
*
* @var string
*/
private $pluginName;
/**
* Constructor.
*
* @param string $pluginName Name of the plugin to load metadata.
*/
public function __construct($pluginName)
{
$this->pluginName = $pluginName;
}
/**
* Loads plugin metadata. @see Plugin::getInformation.
*
* @return array
*/
public function load()
{
return array_merge(
$this->getDefaultPluginInformation(),
$this->loadPluginInfoJson()
);
}
public function hasPluginJson()
{
$hasJson = $this->loadPluginInfoJson();
return !empty($hasJson);
}
private function getDefaultPluginInformation()
{
$descriptionKey = $this->pluginName . '_PluginDescription';
return array(
'description' => Piwik::translate($descriptionKey),
'homepage' => 'http://piwik.org/',
'authors' => array(array('name' => 'Piwik', 'homepage' => 'http://piwik.org/')),
'license' => 'GPL v3+',
'license_homepage' => 'http://www.gnu.org/licenses/gpl.html',
'version' => Version::VERSION,
'theme' => false,
'require' => array()
);
}
private function loadPluginInfoJson()
{
$path = \Piwik\Plugin\Manager::getPluginsDirectory() . $this->pluginName . '/' . self::PLUGIN_JSON_FILENAME;
return $this->loadJsonMetadata($path);
}
private function loadJsonMetadata($path)
{
if (!file_exists($path)) {
return array();
}
$json = file_get_contents($path);
if (!$json) {
return array();
}
$info = Common::json_decode($json, $assoc = true);
if (!is_array($info)
|| empty($info)
) {
throw new Exception("Invalid JSON file: $path");
}
return $info;
}
}

View file

@ -0,0 +1,378 @@
<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin;
use Piwik\Option;
use Piwik\Piwik;
use Piwik\Settings\Setting;
use Piwik\Settings\StorageInterface;
use Piwik\SettingsServer;
/**
* Base class of all plugin settings providers. Plugins that define their own configuration settings
* can extend this class to easily make their settings available to Piwik users.
*
* Descendants of this class should implement the {@link init()} method and call the
* {@link addSetting()} method for each of the plugin's settings.
*
* For an example, see the {@link Piwik\Plugins\ExampleSettingsPlugin\ExampleSettingsPlugin} plugin.
*
* @api
*/
abstract class Settings implements StorageInterface
{
const TYPE_INT = 'integer';
const TYPE_FLOAT = 'float';
const TYPE_STRING = 'string';
const TYPE_BOOL = 'boolean';
const TYPE_ARRAY = 'array';
const CONTROL_RADIO = 'radio';
const CONTROL_TEXT = 'text';
const CONTROL_TEXTAREA = 'textarea';
const CONTROL_CHECKBOX = 'checkbox';
const CONTROL_PASSWORD = 'password';
const CONTROL_MULTI_SELECT = 'multiselect';
const CONTROL_SINGLE_SELECT = 'select';
/**
* An array containing all available settings: Array ( [setting-name] => [setting] )
*
* @var Settings[]
*/
private $settings = array();
/**
* Array containing all plugin settings values: Array( [setting-key] => [setting-value] ).
*
* @var array
*/
private $settingsValues = array();
private $introduction;
private $pluginName;
/**
* Constructor.
*
* @param string $pluginName The name of the plugin these settings are for.
*/
public function __construct($pluginName)
{
$this->pluginName = $pluginName;
$this->init();
$this->loadSettings();
}
/**
* Implemented by descendants. This method should define plugin settings (via the
* {@link addSetting()}) method and set the introduction text (via the
* {@link setIntroduction()}).
*/
abstract protected function init();
/**
* Sets the text used to introduce this plugin's settings in the _Plugin Settings_ page.
*
* @param string $introduction
*/
protected function setIntroduction($introduction)
{
$this->introduction = $introduction;
}
/**
* Returns the introduction text for this plugin's settings.
*
* @return string
*/
public function getIntroduction()
{
return $this->introduction;
}
/**
* Returns the settings that can be displayed for the current user.
*
* @return Setting[]
*/
public function getSettingsForCurrentUser()
{
$settings = array_filter($this->getSettings(), function (Setting $setting) {
return $setting->canBeDisplayedForCurrentUser();
});
uasort($settings, function ($setting1, $setting2) use ($settings) {
/** @var Setting $setting1 */ /** @var Setting $setting2 */
if ($setting1->getOrder() == $setting2->getOrder()) {
// preserve order for settings having same order
foreach ($settings as $setting) {
if ($setting1 === $setting) {
return -1;
}
if ($setting2 === $setting) {
return 1;
}
}
return 0;
}
return $setting1->getOrder() > $setting2->getOrder() ? -1 : 1;
});
return $settings;
}
/**
* Returns all available settings. This will include settings that are not available
* to the current user (such as settings available only to the Super User).
*
* @return Setting[]
*/
public function getSettings()
{
return $this->settings;
}
/**
* Saves (persists) the current setting values in the database.
*/
public function save()
{
Option::set($this->getOptionKey(), serialize($this->settingsValues));
}
/**
* Removes all settings for this plugin from the database. Useful when uninstalling
* a plugin.
*/
public function removeAllPluginSettings()
{
Piwik::checkUserHasSuperUserAccess();
Option::delete($this->getOptionKey());
$this->settingsValues = array();
}
/**
* Returns the current value for a setting. If no value is stored, the default value
* is be returned.
*
* @param Setting $setting
* @return mixed
* @throws \Exception If the setting does not exist or if the current user is not allowed to change the value
* of this setting.
*/
public function getSettingValue(Setting $setting)
{
$this->checkIsValidSetting($setting->getName());
if (array_key_exists($setting->getKey(), $this->settingsValues)) {
return $this->settingsValues[$setting->getKey()];
}
return $setting->defaultValue;
}
/**
* Sets (overwrites) the value of a setting in memory. To persist the change, {@link save()} must be
* called afterwards, otherwise the change has no effect.
*
* Before the setting is changed, the {@link Piwik\Settings\Setting::$validate} and
* {@link Piwik\Settings\Setting::$transform} closures will be invoked (if defined). If there is no validation
* filter, the setting value will be casted to the appropriate data type.
*
* @param Setting $setting
* @param string $value
* @throws \Exception If the setting does not exist or if the current user is not allowed to change the value
* of this setting.
*/
public function setSettingValue(Setting $setting, $value)
{
$this->checkIsValidSetting($setting->getName());
if ($setting->validate && $setting->validate instanceof \Closure) {
call_user_func($setting->validate, $value, $setting);
}
if ($setting->transform && $setting->transform instanceof \Closure) {
$value = call_user_func($setting->transform, $value, $setting);
} elseif (isset($setting->type)) {
settype($value, $setting->type);
}
$this->settingsValues[$setting->getKey()] = $value;
}
/**
* Unsets a setting value in memory. To persist the change, {@link save()} must be
* called afterwards, otherwise the change has no effect.
*
* @param Setting $setting
*/
public function removeSettingValue(Setting $setting)
{
$this->checkHasEnoughPermission($setting);
$key = $setting->getKey();
if (array_key_exists($key, $this->settingsValues)) {
unset($this->settingsValues[$key]);
}
}
/**
* Makes a new plugin setting available.
*
* @param Setting $setting
* @throws \Exception If there is a setting with the same name that already exists.
* If the name contains non-alphanumeric characters.
*/
protected function addSetting(Setting $setting)
{
if (!ctype_alnum($setting->getName())) {
$msg = sprintf('The setting name "%s" in plugin "%s" is not valid. Only alpha and numerical characters are allowed', $setting->getName(), $this->pluginName);
throw new \Exception($msg);
}
if (array_key_exists($setting->getName(), $this->settings)) {
throw new \Exception(sprintf('A setting with name "%s" does already exist for plugin "%s"', $setting->getName(), $this->pluginName));
}
$this->setDefaultTypeAndFieldIfNeeded($setting);
$this->addValidatorIfNeeded($setting);
$setting->setStorage($this);
$this->settings[$setting->getName()] = $setting;
}
private function getOptionKey()
{
return 'Plugin_' . $this->pluginName . '_Settings';
}
private function loadSettings()
{
$values = Option::get($this->getOptionKey());
if (!empty($values)) {
$this->settingsValues = unserialize($values);
}
}
private function checkIsValidSetting($name)
{
$setting = $this->getSetting($name);
if (empty($setting)) {
throw new \Exception(sprintf('The setting %s does not exist', $name));
}
$this->checkHasEnoughPermission($setting);
}
/**
* @param $name
* @return Setting|null
*/
private function getSetting($name)
{
if (array_key_exists($name, $this->settings)) {
return $this->settings[$name];
}
}
private function getDefaultType($controlType)
{
$defaultTypes = array(
static::CONTROL_TEXT => static::TYPE_STRING,
static::CONTROL_TEXTAREA => static::TYPE_STRING,
static::CONTROL_PASSWORD => static::TYPE_STRING,
static::CONTROL_CHECKBOX => static::TYPE_BOOL,
static::CONTROL_MULTI_SELECT => static::TYPE_ARRAY,
static::CONTROL_RADIO => static::TYPE_STRING,
static::CONTROL_SINGLE_SELECT => static::TYPE_STRING,
);
return $defaultTypes[$controlType];
}
private function getDefaultCONTROL($type)
{
$defaultControlTypes = array(
static::TYPE_INT => static::CONTROL_TEXT,
static::TYPE_FLOAT => static::CONTROL_TEXT,
static::TYPE_STRING => static::CONTROL_TEXT,
static::TYPE_BOOL => static::CONTROL_CHECKBOX,
static::TYPE_ARRAY => static::CONTROL_MULTI_SELECT,
);
return $defaultControlTypes[$type];
}
/**
* @param $setting
* @throws \Exception
*/
private function checkHasEnoughPermission(Setting $setting)
{
// When the request is a Tracker request, allow plugins to read/write settings
if(SettingsServer::isTrackerApiRequest()) {
return;
}
if (!$setting->canBeDisplayedForCurrentUser()) {
$errorMsg = Piwik::translate('CoreAdminHome_PluginSettingChangeNotAllowed', array($setting->getName(), $this->pluginName));
throw new \Exception($errorMsg);
}
}
private function setDefaultTypeAndFieldIfNeeded(Setting $setting)
{
if (!is_null($setting->uiControlType) && is_null($setting->type)) {
$setting->type = $this->getDefaultType($setting->uiControlType);
} elseif (!is_null($setting->type) && is_null($setting->uiControlType)) {
$setting->uiControlType = $this->getDefaultCONTROL($setting->type);
} elseif (is_null($setting->uiControlType) && is_null($setting->type)) {
$setting->type = static::TYPE_STRING;
$setting->uiControlType = static::CONTROL_TEXT;
}
}
private function addValidatorIfNeeded(Setting $setting)
{
if (!is_null($setting->validate) || is_null($setting->availableValues)) {
return;
}
$pluginName = $this->pluginName;
$setting->validate = function ($value) use ($setting, $pluginName) {
$errorMsg = Piwik::translate('CoreAdminHome_PluginSettingsValueNotAllowed',
array($setting->title, $pluginName));
if (is_array($value) && $setting->type == Settings::TYPE_ARRAY) {
foreach ($value as $val) {
if (!array_key_exists($val, $setting->availableValues)) {
throw new \Exception($errorMsg);
}
}
} else {
if (!array_key_exists($value, $setting->availableValues)) {
throw new \Exception($errorMsg);
}
}
};
}
}

View file

@ -0,0 +1,459 @@
<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin;
use Piwik\API\Request;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Period;
use Piwik\Piwik;
use Piwik\View;
use Piwik\View\ViewInterface;
use Piwik\ViewDataTable\Config as VizConfig;
use Piwik\ViewDataTable\Manager as ViewDataTableManager;
use Piwik\ViewDataTable\Request as ViewDataTableRequest;
use Piwik\ViewDataTable\RequestConfig as VizRequest;
/**
* The base class of all report visualizations.
*
* ViewDataTable instances load analytics data via Piwik's Reporting API and then output some
* type of visualization of that data.
*
* Visualizations can be in any format. HTML-based visualizations should extend
* {@link Visualization}. Visualizations that use other formats, such as visualizations
* that output an image, should extend ViewDataTable directly.
*
* ### Creating ViewDataTables
*
* ViewDataTable instances are not created via the new operator, instead the {@link Piwik\ViewDataTable\Factory}
* class is used.
*
* The specific subclass to create is determined, first, by the **viewDataTable** query paramater.
* If this parameter is not set, then the default visualization type for the report being
* displayed is used.
*
* ### Configuring ViewDataTables
*
* **Display properties**
*
* ViewDataTable output can be customized by setting one of many available display
* properties. Display properties are stored as fields in {@link Piwik\ViewDataTable\Config} objects.
* ViewDataTables store a {@link Piwik\ViewDataTable\Config} object in the {@link $config} field.
*
* Display properties can be set at any time before rendering.
*
* **Request properties**
*
* Request properties are similar to display properties in the way they are set. They are,
* however, not used to customize ViewDataTable instances, but in the request to Piwik's
* API when loading analytics data.
*
* Request properties are set by setting the fields of a {@link Piwik\ViewDataTable\RequestConfig} object stored in
* the {@link $requestConfig} field. They can be set at any time before rendering.
* Setting them after data is loaded will have no effect.
*
* **Customizing how reports are displayed**
*
* Each individual report should be rendered in its own controller method. There are two
* ways to render a report within its controller method. You can either:
*
* 1. manually create and configure a ViewDataTable instance
* 2. invoke {@link Piwik\Plugin\Controller::renderReport} and configure the ViewDataTable instance
* in the {@hook ViewDataTable.configure} event.
*
* ViewDataTable instances are configured by setting and modifying display properties and request
* properties.
*
* ### Creating new visualizations
*
* New visualizations can be created by extending the ViewDataTable class or one of its
* descendants. To learn more [read our guide on creating new visualizations](/guides/visualizing-report-data#creating-new-visualizations).
*
* ### Examples
*
* **Manually configuring a ViewDataTable**
*
* // a controller method that displays a single report
* public function myReport()
* {
* $view = \Piwik\ViewDataTable\Factory::build('table', 'MyPlugin.myReport');
* $view->config->show_limit_control = true;
* $view->config->translations['myFancyMetric'] = "My Fancy Metric";
* // ...
* return $view->render();
* }
*
* **Using {@link Piwik\Plugin\Controller::renderReport}**
*
* First, a controller method that displays a single report:
*
* public function myReport()
* {
* return $this->renderReport(__FUNCTION__);`
* }
*
* Then the event handler for the {@hook ViewDataTable.configure} event:
*
* public function configureViewDataTable(ViewDataTable $view)
* {
* switch ($view->requestConfig->apiMethodToRequestDataTable) {
* case 'MyPlugin.myReport':
* $view->config->show_limit_control = true;
* $view->config->translations['myFancyMetric'] = "My Fancy Metric";
* // ...
* break;
* }
* }
*
* **Using custom configuration objects in a new visualization**
*
* class MyVisualizationConfig extends Piwik\ViewDataTable\Config
* {
* public $my_new_property = true;
* }
*
* class MyVisualizationRequestConfig extends Piwik\ViewDataTable\RequestConfig
* {
* public $my_new_property = false;
* }
*
* class MyVisualization extends Piwik\Plugin\ViewDataTable
* {
* public static function getDefaultConfig()
* {
* return new MyVisualizationConfig();
* }
*
* public static function getDefaultRequestConfig()
* {
* return new MyVisualizationRequestConfig();
* }
* }
*
*
* @api
*/
abstract class ViewDataTable implements ViewInterface
{
const ID = '';
/**
* DataTable loaded from the API for this ViewDataTable.
*
* @var DataTable
*/
protected $dataTable = null;
/**
* Contains display properties for this visualization.
*
* @var \Piwik\ViewDataTable\Config
*/
public $config;
/**
* Contains request properties for this visualization.
*
* @var \Piwik\ViewDataTable\RequestConfig
*/
public $requestConfig;
/**
* @var ViewDataTableRequest
*/
protected $request;
/**
* Constructor. Initializes display and request properties to their default values.
* Posts the {@hook ViewDataTable.configure} event which plugins can use to configure the
* way reports are displayed.
*/
public function __construct($controllerAction, $apiMethodToRequestDataTable)
{
list($controllerName, $controllerAction) = explode('.', $controllerAction);
$this->requestConfig = static::getDefaultRequestConfig();
$this->config = static::getDefaultConfig();
$this->config->subtable_controller_action = $controllerAction;
$this->config->setController($controllerName, $controllerAction);
$this->request = new ViewDataTableRequest($this->requestConfig);
$this->requestConfig->idSubtable = Common::getRequestVar('idSubtable', false, 'int');
$this->config->self_url = Request::getBaseReportUrl($controllerName, $controllerAction);
$this->requestConfig->apiMethodToRequestDataTable = $apiMethodToRequestDataTable;
/**
* Triggered during {@link ViewDataTable} construction. Subscribers should customize
* the view based on the report that is being displayed.
*
* Plugins that define their own reports must subscribe to this event in order to
* specify how the Piwik UI should display the report.
*
* **Example**
*
* // event handler
* public function configureViewDataTable(ViewDataTable $view)
* {
* switch ($view->requestConfig->apiMethodToRequestDataTable) {
* case 'VisitTime.getVisitInformationPerServerTime':
* $view->config->enable_sort = true;
* $view->requestConfig->filter_limit = 10;
* break;
* }
* }
*
* @param ViewDataTable $view The instance to configure.
*/
Piwik::postEvent('ViewDataTable.configure', array($this));
$this->assignRelatedReportsTitle();
$this->config->show_footer_icons = (false == $this->requestConfig->idSubtable);
// the exclude low population threshold value is sometimes obtained by requesting data.
// to avoid issuing unecessary requests when display properties are determined by metadata,
// we allow it to be a closure.
if (isset($this->requestConfig->filter_excludelowpop_value)
&& $this->requestConfig->filter_excludelowpop_value instanceof \Closure
) {
$function = $this->requestConfig->filter_excludelowpop_value;
$this->requestConfig->filter_excludelowpop_value = $function();
}
$this->overrideViewPropertiesWithQueryParams();
}
protected function assignRelatedReportsTitle()
{
if(!empty($this->config->related_reports_title)) {
// title already assigned by a plugin
return;
}
if(count($this->config->related_reports) == 1) {
$this->config->related_reports_title = Piwik::translate('General_RelatedReport') . ':';
} else {
$this->config->related_reports_title = Piwik::translate('General_RelatedReports') . ':';
}
}
/**
* Returns the default config instance.
*
* Visualizations that define their own display properties should override this method and
* return an instance of their new {@link Piwik\ViewDataTable\Config} descendant.
*
* See the last example {@link ViewDataTable here} for more information.
*
* @return \Piwik\ViewDataTable\Config
*/
public static function getDefaultConfig()
{
return new VizConfig();
}
/**
* Returns the default request config instance.
*
* Visualizations that define their own request properties should override this method and
* return an instance of their new {@link Piwik\ViewDataTable\RequestConfig} descendant.
*
* See the last example {@link ViewDataTable here} for more information.
*
* @return \Piwik\ViewDataTable\RequestConfig
*/
public static function getDefaultRequestConfig()
{
return new VizRequest();
}
protected function loadDataTableFromAPI($fixedRequestParams = array())
{
if (!is_null($this->dataTable)) {
// data table is already there
// this happens when setDataTable has been used
return $this->dataTable;
}
$this->dataTable = $this->request->loadDataTableFromAPI($fixedRequestParams);
return $this->dataTable;
}
/**
* Returns the viewDataTable ID for this DataTable visualization.
*
* Derived classes should not override this method. They should instead declare a const ID field
* with the viewDataTable ID.
*
* @throws \Exception
* @return string
*/
public static function getViewDataTableId()
{
$id = static::ID;
if (empty($id)) {
$message = sprintf('ViewDataTable %s does not define an ID. Set the ID constant to fix this issue', get_called_class());
throw new \Exception($message);
}
return $id;
}
/**
* Returns `true` if this instance's or any of its ancestors' viewDataTable IDs equals the supplied ID,
* `false` if otherwise.
*
* Can be used to test whether a ViewDataTable object is an instance of a certain visualization or not,
* without having to know where that visualization is.
*
* @param string $viewDataTableId The viewDataTable ID to check for, eg, `'table'`.
* @return bool
*/
public function isViewDataTableId($viewDataTableId)
{
$myIds = ViewDataTableManager::getIdsWithInheritance(get_called_class());
return in_array($viewDataTableId, $myIds);
}
/**
* Returns the DataTable loaded from the API.
*
* @return DataTable
* @throws \Exception if not yet loaded.
*/
public function getDataTable()
{
if (is_null($this->dataTable)) {
throw new \Exception("The DataTable object has not yet been created");
}
return $this->dataTable;
}
/**
* To prevent calling an API multiple times, the DataTable can be set directly.
* It won't be loaded from the API in this case.
*
* @param DataTable $dataTable The DataTable to use.
* @return void
*/
public function setDataTable($dataTable)
{
$this->dataTable = $dataTable;
}
/**
* Checks that the API returned a normal DataTable (as opposed to DataTable\Map)
* @throws \Exception
* @return void
*/
protected function checkStandardDataTable()
{
Piwik::checkObjectTypeIs($this->dataTable, array('\Piwik\DataTable'));
}
/**
* Requests all needed data and renders the view.
*
* @return string The result of rendering.
*/
public function render()
{
$view = $this->buildView();
return $view->render();
}
abstract protected function buildView();
protected function getDefaultDataTableCssClass()
{
return 'dataTableViz' . Piwik::getUnnamespacedClassName(get_class($this));
}
/**
* Returns the list of view properties that can be overriden by query parameters.
*
* @return array
*/
protected function getOverridableProperties()
{
return array_merge($this->config->overridableProperties, $this->requestConfig->overridableProperties);
}
private function overrideViewPropertiesWithQueryParams()
{
$properties = $this->getOverridableProperties();
foreach ($properties as $name) {
if (property_exists($this->requestConfig, $name)) {
$this->requestConfig->$name = $this->getPropertyFromQueryParam($name, $this->requestConfig->$name);
} elseif (property_exists($this->config, $name)) {
$this->config->$name = $this->getPropertyFromQueryParam($name, $this->config->$name);
}
}
// handle special 'columns' query parameter
$columns = Common::getRequestVar('columns', false);
if (false !== $columns) {
$this->config->columns_to_display = Piwik::getArrayFromApiParameter($columns);
array_unshift($this->config->columns_to_display, 'label');
}
}
protected function getPropertyFromQueryParam($name, $defaultValue)
{
$type = is_numeric($defaultValue) ? 'int' : null;
return Common::getRequestVar($name, $defaultValue, $type);
}
/**
* Returns `true` if this instance will request a single DataTable, `false` if requesting
* more than one.
*
* @return bool
*/
public function isRequestingSingleDataTable()
{
$requestArray = $this->request->getRequestArray() + $_GET + $_POST;
$date = Common::getRequestVar('date', null, 'string', $requestArray);
$period = Common::getRequestVar('period', null, 'string', $requestArray);
$idSite = Common::getRequestVar('idSite', null, 'string', $requestArray);
if (Period::isMultiplePeriod($date, $period)
|| strpos($idSite, ',') !== false
|| $idSite == 'all'
) {
return false;
}
return true;
}
/**
* Returns `true` if this visualization can display some type of data or not.
*
* New visualization classes should override this method if they can only visualize certain
* types of data. The evolution graph visualization, for example, can only visualize
* sets of DataTables. If the API method used results in a single DataTable, the evolution
* graph footer icon should not be displayed.
*
* @param ViewDataTable $view Contains the API request being checked.
* @return bool
*/
public static function canDisplayViewDataTable(ViewDataTable $view)
{
return $view->config->show_all_views_icons;
}
}

View file

@ -0,0 +1,595 @@
<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Date;
use Piwik\Log;
use Piwik\MetricsFormatter;
use Piwik\NoAccessException;
use Piwik\Option;
use Piwik\Period;
use Piwik\Piwik;
use Piwik\Plugins\PrivacyManager\PrivacyManager;
use Piwik\View;
use Piwik\ViewDataTable\Manager as ViewDataTableManager;
/**
* The base class for report visualizations that output HTML and use JavaScript.
*
* Report visualizations that extend from this class will be displayed like all others in
* the Piwik UI. The following extra UI controls will be displayed around the visualization
* itself:
*
* - report documentation,
* - a footer message (if {@link Piwik\ViewDataTable\Config::$show_footer_message} is set),
* - a list of links to related reports (if {@link Piwik\ViewDataTable\Config::$related_reports} is set),
* - a button that allows users to switch visualizations,
* - a control that allows users to export report data in different formats,
* - a limit control that allows users to change the amount of rows displayed (if
* {@link Piwik\ViewDataTable\Config::$show_limit_control} is true),
* - and more depending on the visualization.
*
* ### Rendering Process
*
* The following process is used to render reports:
*
* - The report is loaded through Piwik's Reporting API.
* - The display and request properties that require report data in order to determine a default
* value are defaulted. These properties are:
*
* - {@link Piwik\ViewDataTable\Config::$columns_to_display}
* - {@link Piwik\ViewDataTable\RequestConfig::$filter_sort_column}
* - {@link Piwik\ViewDataTable\RequestConfig::$filter_sort_order}
*
* - Priority filters are applied to the report (see {@link Piwik\ViewDataTable\Config::$filters}).
* - The filters that are applied to every report in the Reporting API (called **generic filters**)
* are applied. (see {@link Piwik\API\Request})
* - The report's queued filters are applied.
* - A {@link Piwik\View} instance is created and rendered.
*
* ### Rendering Hooks
*
* The Visualization class defines several overridable methods that are called at specific
* points during the rendering process. Derived classes can override these methods change
* the data that is displayed or set custom properties.
*
* The overridable methods (called **rendering hooks**) are as follows:
*
* - **beforeLoadDataTable**: Called at the start of the rendering process before any data
* is loaded.
* - **beforeGenericFiltersAreAppliedToLoadedDataTable**: Called after data is loaded and after priority
* filters are called, but before other filters. This
* method should be used if you need the report's
* entire dataset.
* - **afterGenericFiltersAreAppliedToLoadedDataTable**: Called after generic filters are applied, but before
* queued filters are applied.
* - **afterAllFiltersAreApplied**: Called after data is loaded and all filters are applied.
* - **beforeRender**: Called immediately before a {@link Piwik\View} is created and rendered.
* - **isThereDataToDisplay**: Called after a {@link Piwik\View} is created to determine if the report has
* data or not. If not, a message is displayed to the user.
*
* ### The DataTable JavaScript class
*
* In the UI, visualization behavior is provided by logic in the **DataTable** JavaScript class.
* When creating new visualizations, the **DataTable** JavaScript class (or one of its existing
* descendants) should be extended.
*
* To learn more read the [Visualizing Report Data](/guides/visualizing-report-data#creating-new-visualizations)
* guide.
*
* ### Examples
*
* **Changing the data that is loaded**
*
* class MyVisualization extends Visualization
* {
* // load the previous period's data as well as the requested data. this will change
* // $this->dataTable from a DataTable instance to a DataTable\Map instance.
* public function beforeLoadDataTable()
* {
* $date = Common::getRequestVar('date');
* list($previousDate, $ignore) = Range::getLastDate($date, $period);
*
* $this->requestConfig->request_parameters_to_modify['date'] = $previousDate . ',' . $date;
* }
*
* // since we load the previous period's data too, we need to override the logic to
* // check if there is data or not.
* public function isThereDataToDisplay()
* {
* $tables = $this->dataTable->getDataTables()
* $requestedDataTable = end($tables);
*
* return $requestedDataTable->getRowsCount() != 0;
* }
* }
*
* **Force properties to be set to certain values**
*
* class MyVisualization extends Visualization
* {
* // ensure that some properties are set to certain values before rendering.
* // this will overwrite any changes made by plugins that use this visualization.
* public function beforeRender()
* {
* $this->config->max_graph_elements = false;
* $this->config->datatable_js_type = 'MyVisualization';
* $this->config->show_flatten_table = false;
* $this->config->show_pagination_control = false;
* $this->config->show_offset_information = false;
* }
* }
*/
class Visualization extends ViewDataTable
{
/**
* The Twig template file to use when rendering, eg, `"@MyPlugin/_myVisualization.twig"`.
*
* Must be defined by classes that extend Visualization.
*
* @api
*/
const TEMPLATE_FILE = '';
private $templateVars = array();
private $reportLastUpdatedMessage = null;
private $metadata = null;
final public function __construct($controllerAction, $apiMethodToRequestDataTable)
{
$templateFile = static::TEMPLATE_FILE;
if (empty($templateFile)) {
throw new \Exception('You have not defined a constant named TEMPLATE_FILE in your visualization class.');
}
parent::__construct($controllerAction, $apiMethodToRequestDataTable);
}
protected function buildView()
{
$this->overrideSomeConfigPropertiesIfNeeded();
try {
$this->beforeLoadDataTable();
$this->loadDataTableFromAPI(array('disable_generic_filters' => 1));
$this->postDataTableLoadedFromAPI();
$requestPropertiesAfterLoadDataTable = $this->requestConfig->getProperties();
$this->applyFilters();
$this->afterAllFiltersAreApplied();
$this->beforeRender();
$this->logMessageIfRequestPropertiesHaveChanged($requestPropertiesAfterLoadDataTable);
} catch (NoAccessException $e) {
throw $e;
} catch (\Exception $e) {
Log::warning("Failed to get data from API: " . $e->getMessage() . "\n" . $e->getTraceAsString());
$loadingError = array('message' => $e->getMessage());
}
$view = new View("@CoreHome/_dataTable");
if (!empty($loadingError)) {
$view->error = $loadingError;
}
$view->assign($this->templateVars);
$view->visualization = $this;
$view->visualizationTemplate = static::TEMPLATE_FILE;
$view->visualizationCssClass = $this->getDefaultDataTableCssClass();
if (null === $this->dataTable) {
$view->dataTable = null;
} else {
$view->dataTableHasNoData = !$this->isThereDataToDisplay();
$view->dataTable = $this->dataTable;
// if it's likely that the report data for this data table has been purged,
// set whether we should display a message to that effect.
$view->showReportDataWasPurgedMessage = $this->hasReportBeenPurged();
$view->deleteReportsOlderThan = Option::get('delete_reports_older_than');
}
$view->idSubtable = $this->requestConfig->idSubtable;
$view->clientSideParameters = $this->getClientSideParametersToSet();
$view->clientSideProperties = $this->getClientSidePropertiesToSet();
$view->properties = array_merge($this->requestConfig->getProperties(), $this->config->getProperties());
$view->reportLastUpdatedMessage = $this->reportLastUpdatedMessage;
$view->footerIcons = $this->config->footer_icons;
$view->isWidget = Common::getRequestVar('widget', 0, 'int');
return $view;
}
private function overrideSomeConfigPropertiesIfNeeded()
{
if (empty($this->config->footer_icons)) {
$this->config->footer_icons = ViewDataTableManager::configureFooterIcons($this);
}
if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated('Goals')) {
$this->config->show_goals = false;
}
}
/**
* Assigns a template variable making it available in the Twig template specified by
* {@link TEMPLATE_FILE}.
*
* @param array|string $vars One or more variable names to set.
* @param mixed $value The value to set each variable to.
* @api
*/
public function assignTemplateVar($vars, $value = null)
{
if (is_string($vars)) {
$this->templateVars[$vars] = $value;
} elseif (is_array($vars)) {
foreach ($vars as $key => $value) {
$this->templateVars[$key] = $value;
}
}
}
/**
* Returns `true` if there is data to display, `false` if otherwise.
*
* Derived classes should override this method if they change the amount of data that is loaded.
*
* @api
*/
protected function isThereDataToDisplay()
{
return !empty($this->dataTable) && 0 < $this->dataTable->getRowsCount();
}
/**
* Hook called after the dataTable has been loaded from the API
* Can be used to add, delete or modify the data freshly loaded
*
* @return bool
*/
private function postDataTableLoadedFromAPI()
{
$columns = $this->dataTable->getColumns();
$hasNbVisits = in_array('nb_visits', $columns);
$hasNbUniqVisitors = in_array('nb_uniq_visitors', $columns);
// default columns_to_display to label, nb_uniq_visitors/nb_visits if those columns exist in the
// dataset. otherwise, default to all columns in dataset.
if (empty($this->config->columns_to_display)) {
$this->config->setDefaultColumnsToDisplay($columns, $hasNbVisits, $hasNbUniqVisitors);
}
if (!empty($this->dataTable)) {
$this->removeEmptyColumnsFromDisplay();
}
if (empty($this->requestConfig->filter_sort_column)) {
$this->requestConfig->setDefaultSort($this->config->columns_to_display, $hasNbUniqVisitors);
}
// deal w/ table metadata
if ($this->dataTable instanceof DataTable) {
$this->metadata = $this->dataTable->getAllTableMetadata();
if (isset($this->metadata[DataTable::ARCHIVED_DATE_METADATA_NAME])) {
$this->config->report_last_updated_message = $this->makePrettyArchivedOnText();
}
}
}
private function applyFilters()
{
list($priorityFilters, $otherFilters) = $this->config->getFiltersToRun();
// First, filters that delete rows
foreach ($priorityFilters as $filter) {
$this->dataTable->filter($filter[0], $filter[1]);
}
$this->beforeGenericFiltersAreAppliedToLoadedDataTable();
if (!$this->requestConfig->areGenericFiltersDisabled()) {
$this->applyGenericFilters();
}
$this->afterGenericFiltersAreAppliedToLoadedDataTable();
// queue other filters so they can be applied later if queued filters are disabled
foreach ($otherFilters as $filter) {
$this->dataTable->queueFilter($filter[0], $filter[1]);
}
// Finally, apply datatable filters that were queued (should be 'presentation' filters that
// do not affect the number of rows)
if (!$this->requestConfig->areQueuedFiltersDisabled()) {
$this->dataTable->applyQueuedFilters();
}
}
private function removeEmptyColumnsFromDisplay()
{
if ($this->dataTable instanceof DataTable\Map) {
$emptyColumns = $this->dataTable->getMetadataIntersectArray(DataTable::EMPTY_COLUMNS_METADATA_NAME);
} else {
$emptyColumns = $this->dataTable->getMetadata(DataTable::EMPTY_COLUMNS_METADATA_NAME);
}
if (is_array($emptyColumns)) {
foreach ($emptyColumns as $emptyColumn) {
$key = array_search($emptyColumn, $this->config->columns_to_display);
if ($key !== false) {
unset($this->config->columns_to_display[$key]);
}
}
$this->config->columns_to_display = array_values($this->config->columns_to_display);
}
}
/**
* Returns prettified and translated text that describes when a report was last updated.
*
* @return string
*/
private function makePrettyArchivedOnText()
{
$dateText = $this->metadata[DataTable::ARCHIVED_DATE_METADATA_NAME];
$date = Date::factory($dateText);
$today = mktime(0, 0, 0);
if ($date->getTimestamp() > $today) {
$elapsedSeconds = time() - $date->getTimestamp();
$timeAgo = MetricsFormatter::getPrettyTimeFromSeconds($elapsedSeconds);
return Piwik::translate('CoreHome_ReportGeneratedXAgo', $timeAgo);
}
$prettyDate = $date->getLocalized("%longYear%, %longMonth% %day%") . $date->toString('S');
return Piwik::translate('CoreHome_ReportGeneratedOn', $prettyDate);
}
/**
* Returns true if it is likely that the data for this report has been purged and if the
* user should be told about that.
*
* In order for this function to return true, the following must also be true:
* - The data table for this report must either be empty or not have been fetched.
* - The period of this report is not a multiple period.
* - The date of this report must be older than the delete_reports_older_than config option.
* @return bool
*/
private function hasReportBeenPurged()
{
if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated('PrivacyManager')) {
return false;
}
return PrivacyManager::hasReportBeenPurged($this->dataTable);
}
/**
* Returns array of properties that should be visible to client side JavaScript. The data
* will be available in the data-props HTML attribute of the .dataTable div.
*
* @return array Maps property names w/ property values.
*/
private function getClientSidePropertiesToSet()
{
$result = array();
foreach ($this->config->clientSideProperties as $name) {
if (property_exists($this->requestConfig, $name)) {
$result[$name] = $this->getIntIfValueIsBool($this->requestConfig->$name);
} else if (property_exists($this->config, $name)) {
$result[$name] = $this->getIntIfValueIsBool($this->config->$name);
}
}
return $result;
}
private function getIntIfValueIsBool($value)
{
return is_bool($value) ? (int)$value : $value;
}
/**
* This functions reads the customization values for the DataTable and returns an array (name,value) to be printed in Javascript.
* This array defines things such as:
* - name of the module & action to call to request data for this table
* - optional filters information, eg. filter_limit and filter_offset
* - etc.
*
* The values are loaded:
* - from the generic filters that are applied by default @see Piwik\API\DataTableGenericFilter::getGenericFiltersInformation()
* - from the values already available in the GET array
* - from the values set using methods from this class (eg. setSearchPattern(), setLimit(), etc.)
*
* @return array eg. array('show_offset_information' => 0, 'show_...
*/
protected function getClientSideParametersToSet()
{
// build javascript variables to set
$javascriptVariablesToSet = array();
foreach ($this->config->custom_parameters as $name => $value) {
$javascriptVariablesToSet[$name] = $value;
}
foreach ($_GET as $name => $value) {
try {
$requestValue = Common::getRequestVar($name);
} catch (\Exception $e) {
$requestValue = '';
}
$javascriptVariablesToSet[$name] = $requestValue;
}
foreach ($this->requestConfig->clientSideParameters as $name) {
if (isset($javascriptVariablesToSet[$name])) {
continue;
}
$valueToConvert = false;
if (property_exists($this->requestConfig, $name)) {
$valueToConvert = $this->requestConfig->$name;
} else if (property_exists($this->config, $name)) {
$valueToConvert = $this->config->$name;
}
if (false !== $valueToConvert) {
$javascriptVariablesToSet[$name] = $this->getIntIfValueIsBool($valueToConvert);
}
}
$javascriptVariablesToSet['module'] = $this->config->controllerName;
$javascriptVariablesToSet['action'] = $this->config->controllerAction;
if (!isset($javascriptVariablesToSet['viewDataTable'])) {
$javascriptVariablesToSet['viewDataTable'] = static::getViewDataTableId();
}
if ($this->dataTable &&
// Set doesn't have the method
!($this->dataTable instanceof DataTable\Map)
&& empty($javascriptVariablesToSet['totalRows'])
) {
$javascriptVariablesToSet['totalRows'] =
$this->dataTable->getMetadata(DataTable::TOTAL_ROWS_BEFORE_LIMIT_METADATA_NAME) ?: $this->dataTable->getRowsCount();
}
$deleteFromJavascriptVariables = array(
'filter_excludelowpop',
'filter_excludelowpop_value',
);
foreach ($deleteFromJavascriptVariables as $name) {
if (isset($javascriptVariablesToSet[$name])) {
unset($javascriptVariablesToSet[$name]);
}
}
$rawSegment = \Piwik\API\Request::getRawSegmentFromRequest();
if (!empty($rawSegment)) {
$javascriptVariablesToSet['segment'] = $rawSegment;
}
return $javascriptVariablesToSet;
}
/**
* Hook that is called before loading report data from the API.
*
* Use this method to change the request parameters that is sent to the API when requesting
* data.
*/
public function beforeLoadDataTable()
{
}
/**
* Hook that is executed before generic filters are applied.
*
* Use this method if you need access to the entire dataset (since generic filters will
* limit and truncate reports).
*/
public function beforeGenericFiltersAreAppliedToLoadedDataTable()
{
}
/**
* Hook that is executed after generic filters are applied.
*/
public function afterGenericFiltersAreAppliedToLoadedDataTable()
{
}
/**
* Hook that is executed after the report data is loaded and after all filters have been applied.
* Use this method to format the report data before the view is rendered.
*/
public function afterAllFiltersAreApplied()
{
}
/**
* Hook that is executed directly before rendering. Use this hook to force display properties to
* be a certain value, despite changes from plugins and query parameters.
*/
public function beforeRender()
{
// eg $this->config->showFooterColumns = true;
}
/**
* Second, generic filters (Sort, Limit, Replace Column Names, etc.)
*/
private function applyGenericFilters()
{
$requestArray = $this->request->getRequestArray();
$request = \Piwik\API\Request::getRequestArrayFromString($requestArray);
if (false === $this->config->enable_sort) {
$request['filter_sort_column'] = '';
$request['filter_sort_order'] = '';
}
$genericFilter = new \Piwik\API\DataTableGenericFilter($request);
$genericFilter->filter($this->dataTable);
}
private function logMessageIfRequestPropertiesHaveChanged(array $requestPropertiesBefore)
{
$requestProperties = $this->requestConfig->getProperties();
$diff = array_diff_assoc($this->makeSureArrayContainsOnlyStrings($requestProperties),
$this->makeSureArrayContainsOnlyStrings($requestPropertiesBefore));
if (empty($diff)) {
return;
}
$details = array(
'changedProperties' => $diff,
'apiMethod' => $this->requestConfig->apiMethodToRequestDataTable,
'controller' => $this->config->controllerName . '.' . $this->config->controllerAction,
'viewDataTable' => static::getViewDataTableId()
);
$message = 'Some ViewDataTable::requestConfig properties have changed after requesting the data table. '
. 'That means the changed values had probably no effect. For instance in beforeRender() hook. '
. 'Probably a bug? Details:'
. print_r($details, 1);
Log::warning($message);
}
private function makeSureArrayContainsOnlyStrings($array)
{
$result = array();
foreach ($array as $key => $value) {
$result[$key] = json_encode($value);
}
return $result;
}
}