add icons for Character groups

This commit is contained in:
coderkun 2014-04-29 14:18:04 +02:00
commit 2d9a41a5fe
3461 changed files with 594457 additions and 0 deletions

View file

@ -0,0 +1,33 @@
<Files ~ "\.(php|php4|php5|inc|tpl|in|twig)$">
<IfModule mod_access.c>
Deny from all
Require all denied
</IfModule>
<IfModule !mod_access_compat>
<IfModule mod_authz_host.c>
Deny from all
Require all denied
</IfModule>
</IfModule>
<IfModule mod_access_compat>
Deny from all
Require all denied
</IfModule>
</Files>
<Files ~ "\.(test\.php|gif|ico|jpg|png|svg|js|css|swf)$">
<IfModule mod_access.c>
Allow from all
Require all granted
</IfModule>
<IfModule !mod_access_compat>
<IfModule mod_authz_host.c>
Allow from all
Require all granted
</IfModule>
</IfModule>
<IfModule mod_access_compat>
Allow from all
Require all granted
</IfModule>
Satisfy any
</Files>

View file

@ -0,0 +1,723 @@
<?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\Plugins\API;
use Piwik\API\Proxy;
use Piwik\API\Request;
use Piwik\Config;
use Piwik\DataTable\Filter\ColumnDelete;
use Piwik\DataTable\Row;
use Piwik\DataTable;
use Piwik\Date;
use Piwik\Menu\MenuTop;
use Piwik\Metrics;
use Piwik\Period\Range;
use Piwik\Piwik;
use Piwik\Plugins\CoreAdminHome\CustomLogo;
use Piwik\Tracker\GoalManager;
use Piwik\Translate;
use Piwik\Version;
require_once PIWIK_INCLUDE_PATH . '/core/Config.php';
/**
* This API is the <a href='http://piwik.org/docs/analytics-api/metadata/' target='_blank'>Metadata API</a>: it gives information about all other available APIs methods, as well as providing
* human readable and more complete outputs than normal API methods.
*
* Some of the information that is returned by the Metadata API:
* <ul>
* <li>the dynamically generated list of all API methods via "getReportMetadata"</li>
* <li>the list of metrics that will be returned by each method, along with their human readable name, via "getDefaultMetrics" and "getDefaultProcessedMetrics"</li>
* <li>the list of segments metadata supported by all functions that have a 'segment' parameter</li>
* <li>the (truly magic) method "getProcessedReport" will return a human readable version of any other report, and include the processed metrics such as
* conversion rate, time on site, etc. which are not directly available in other methods.</li>
* <li>the method "getSuggestedValuesForSegment" returns top suggested values for a particular segment. It uses the Live.getLastVisitsDetails API to fetch the most recently used values, and will return the most often used values first.</li>
* </ul>
* The Metadata API is for example used by the Piwik Mobile App to automatically display all Piwik reports, with translated report & columns names and nicely formatted values.
* More information on the <a href='http://piwik.org/docs/analytics-api/metadata/' target='_blank'>Metadata API documentation page</a>
*
* @method static \Piwik\Plugins\API\API getInstance()
*/
class API extends \Piwik\Plugin\API
{
/**
* Get Piwik version
* @return string
*/
public function getPiwikVersion()
{
Piwik::checkUserHasSomeViewAccess();
return Version::VERSION;
}
/**
* Returns the section [APISettings] if defined in config.ini.php
* @return array
*/
public function getSettings()
{
return Config::getInstance()->APISettings;
}
/**
* Default translations for many core metrics.
* This is used for exports with translated labels. The exports contain columns that
* are not visible in the UI and not present in the API meta data. These columns are
* translated here.
* @return array
*/
static public function getDefaultMetricTranslations()
{
return Metrics::getDefaultMetricTranslations();
}
public function getSegmentsMetadata($idSites = array(), $_hideImplementationData = true)
{
$segments = array();
/**
* Triggered when gathering all available segment dimensions.
*
* This event can be used to make new segment dimensions available.
*
* **Example**
*
* public function getSegmentsMetadata(&$segments, $idSites)
* {
* $segments[] = array(
* 'type' => 'dimension',
* 'category' => Piwik::translate('General_Visit'),
* 'name' => 'General_VisitorIP',
* 'segment' => 'visitIp',
* 'acceptedValues' => '13.54.122.1, etc.',
* 'sqlSegment' => 'log_visit.location_ip',
* 'sqlFilter' => array('Piwik\IP', 'P2N'),
* 'permission' => $isAuthenticatedWithViewAccess,
* );
* }
*
* @param array &$dimensions The list of available segment dimensions. Append to this list to add
* new segments. Each element in this list must contain the
* following information:
*
* - **type**: Either `'metric'` or `'dimension'`. `'metric'` means
* the value is a numeric and `'dimension'` means it is
* a string. Also, `'metric'` values will be displayed
* under **Visit (metrics)** in the Segment Editor.
* - **category**: The segment category name. This can be an existing
* segment category visible in the segment editor.
* - **name**: The pretty name of the segment. Can be a translation token.
* - **segment**: The segment name, eg, `'visitIp'` or `'searches'`.
* - **acceptedValues**: A string describing one or two exacmple values, eg
* `'13.54.122.1, etc.'`.
* - **sqlSegment**: The table column this segment will segment by.
* For example, `'log_visit.location_ip'` for the
* **visitIp** segment.
* - **sqlFilter**: A PHP callback to apply to segment values before
* they are used in SQL.
* - **permission**: True if the current user has view access to this
* segment, false if otherwise.
* @param array $idSites The list of site IDs we're getting the available segments
* for. Some segments (such as Goal segments) depend on the
* site.
*/
Piwik::postEvent('API.getSegmentDimensionMetadata', array(&$segments, $idSites));
$isAuthenticatedWithViewAccess = Piwik::isUserHasViewAccess($idSites) && !Piwik::isUserIsAnonymous();
$segments[] = array(
'type' => 'dimension',
'category' => Piwik::translate('General_Visit'),
'name' => 'General_VisitorID',
'segment' => 'visitorId',
'acceptedValues' => '34c31e04394bdc63 - any 16 Hexadecimal chars ID, which can be fetched using the Tracking API function getVisitorId()',
'sqlSegment' => 'log_visit.idvisitor',
'sqlFilterValue' => array('Piwik\Common', 'convertVisitorIdToBin'),
'permission' => $isAuthenticatedWithViewAccess,
);
$segments[] = array(
'type' => 'dimension',
'category' => Piwik::translate('General_Visit'),
'name' => Piwik::translate('General_Visit') . " ID",
'segment' => 'visitId',
'acceptedValues' => 'Any integer. ',
'sqlSegment' => 'log_visit.idvisit',
'permission' => $isAuthenticatedWithViewAccess,
);
$segments[] = array(
'type' => 'metric',
'category' => Piwik::translate('General_Visit'),
'name' => 'General_VisitorIP',
'segment' => 'visitIp',
'acceptedValues' => '13.54.122.1. </code>Select IP ranges with notation: <code>visitIp>13.54.122.0;visitIp<13.54.122.255',
'sqlSegment' => 'log_visit.location_ip',
'sqlFilterValue' => array('Piwik\IP', 'P2N'),
'permission' => $isAuthenticatedWithViewAccess,
);
$segments[] = array(
'type' => 'metric',
'category' => Piwik::translate('General_Visit'),
'name' => 'General_NbActions',
'segment' => 'actions',
'sqlSegment' => 'log_visit.visit_total_actions',
);
$segments[] = array(
'type' => 'metric',
'category' => Piwik::translate('General_Visit'),
'name' => 'General_NbSearches',
'segment' => 'searches',
'sqlSegment' => 'log_visit.visit_total_searches',
'acceptedValues' => 'To select all visits who used internal Site Search, use: &segment=searches>0',
);
$segments[] = array(
'type' => 'metric',
'category' => Piwik::translate('General_Visit'),
'name' => 'General_ColumnVisitDuration',
'segment' => 'visitDuration',
'sqlSegment' => 'log_visit.visit_total_time',
);
$segments[] = array(
'type' => 'dimension',
'category' => Piwik::translate('General_Visit'),
'name' => Piwik::translate('General_VisitType'),
'segment' => 'visitorType',
'acceptedValues' => 'new, returning, returningCustomer' . ". " . Piwik::translate('General_VisitTypeExample', '"&segment=visitorType==returning,visitorType==returningCustomer"'),
'sqlSegment' => 'log_visit.visitor_returning',
'sqlFilterValue' => function ($type) {
return $type == "new" ? 0 : ($type == "returning" ? 1 : 2);
}
);
$segments[] = array(
'type' => 'metric',
'category' => Piwik::translate('General_Visit'),
'name' => 'General_DaysSinceLastVisit',
'segment' => 'daysSinceLastVisit',
'sqlSegment' => 'log_visit.visitor_days_since_last',
);
$segments[] = array(
'type' => 'metric',
'category' => Piwik::translate('General_Visit'),
'name' => 'General_DaysSinceFirstVisit',
'segment' => 'daysSinceFirstVisit',
'sqlSegment' => 'log_visit.visitor_days_since_first',
);
$segments[] = array(
'type' => 'metric',
'category' => Piwik::translate('General_Visit'),
'name' => 'General_NumberOfVisits',
'segment' => 'visitCount',
'sqlSegment' => 'log_visit.visitor_count_visits',
);
$segments[] = array(
'type' => 'dimension',
'category' => Piwik::translate('General_Visit'),
'name' => 'General_VisitConvertedGoal',
'segment' => 'visitConverted',
'acceptedValues' => '0, 1',
'sqlSegment' => 'log_visit.visit_goal_converted',
);
$segments[] = array(
'type' => 'dimension',
'category' => Piwik::translate('General_Visit'),
'name' => Piwik::translate('General_EcommerceVisitStatusDesc'),
'segment' => 'visitEcommerceStatus',
'acceptedValues' => implode(", ", self::$visitEcommerceStatus)
. '. ' . Piwik::translate('General_EcommerceVisitStatusEg', '"&segment=visitEcommerceStatus==ordered,visitEcommerceStatus==orderedThenAbandonedCart"'),
'sqlSegment' => 'log_visit.visit_goal_buyer',
'sqlFilterValue' => __NAMESPACE__ . '\API::getVisitEcommerceStatus',
);
$segments[] = array(
'type' => 'metric',
'category' => Piwik::translate('General_Visit'),
'name' => 'General_DaysSinceLastEcommerceOrder',
'segment' => 'daysSinceLastEcommerceOrder',
'sqlSegment' => 'log_visit.visitor_days_since_order',
);
foreach ($segments as &$segment) {
$segment['name'] = Piwik::translate($segment['name']);
$segment['category'] = Piwik::translate($segment['category']);
if ($_hideImplementationData) {
unset($segment['sqlFilter']);
unset($segment['sqlFilterValue']);
unset($segment['sqlSegment']);
}
}
usort($segments, array($this, 'sortSegments'));
return $segments;
}
static protected $visitEcommerceStatus = array(
GoalManager::TYPE_BUYER_NONE => 'none',
GoalManager::TYPE_BUYER_ORDERED => 'ordered',
GoalManager::TYPE_BUYER_OPEN_CART => 'abandonedCart',
GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART => 'orderedThenAbandonedCart',
);
/**
* @ignore
*/
static public function getVisitEcommerceStatusFromId($id)
{
if (!isset(self::$visitEcommerceStatus[$id])) {
throw new \Exception("Unexpected ECommerce status value ");
}
return self::$visitEcommerceStatus[$id];
}
/**
* @ignore
*/
static public function getVisitEcommerceStatus($status)
{
$id = array_search($status, self::$visitEcommerceStatus);
if ($id === false) {
throw new \Exception("Invalid 'visitEcommerceStatus' segment value $status");
}
return $id;
}
private function sortSegments($row1, $row2)
{
$columns = array('type', 'category', 'name', 'segment');
foreach ($columns as $column) {
// Keep segments ordered alphabetically inside categories..
$type = -1;
if ($column == 'name') $type = 1;
$compare = $type * strcmp($row1[$column], $row2[$column]);
// hack so that custom variables "page" are grouped together in the doc
if ($row1['category'] == Piwik::translate('CustomVariables_CustomVariables')
&& $row1['category'] == $row2['category']
) {
$compare = strcmp($row1['segment'], $row2['segment']);
return $compare;
}
if ($compare != 0) {
return $compare;
}
}
return $compare;
}
/**
* Returns the url to application logo (~280x110px)
*
* @param bool $pathOnly If true, returns path relative to doc root. Otherwise, returns a URL.
* @return string
*/
public function getLogoUrl($pathOnly = false)
{
$logo = new CustomLogo();
return $logo->getLogoUrl($pathOnly);
}
/**
* Returns the url to header logo (~127x50px)
*
* @param bool $pathOnly If true, returns path relative to doc root. Otherwise, returns a URL.
* @return string
*/
public function getHeaderLogoUrl($pathOnly = false)
{
$logo = new CustomLogo();
return $logo->getHeaderLogoUrl($pathOnly);
}
/**
* Returns the URL to application SVG Logo
*
* @ignore
* @param bool $pathOnly If true, returns path relative to doc root. Otherwise, returns a URL.
* @return string
*/
public function getSVGLogoUrl($pathOnly = false)
{
$logo = new CustomLogo();
return $logo->getSVGLogoUrl($pathOnly);
}
/**
* Returns whether there is an SVG Logo available.
* @ignore
* @return bool
*/
public function hasSVGLogo()
{
$logo = new CustomLogo();
return $logo->hasSVGLogo();
}
/**
* Loads reports metadata, then return the requested one,
* matching optional API parameters.
*/
public function getMetadata($idSite, $apiModule, $apiAction, $apiParameters = array(), $language = false,
$period = false, $date = false, $hideMetricsDoc = false, $showSubtableReports = false)
{
Translate::reloadLanguage($language);
$reporter = new ProcessedReport();
$metadata = $reporter->getMetadata($idSite, $apiModule, $apiAction, $apiParameters, $language, $period, $date, $hideMetricsDoc, $showSubtableReports);
return $metadata;
}
/**
* Triggers a hook to ask plugins for available Reports.
* Returns metadata information about each report (category, name, dimension, metrics, etc.)
*
* @param string $idSites Comma separated list of website Ids
* @param bool|string $period
* @param bool|Date $date
* @param bool $hideMetricsDoc
* @param bool $showSubtableReports
* @return array
*/
public function getReportMetadata($idSites = '', $period = false, $date = false, $hideMetricsDoc = false,
$showSubtableReports = false)
{
$reporter = new ProcessedReport();
$metadata = $reporter->getReportMetadata($idSites, $period, $date, $hideMetricsDoc, $showSubtableReports);
return $metadata;
}
public function getProcessedReport($idSite, $period, $date, $apiModule, $apiAction, $segment = false,
$apiParameters = false, $idGoal = false, $language = false,
$showTimer = true, $hideMetricsDoc = false, $idSubtable = false, $showRawMetrics = false)
{
$reporter = new ProcessedReport();
$processed = $reporter->getProcessedReport($idSite, $period, $date, $apiModule, $apiAction, $segment,
$apiParameters, $idGoal, $language, $showTimer, $hideMetricsDoc, $idSubtable, $showRawMetrics);
return $processed;
}
/**
* Get a combined report of the *.get API methods.
*/
public function get($idSite, $period, $date, $segment = false, $columns = false)
{
$columns = Piwik::getArrayFromApiParameter($columns);
// build columns map for faster checks later on
$columnsMap = array();
foreach ($columns as $column) {
$columnsMap[$column] = true;
}
// find out which columns belong to which plugin
$columnsByPlugin = array();
$meta = \Piwik\Plugins\API\API::getInstance()->getReportMetadata($idSite, $period, $date);
foreach ($meta as $reportMeta) {
// scan all *.get reports
if ($reportMeta['action'] == 'get'
&& !isset($reportMeta['parameters'])
&& $reportMeta['module'] != 'API'
&& !empty($reportMeta['metrics'])
) {
$plugin = $reportMeta['module'];
foreach ($reportMeta['metrics'] as $column => $columnTranslation) {
// a metric from this report has been requested
if (isset($columnsMap[$column])
// or by default, return all metrics
|| empty($columnsMap)
) {
$columnsByPlugin[$plugin][] = $column;
}
}
}
}
krsort($columnsByPlugin);
$mergedDataTable = false;
$params = compact('idSite', 'period', 'date', 'segment', 'idGoal');
foreach ($columnsByPlugin as $plugin => $columns) {
// load the data
$className = Request::getClassNameAPI($plugin);
$params['columns'] = implode(',', $columns);
$dataTable = Proxy::getInstance()->call($className, 'get', $params);
// make sure the table has all columns
$array = ($dataTable instanceof DataTable\Map ? $dataTable->getDataTables() : array($dataTable));
foreach ($array as $table) {
// we don't support idSites=all&date=DATE1,DATE2
if ($table instanceof DataTable) {
$firstRow = $table->getFirstRow();
if (!$firstRow) {
$firstRow = new Row;
$table->addRow($firstRow);
}
foreach ($columns as $column) {
if ($firstRow->getColumn($column) === false) {
$firstRow->setColumn($column, 0);
}
}
}
}
// merge reports
if ($mergedDataTable === false) {
$mergedDataTable = $dataTable;
} else {
$this->mergeDataTables($mergedDataTable, $dataTable);
}
}
return $mergedDataTable;
}
/**
* Merge the columns of two data tables.
* Manipulates the first table.
*/
private function mergeDataTables($table1, $table2)
{
// handle table arrays
if ($table1 instanceof DataTable\Map && $table2 instanceof DataTable\Map) {
$subTables2 = $table2->getDataTables();
foreach ($table1->getDataTables() as $index => $subTable1) {
$subTable2 = $subTables2[$index];
$this->mergeDataTables($subTable1, $subTable2);
}
return;
}
$firstRow1 = $table1->getFirstRow();
$firstRow2 = $table2->getFirstRow();
if ($firstRow2 instanceof Row) {
foreach ($firstRow2->getColumns() as $metric => $value) {
$firstRow1->setColumn($metric, $value);
}
}
}
/**
* Given an API report to query (eg. "Referrers.getKeywords", and a Label (eg. "free%20software"),
* this function will query the API for the previous days/weeks/etc. and will return
* a ready to use data structure containing the metrics for the requested Label, along with enriched information (min/max values, etc.)
*
* @param int $idSite
* @param string $period
* @param Date $date
* @param string $apiModule
* @param string $apiAction
* @param bool|string $label
* @param bool|string $segment
* @param bool|string $column
* @param bool|string $language
* @param bool|int $idGoal
* @param bool|string $legendAppendMetric
* @param bool|string $labelUseAbsoluteUrl
* @return array
*/
public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true)
{
$rowEvolution = new RowEvolution();
return $rowEvolution->getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label, $segment, $column,
$language, $idGoal, $legendAppendMetric, $labelUseAbsoluteUrl);
}
public function getLastDate($date, $period)
{
$lastDate = Range::getLastDate($date, $period);
return array_shift($lastDate);
}
/**
* Performs multiple API requests at once and returns every result.
*
* @param array $urls The array of API requests.
* @return array
*/
public function getBulkRequest($urls)
{
if (empty($urls)) {
return array();
}
$urls = array_map('urldecode', $urls);
$urls = array_map(array('Piwik\Common', 'unsanitizeInputValue'), $urls);
$result = array();
foreach ($urls as $url) {
$req = new Request($url . '&format=php&serialize=0');
$result[] = $req->process();
}
return $result;
}
/**
* Given a segment, will return a list of the most used values for this particular segment.
* @param $segmentName
* @param $idSite
* @throws \Exception
* @return array
*/
public function getSuggestedValuesForSegment($segmentName, $idSite)
{
Piwik::checkUserHasViewAccess($idSite);
$maxSuggestionsToReturn = 30;
$segmentsMetadata = $this->getSegmentsMetadata($idSite, $_hideImplementationData = false);
$segmentFound = false;
foreach ($segmentsMetadata as $segmentMetadata) {
if ($segmentMetadata['segment'] == $segmentName) {
$segmentFound = $segmentMetadata;
break;
}
}
if (empty($segmentFound)) {
throw new \Exception("Requested segment not found.");
}
$startDate = Date::now()->subDay(60)->toString();
$requestLastVisits = "method=Live.getLastVisitsDetails
&idSite=$idSite
&period=range
&date=$startDate,today
&format=original
&serialize=0
&flat=1";
// Select non empty fields only
// Note: this optimization has only a very minor impact
$requestLastVisits .= "&segment=$segmentName" . urlencode('!=');
// By default Live fetches all actions for all visitors, but we'd rather do this only when required
if ($this->doesSegmentNeedActionsData($segmentName)) {
$requestLastVisits .= "&filter_limit=500";
} else {
$requestLastVisits .= "&doNotFetchActions=1";
$requestLastVisits .= "&filter_limit=1000";
}
$request = new Request($requestLastVisits);
$table = $request->process();
if (empty($table)) {
throw new \Exception("There was no data to suggest for $segmentName");
}
// Cleanup data to return the top suggested (non empty) labels for this segment
$values = $table->getColumn($segmentName);
// Select also flattened keys (custom variables "page" scope, page URLs for one visit, page titles for one visit)
$valuesBis = $table->getColumnsStartingWith($segmentName . ColumnDelete::APPEND_TO_COLUMN_NAME_TO_KEEP);
$values = array_merge($values, $valuesBis);
$values = $this->getMostFrequentValues($values);
$values = array_slice($values, 0, $maxSuggestionsToReturn);
$values = array_map(array('Piwik\Common', 'unsanitizeInputValue'), $values);
return $values;
}
/**
* @param $segmentName
* @return bool
*/
protected function doesSegmentNeedActionsData($segmentName)
{
// If you update this, also update flattenVisitorDetailsArray
$segmentsNeedActionsInfo = array('visitConvertedGoalId',
'pageUrl', 'pageTitle', 'siteSearchKeyword',
'entryPageTitle', 'entryPageUrl', 'exitPageTitle', 'exitPageUrl');
$isCustomVariablePage = stripos($segmentName, 'customVariablePage') !== false;
$isEventSegment = stripos($segmentName, 'event') !== false;
$doesSegmentNeedActionsInfo = in_array($segmentName, $segmentsNeedActionsInfo) || $isCustomVariablePage || $isEventSegment;
return $doesSegmentNeedActionsInfo;
}
/**
* @param $values
* @param $value
* @return array
*/
private function getMostFrequentValues($values)
{
// remove false values (while keeping zeros)
$values = array_filter($values, 'strlen');
// array_count_values requires strings or integer, convert floats to string (mysqli)
foreach ($values as &$value) {
if (is_numeric($value)) {
$value = (string)round($value, 3);
}
}
// we have a list of all values. let's show the most frequently used first.
$values = array_count_values($values);
arsort($values);
$values = array_keys($values);
return $values;
}
}
/**
*/
class Plugin extends \Piwik\Plugin
{
public function __construct()
{
// this class is named 'Plugin', manually set the 'API' plugin
parent::__construct($pluginName = 'API');
}
/**
* @see Piwik\Plugin::getListHooksRegistered
*/
public function getListHooksRegistered()
{
return array(
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
'Menu.Top.addItems' => 'addTopMenu',
);
}
public function addTopMenu()
{
$apiUrlParams = array('module' => 'API', 'action' => 'listAllAPI', 'segment' => false);
$tooltip = Piwik::translate('API_TopLinkTooltip');
MenuTop::addEntry('General_API', $apiUrlParams, true, 7, $isHTML = false, $tooltip);
$this->addTopMenuMobileApp();
}
protected function addTopMenuMobileApp()
{
if (empty($_SERVER['HTTP_USER_AGENT'])) {
return;
}
if (!class_exists("DeviceDetector")) {
throw new \Exception("DeviceDetector could not be found, maybe you are using Piwik from git and need to have update Composer. <br>php composer.phar update");
}
$ua = new \DeviceDetector($_SERVER['HTTP_USER_AGENT']);
$ua->parse();
$os = $ua->getOs('short_name');
if ($os && in_array($os, array('AND', 'IOS'))) {
MenuTop::addEntry('Piwik Mobile App', array('module' => 'Proxy', 'action' => 'redirect', 'url' => 'http://piwik.org/mobile/'), true, 4);
}
}
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = "plugins/API/stylesheets/listAllAPI.less";
}
}

View file

@ -0,0 +1,128 @@
<?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\Plugins\API;
use Piwik\API\DocumentationGenerator;
use Piwik\API\Proxy;
use Piwik\API\Request;
use Piwik\Common;
use Piwik\Config;
use Piwik\Piwik;
use Piwik\View;
/**
*
*/
class Controller extends \Piwik\Plugin\Controller
{
function index()
{
// when calling the API through http, we limit the number of returned results
if (!isset($_GET['filter_limit'])) {
$_GET['filter_limit'] = Config::getInstance()->General['API_datatable_default_limit'];
}
$request = new Request('token_auth=' . Common::getRequestVar('token_auth', 'anonymous', 'string'));
return $request->process();
}
public function listAllMethods()
{
$ApiDocumentation = new DocumentationGenerator();
return $ApiDocumentation->getAllInterfaceString($outputExampleUrls = true, $prefixUrls = Common::getRequestVar('prefixUrl', ''));
}
public function listAllAPI()
{
$view = new View("@API/listAllAPI");
$this->setGeneralVariablesView($view);
$ApiDocumentation = new DocumentationGenerator();
$view->countLoadedAPI = Proxy::getInstance()->getCountRegisteredClasses();
$view->list_api_methods_with_links = $ApiDocumentation->getAllInterfaceString();
return $view->render();
}
public function listSegments()
{
$segments = API::getInstance()->getSegmentsMetadata($this->idSite);
$tableDimensions = $tableMetrics = '';
$customVariables = 0;
$lastCategory = array();
foreach ($segments as $segment) {
// Eg. Event Value is a metric, not in the Visit metric category,
// we make sure it is displayed along with the Events dimensions
if($segment['type'] == 'metric' && $segment['category'] != Piwik::translate('General_Visit')) {
$segment['type'] = 'dimension';
}
$onlyDisplay = array('customVariableName1', 'customVariableName2',
'customVariableValue1', 'customVariableValue2',
'customVariablePageName1', 'customVariablePageValue1');
$customVariableWillBeDisplayed = in_array($segment['segment'], $onlyDisplay);
// Don't display more than 4 custom variables name/value rows
if ($segment['category'] == 'Custom Variables'
&& !$customVariableWillBeDisplayed
) {
continue;
}
$thisCategory = $segment['category'];
$output = '';
if (empty($lastCategory[$segment['type']])
|| $lastCategory[$segment['type']] != $thisCategory
) {
$output .= '<tr><td class="segmentCategory" colspan="2"><b>' . $thisCategory . '</b></td></tr>';
}
$lastCategory[$segment['type']] = $thisCategory;
$exampleValues = isset($segment['acceptedValues'])
? 'Example values: <code>' . $segment['acceptedValues'] . '</code>'
: '';
$restrictedToAdmin = isset($segment['permission']) ? '<br/>Note: This segment can only be used by an Admin user' : '';
$output .= '<tr>
<td class="segmentString">' . $segment['segment'] . '</td>
<td class="segmentName">' . $segment['name'] . $restrictedToAdmin . '<br/>' . $exampleValues . ' </td>
</tr>';
// Show only 2 custom variables and display message for rest
if ($customVariableWillBeDisplayed) {
$customVariables++;
if ($customVariables == count($onlyDisplay)) {
$output .= '<tr><td colspan="2"> There are 5 custom variables available, so you can segment across any segment name and value range.
<br/>For example, <code>customVariableName1==Type;customVariableValue1==Customer</code>
<br/>Returns all visitors that have the Custom Variable "Type" set to "Customer".
<br/>Custom Variables of scope "page" can be queried separately. For example, to query the Custom Variable of scope "page",
<br/>stored in index 1, you would use the segment <code>customVariablePageName1==ArticleLanguage;customVariablePageValue1==FR</code>
</td></tr>';
}
}
if ($segment['type'] == 'dimension') {
$tableDimensions .= $output;
} else {
$tableMetrics .= $output;
}
}
return "
<strong>Dimensions</strong>
<table>
$tableDimensions
</table>
<br/>
<strong>Metrics</strong>
<table>
$tableMetrics
</table>
";
}
}

View file

@ -0,0 +1,718 @@
<?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\Plugins\API;
use Exception;
use Piwik\API\Request;
use Piwik\Archive\DataTableFactory;
use Piwik\Common;
use Piwik\DataTable\Row;
use Piwik\DataTable\Simple;
use Piwik\DataTable;
use Piwik\Date;
use Piwik\Metrics;
use Piwik\MetricsFormatter;
use Piwik\Period;
use Piwik\Piwik;
use Piwik\Site;
use Piwik\Timer;
use Piwik\Url;
class ProcessedReport
{
/**
* Loads reports metadata, then return the requested one,
* matching optional API parameters.
*/
public function getMetadata($idSite, $apiModule, $apiAction, $apiParameters = array(), $language = false,
$period = false, $date = false, $hideMetricsDoc = false, $showSubtableReports = false)
{
$reportsMetadata = $this->getReportMetadata($idSite, $period, $date, $hideMetricsDoc, $showSubtableReports);
foreach ($reportsMetadata as $report) {
// See ArchiveProcessor/Aggregator.php - unique visitors are not processed for period != day
if (($period && $period != 'day') && !($apiModule == 'VisitsSummary' && $apiAction == 'get')) {
unset($report['metrics']['nb_uniq_visitors']);
}
if ($report['module'] == $apiModule
&& $report['action'] == $apiAction
) {
// No custom parameters
if (empty($apiParameters)
&& empty($report['parameters'])
) {
return array($report);
}
if (empty($report['parameters'])) {
continue;
}
$diff = array_diff($report['parameters'], $apiParameters);
if (empty($diff)) {
return array($report);
}
}
}
return false;
}
/**
* Verfies whether the given report exists for the given site.
*
* @param int $idSite
* @param string $apiMethodUniqueId For example 'MultiSites_getAll'
*
* @return bool
*/
public function isValidReportForSite($idSite, $apiMethodUniqueId)
{
$report = $this->getReportMetadataByUniqueId($idSite, $apiMethodUniqueId);
return !empty($report);
}
/**
* Verfies whether the given metric belongs to the given report.
*
* @param int $idSite
* @param string $metric For example 'nb_visits'
* @param string $apiMethodUniqueId For example 'MultiSites_getAll'
*
* @return bool
*/
public function isValidMetricForReport($metric, $idSite, $apiMethodUniqueId)
{
$translation = $this->translateMetric($metric, $idSite, $apiMethodUniqueId);
return !empty($translation);
}
public function getReportMetadataByUniqueId($idSite, $apiMethodUniqueId)
{
$metadata = $this->getReportMetadata(array($idSite));
foreach ($metadata as $report) {
if ($report['uniqueId'] == $apiMethodUniqueId) {
return $report;
}
}
}
/**
* Translates the given metric in case the report exists and in case the metric acutally belongs to the report.
*
* @param string $metric For example 'nb_visits'
* @param int $idSite
* @param string $apiMethodUniqueId For example 'MultiSites_getAll'
*
* @return null|string
*/
public function translateMetric($metric, $idSite, $apiMethodUniqueId)
{
$report = $this->getReportMetadataByUniqueId($idSite, $apiMethodUniqueId);
if (empty($report)) {
return;
}
$properties = array('metrics', 'processedMetrics', 'processedMetricsGoal');
foreach ($properties as $prop) {
if (!empty($report[$prop]) && is_array($report[$prop]) && array_key_exists($metric, $report[$prop])) {
return $report[$prop][$metric];
}
}
}
/**
* Triggers a hook to ask plugins for available Reports.
* Returns metadata information about each report (category, name, dimension, metrics, etc.)
*
* @param string $idSites Comma separated list of website Ids
* @param bool|string $period
* @param bool|Date $date
* @param bool $hideMetricsDoc
* @param bool $showSubtableReports
* @return array
*/
public function getReportMetadata($idSites, $period = false, $date = false, $hideMetricsDoc = false, $showSubtableReports = false)
{
$idSites = Site::getIdSitesFromIdSitesString($idSites);
if (!empty($idSites)) {
Piwik::checkUserHasViewAccess($idSites);
}
$parameters = array('idSites' => $idSites, 'period' => $period, 'date' => $date);
$availableReports = array();
/**
* Triggered when gathering metadata for all available reports.
*
* Plugins that define new reports should use this event to make them available in via
* the metadata API. By doing so, the report will become available in scheduled reports
* as well as in the Piwik Mobile App. In fact, any third party app that uses the metadata
* API will automatically have access to the new report.
*
* @param string &$availableReports The list of available reports. Append to this list
* to make a report available.
*
* Every element of this array must contain the following
* information:
*
* - **category**: A translated string describing the report's category.
* - **name**: The translated display title of the report.
* - **module**: The plugin of the report.
* - **action**: The API method that serves the report.
*
* The following information is optional:
*
* - **dimension**: The report's [dimension](/guides/all-about-analytics-data#dimensions) if any.
* - **metrics**: An array mapping metric names with their display names.
* - **metricsDocumentation**: An array mapping metric names with their
* translated documentation.
* - **processedMetrics**: The array of metrics in the report that are
* calculated using existing metrics. Can be set to
* `false` if the report contains no processed
* metrics.
* - **order**: The order of the report in the list of reports
* with the same category.
*
* @param array $parameters Contains the values of the sites and period we are
* getting reports for. Some reports depend on this data.
* For example, Goals reports depend on the site IDs being
* requested. Contains the following information:
*
* - **idSites**: The array of site IDs we are getting reports for.
* - **period**: The period type, eg, `'day'`, `'week'`, `'month'`,
* `'year'`, `'range'`.
* - **date**: A string date within the period or a date range, eg,
* `'2013-01-01'` or `'2012-01-01,2013-01-01'`.
*
* TODO: put dimensions section in all about analytics data
*/
Piwik::postEvent('API.getReportMetadata', array(&$availableReports, $parameters));
foreach ($availableReports as &$availableReport) {
if (!isset($availableReport['metrics'])) {
$availableReport['metrics'] = Metrics::getDefaultMetrics();
}
if (!isset($availableReport['processedMetrics'])) {
$availableReport['processedMetrics'] = Metrics::getDefaultProcessedMetrics();
}
if ($hideMetricsDoc) // remove metric documentation if it's not wanted
{
unset($availableReport['metricsDocumentation']);
} else if (!isset($availableReport['metricsDocumentation'])) {
// set metric documentation to default if it's not set
$availableReport['metricsDocumentation'] = Metrics::getDefaultMetricsDocumentation();
}
}
/**
* Triggered after all available reports are collected.
*
* This event can be used to modify the report metadata of reports in other plugins. You
* could, for example, add custom metrics to every report or remove reports from the list
* of available reports.
*
* @param array &$availableReports List of all report metadata. Read the {@hook API.getReportMetadata}
* docs to see what this array contains.
* @param array $parameters Contains the values of the sites and period we are
* getting reports for. Some report depend on this data.
* For example, Goals reports depend on the site IDs being
* request. Contains the following information:
*
* - **idSites**: The array of site IDs we are getting reports for.
* - **period**: The period type, eg, `'day'`, `'week'`, `'month'`,
* `'year'`, `'range'`.
* - **date**: A string date within the period or a date range, eg,
* `'2013-01-01'` or `'2012-01-01,2013-01-01'`.
*/
Piwik::postEvent('API.getReportMetadata.end', array(&$availableReports, $parameters));
// Sort results to ensure consistent order
usort($availableReports, array($this, 'sort'));
// Add the magic API.get report metadata aggregating all plugins API.get API calls automatically
$this->addApiGetMetdata($availableReports);
$knownMetrics = array_merge(Metrics::getDefaultMetrics(), Metrics::getDefaultProcessedMetrics());
foreach ($availableReports as &$availableReport) {
// Ensure all metrics have a translation
$metrics = $availableReport['metrics'];
$cleanedMetrics = array();
foreach ($metrics as $metricId => $metricTranslation) {
// When simply the column name was given, ie 'metric' => array( 'nb_visits' )
// $metricTranslation is in this case nb_visits. We look for a known translation.
if (is_numeric($metricId)
&& isset($knownMetrics[$metricTranslation])
) {
$metricId = $metricTranslation;
$metricTranslation = $knownMetrics[$metricTranslation];
}
$cleanedMetrics[$metricId] = $metricTranslation;
}
$availableReport['metrics'] = $cleanedMetrics;
// if hide/show columns specified, hide/show metrics & docs
$availableReport['metrics'] = $this->hideShowMetrics($availableReport['metrics']);
if (isset($availableReport['processedMetrics'])) {
$availableReport['processedMetrics'] = $this->hideShowMetrics($availableReport['processedMetrics']);
}
if (isset($availableReport['metricsDocumentation'])) {
$availableReport['metricsDocumentation'] =
$this->hideShowMetrics($availableReport['metricsDocumentation']);
}
// Remove array elements that are false (to clean up API output)
foreach ($availableReport as $attributeName => $attributeValue) {
if (empty($attributeValue)) {
unset($availableReport[$attributeName]);
}
}
// when there are per goal metrics, don't display conversion_rate since it can differ from per goal sum
if (isset($availableReport['metricsGoal'])) {
unset($availableReport['processedMetrics']['conversion_rate']);
unset($availableReport['metricsGoal']['conversion_rate']);
}
// Processing a uniqueId for each report,
// can be used by UIs as a key to match a given report
$uniqueId = $availableReport['module'] . '_' . $availableReport['action'];
if (!empty($availableReport['parameters'])) {
foreach ($availableReport['parameters'] as $key => $value) {
$uniqueId .= '_' . $key . '--' . $value;
}
}
$availableReport['uniqueId'] = $uniqueId;
// Order is used to order reports internally, but not meant to be used outside
unset($availableReport['order']);
}
// remove subtable reports
if (!$showSubtableReports) {
foreach ($availableReports as $idx => $report) {
if (isset($report['isSubtableReport']) && $report['isSubtableReport']) {
unset($availableReports[$idx]);
}
}
}
return array_values($availableReports); // make sure array has contiguous key values
}
/**
* API metadata are sorted by category/name,
* with a little tweak to replicate the standard Piwik category ordering
*
* @param string $a
* @param string $b
* @return int
*/
private function sort($a, $b)
{
static $order = null;
if (is_null($order)) {
$order = array(
Piwik::translate('General_MultiSitesSummary'),
Piwik::translate('VisitsSummary_VisitsSummary'),
Piwik::translate('Goals_Ecommerce'),
Piwik::translate('General_Actions'),
Piwik::translate('Events_Events'),
Piwik::translate('Actions_SubmenuSitesearch'),
Piwik::translate('Referrers_Referrers'),
Piwik::translate('Goals_Goals'),
Piwik::translate('General_Visitors'),
Piwik::translate('DevicesDetection_DevicesDetection'),
Piwik::translate('UserSettings_VisitorSettings'),
);
}
return ($category = strcmp(array_search($a['category'], $order), array_search($b['category'], $order))) == 0
? (@$a['order'] < @$b['order'] ? -1 : 1)
: $category;
}
/**
* Add the metadata for the API.get report
* In other plugins, this would hook on 'API.getReportMetadata'
*/
private function addApiGetMetdata(&$availableReports)
{
$metadata = array(
'category' => Piwik::translate('General_API'),
'name' => Piwik::translate('General_MainMetrics'),
'module' => 'API',
'action' => 'get',
'metrics' => array(),
'processedMetrics' => array(),
'metricsDocumentation' => array(),
'order' => 1
);
$indexesToMerge = array('metrics', 'processedMetrics', 'metricsDocumentation');
foreach ($availableReports as $report) {
if ($report['action'] == 'get') {
foreach ($indexesToMerge as $index) {
if (isset($report[$index])
&& is_array($report[$index])
) {
$metadata[$index] = array_merge($metadata[$index], $report[$index]);
}
}
}
}
$availableReports[] = $metadata;
}
public function getProcessedReport($idSite, $period, $date, $apiModule, $apiAction, $segment = false,
$apiParameters = false, $idGoal = false, $language = false,
$showTimer = true, $hideMetricsDoc = false, $idSubtable = false, $showRawMetrics = false)
{
$timer = new Timer();
if (empty($apiParameters)) {
$apiParameters = array();
}
if (!empty($idGoal)
&& empty($apiParameters['idGoal'])
) {
$apiParameters['idGoal'] = $idGoal;
}
// Is this report found in the Metadata available reports?
$reportMetadata = $this->getMetadata($idSite, $apiModule, $apiAction, $apiParameters, $language,
$period, $date, $hideMetricsDoc, $showSubtableReports = true);
if (empty($reportMetadata)) {
throw new Exception("Requested report $apiModule.$apiAction for Website id=$idSite not found in the list of available reports. \n");
}
$reportMetadata = reset($reportMetadata);
// Generate Api call URL passing custom parameters
$parameters = array_merge($apiParameters, array(
'method' => $apiModule . '.' . $apiAction,
'idSite' => $idSite,
'period' => $period,
'date' => $date,
'format' => 'original',
'serialize' => '0',
'language' => $language,
'idSubtable' => $idSubtable,
));
if (!empty($segment)) $parameters['segment'] = $segment;
$url = Url::getQueryStringFromParameters($parameters);
$request = new Request($url);
try {
/** @var DataTable */
$dataTable = $request->process();
} catch (Exception $e) {
throw new Exception("API returned an error: " . $e->getMessage() . " at " . basename($e->getFile()) . ":" . $e->getLine() . "\n");
}
list($newReport, $columns, $rowsMetadata, $totals) = $this->handleTableReport($idSite, $dataTable, $reportMetadata, $showRawMetrics);
foreach ($columns as $columnId => &$name) {
$name = ucfirst($name);
}
$website = new Site($idSite);
$period = Period::factory($period, $date);
$period = $period->getLocalizedLongString();
$return = array(
'website' => $website->getName(),
'prettyDate' => $period,
'metadata' => $reportMetadata,
'columns' => $columns,
'reportData' => $newReport,
'reportMetadata' => $rowsMetadata,
'reportTotal' => $totals
);
if ($showTimer) {
$return['timerMillis'] = $timer->getTimeMs(0);
}
return $return;
}
/**
* Enhance a $dataTable using metadata :
*
* - remove metrics based on $reportMetadata['metrics']
* - add 0 valued metrics if $dataTable doesn't provide all $reportMetadata['metrics']
* - format metric values to a 'human readable' format
* - extract row metadata to a separate Simple|Set : $rowsMetadata
* - translate metric names to a separate array : $columns
*
* @param int $idSite enables monetary value formatting based on site currency
* @param \Piwik\DataTable\Map|\Piwik\DataTable\Simple $dataTable
* @param array $reportMetadata
* @param bool $showRawMetrics
* @return array Simple|Set $newReport with human readable format & array $columns list of translated column names & Simple|Set $rowsMetadata
*/
private function handleTableReport($idSite, $dataTable, &$reportMetadata, $showRawMetrics = false)
{
$hasDimension = isset($reportMetadata['dimension']);
$columns = $reportMetadata['metrics'];
if ($hasDimension) {
$columns = array_merge(
array('label' => $reportMetadata['dimension']),
$columns
);
if (isset($reportMetadata['processedMetrics'])) {
$processedMetricsAdded = Metrics::getDefaultProcessedMetrics();
foreach ($processedMetricsAdded as $processedMetricId => $processedMetricTranslation) {
// this processed metric can be displayed for this report
if (isset($reportMetadata['processedMetrics'][$processedMetricId])) {
$columns[$processedMetricId] = $processedMetricTranslation;
}
}
}
// Display the global Goal metrics
if (isset($reportMetadata['metricsGoal'])) {
$metricsGoalDisplay = array('revenue');
// Add processed metrics to be displayed for this report
foreach ($metricsGoalDisplay as $goalMetricId) {
if (isset($reportMetadata['metricsGoal'][$goalMetricId])) {
$columns[$goalMetricId] = $reportMetadata['metricsGoal'][$goalMetricId];
}
}
}
if (isset($reportMetadata['processedMetrics'])) {
// Add processed metrics
$dataTable->filter('AddColumnsProcessedMetrics', array($deleteRowsWithNoVisit = false));
}
}
$columns = $this->hideShowMetrics($columns);
$totals = array();
// $dataTable is an instance of Set when multiple periods requested
if ($dataTable instanceof DataTable\Map) {
// Need a new Set to store the 'human readable' values
$newReport = new DataTable\Map();
$newReport->setKeyName("prettyDate");
// Need a new Set to store report metadata
$rowsMetadata = new DataTable\Map();
$rowsMetadata->setKeyName("prettyDate");
// Process each Simple entry
foreach ($dataTable->getDataTables() as $label => $simpleDataTable) {
$this->removeEmptyColumns($columns, $reportMetadata, $simpleDataTable);
list($enhancedSimpleDataTable, $rowMetadata) = $this->handleSimpleDataTable($idSite, $simpleDataTable, $columns, $hasDimension, $showRawMetrics);
$enhancedSimpleDataTable->setAllTableMetadata($simpleDataTable->getAllTableMetadata());
$period = $simpleDataTable->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getLocalizedLongString();
$newReport->addTable($enhancedSimpleDataTable, $period);
$rowsMetadata->addTable($rowMetadata, $period);
$totals = $this->aggregateReportTotalValues($simpleDataTable, $totals);
}
} else {
$this->removeEmptyColumns($columns, $reportMetadata, $dataTable);
list($newReport, $rowsMetadata) = $this->handleSimpleDataTable($idSite, $dataTable, $columns, $hasDimension, $showRawMetrics);
$totals = $this->aggregateReportTotalValues($dataTable, $totals);
}
return array(
$newReport,
$columns,
$rowsMetadata,
$totals
);
}
/**
* Removes metrics from the list of columns and the report meta data if they are marked empty
* in the data table meta data.
*/
private function removeEmptyColumns(&$columns, &$reportMetadata, $dataTable)
{
$emptyColumns = $dataTable->getMetadata(DataTable::EMPTY_COLUMNS_METADATA_NAME);
if (!is_array($emptyColumns)) {
return;
}
$columns = $this->hideShowMetrics($columns, $emptyColumns);
if (isset($reportMetadata['metrics'])) {
$reportMetadata['metrics'] = $this->hideShowMetrics($reportMetadata['metrics'], $emptyColumns);
}
if (isset($reportMetadata['metricsDocumentation'])) {
$reportMetadata['metricsDocumentation'] = $this->hideShowMetrics($reportMetadata['metricsDocumentation'], $emptyColumns);
}
}
/**
* Removes column names from an array based on the values in the hideColumns,
* showColumns query parameters. This is a hack that provides the ColumnDelete
* filter functionality in processed reports.
*
* @param array $columns List of metrics shown in a processed report.
* @param array $emptyColumns Empty columns from the data table meta data.
* @return array Filtered list of metrics.
*/
private function hideShowMetrics($columns, $emptyColumns = array())
{
if (!is_array($columns)) {
return $columns;
}
// remove columns if hideColumns query parameters exist
$columnsToRemove = Common::getRequestVar('hideColumns', '');
if ($columnsToRemove != '') {
$columnsToRemove = explode(',', $columnsToRemove);
foreach ($columnsToRemove as $name) {
// if a column to remove is in the column list, remove it
if (isset($columns[$name])) {
unset($columns[$name]);
}
}
}
// remove columns if showColumns query parameters exist
$columnsToKeep = Common::getRequestVar('showColumns', '');
if ($columnsToKeep != '') {
$columnsToKeep = explode(',', $columnsToKeep);
$columnsToKeep[] = 'label';
foreach ($columns as $name => $ignore) {
// if the current column should not be kept, remove it
$idx = array_search($name, $columnsToKeep);
if ($idx === false) // if $name is not in $columnsToKeep
{
unset($columns[$name]);
}
}
}
// remove empty columns
if (is_array($emptyColumns)) {
foreach ($emptyColumns as $column) {
if (isset($columns[$column])) {
unset($columns[$column]);
}
}
}
return $columns;
}
/**
* Enhance $simpleDataTable using metadata :
*
* - remove metrics based on $reportMetadata['metrics']
* - add 0 valued metrics if $simpleDataTable doesn't provide all $reportMetadata['metrics']
* - format metric values to a 'human readable' format
* - extract row metadata to a separate Simple $rowsMetadata
*
* @param int $idSite enables monetary value formatting based on site currency
* @param Simple $simpleDataTable
* @param array $metadataColumns
* @param boolean $hasDimension
* @param bool $returnRawMetrics If set to true, the original metrics will be returned
*
* @return array DataTable $enhancedDataTable filtered metrics with human readable format & Simple $rowsMetadata
*/
private function handleSimpleDataTable($idSite, $simpleDataTable, $metadataColumns, $hasDimension, $returnRawMetrics = false)
{
// new DataTable to store metadata
$rowsMetadata = new DataTable();
// new DataTable to store 'human readable' values
if ($hasDimension) {
$enhancedDataTable = new DataTable();
} else {
$enhancedDataTable = new Simple();
}
// add missing metrics
foreach ($simpleDataTable->getRows() as $row) {
$rowMetrics = $row->getColumns();
foreach ($metadataColumns as $id => $name) {
if (!isset($rowMetrics[$id])) {
$row->addColumn($id, 0);
}
}
}
foreach ($simpleDataTable->getRows() as $row) {
$enhancedRow = new Row();
$enhancedDataTable->addRow($enhancedRow);
$rowMetrics = $row->getColumns();
foreach ($rowMetrics as $columnName => $columnValue) {
// filter metrics according to metadata definition
if (isset($metadataColumns[$columnName])) {
// generate 'human readable' metric values
$prettyValue = MetricsFormatter::getPrettyValue($idSite, $columnName, $columnValue, $htmlAllowed = false);
$enhancedRow->addColumn($columnName, $prettyValue);
} // For example the Maps Widget requires the raw metrics to do advanced datavis
elseif ($returnRawMetrics) {
$enhancedRow->addColumn($columnName, $columnValue);
}
}
// If report has a dimension, extract metadata into a distinct DataTable
if ($hasDimension) {
$rowMetadata = $row->getMetadata();
$idSubDataTable = $row->getIdSubDataTable();
// Create a row metadata only if there are metadata to insert
if (count($rowMetadata) > 0 || !is_null($idSubDataTable)) {
$metadataRow = new Row();
$rowsMetadata->addRow($metadataRow);
foreach ($rowMetadata as $metadataKey => $metadataValue) {
$metadataRow->addColumn($metadataKey, $metadataValue);
}
if (!is_null($idSubDataTable)) {
$metadataRow->addColumn('idsubdatatable', $idSubDataTable);
}
}
}
}
return array(
$enhancedDataTable,
$rowsMetadata
);
}
private function aggregateReportTotalValues($simpleDataTable, $totals)
{
$metadataTotals = $simpleDataTable->getMetadata('totals');
if (empty($metadataTotals)) {
return $totals;
}
$simpleTotals = $this->hideShowMetrics($metadataTotals);
foreach ($simpleTotals as $metric => $value) {
if (!array_key_exists($metric, $totals)) {
$totals[$metric] = $value;
} else {
$totals[$metric] += $value;
}
}
return $totals;
}
}

View file

@ -0,0 +1,529 @@
<?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\Plugins\API;
use Exception;
use Piwik\API\DataTableManipulator\LabelFilter;
use Piwik\API\Request;
use Piwik\API\ResponseBuilder;
use Piwik\Common;
use Piwik\DataTable\Filter\CalculateEvolutionFilter;
use Piwik\DataTable\Filter\SafeDecodeLabel;
use Piwik\DataTable\Row;
use Piwik\DataTable;
use Piwik\Period;
use Piwik\Piwik;
use Piwik\Url;
use Piwik\Site;
/**
* This class generates a Row evolution dataset, from input request
*
*/
class RowEvolution
{
private static $actionsUrlReports = array(
'getPageUrls',
'getPageUrlsFollowingSiteSearch',
'getEntryPageUrls',
'getExitPageUrls',
'getPageUrl'
);
public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true)
{
// validation of requested $period & $date
if ($period == 'range') {
// load days in the range
$period = 'day';
}
if (!Period::isMultiplePeriod($date, $period)) {
throw new Exception("Row evolutions can not be processed with this combination of \'date\' and \'period\' parameters.");
}
$label = ResponseBuilder::unsanitizeLabelParameter($label);
$labels = Piwik::getArrayFromApiParameter($label);
$metadata = $this->getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal);
$dataTable = $this->loadRowEvolutionDataFromAPI($metadata, $idSite, $period, $date, $apiModule, $apiAction, $labels, $segment, $idGoal);
if (empty($labels)) {
$labels = $this->getLabelsFromDataTable($dataTable, $labels);
$dataTable = $this->enrichRowAddMetadataLabelIndex($labels, $dataTable);
}
if (count($labels) != 1) {
$data = $this->getMultiRowEvolution(
$dataTable,
$metadata,
$apiModule,
$apiAction,
$labels,
$column,
$legendAppendMetric,
$labelUseAbsoluteUrl
);
} else {
$data = $this->getSingleRowEvolution(
$idSite,
$dataTable,
$metadata,
$apiModule,
$apiAction,
$labels[0],
$labelUseAbsoluteUrl
);
}
return $data;
}
/**
* @param array $labels
* @param DataTable\Map $dataTable
* @return mixed
*/
protected function enrichRowAddMetadataLabelIndex($labels, $dataTable)
{
// set label index metadata
$labelsToIndex = array_flip($labels);
foreach ($dataTable->getDataTables() as $table) {
foreach ($table->getRows() as $row) {
$label = $row->getColumn('label');
if (isset($labelsToIndex[$label])) {
$row->setMetadata(LabelFilter::FLAG_IS_ROW_EVOLUTION, $labelsToIndex[$label]);
}
}
}
return $dataTable;
}
/**
* @param DataTable\Map $dataTable
* @param array $labels
* @return array
*/
protected function getLabelsFromDataTable($dataTable, $labels)
{
// if no labels specified, use all possible labels as list
foreach ($dataTable->getDataTables() as $table) {
$labels = array_merge($labels, $table->getColumn('label'));
}
$labels = array_values(array_unique($labels));
// if the filter_limit query param is set, treat it as a request to limit
// the number of labels used
$limit = Common::getRequestVar('filter_limit', false);
if ($limit != false
&& $limit >= 0
) {
$labels = array_slice($labels, 0, $limit);
}
return $labels;
}
/**
* Get row evolution for a single label
* @param DataTable\Map $dataTable
* @param array $metadata
* @param string $apiModule
* @param string $apiAction
* @param string $label
* @param bool $labelUseAbsoluteUrl
* @return array containing report data, metadata, label, logo
*/
private function getSingleRowEvolution($idSite, $dataTable, $metadata, $apiModule, $apiAction, $label, $labelUseAbsoluteUrl = true)
{
$metricNames = array_keys($metadata['metrics']);
$logo = $actualLabel = false;
$urlFound = false;
foreach ($dataTable->getDataTables() as $date => $subTable) {
/** @var $subTable DataTable */
$subTable->applyQueuedFilters();
if ($subTable->getRowsCount() > 0) {
/** @var $row Row */
$row = $subTable->getFirstRow();
if (!$actualLabel) {
$logo = $row->getMetadata('logo');
$actualLabel = $this->getRowUrlForEvolutionLabel($row, $apiModule, $apiAction, $labelUseAbsoluteUrl);
$urlFound = $actualLabel !== false;
if (empty($actualLabel)) {
$actualLabel = $row->getColumn('label');
}
}
// remove all columns that are not in the available metrics.
// this removes the label as well (which is desired for two reasons: (1) it was passed
// in the request, (2) it would cause the evolution graph to show the label in the legend).
foreach ($row->getColumns() as $column => $value) {
if (!in_array($column, $metricNames) && $column != 'label_html') {
$row->deleteColumn($column);
}
}
$row->deleteMetadata();
}
}
$this->enhanceRowEvolutionMetaData($metadata, $dataTable);
// if we have a recursive label and no url, use the path
if (!$urlFound) {
$actualLabel = $this->formatQueryLabelForDisplay($idSite, $apiModule, $apiAction, $label);
}
$return = array(
'label' => SafeDecodeLabel::decodeLabelSafe($actualLabel),
'reportData' => $dataTable,
'metadata' => $metadata
);
if (!empty($logo)) {
$return['logo'] = $logo;
}
return $return;
}
private function formatQueryLabelForDisplay($idSite, $apiModule, $apiAction, $label)
{
// rows with subtables do not contain URL metadata. this hack makes sure the label titles in row
// evolution popovers look like URLs.
if ($apiModule == 'Actions'
&& in_array($apiAction, self::$actionsUrlReports)
) {
$mainUrl = Site::getMainUrlFor($idSite);
$mainUrlHost = @parse_url($mainUrl, PHP_URL_HOST);
$replaceRegex = "/\\s*" . preg_quote(LabelFilter::SEPARATOR_RECURSIVE_LABEL) . "\\s*/";
$cleanLabel = preg_replace($replaceRegex, '/', $label);
return $mainUrlHost . '/' . $cleanLabel . '/';
} else {
return str_replace(LabelFilter::SEPARATOR_RECURSIVE_LABEL, ' - ', $label);
}
}
/**
* @param Row $row
* @param string $apiModule
* @param string $apiAction
* @param bool $labelUseAbsoluteUrl
* @return bool|string
*/
private function getRowUrlForEvolutionLabel($row, $apiModule, $apiAction, $labelUseAbsoluteUrl)
{
$url = $row->getMetadata('url');
if ($url
&& ($apiModule == 'Actions'
|| ($apiModule == 'Referrers'
&& $apiAction == 'getWebsites'))
&& $labelUseAbsoluteUrl
) {
$actualLabel = preg_replace(';^http(s)?://(www.)?;i', '', $url);
return $actualLabel;
}
return false;
}
/**
* @param array $metadata see getRowEvolutionMetaData()
* @param int $idSite
* @param string $period
* @param string $date
* @param string $apiModule
* @param string $apiAction
* @param string|bool $label
* @param string|bool $segment
* @param int|bool $idGoal
* @throws Exception
* @return DataTable\Map|DataTable
*/
private function loadRowEvolutionDataFromAPI($metadata, $idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $idGoal = false)
{
if (!is_array($label)) {
$label = array($label);
}
$label = array_map('rawurlencode', $label);
$parameters = array(
'method' => $apiModule . '.' . $apiAction,
'label' => $label,
'idSite' => $idSite,
'period' => $period,
'date' => $date,
'format' => 'original',
'serialize' => '0',
'segment' => $segment,
'idGoal' => $idGoal,
// data for row evolution should NOT be limited
'filter_limit' => -1,
// if more than one label is used, we add metadata to ensure we know which
// row corresponds with which label (since the labels can change, and rows
// can be sorted in a different order)
'labelFilterAddLabelIndex' => count($label) > 1 ? 1 : 0,
);
// add "processed metrics" like actions per visit or bounce rate
// note: some reports should not be filtered with AddColumnProcessedMetrics
// specifically, reports without the Metrics::INDEX_NB_VISITS metric such as Goals.getVisitsUntilConversion & Goal.getDaysToConversion
// this is because the AddColumnProcessedMetrics filter removes all datable rows lacking this metric
if( isset($metadata['metrics']['nb_visits'])
&& !empty($label)) {
$parameters['filter_add_columns_when_show_all_columns'] = '1';
}
$url = Url::getQueryStringFromParameters($parameters);
$request = new Request($url);
try {
$dataTable = $request->process();
} catch (Exception $e) {
throw new Exception("API returned an error: " . $e->getMessage() . "\n");
}
return $dataTable;
}
/**
* For a given API report, returns a simpler version
* of the metadata (will return only the metrics and the dimension name)
* @param $idSite
* @param $period
* @param $date
* @param $apiModule
* @param $apiAction
* @param $language
* @param $idGoal
* @throws Exception
* @return array
*/
private function getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $idGoal = false)
{
$apiParameters = array();
if (!empty($idGoal) && $idGoal > 0) {
$apiParameters = array('idGoal' => $idGoal);
}
$reportMetadata = API::getInstance()->getMetadata($idSite, $apiModule, $apiAction, $apiParameters, $language,
$period, $date, $hideMetricsDoc = false, $showSubtableReports = true);
if (empty($reportMetadata)) {
throw new Exception("Requested report $apiModule.$apiAction for Website id=$idSite "
. "not found in the list of available reports. \n");
}
$reportMetadata = reset($reportMetadata);
$metrics = $reportMetadata['metrics'];
if (isset($reportMetadata['processedMetrics']) && is_array($reportMetadata['processedMetrics'])) {
$metrics = $metrics + $reportMetadata['processedMetrics'];
}
$dimension = $reportMetadata['dimension'];
return compact('metrics', 'dimension');
}
/**
* Given the Row evolution dataTable, and the associated metadata,
* enriches the metadata with min/max values, and % change between the first period and the last one
* @param array $metadata
* @param DataTable\Map $dataTable
*/
private function enhanceRowEvolutionMetaData(&$metadata, $dataTable)
{
// prepare result array for metrics
$metricsResult = array();
foreach ($metadata['metrics'] as $metric => $name) {
$metricsResult[$metric] = array('name' => $name);
if (!empty($metadata['logos'][$metric])) {
$metricsResult[$metric]['logo'] = $metadata['logos'][$metric];
}
}
unset($metadata['logos']);
$subDataTables = $dataTable->getDataTables();
$firstDataTable = reset($subDataTables);
$firstDataTableRow = $firstDataTable->getFirstRow();
$lastDataTable = end($subDataTables);
$lastDataTableRow = $lastDataTable->getFirstRow();
// Process min/max values
$firstNonZeroFound = array();
foreach ($subDataTables as $subDataTable) {
// $subDataTable is the report for one period, it has only one row
$firstRow = $subDataTable->getFirstRow();
foreach ($metadata['metrics'] as $metric => $label) {
$value = $firstRow ? floatval($firstRow->getColumn($metric)) : 0;
if ($value > 0) {
$firstNonZeroFound[$metric] = true;
} else if (!isset($firstNonZeroFound[$metric])) {
continue;
}
if (!isset($metricsResult[$metric]['min'])
|| $metricsResult[$metric]['min'] > $value
) {
$metricsResult[$metric]['min'] = $value;
}
if (!isset($metricsResult[$metric]['max'])
|| $metricsResult[$metric]['max'] < $value
) {
$metricsResult[$metric]['max'] = $value;
}
}
}
// Process % change between first/last values
foreach ($metadata['metrics'] as $metric => $label) {
$first = $firstDataTableRow ? floatval($firstDataTableRow->getColumn($metric)) : 0;
$last = $lastDataTableRow ? floatval($lastDataTableRow->getColumn($metric)) : 0;
// do not calculate evolution if the first value is 0 (to avoid divide-by-zero)
if ($first == 0) {
continue;
}
$change = CalculateEvolutionFilter::calculate($last, $first, $quotientPrecision = 0);
$change = CalculateEvolutionFilter::prependPlusSignToNumber($change);
$metricsResult[$metric]['change'] = $change;
}
$metadata['metrics'] = $metricsResult;
}
/** Get row evolution for a multiple labels */
private function getMultiRowEvolution(DataTable\Map $dataTable, $metadata, $apiModule, $apiAction, $labels, $column,
$legendAppendMetric = true,
$labelUseAbsoluteUrl = true)
{
if (!isset($metadata['metrics'][$column])) {
// invalid column => use the first one that's available
$metrics = array_keys($metadata['metrics']);
$column = reset($metrics);
}
// get the processed label and logo (if any) for every requested label
$actualLabels = $logos = array();
foreach ($labels as $labelIdx => $label) {
foreach ($dataTable->getDataTables() as $table) {
$labelRow = $this->getRowEvolutionRowFromLabelIdx($table, $labelIdx);
if ($labelRow) {
$actualLabels[$labelIdx] = $this->getRowUrlForEvolutionLabel(
$labelRow, $apiModule, $apiAction, $labelUseAbsoluteUrl);
$prettyLabel = $labelRow->getColumn('label_html');
if($prettyLabel !== false) {
$actualLabels[$labelIdx] = $prettyLabel;
}
$logos[$labelIdx] = $labelRow->getMetadata('logo');
if (!empty($actualLabels[$labelIdx])) {
break;
}
}
}
if (empty($actualLabels[$labelIdx])) {
$cleanLabel = $this->cleanOriginalLabel($label);
$actualLabels[$labelIdx] = $cleanLabel;
}
}
// convert rows to be array($column.'_'.$labelIdx => $value) as opposed to
// array('label' => $label, 'column' => $value).
$dataTableMulti = $dataTable->getEmptyClone();
foreach ($dataTable->getDataTables() as $tableLabel => $table) {
$newRow = new Row();
foreach ($labels as $labelIdx => $label) {
$row = $this->getRowEvolutionRowFromLabelIdx($table, $labelIdx);
$value = 0;
if ($row) {
$value = $row->getColumn($column);
$value = floatVal(str_replace(',', '.', $value));
}
if ($value == '') {
$value = 0;
}
$newLabel = $column . '_' . (int)$labelIdx;
$newRow->addColumn($newLabel, $value);
}
$newTable = $table->getEmptyClone();
if (!empty($labels)) { // only add a row if the row has data (no labels === no data)
$newTable->addRow($newRow);
}
$dataTableMulti->addTable($newTable, $tableLabel);
}
// the available metrics for the report are returned as metadata / columns
$metadata['columns'] = $metadata['metrics'];
// metadata / metrics should document the rows that are compared
// this way, UI code can be reused
$metadata['metrics'] = array();
foreach ($actualLabels as $labelIndex => $label) {
if ($legendAppendMetric) {
$label .= ' (' . $metadata['columns'][$column] . ')';
}
$metricName = $column . '_' . $labelIndex;
$metadata['metrics'][$metricName] = $label;
if (!empty($logos[$labelIndex])) {
$metadata['logos'][$metricName] = $logos[$labelIndex];
}
}
$this->enhanceRowEvolutionMetaData($metadata, $dataTableMulti);
return array(
'column' => $column,
'reportData' => $dataTableMulti,
'metadata' => $metadata
);
}
/**
* Returns the row in a datatable by its LabelFilter::FLAG_IS_ROW_EVOLUTION metadata.
*
* @param DataTable $table
* @param int $labelIdx
* @return Row|false
*/
private function getRowEvolutionRowFromLabelIdx($table, $labelIdx)
{
$labelIdx = (int)$labelIdx;
foreach ($table->getRows() as $row) {
if ($row->getMetadata(LabelFilter::FLAG_IS_ROW_EVOLUTION) === $labelIdx) {
return $row;
}
}
return false;
}
/**
* Returns a prettier, more comprehensible version of a row evolution label for display.
*/
private function cleanOriginalLabel($label)
{
$label = str_replace(LabelFilter::SEPARATOR_RECURSIVE_LABEL, ' - ', $label);
$label = SafeDecodeLabel::decodeLabelSafe($label);
return $label;
}
}

View file

@ -0,0 +1,48 @@
#token_auth {
background-color: #E8FFE9;
border: 1px solid #00CC3A;
margin: 0 0 16px 8px;
padding: 12px;
line-height: 4em;
}
.example, .example A {
color: #9E9E9E;
}
.page_api {
padding: 0 15px 0 15px;
}
.page_api h2 {
border-bottom: 1px solid #DADADA;
margin: 10px -15px 15px 0;
padding: 0 0 5px 0;
font-size: 24px;
width:100%;
}
.page_api p {
line-height: 140%;
padding-bottom: 20px;
}
.apiFirstLine {
font-weight: bold;
padding-bottom: 10px;
}
.page_api ul {
list-style: disc outside none;
margin-left: 25px;
}
.apiDescription {
line-height: 1.5em;
padding-bottom: 1em;
}
.apiMethod {
margin-bottom: 5px;
margin-left: 20px;
}

View file

@ -0,0 +1,32 @@
{% extends 'dashboard.twig' %}
{% set showMenu=false %}
{% block content %}
{% include "@CoreHome/_siteSelectHeader.twig" %}
<div class="page_api pageWrap">
<div class="top_controls">
{% include "@CoreHome/_periodSelect.twig" %}
</div>
<h2>{{ 'API_QuickDocumentationTitle'|translate }}</h2>
<p>{{ 'API_PluginDescription'|translate }}</p>
<p>
<strong>{{ 'API_MoreInformation'|translate("<a target='_blank' href='?module=Proxy&action=redirect&url=http://piwik.org/docs/analytics-api'>","</a>","<a target='_blank' href='?module=Proxy&action=redirect&url=http://piwik.org/docs/analytics-api/reference'>","</a>")|raw }}</strong>
</p>
<h2>{{ 'API_UserAuthentication'|translate }}</h2>
<p>
{{ 'API_UsingTokenAuth'|translate('<b>','</b>',"")|raw }}<br/>
<span id='token_auth'>&amp;token_auth=<strong>{{ token_auth }}</strong></span><br/>
{{ 'API_KeepTokenSecret'|translate('<b>','</b>')|raw }}
{{ list_api_methods_with_links|raw }}
<br/>
</div>
{% endblock %}

View file

@ -0,0 +1,596 @@
<?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\Plugins\Actions;
use Exception;
use Piwik\API\Request;
use Piwik\Archive;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Date;
use Piwik\Metrics;
use Piwik\Piwik;
use Piwik\Plugins\CustomVariables\API as APICustomVariables;
use Piwik\Tracker\Action;
use Piwik\Tracker\ActionSiteSearch;
use Piwik\Tracker\PageUrl;
/**
* The Actions API lets you request reports for all your Visitor Actions: Page URLs, Page titles (Piwik Events),
* File Downloads and Clicks on external websites.
*
* For example, "getPageTitles" will return all your page titles along with standard <a href='http://piwik.org/docs/analytics-api/reference/#toc-metric-definitions' target='_blank'>Actions metrics</a> for each row.
*
* It is also possible to request data for a specific Page Title with "getPageTitle"
* and setting the parameter pageName to the page title you wish to request.
* Similarly, you can request metrics for a given Page URL via "getPageUrl", a Download file via "getDownload"
* and an outlink via "getOutlink".
*
* Note: pageName, pageUrl, outlinkUrl, downloadUrl parameters must be URL encoded before you call the API.
* @method static \Piwik\Plugins\Actions\API getInstance()
*/
class API extends \Piwik\Plugin\API
{
/**
* Returns the list of metrics (pages, downloads, outlinks)
*
* @param int $idSite
* @param string $period
* @param string $date
* @param bool|string $segment
* @param bool|array $columns
* @return DataTable
*/
public function get($idSite, $period, $date, $segment = false, $columns = false)
{
Piwik::checkUserHasViewAccess($idSite);
$archive = Archive::build($idSite, $period, $date, $segment);
$metrics = Archiver::$actionsAggregateMetrics;
$metrics['Actions_avg_time_generation'] = 'avg_time_generation';
// get requested columns
$columns = Piwik::getArrayFromApiParameter($columns);
if (!empty($columns)) {
// get the columns that are available and requested
$columns = array_intersect($columns, array_values($metrics));
$columns = array_values($columns); // make sure indexes are right
$nameReplace = array();
foreach ($columns as $i => $column) {
$fullColumn = array_search($column, $metrics);
$columns[$i] = $fullColumn;
$nameReplace[$fullColumn] = $column;
}
if (false !== ($avgGenerationTimeRequested = array_search('Actions_avg_time_generation', $columns))) {
unset($columns[$avgGenerationTimeRequested]);
$avgGenerationTimeRequested = true;
}
} else {
// get all columns
unset($metrics['Actions_avg_time_generation']);
$columns = array_keys($metrics);
$nameReplace = & $metrics;
$avgGenerationTimeRequested = true;
}
if ($avgGenerationTimeRequested) {
$tempColumns[] = Archiver::METRIC_SUM_TIME_RECORD_NAME;
$tempColumns[] = Archiver::METRIC_HITS_TIMED_RECORD_NAME;
$columns = array_merge($columns, $tempColumns);
$columns = array_unique($columns);
$nameReplace[Archiver::METRIC_SUM_TIME_RECORD_NAME] = 'sum_time_generation';
$nameReplace[Archiver::METRIC_HITS_TIMED_RECORD_NAME] = 'nb_hits_with_time_generation';
}
$table = $archive->getDataTableFromNumeric($columns);
// replace labels (remove Actions_)
$table->filter('ReplaceColumnNames', array($nameReplace));
// compute avg generation time
if ($avgGenerationTimeRequested) {
$table->filter('ColumnCallbackAddColumnQuotient', array('avg_time_generation', 'sum_time_generation', 'nb_hits_with_time_generation', 3));
$table->deleteColumns(array('sum_time_generation', 'nb_hits_with_time_generation'));
}
return $table;
}
/**
* @param int $idSite
* @param string $period
* @param Date $date
* @param bool $segment
* @param bool $expanded
* @param bool|int $idSubtable
* @param bool|int $depth
*
* @return DataTable|DataTable\Map
*/
public function getPageUrls($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false,
$depth = false)
{
$dataTable = $this->getDataTableFromArchive('Actions_actions_url', $idSite, $period, $date, $segment, $expanded, $idSubtable, $depth);
$this->filterPageDatatable($dataTable);
$this->filterActionsDataTable($dataTable, $expanded);
return $dataTable;
}
/**
* @param int $idSite
* @param string $period
* @param Date $date
* @param bool $segment
* @param bool $expanded
* @param bool $idSubtable
*
* @return DataTable|DataTable\Map
*/
public function getPageUrlsFollowingSiteSearch($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
{
$dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable);
$this->keepPagesFollowingSearch($dataTable);
return $dataTable;
}
/**
* @param int $idSite
* @param string $period
* @param Date $date
* @param bool $segment
* @param bool $expanded
* @param bool $idSubtable
*
* @return DataTable|DataTable\Map
*/
public function getPageTitlesFollowingSiteSearch($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
{
$dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable);
$this->keepPagesFollowingSearch($dataTable);
return $dataTable;
}
/**
* @param DataTable $dataTable
*/
protected function keepPagesFollowingSearch($dataTable)
{
// Keep only pages which are following site search
$dataTable->filter('ColumnCallbackDeleteRow', array(
'nb_hits_following_search',
function ($value) {
return $value <= 0;
}
));
}
/**
* Returns a DataTable with analytics information for every unique entry page URL, for
* the specified site, period & segment.
*/
public function getEntryPageUrls($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
{
$dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable);
$this->filterNonEntryActions($dataTable);
return $dataTable;
}
/**
* Returns a DataTable with analytics information for every unique exit page URL, for
* the specified site, period & segment.
*/
public function getExitPageUrls($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
{
$dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable);
$this->filterNonExitActions($dataTable);
return $dataTable;
}
public function getPageUrl($pageUrl, $idSite, $period, $date, $segment = false)
{
$callBackParameters = array('Actions_actions_url', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false);
$dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageUrl, Action::TYPE_PAGE_URL);
$this->filterPageDatatable($dataTable);
$this->filterActionsDataTable($dataTable);
return $dataTable;
}
public function getPageTitles($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
{
$dataTable = $this->getDataTableFromArchive('Actions_actions', $idSite, $period, $date, $segment, $expanded, $idSubtable);
$this->filterPageDatatable($dataTable);
$this->filterActionsDataTable($dataTable, $expanded);
return $dataTable;
}
/**
* Returns a DataTable with analytics information for every unique entry page title
* for the given site, time period & segment.
*/
public function getEntryPageTitles($idSite, $period, $date, $segment = false, $expanded = false,
$idSubtable = false)
{
$dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable);
$this->filterNonEntryActions($dataTable);
return $dataTable;
}
/**
* Returns a DataTable with analytics information for every unique exit page title
* for the given site, time period & segment.
*/
public function getExitPageTitles($idSite, $period, $date, $segment = false, $expanded = false,
$idSubtable = false)
{
$dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable);
$this->filterNonExitActions($dataTable);
return $dataTable;
}
public function getPageTitle($pageName, $idSite, $period, $date, $segment = false)
{
$callBackParameters = array('Actions_actions', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false);
$dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageName, Action::TYPE_PAGE_TITLE);
$this->filterPageDatatable($dataTable);
$this->filterActionsDataTable($dataTable);
return $dataTable;
}
public function getDownloads($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
{
$dataTable = $this->getDataTableFromArchive('Actions_downloads', $idSite, $period, $date, $segment, $expanded, $idSubtable);
$this->filterActionsDataTable($dataTable, $expanded);
return $dataTable;
}
public function getDownload($downloadUrl, $idSite, $period, $date, $segment = false)
{
$callBackParameters = array('Actions_downloads', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false);
$dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $downloadUrl, Action::TYPE_DOWNLOAD);
$this->filterActionsDataTable($dataTable);
return $dataTable;
}
public function getOutlinks($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
{
$dataTable = $this->getDataTableFromArchive('Actions_outlink', $idSite, $period, $date, $segment, $expanded, $idSubtable);
$this->filterActionsDataTable($dataTable, $expanded);
return $dataTable;
}
public function getOutlink($outlinkUrl, $idSite, $period, $date, $segment = false)
{
$callBackParameters = array('Actions_outlink', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false);
$dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $outlinkUrl, Action::TYPE_OUTLINK);
$this->filterActionsDataTable($dataTable);
return $dataTable;
}
public function getSiteSearchKeywords($idSite, $period, $date, $segment = false)
{
$dataTable = $this->getSiteSearchKeywordsRaw($idSite, $period, $date, $segment);
$dataTable->deleteColumn(Metrics::INDEX_SITE_SEARCH_HAS_NO_RESULT);
$this->filterPageDatatable($dataTable);
$this->filterActionsDataTable($dataTable);
$this->addPagesPerSearchColumn($dataTable);
return $dataTable;
}
/**
* Visitors can search, and then click "next" to view more results. This is the average number of search results pages viewed for this keyword.
*
* @param DataTable|DataTable\Simple|DataTable\Map $dataTable
* @param string $columnToRead
*/
protected function addPagesPerSearchColumn($dataTable, $columnToRead = 'nb_hits')
{
$dataTable->filter('ColumnCallbackAddColumnQuotient', array('nb_pages_per_search', $columnToRead, 'nb_visits', $precision = 1));
}
protected function getSiteSearchKeywordsRaw($idSite, $period, $date, $segment)
{
$dataTable = $this->getDataTableFromArchive('Actions_sitesearch', $idSite, $period, $date, $segment, $expanded = false);
return $dataTable;
}
public function getSiteSearchNoResultKeywords($idSite, $period, $date, $segment = false)
{
$dataTable = $this->getSiteSearchKeywordsRaw($idSite, $period, $date, $segment);
// Delete all rows that have some results
$dataTable->filter('ColumnCallbackDeleteRow',
array(
Metrics::INDEX_SITE_SEARCH_HAS_NO_RESULT,
function ($value) {
return $value < 1;
}
));
$dataTable->deleteRow(DataTable::ID_SUMMARY_ROW);
$dataTable->deleteColumn(Metrics::INDEX_SITE_SEARCH_HAS_NO_RESULT);
$this->filterPageDatatable($dataTable);
$this->filterActionsDataTable($dataTable);
$this->addPagesPerSearchColumn($dataTable);
return $dataTable;
}
/**
* @param int $idSite
* @param string $period
* @param Date $date
* @param bool $segment
*
* @return DataTable|DataTable\Map
*/
public function getSiteSearchCategories($idSite, $period, $date, $segment = false)
{
Actions::checkCustomVariablesPluginEnabled();
$customVariables = APICustomVariables::getInstance()->getCustomVariables($idSite, $period, $date, $segment, $expanded = false, $_leavePiwikCoreVariables = true);
$customVarNameToLookFor = ActionSiteSearch::CVAR_KEY_SEARCH_CATEGORY;
$dataTable = new DataTable();
// Handle case where date=last30&period=day
// FIXMEA: this logic should really be refactored somewhere, this is ugly!
if ($customVariables instanceof DataTable\Map) {
$dataTable = $customVariables->getEmptyClone();
$customVariableDatatables = $customVariables->getDataTables();
$dataTables = $dataTable->getDataTables();
foreach ($customVariableDatatables as $key => $customVariableTableForDate) {
// we do not enter the IF, in the case idSite=1,3 AND period=day&date=datefrom,dateto,
if ($customVariableTableForDate instanceof DataTable
&& $customVariableTableForDate->getMetadata(Archive\DataTableFactory::TABLE_METADATA_PERIOD_INDEX)
) {
$row = $customVariableTableForDate->getRowFromLabel($customVarNameToLookFor);
if ($row) {
$dateRewrite = $customVariableTableForDate->getMetadata(Archive\DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getDateStart()->toString();
$idSubtable = $row->getIdSubDataTable();
$categories = APICustomVariables::getInstance()->getCustomVariablesValuesFromNameId($idSite, $period, $dateRewrite, $idSubtable, $segment);
$dataTable->addTable($categories, $key);
}
}
}
} elseif ($customVariables instanceof DataTable) {
$row = $customVariables->getRowFromLabel($customVarNameToLookFor);
if ($row) {
$idSubtable = $row->getIdSubDataTable();
$dataTable = APICustomVariables::getInstance()->getCustomVariablesValuesFromNameId($idSite, $period, $date, $idSubtable, $segment);
}
}
$this->filterActionsDataTable($dataTable);
$this->addPagesPerSearchColumn($dataTable, $columnToRead = 'nb_actions');
return $dataTable;
}
/**
* Will search in the DataTable for a Label matching the searched string
* and return only the matching row, or an empty datatable
*/
protected function getFilterPageDatatableSearch($callBackParameters, $search, $actionType, $table = false,
$searchTree = false)
{
if ($searchTree === false) {
// build the query parts that are searched inside the tree
if ($actionType == Action::TYPE_PAGE_TITLE) {
$searchedString = Common::unsanitizeInputValue($search);
} else {
$idSite = $callBackParameters[1];
try {
$searchedString = PageUrl::excludeQueryParametersFromUrl($search, $idSite);
} catch (Exception $e) {
$searchedString = $search;
}
}
ArchivingHelper::reloadConfig();
$searchTree = ArchivingHelper::getActionExplodedNames($searchedString, $actionType);
}
if ($table === false) {
// fetch the data table
$table = call_user_func_array(array($this, 'getDataTableFromArchive'), $callBackParameters);
if ($table instanceof DataTable\Map) {
// search an array of tables, e.g. when using date=last30
// note that if the root is an array, we filter all children
// if an array occurs inside the nested table, we only look for the first match (see below)
$dataTableMap = $table->getEmptyClone();
foreach ($table->getDataTables() as $label => $subTable) {
$newSubTable = $this->doFilterPageDatatableSearch($callBackParameters, $subTable, $searchTree);
$dataTableMap->addTable($newSubTable, $label);
}
return $dataTableMap;
}
}
return $this->doFilterPageDatatableSearch($callBackParameters, $table, $searchTree);
}
/**
* This looks very similar to LabelFilter.php should it be refactored somehow? FIXME
*/
protected function doFilterPageDatatableSearch($callBackParameters, $table, $searchTree)
{
// filter a data table array
if ($table instanceof DataTable\Map) {
foreach ($table->getDataTables() as $subTable) {
$filteredSubTable = $this->doFilterPageDatatableSearch($callBackParameters, $subTable, $searchTree);
if ($filteredSubTable->getRowsCount() > 0) {
// match found in a sub table, return and stop searching the others
return $filteredSubTable;
}
}
// nothing found in all sub tables
return new DataTable;
}
// filter regular data table
if ($table instanceof DataTable) {
// search for the first part of the tree search
$search = array_shift($searchTree);
$row = $table->getRowFromLabel($search);
if ($row === false) {
// not found
$result = new DataTable;
$result->setAllTableMetadata($table->getAllTableMetadata());
return $result;
}
// end of tree search reached
if (count($searchTree) == 0) {
$result = new DataTable();
$result->addRow($row);
$result->setAllTableMetadata($table->getAllTableMetadata());
return $result;
}
// match found on this level and more levels remaining: go deeper
$idSubTable = $row->getIdSubDataTable();
$callBackParameters[6] = $idSubTable;
$table = call_user_func_array(array($this, 'getDataTableFromArchive'), $callBackParameters);
return $this->doFilterPageDatatableSearch($callBackParameters, $table, $searchTree);
}
throw new Exception("For this API function, DataTable " . get_class($table) . " is not supported");
}
/**
* Common filters for Page URLs and Page Titles
*
* @param DataTable|DataTable\Simple|DataTable\Map $dataTable
*/
protected function filterPageDatatable($dataTable)
{
$columnsToRemove = array('bounce_rate');
$dataTable->queueFilter('ColumnDelete', array($columnsToRemove));
// Average time on page = total time on page / number visits on that page
$dataTable->queueFilter('ColumnCallbackAddColumnQuotient',
array('avg_time_on_page',
'sum_time_spent',
'nb_visits',
0)
);
// Bounce rate = single page visits on this page / visits started on this page
$dataTable->queueFilter('ColumnCallbackAddColumnPercentage',
array('bounce_rate',
'entry_bounce_count',
'entry_nb_visits',
0));
// % Exit = Number of visits that finished on this page / visits on this page
$dataTable->queueFilter('ColumnCallbackAddColumnPercentage',
array('exit_rate',
'exit_nb_visits',
'nb_visits',
0)
);
// Handle performance analytics
$hasTimeGeneration = (array_sum($dataTable->getColumn(Metrics::INDEX_PAGE_SUM_TIME_GENERATION)) > 0);
if ($hasTimeGeneration) {
// Average generation time = total generation time / number of pageviews
$precisionAvgTimeGeneration = 3;
$dataTable->queueFilter('ColumnCallbackAddColumnQuotient',
array('avg_time_generation',
'sum_time_generation',
'nb_hits_with_time_generation',
$precisionAvgTimeGeneration)
);
$dataTable->queueFilter('ColumnDelete', array(array('sum_time_generation')));
} else {
// No generation time: remove it from the API output and add it to empty_columns metadata, so that
// the columns can also be removed from the view
$dataTable->filter('ColumnDelete', array(array(
Metrics::INDEX_PAGE_SUM_TIME_GENERATION,
Metrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION,
Metrics::INDEX_PAGE_MIN_TIME_GENERATION,
Metrics::INDEX_PAGE_MAX_TIME_GENERATION
)));
if ($dataTable instanceof DataTable) {
$emptyColumns = $dataTable->getMetadata(DataTable::EMPTY_COLUMNS_METADATA_NAME);
if (!is_array($emptyColumns)) {
$emptyColumns = array();
}
$emptyColumns[] = 'sum_time_generation';
$emptyColumns[] = 'avg_time_generation';
$emptyColumns[] = 'min_time_generation';
$emptyColumns[] = 'max_time_generation';
$dataTable->setMetadata(DataTable::EMPTY_COLUMNS_METADATA_NAME, $emptyColumns);
}
}
}
/**
* Common filters for all Actions API
*
* @param DataTable|DataTable\Simple|DataTable\Map $dataTable
* @param bool $expanded
*/
protected function filterActionsDataTable($dataTable, $expanded = false)
{
// Must be applied before Sort in this case, since the DataTable can contain both int and strings indexes
// (in the transition period between pre 1.2 and post 1.2 datatable structure)
$dataTable->filter('ReplaceColumnNames');
$dataTable->filter('Sort', array('nb_visits', 'desc', $naturalSort = false, $expanded));
$dataTable->queueFilter('ReplaceSummaryRowLabel');
}
/**
* Removes DataTable rows referencing actions that were never the first action of a visit.
*
* @param DataTable $dataTable
*/
private function filterNonEntryActions($dataTable)
{
$dataTable->filter('ColumnCallbackDeleteRow',
array('entry_nb_visits',
function ($visits) {
return !strlen($visits);
}
)
);
}
/**
* Removes DataTable rows referencing actions that were never the last action of a visit.
*
* @param DataTable $dataTable
*/
private function filterNonExitActions($dataTable)
{
$dataTable->filter('ColumnCallbackDeleteRow',
array('exit_nb_visits',
function ($visits) {
return !strlen($visits);
})
);
}
protected function getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded = false, $idSubtable = null, $depth = null)
{
$skipAggregationOfSubTables = false;
if ($period == 'range'
&& empty($idSubtable)
&& empty($expanded)
&& !Request::shouldLoadFlatten()
) {
$skipAggregationOfSubTables = false;
}
return Archive::getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded, $idSubtable, $skipAggregationOfSubTables, $depth);
}
}

View file

@ -0,0 +1,937 @@
<?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\Plugins\Actions;
use Piwik\API\Request;
use Piwik\ArchiveProcessor;
use Piwik\Common;
use Piwik\Db;
use Piwik\Menu\MenuMain;
use Piwik\MetricsFormatter;
use Piwik\Piwik;
use Piwik\Plugin\ViewDataTable;
use Piwik\Plugins\CoreVisualizations\Visualizations\HtmlTable;
use Piwik\Site;
use Piwik\WidgetsList;
/**
* Actions plugin
*
* Reports about the page views, the outlinks and downloads.
*
*/
class Actions extends \Piwik\Plugin
{
const ACTIONS_REPORT_ROWS_DISPLAY = 100;
/**
* @see Piwik\Plugin::getListHooksRegistered
*/
public function getListHooksRegistered()
{
$hooks = array(
'WidgetsList.addWidgets' => 'addWidgets',
'Menu.Reporting.addItems' => 'addMenus',
'API.getReportMetadata' => 'getReportMetadata',
'API.getSegmentDimensionMetadata' => 'getSegmentsMetadata',
'ViewDataTable.configure' => 'configureViewDataTable',
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'Insights.addReportToOverview' => 'addReportToInsightsOverview'
);
return $hooks;
}
public function addReportToInsightsOverview(&$reports)
{
$reports['Actions_getPageUrls'] = array();
$reports['Actions_getPageTitles'] = array();
$reports['Actions_getDownloads'] = array('flat' => 1);
}
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = "plugins/Actions/stylesheets/dataTableActions.less";
}
public function getJsFiles(&$jsFiles)
{
$jsFiles[] = "plugins/Actions/javascripts/actionsDataTable.js";
}
public function getSegmentsMetadata(&$segments)
{
$sqlFilter = '\\Piwik\\Tracker\\TableLogAction::getIdActionFromSegment';
// entry and exit pages of visit
$segments[] = array(
'type' => 'dimension',
'category' => 'General_Actions',
'name' => 'Actions_ColumnEntryPageURL',
'segment' => 'entryPageUrl',
'sqlSegment' => 'log_visit.visit_entry_idaction_url',
'sqlFilter' => $sqlFilter,
);
$segments[] = array(
'type' => 'dimension',
'category' => 'General_Actions',
'name' => 'Actions_ColumnEntryPageTitle',
'segment' => 'entryPageTitle',
'sqlSegment' => 'log_visit.visit_entry_idaction_name',
'sqlFilter' => $sqlFilter,
);
$segments[] = array(
'type' => 'dimension',
'category' => 'General_Actions',
'name' => 'Actions_ColumnExitPageURL',
'segment' => 'exitPageUrl',
'sqlSegment' => 'log_visit.visit_exit_idaction_url',
'sqlFilter' => $sqlFilter,
);
$segments[] = array(
'type' => 'dimension',
'category' => 'General_Actions',
'name' => 'Actions_ColumnExitPageTitle',
'segment' => 'exitPageTitle',
'sqlSegment' => 'log_visit.visit_exit_idaction_name',
'sqlFilter' => $sqlFilter,
);
// single pages
$segments[] = array(
'type' => 'dimension',
'category' => 'General_Actions',
'name' => 'Actions_ColumnPageURL',
'segment' => 'pageUrl',
'sqlSegment' => 'log_link_visit_action.idaction_url',
'sqlFilter' => $sqlFilter,
'acceptedValues' => "All these segments must be URL encoded, for example: " . urlencode('http://example.com/path/page?query'),
);
$segments[] = array(
'type' => 'dimension',
'category' => 'General_Actions',
'name' => 'Actions_ColumnPageName',
'segment' => 'pageTitle',
'sqlSegment' => 'log_link_visit_action.idaction_name',
'sqlFilter' => $sqlFilter,
);
$segments[] = array(
'type' => 'dimension',
'category' => 'General_Actions',
'name' => 'Actions_SiteSearchKeyword',
'segment' => 'siteSearchKeyword',
'sqlSegment' => 'log_link_visit_action.idaction_name',
'sqlFilter' => $sqlFilter,
);
}
public function getReportMetadata(&$reports)
{
$reports[] = array(
'category' => Piwik::translate('General_Actions'),
'name' => Piwik::translate('General_Actions') . ' - ' . Piwik::translate('General_MainMetrics'),
'module' => 'Actions',
'action' => 'get',
'metrics' => array(
'nb_pageviews' => Piwik::translate('General_ColumnPageviews'),
'nb_uniq_pageviews' => Piwik::translate('General_ColumnUniquePageviews'),
'nb_downloads' => Piwik::translate('General_Downloads'),
'nb_uniq_downloads' => Piwik::translate('Actions_ColumnUniqueDownloads'),
'nb_outlinks' => Piwik::translate('General_Outlinks'),
'nb_uniq_outlinks' => Piwik::translate('Actions_ColumnUniqueOutlinks'),
'nb_searches' => Piwik::translate('Actions_ColumnSearches'),
'nb_keywords' => Piwik::translate('Actions_ColumnSiteSearchKeywords'),
'avg_time_generation' => Piwik::translate('General_ColumnAverageGenerationTime'),
),
'metricsDocumentation' => array(
'nb_pageviews' => Piwik::translate('General_ColumnPageviewsDocumentation'),
'nb_uniq_pageviews' => Piwik::translate('General_ColumnUniquePageviewsDocumentation'),
'nb_downloads' => Piwik::translate('Actions_ColumnClicksDocumentation'),
'nb_uniq_downloads' => Piwik::translate('Actions_ColumnUniqueClicksDocumentation'),
'nb_outlinks' => Piwik::translate('Actions_ColumnClicksDocumentation'),
'nb_uniq_outlinks' => Piwik::translate('Actions_ColumnUniqueClicksDocumentation'),
'nb_searches' => Piwik::translate('Actions_ColumnSearchesDocumentation'),
'avg_time_generation' => Piwik::translate('General_ColumnAverageGenerationTimeDocumentation'),
// 'nb_keywords' => Piwik::translate('Actions_ColumnSiteSearchKeywords'),
),
'processedMetrics' => false,
'order' => 1
);
$metrics = array(
'nb_hits' => Piwik::translate('General_ColumnPageviews'),
'nb_visits' => Piwik::translate('General_ColumnUniquePageviews'),
'bounce_rate' => Piwik::translate('General_ColumnBounceRate'),
'avg_time_on_page' => Piwik::translate('General_ColumnAverageTimeOnPage'),
'exit_rate' => Piwik::translate('General_ColumnExitRate'),
'avg_time_generation' => Piwik::translate('General_ColumnAverageGenerationTime')
);
$documentation = array(
'nb_hits' => Piwik::translate('General_ColumnPageviewsDocumentation'),
'nb_visits' => Piwik::translate('General_ColumnUniquePageviewsDocumentation'),
'bounce_rate' => Piwik::translate('General_ColumnPageBounceRateDocumentation'),
'avg_time_on_page' => Piwik::translate('General_ColumnAverageTimeOnPageDocumentation'),
'exit_rate' => Piwik::translate('General_ColumnExitRateDocumentation'),
'avg_time_generation' => Piwik::translate('General_ColumnAverageGenerationTimeDocumentation'),
);
// pages report
$reports[] = array(
'category' => Piwik::translate('General_Actions'),
'name' => Piwik::translate('Actions_PageUrls'),
'module' => 'Actions',
'action' => 'getPageUrls',
'dimension' => Piwik::translate('Actions_ColumnPageURL'),
'metrics' => $metrics,
'metricsDocumentation' => $documentation,
'documentation' => Piwik::translate('Actions_PagesReportDocumentation', '<br />')
. '<br />' . Piwik::translate('General_UsePlusMinusIconsDocumentation'),
'processedMetrics' => false,
'actionToLoadSubTables' => 'getPageUrls',
'order' => 2
);
// entry pages report
$reports[] = array(
'category' => Piwik::translate('General_Actions'),
'name' => Piwik::translate('Actions_SubmenuPagesEntry'),
'module' => 'Actions',
'action' => 'getEntryPageUrls',
'dimension' => Piwik::translate('Actions_ColumnPageURL'),
'metrics' => array(
'entry_nb_visits' => Piwik::translate('General_ColumnEntrances'),
'entry_bounce_count' => Piwik::translate('General_ColumnBounces'),
'bounce_rate' => Piwik::translate('General_ColumnBounceRate'),
),
'metricsDocumentation' => array(
'entry_nb_visits' => Piwik::translate('General_ColumnEntrancesDocumentation'),
'entry_bounce_count' => Piwik::translate('General_ColumnBouncesDocumentation'),
'bounce_rate' => Piwik::translate('General_ColumnBounceRateForPageDocumentation')
),
'documentation' => Piwik::translate('Actions_EntryPagesReportDocumentation', '<br />')
. ' ' . Piwik::translate('General_UsePlusMinusIconsDocumentation'),
'processedMetrics' => false,
'actionToLoadSubTables' => 'getEntryPageUrls',
'order' => 3
);
// exit pages report
$reports[] = array(
'category' => Piwik::translate('General_Actions'),
'name' => Piwik::translate('Actions_SubmenuPagesExit'),
'module' => 'Actions',
'action' => 'getExitPageUrls',
'dimension' => Piwik::translate('Actions_ColumnPageURL'),
'metrics' => array(
'exit_nb_visits' => Piwik::translate('General_ColumnExits'),
'nb_visits' => Piwik::translate('General_ColumnUniquePageviews'),
'exit_rate' => Piwik::translate('General_ColumnExitRate')
),
'metricsDocumentation' => array(
'exit_nb_visits' => Piwik::translate('General_ColumnExitsDocumentation'),
'nb_visits' => Piwik::translate('General_ColumnUniquePageviewsDocumentation'),
'exit_rate' => Piwik::translate('General_ColumnExitRateDocumentation')
),
'documentation' => Piwik::translate('Actions_ExitPagesReportDocumentation', '<br />')
. ' ' . Piwik::translate('General_UsePlusMinusIconsDocumentation'),
'processedMetrics' => false,
'actionToLoadSubTables' => 'getExitPageUrls',
'order' => 4
);
// page titles report
$reports[] = array(
'category' => Piwik::translate('General_Actions'),
'name' => Piwik::translate('Actions_SubmenuPageTitles'),
'module' => 'Actions',
'action' => 'getPageTitles',
'dimension' => Piwik::translate('Actions_ColumnPageName'),
'metrics' => $metrics,
'metricsDocumentation' => $documentation,
'documentation' => Piwik::translate('Actions_PageTitlesReportDocumentation', array('<br />', htmlentities('<title>'))),
'processedMetrics' => false,
'actionToLoadSubTables' => 'getPageTitles',
'order' => 5,
);
// entry page titles report
$reports[] = array(
'category' => Piwik::translate('General_Actions'),
'name' => Piwik::translate('Actions_EntryPageTitles'),
'module' => 'Actions',
'action' => 'getEntryPageTitles',
'dimension' => Piwik::translate('Actions_ColumnPageName'),
'metrics' => array(
'entry_nb_visits' => Piwik::translate('General_ColumnEntrances'),
'entry_bounce_count' => Piwik::translate('General_ColumnBounces'),
'bounce_rate' => Piwik::translate('General_ColumnBounceRate'),
),
'metricsDocumentation' => array(
'entry_nb_visits' => Piwik::translate('General_ColumnEntrancesDocumentation'),
'entry_bounce_count' => Piwik::translate('General_ColumnBouncesDocumentation'),
'bounce_rate' => Piwik::translate('General_ColumnBounceRateForPageDocumentation')
),
'documentation' => Piwik::translate('Actions_ExitPageTitlesReportDocumentation', '<br />')
. ' ' . Piwik::translate('General_UsePlusMinusIconsDocumentation'),
'processedMetrics' => false,
'actionToLoadSubTables' => 'getEntryPageTitles',
'order' => 6
);
// exit page titles report
$reports[] = array(
'category' => Piwik::translate('General_Actions'),
'name' => Piwik::translate('Actions_ExitPageTitles'),
'module' => 'Actions',
'action' => 'getExitPageTitles',
'dimension' => Piwik::translate('Actions_ColumnPageName'),
'metrics' => array(
'exit_nb_visits' => Piwik::translate('General_ColumnExits'),
'nb_visits' => Piwik::translate('General_ColumnUniquePageviews'),
'exit_rate' => Piwik::translate('General_ColumnExitRate')
),
'metricsDocumentation' => array(
'exit_nb_visits' => Piwik::translate('General_ColumnExitsDocumentation'),
'nb_visits' => Piwik::translate('General_ColumnUniquePageviewsDocumentation'),
'exit_rate' => Piwik::translate('General_ColumnExitRateDocumentation')
),
'documentation' => Piwik::translate('Actions_EntryPageTitlesReportDocumentation', '<br />')
. ' ' . Piwik::translate('General_UsePlusMinusIconsDocumentation'),
'processedMetrics' => false,
'actionToLoadSubTables' => 'getExitPageTitles',
'order' => 7
);
$documentation = array(
'nb_visits' => Piwik::translate('Actions_ColumnUniqueClicksDocumentation'),
'nb_hits' => Piwik::translate('Actions_ColumnClicksDocumentation')
);
// outlinks report
$reports[] = array(
'category' => Piwik::translate('General_Actions'),
'name' => Piwik::translate('General_Outlinks'),
'module' => 'Actions',
'action' => 'getOutlinks',
'dimension' => Piwik::translate('Actions_ColumnClickedURL'),
'metrics' => array(
'nb_visits' => Piwik::translate('Actions_ColumnUniqueClicks'),
'nb_hits' => Piwik::translate('Actions_ColumnClicks')
),
'metricsDocumentation' => $documentation,
'documentation' => Piwik::translate('Actions_OutlinksReportDocumentation') . ' '
. Piwik::translate('Actions_OutlinkDocumentation') . '<br />'
. Piwik::translate('General_UsePlusMinusIconsDocumentation'),
'processedMetrics' => false,
'actionToLoadSubTables' => 'getOutlinks',
'order' => 8,
);
// downloads report
$reports[] = array(
'category' => Piwik::translate('General_Actions'),
'name' => Piwik::translate('General_Downloads'),
'module' => 'Actions',
'action' => 'getDownloads',
'dimension' => Piwik::translate('Actions_ColumnDownloadURL'),
'metrics' => array(
'nb_visits' => Piwik::translate('Actions_ColumnUniqueDownloads'),
'nb_hits' => Piwik::translate('General_Downloads')
),
'metricsDocumentation' => $documentation,
'documentation' => Piwik::translate('Actions_DownloadsReportDocumentation', '<br />'),
'processedMetrics' => false,
'actionToLoadSubTables' => 'getDownloads',
'order' => 9,
);
if ($this->isSiteSearchEnabled()) {
// Search Keywords
$reports[] = array(
'category' => Piwik::translate('Actions_SubmenuSitesearch'),
'name' => Piwik::translate('Actions_WidgetSearchKeywords'),
'module' => 'Actions',
'action' => 'getSiteSearchKeywords',
'dimension' => Piwik::translate('General_ColumnKeyword'),
'metrics' => array(
'nb_visits' => Piwik::translate('Actions_ColumnSearches'),
'nb_pages_per_search' => Piwik::translate('Actions_ColumnPagesPerSearch'),
'exit_rate' => Piwik::translate('Actions_ColumnSearchExits'),
),
'metricsDocumentation' => array(
'nb_visits' => Piwik::translate('Actions_ColumnSearchesDocumentation'),
'nb_pages_per_search' => Piwik::translate('Actions_ColumnPagesPerSearchDocumentation'),
'exit_rate' => Piwik::translate('Actions_ColumnSearchExitsDocumentation'),
),
'documentation' => Piwik::translate('Actions_SiteSearchKeywordsDocumentation') . '<br/><br/>' . Piwik::translate('Actions_SiteSearchIntro') . '<br/><br/>'
. '<a href="http://piwik.org/docs/site-search/" target="_blank">' . Piwik::translate('Actions_LearnMoreAboutSiteSearchLink') . '</a>',
'processedMetrics' => false,
'order' => 15
);
// No Result Search Keywords
$reports[] = array(
'category' => Piwik::translate('Actions_SubmenuSitesearch'),
'name' => Piwik::translate('Actions_WidgetSearchNoResultKeywords'),
'module' => 'Actions',
'action' => 'getSiteSearchNoResultKeywords',
'dimension' => Piwik::translate('Actions_ColumnNoResultKeyword'),
'metrics' => array(
'nb_visits' => Piwik::translate('Actions_ColumnSearches'),
'exit_rate' => Piwik::translate('Actions_ColumnSearchExits'),
),
'metricsDocumentation' => array(
'nb_visits' => Piwik::translate('Actions_ColumnSearchesDocumentation'),
'exit_rate' => Piwik::translate('Actions_ColumnSearchExitsDocumentation'),
),
'documentation' => Piwik::translate('Actions_SiteSearchIntro') . '<br /><br />' . Piwik::translate('Actions_SiteSearchKeywordsNoResultDocumentation'),
'processedMetrics' => false,
'order' => 16
);
if (self::isCustomVariablesPluginsEnabled()) {
// Search Categories
$reports[] = array(
'category' => Piwik::translate('Actions_SubmenuSitesearch'),
'name' => Piwik::translate('Actions_WidgetSearchCategories'),
'module' => 'Actions',
'action' => 'getSiteSearchCategories',
'dimension' => Piwik::translate('Actions_ColumnSearchCategory'),
'metrics' => array(
'nb_visits' => Piwik::translate('Actions_ColumnSearches'),
'nb_pages_per_search' => Piwik::translate('Actions_ColumnPagesPerSearch'),
'exit_rate' => Piwik::translate('Actions_ColumnSearchExits'),
),
'metricsDocumentation' => array(
'nb_visits' => Piwik::translate('Actions_ColumnSearchesDocumentation'),
'nb_pages_per_search' => Piwik::translate('Actions_ColumnPagesPerSearchDocumentation'),
'exit_rate' => Piwik::translate('Actions_ColumnSearchExitsDocumentation'),
),
'documentation' => Piwik::translate('Actions_SiteSearchCategories1') . '<br/>' . Piwik::translate('Actions_SiteSearchCategories2'),
'processedMetrics' => false,
'order' => 17
);
}
$documentation = Piwik::translate('Actions_SiteSearchFollowingPagesDoc') . '<br/>' . Piwik::translate('General_UsePlusMinusIconsDocumentation');
// Pages URLs following Search
$reports[] = array(
'category' => Piwik::translate('Actions_SubmenuSitesearch'),
'name' => Piwik::translate('Actions_WidgetPageUrlsFollowingSearch'),
'module' => 'Actions',
'action' => 'getPageUrlsFollowingSiteSearch',
'dimension' => Piwik::translate('General_ColumnDestinationPage'),
'metrics' => array(
'nb_hits_following_search' => Piwik::translate('General_ColumnViewedAfterSearch'),
'nb_hits' => Piwik::translate('General_ColumnTotalPageviews'),
),
'metricsDocumentation' => array(
'nb_hits_following_search' => Piwik::translate('General_ColumnViewedAfterSearchDocumentation'),
'nb_hits' => Piwik::translate('General_ColumnPageviewsDocumentation'),
),
'documentation' => $documentation,
'processedMetrics' => false,
'order' => 18
);
// Pages Titles following Search
$reports[] = array(
'category' => Piwik::translate('Actions_SubmenuSitesearch'),
'name' => Piwik::translate('Actions_WidgetPageTitlesFollowingSearch'),
'module' => 'Actions',
'action' => 'getPageTitlesFollowingSiteSearch',
'dimension' => Piwik::translate('General_ColumnDestinationPage'),
'metrics' => array(
'nb_hits_following_search' => Piwik::translate('General_ColumnViewedAfterSearch'),
'nb_hits' => Piwik::translate('General_ColumnTotalPageviews'),
),
'metricsDocumentation' => array(
'nb_hits_following_search' => Piwik::translate('General_ColumnViewedAfterSearchDocumentation'),
'nb_hits' => Piwik::translate('General_ColumnPageviewsDocumentation'),
),
'documentation' => $documentation,
'processedMetrics' => false,
'order' => 19
);
}
}
function addWidgets()
{
WidgetsList::add('General_Actions', 'General_Pages', 'Actions', 'getPageUrls');
WidgetsList::add('General_Actions', 'Actions_WidgetPageTitles', 'Actions', 'getPageTitles');
WidgetsList::add('General_Actions', 'General_Outlinks', 'Actions', 'getOutlinks');
WidgetsList::add('General_Actions', 'General_Downloads', 'Actions', 'getDownloads');
WidgetsList::add('General_Actions', 'Actions_WidgetPagesEntry', 'Actions', 'getEntryPageUrls');
WidgetsList::add('General_Actions', 'Actions_WidgetPagesExit', 'Actions', 'getExitPageUrls');
WidgetsList::add('General_Actions', 'Actions_WidgetEntryPageTitles', 'Actions', 'getEntryPageTitles');
WidgetsList::add('General_Actions', 'Actions_WidgetExitPageTitles', 'Actions', 'getExitPageTitles');
if ($this->isSiteSearchEnabled()) {
WidgetsList::add('Actions_SubmenuSitesearch', 'Actions_WidgetSearchKeywords', 'Actions', 'getSiteSearchKeywords');
if (self::isCustomVariablesPluginsEnabled()) {
WidgetsList::add('Actions_SubmenuSitesearch', 'Actions_WidgetSearchCategories', 'Actions', 'getSiteSearchCategories');
}
WidgetsList::add('Actions_SubmenuSitesearch', 'Actions_WidgetSearchNoResultKeywords', 'Actions', 'getSiteSearchNoResultKeywords');
WidgetsList::add('Actions_SubmenuSitesearch', 'Actions_WidgetPageUrlsFollowingSearch', 'Actions', 'getPageUrlsFollowingSiteSearch');
WidgetsList::add('Actions_SubmenuSitesearch', 'Actions_WidgetPageTitlesFollowingSearch', 'Actions', 'getPageTitlesFollowingSiteSearch');
}
}
function addMenus()
{
MenuMain::getInstance()->add('General_Actions', '', array('module' => 'Actions', 'action' => 'indexPageUrls'), true, 15);
MenuMain::getInstance()->add('General_Actions', 'General_Pages', array('module' => 'Actions', 'action' => 'indexPageUrls'), true, 1);
MenuMain::getInstance()->add('General_Actions', 'Actions_SubmenuPagesEntry', array('module' => 'Actions', 'action' => 'indexEntryPageUrls'), true, 2);
MenuMain::getInstance()->add('General_Actions', 'Actions_SubmenuPagesExit', array('module' => 'Actions', 'action' => 'indexExitPageUrls'), true, 3);
MenuMain::getInstance()->add('General_Actions', 'Actions_SubmenuPageTitles', array('module' => 'Actions', 'action' => 'indexPageTitles'), true, 4);
MenuMain::getInstance()->add('General_Actions', 'General_Outlinks', array('module' => 'Actions', 'action' => 'indexOutlinks'), true, 6);
MenuMain::getInstance()->add('General_Actions', 'General_Downloads', array('module' => 'Actions', 'action' => 'indexDownloads'), true, 7);
if ($this->isSiteSearchEnabled()) {
MenuMain::getInstance()->add('General_Actions', 'Actions_SubmenuSitesearch', array('module' => 'Actions', 'action' => 'indexSiteSearch'), true, 5);
}
}
protected function isSiteSearchEnabled()
{
$idSite = Common::getRequestVar('idSite', 0, 'int');
$idSites = Common::getRequestVar('idSites', '', 'string');
$idSites = Site::getIdSitesFromIdSitesString($idSites, true);
if (!empty($idSite)) {
$idSites[] = $idSite;
}
if (empty($idSites)) {
return false;
}
foreach ($idSites as $idSite) {
if (!Site::isSiteSearchEnabledFor($idSite)) {
return false;
}
}
return true;
}
static public function checkCustomVariablesPluginEnabled()
{
if (!self::isCustomVariablesPluginsEnabled()) {
throw new \Exception("To Track Site Search Categories, please ask the Piwik Administrator to enable the 'Custom Variables' plugin in Settings > Plugins.");
}
}
static protected function isCustomVariablesPluginsEnabled()
{
return \Piwik\Plugin\Manager::getInstance()->isPluginActivated('CustomVariables');
}
public function configureViewDataTable(ViewDataTable $view)
{
switch ($view->requestConfig->apiMethodToRequestDataTable) {
case 'Actions.getPageUrls':
$this->configureViewForPageUrls($view);
break;
case 'Actions.getEntryPageUrls':
$this->configureViewForEntryPageUrls($view);
break;
case 'Actions.getExitPageUrls':
$this->configureViewForExitPageUrls($view);
break;
case 'Actions.getSiteSearchKeywords':
$this->configureViewForSiteSearchKeywords($view);
break;
case 'Actions.getSiteSearchNoResultKeywords':
$this->configureViewForSiteSearchNoResultKeywords($view);
break;
case 'Actions.getSiteSearchCategories':
$this->configureViewForSiteSearchCategories($view);
break;
case 'Actions.getPageUrlsFollowingSiteSearch':
$this->configureViewForGetPageUrlsOrTitlesFollowingSiteSearch($view, false);
break;
case 'Actions.getPageTitlesFollowingSiteSearch':
$this->configureViewForGetPageUrlsOrTitlesFollowingSiteSearch($view, true);
break;
case 'Actions.getPageTitles':
$this->configureViewForGetPageTitles($view);
break;
case 'Actions.getEntryPageTitles':
$this->configureViewForGetEntryPageTitles($view);
break;
case 'Actions.getExitPageTitles':
$this->configureViewForGetExitPageTitles($view);
break;
case 'Actions.getDownloads':
$this->configureViewForGetDownloads($view);
break;
case 'Actions.getOutlinks':
$this->configureViewForGetOutlinks($view);
break;
}
if ($this->pluginName == $view->requestConfig->getApiModuleToRequest()) {
if ($view->isRequestingSingleDataTable()) {
// make sure custom visualizations are shown on actions reports
$view->config->show_all_views_icons = true;
$view->config->show_bar_chart = false;
$view->config->show_pie_chart = false;
$view->config->show_tag_cloud = false;
}
}
}
private function addBaseDisplayProperties(ViewDataTable $view)
{
$view->config->datatable_js_type = 'ActionsDataTable';
$view->config->search_recursive = true;
$view->config->show_table_all_columns = false;
$view->requestConfig->filter_limit = self::ACTIONS_REPORT_ROWS_DISPLAY;
$view->config->show_all_views_icons = false;
if ($view->isViewDataTableId(HtmlTable::ID)) {
$view->config->show_embedded_subtable = true;
}
// if the flat parameter is not provided, make sure it is set to 0 in the URL,
// so users can see that they can set it to 1 (see #3365)
$view->config->custom_parameters = array('flat' => 0);
if (Request::shouldLoadExpanded()) {
if ($view->isViewDataTableId(HtmlTable::ID)) {
$view->config->show_expanded = true;
}
$view->config->filters[] = function ($dataTable) {
Actions::setDataTableRowLevels($dataTable);
};
}
$view->config->filters[] = function ($dataTable) use ($view) {
if ($view->isViewDataTableId(HtmlTable::ID)) {
$view->config->datatable_css_class = 'dataTableActions';
}
};
}
/**
* @param \Piwik\DataTable $dataTable
* @param int $level
*/
public static function setDataTableRowLevels($dataTable, $level = 0)
{
foreach ($dataTable->getRows() as $row) {
$row->setMetadata('css_class', 'level' . $level);
$subtable = $row->getSubtable();
if ($subtable) {
self::setDataTableRowLevels($subtable, $level + 1);
}
}
}
private function addExcludeLowPopDisplayProperties(ViewDataTable $view)
{
if (Common::getRequestVar('enable_filter_excludelowpop', '0', 'string') != '0') {
$view->requestConfig->filter_excludelowpop = 'nb_hits';
$view->requestConfig->filter_excludelowpop_value = function () {
// computing minimum value to exclude (2 percent of the total number of actions)
$visitsInfo = \Piwik\Plugins\VisitsSummary\Controller::getVisitsSummary()->getFirstRow();
$nbActions = $visitsInfo->getColumn('nb_actions');
$nbActionsLowPopulationThreshold = floor(0.02 * $nbActions);
// we remove 1 to make sure some actions/downloads are displayed in the case we have a very few of them
// and each of them has 1 or 2 hits...
return min($visitsInfo->getColumn('max_actions') - 1, $nbActionsLowPopulationThreshold - 1);
};
}
}
private function addPageDisplayProperties(ViewDataTable $view)
{
$view->config->addTranslations(array(
'nb_hits' => Piwik::translate('General_ColumnPageviews'),
'nb_visits' => Piwik::translate('General_ColumnUniquePageviews'),
'avg_time_on_page' => Piwik::translate('General_ColumnAverageTimeOnPage'),
'bounce_rate' => Piwik::translate('General_ColumnBounceRate'),
'exit_rate' => Piwik::translate('General_ColumnExitRate'),
'avg_time_generation' => Piwik::translate('General_ColumnAverageGenerationTime'),
));
// prettify avg_time_on_page column
$getPrettyTimeFromSeconds = '\Piwik\MetricsFormatter::getPrettyTimeFromSeconds';
$view->config->filters[] = array('ColumnCallbackReplace', array('avg_time_on_page', $getPrettyTimeFromSeconds));
// prettify avg_time_generation column
$avgTimeCallback = function ($time) {
return $time ? MetricsFormatter::getPrettyTimeFromSeconds($time, true, true, false) : "-";
};
$view->config->filters[] = array('ColumnCallbackReplace', array('avg_time_generation', $avgTimeCallback));
// add avg_generation_time tooltip
$tooltipCallback = function ($hits, $min, $max) {
if (!$hits) {
return false;
}
return Piwik::translate("Actions_AvgGenerationTimeTooltip", array(
$hits,
"<br />",
MetricsFormatter::getPrettyTimeFromSeconds($min),
MetricsFormatter::getPrettyTimeFromSeconds($max)
));
};
$view->config->filters[] = array('ColumnCallbackAddMetadata',
array(
array('nb_hits_with_time_generation', 'min_time_generation', 'max_time_generation'),
'avg_time_generation_tooltip',
$tooltipCallback
)
);
$this->addExcludeLowPopDisplayProperties($view);
}
public function configureViewForPageUrls(ViewDataTable $view)
{
$view->config->addTranslation('label', Piwik::translate('Actions_ColumnPageURL'));
$view->config->columns_to_display = array('label', 'nb_hits', 'nb_visits', 'bounce_rate',
'avg_time_on_page', 'exit_rate', 'avg_time_generation');
$this->addPageDisplayProperties($view);
$this->addBaseDisplayProperties($view);
}
public function configureViewForEntryPageUrls(ViewDataTable $view)
{
// link to the page, not just the report, but only if not a widget
$widget = Common::getRequestVar('widget', false);
$view->config->self_url = Request::getCurrentUrlWithoutGenericFilters(array(
'module' => 'Actions',
'action' => $widget === false ? 'indexEntryPageUrls' : 'getEntryPageUrls'
));
$view->config->addTranslations(array(
'label' => Piwik::translate('Actions_ColumnEntryPageURL'),
'entry_bounce_count' => Piwik::translate('General_ColumnBounces'),
'entry_nb_visits' => Piwik::translate('General_ColumnEntrances'))
);
$view->config->title = Piwik::translate('Actions_SubmenuPagesEntry');
$view->config->addRelatedReport('Actions.getEntryPageTitles', Piwik::translate('Actions_EntryPageTitles'));
$view->config->columns_to_display = array('label', 'entry_nb_visits', 'entry_bounce_count', 'bounce_rate');
$view->requestConfig->filter_sort_column = 'entry_nb_visits';
$view->requestConfig->filter_sort_order = 'desc';
$this->addPageDisplayProperties($view);
$this->addBaseDisplayProperties($view);
}
public function configureViewForExitPageUrls(ViewDataTable $view)
{
// link to the page, not just the report, but only if not a widget
$widget = Common::getRequestVar('widget', false);
$view->config->self_url = Request::getCurrentUrlWithoutGenericFilters(array(
'module' => 'Actions',
'action' => $widget === false ? 'indexExitPageUrls' : 'getExitPageUrls'
));
$view->config->addTranslations(array(
'label' => Piwik::translate('Actions_ColumnExitPageURL'),
'exit_nb_visits' => Piwik::translate('General_ColumnExits'))
);
$view->config->title = Piwik::translate('Actions_SubmenuPagesExit');
$view->config->addRelatedReport('Actions.getExitPageTitles', Piwik::translate('Actions_ExitPageTitles'));
$view->config->columns_to_display = array('label', 'exit_nb_visits', 'nb_visits', 'exit_rate');
$view->requestConfig->filter_sort_column = 'exit_nb_visits';
$view->requestConfig->filter_sort_order = 'desc';
$this->addPageDisplayProperties($view);
$this->addBaseDisplayProperties($view);
}
private function addSiteSearchDisplayProperties(ViewDataTable $view)
{
$view->config->addTranslations(array(
'nb_visits' => Piwik::translate('Actions_ColumnSearches'),
'exit_rate' => str_replace("% ", "%&nbsp;", Piwik::translate('Actions_ColumnSearchExits')),
'nb_pages_per_search' => Piwik::translate('Actions_ColumnPagesPerSearch')
));
$view->config->show_bar_chart = false;
$view->config->show_table_all_columns = false;
}
public function configureViewForSiteSearchKeywords(ViewDataTable $view)
{
$view->config->addTranslation('label', Piwik::translate('General_ColumnKeyword'));
$view->config->columns_to_display = array('label', 'nb_visits', 'nb_pages_per_search', 'exit_rate');
$this->addSiteSearchDisplayProperties($view);
}
public function configureViewForSiteSearchNoResultKeywords(ViewDataTable $view)
{
$view->config->addTranslation('label', Piwik::translate('Actions_ColumnNoResultKeyword'));
$view->config->columns_to_display = array('label', 'nb_visits', 'exit_rate');
$this->addSiteSearchDisplayProperties($view);
}
public function configureViewForSiteSearchCategories(ViewDataTable $view)
{
$view->config->addTranslations(array(
'label' => Piwik::translate('Actions_ColumnSearchCategory'),
'nb_visits' => Piwik::translate('Actions_ColumnSearches'),
'nb_pages_per_search' => Piwik::translate('Actions_ColumnPagesPerSearch')
));
$view->config->columns_to_display = array('label', 'nb_visits', 'nb_pages_per_search');
$view->config->show_table_all_columns = false;
$view->config->show_bar_chart = false;
if ($view->isViewDataTableId(HtmlTable::ID)) {
$view->config->disable_row_evolution = false;
}
}
public function configureViewForGetPageUrlsOrTitlesFollowingSiteSearch(ViewDataTable $view, $isTitle)
{
$title = $isTitle ? Piwik::translate('Actions_WidgetPageTitlesFollowingSearch')
: Piwik::translate('Actions_WidgetPageUrlsFollowingSearch');
$relatedReports = array(
'Actions.getPageTitlesFollowingSiteSearch' => Piwik::translate('Actions_WidgetPageTitlesFollowingSearch'),
'Actions.getPageUrlsFollowingSiteSearch' => Piwik::translate('Actions_WidgetPageUrlsFollowingSearch'),
);
$view->config->addRelatedReports($relatedReports);
$view->config->addTranslations(array(
'label' => Piwik::translate('General_ColumnDestinationPage'),
'nb_hits_following_search' => Piwik::translate('General_ColumnViewedAfterSearch'),
'nb_hits' => Piwik::translate('General_ColumnTotalPageviews')
));
$view->config->title = $title;
$view->config->columns_to_display = array('label', 'nb_hits_following_search', 'nb_hits');
$view->config->show_exclude_low_population = false;
$view->requestConfig->filter_sort_column = 'nb_hits_following_search';
$view->requestConfig->filter_sort_order = 'desc';
$this->addExcludeLowPopDisplayProperties($view);
$this->addBaseDisplayProperties($view);
}
public function configureViewForGetPageTitles(ViewDataTable $view)
{
// link to the page, not just the report, but only if not a widget
$widget = Common::getRequestVar('widget', false);
$view->config->self_url = Request::getCurrentUrlWithoutGenericFilters(array(
'module' => 'Actions',
'action' => $widget === false ? 'indexPageTitles' : 'getPageTitles'
));
$view->config->title = Piwik::translate('Actions_SubmenuPageTitles');
$view->config->addRelatedReports(array(
'Actions.getEntryPageTitles' => Piwik::translate('Actions_EntryPageTitles'),
'Actions.getExitPageTitles' => Piwik::translate('Actions_ExitPageTitles'),
));
$view->config->addTranslation('label', Piwik::translate('Actions_ColumnPageName'));
$view->config->columns_to_display = array('label', 'nb_hits', 'nb_visits', 'bounce_rate',
'avg_time_on_page', 'exit_rate', 'avg_time_generation');
$this->addPageDisplayProperties($view);
$this->addBaseDisplayProperties($view);
}
public function configureViewForGetEntryPageTitles(ViewDataTable $view)
{
$entryPageUrlAction =
Common::getRequestVar('widget', false) === false ? 'indexEntryPageUrls' : 'getEntryPageUrls';
$view->config->addTranslations(array(
'label' => Piwik::translate('Actions_ColumnEntryPageTitle'),
'entry_bounce_count' => Piwik::translate('General_ColumnBounces'),
'entry_nb_visits' => Piwik::translate('General_ColumnEntrances'),
));
$view->config->addRelatedReports(array(
'Actions.getPageTitles' => Piwik::translate('Actions_SubmenuPageTitles'),
"Actions.$entryPageUrlAction" => Piwik::translate('Actions_SubmenuPagesEntry')
));
$view->config->columns_to_display = array('label', 'entry_nb_visits', 'entry_bounce_count', 'bounce_rate');
$view->config->title = Piwik::translate('Actions_EntryPageTitles');
$view->requestConfig->filter_sort_column = 'entry_nb_visits';
$this->addPageDisplayProperties($view);
$this->addBaseDisplayProperties($view);
}
public function configureViewForGetExitPageTitles(ViewDataTable $view)
{
$exitPageUrlAction =
Common::getRequestVar('widget', false) === false ? 'indexExitPageUrls' : 'getExitPageUrls';
$view->config->addTranslations(array(
'label' => Piwik::translate('Actions_ColumnExitPageTitle'),
'exit_nb_visits' => Piwik::translate('General_ColumnExits'),
));
$view->config->addRelatedReports(array(
'Actions.getPageTitles' => Piwik::translate('Actions_SubmenuPageTitles'),
"Actions.$exitPageUrlAction" => Piwik::translate('Actions_SubmenuPagesExit'),
));
$view->config->title = Piwik::translate('Actions_ExitPageTitles');
$view->config->columns_to_display = array('label', 'exit_nb_visits', 'nb_visits', 'exit_rate');
$this->addPageDisplayProperties($view);
$this->addBaseDisplayProperties($view);
}
public function configureViewForGetDownloads(ViewDataTable $view)
{
$view->config->addTranslations(array(
'label' => Piwik::translate('Actions_ColumnDownloadURL'),
'nb_visits' => Piwik::translate('Actions_ColumnUniqueDownloads'),
'nb_hits' => Piwik::translate('General_Downloads'),
));
$view->config->columns_to_display = array('label', 'nb_visits', 'nb_hits');
$view->config->show_exclude_low_population = false;
$this->addBaseDisplayProperties($view);
}
public function configureViewForGetOutlinks(ViewDataTable $view)
{
$view->config->addTranslations(array(
'label' => Piwik::translate('Actions_ColumnClickedURL'),
'nb_visits' => Piwik::translate('Actions_ColumnUniqueClicks'),
'nb_hits' => Piwik::translate('Actions_ColumnClicks'),
));
$view->config->columns_to_display = array('label', 'nb_visits', 'nb_hits');
$view->config->show_exclude_low_population = false;
$this->addBaseDisplayProperties($view);
}
}

View file

@ -0,0 +1,548 @@
<?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\Plugins\Actions;
use Piwik\DataTable;
use Piwik\Metrics;
use Piwik\RankingQuery;
use Piwik\Tracker\Action;
use Piwik\Tracker\ActionSiteSearch;
/**
* Class encapsulating logic to process Day/Period Archiving for the Actions reports
*
*/
class Archiver extends \Piwik\Plugin\Archiver
{
const DOWNLOADS_RECORD_NAME = 'Actions_downloads';
const OUTLINKS_RECORD_NAME = 'Actions_outlink';
const PAGE_TITLES_RECORD_NAME = 'Actions_actions';
const SITE_SEARCH_RECORD_NAME = 'Actions_sitesearch';
const PAGE_URLS_RECORD_NAME = 'Actions_actions_url';
const METRIC_PAGEVIEWS_RECORD_NAME = 'Actions_nb_pageviews';
const METRIC_UNIQ_PAGEVIEWS_RECORD_NAME = 'Actions_nb_uniq_pageviews';
const METRIC_SUM_TIME_RECORD_NAME = 'Actions_sum_time_generation';
const METRIC_HITS_TIMED_RECORD_NAME = 'Actions_nb_hits_with_time_generation';
const METRIC_DOWNLOADS_RECORD_NAME = 'Actions_nb_downloads';
const METRIC_UNIQ_DOWNLOADS_RECORD_NAME = 'Actions_nb_uniq_downloads';
const METRIC_OUTLINKS_RECORD_NAME = 'Actions_nb_outlinks';
const METRIC_UNIQ_OUTLINKS_RECORD_NAME = 'Actions_nb_uniq_outlinks';
const METRIC_SEARCHES_RECORD_NAME = 'Actions_nb_searches';
const METRIC_KEYWORDS_RECORD_NAME = 'Actions_nb_keywords';
/* Metrics in use by the API Actions.get */
public static $actionsAggregateMetrics = array(
self::METRIC_PAGEVIEWS_RECORD_NAME => 'nb_pageviews',
self::METRIC_UNIQ_PAGEVIEWS_RECORD_NAME => 'nb_uniq_pageviews',
self::METRIC_DOWNLOADS_RECORD_NAME => 'nb_downloads',
self::METRIC_UNIQ_DOWNLOADS_RECORD_NAME => 'nb_uniq_downloads',
self::METRIC_OUTLINKS_RECORD_NAME => 'nb_outlinks',
self::METRIC_UNIQ_OUTLINKS_RECORD_NAME => 'nb_uniq_outlinks',
self::METRIC_SEARCHES_RECORD_NAME => 'nb_searches',
self::METRIC_KEYWORDS_RECORD_NAME => 'nb_keywords',
);
public static $actionTypes = array(
Action::TYPE_PAGE_URL,
Action::TYPE_OUTLINK,
Action::TYPE_DOWNLOAD,
Action::TYPE_PAGE_TITLE,
Action::TYPE_SITE_SEARCH,
);
static protected $columnsToRenameAfterAggregation = array(
Metrics::INDEX_NB_UNIQ_VISITORS => Metrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS,
Metrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => Metrics::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS,
Metrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => Metrics::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS,
);
static public $columnsToDeleteAfterAggregation = array(
Metrics::INDEX_NB_UNIQ_VISITORS,
Metrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS,
Metrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS,
);
private static $columnsAggregationOperation = array(
Metrics::INDEX_PAGE_MAX_TIME_GENERATION => 'max',
Metrics::INDEX_PAGE_MIN_TIME_GENERATION => 'min'
);
protected $actionsTablesByType = null;
protected $isSiteSearchEnabled = false;
function __construct($processor)
{
parent::__construct($processor);
$this->isSiteSearchEnabled = $processor->getParams()->getSite()->isSiteSearchEnabled();
}
/**
* Archives Actions reports for a Day
*
* @return bool
*/
public function aggregateDayReport()
{
$rankingQueryLimit = ArchivingHelper::getRankingQueryLimit();
ArchivingHelper::reloadConfig();
$this->initActionsTables();
$this->archiveDayActions($rankingQueryLimit);
$this->archiveDayEntryActions($rankingQueryLimit);
$this->archiveDayExitActions($rankingQueryLimit);
$this->archiveDayActionsTime($rankingQueryLimit);
$this->insertDayReports();
return true;
}
/**
* @return array
*/
protected function getMetricNames()
{
return array(
self::METRIC_PAGEVIEWS_RECORD_NAME,
self::METRIC_UNIQ_PAGEVIEWS_RECORD_NAME,
self::METRIC_DOWNLOADS_RECORD_NAME,
self::METRIC_UNIQ_DOWNLOADS_RECORD_NAME,
self::METRIC_OUTLINKS_RECORD_NAME,
self::METRIC_UNIQ_OUTLINKS_RECORD_NAME,
self::METRIC_SEARCHES_RECORD_NAME,
self::METRIC_SUM_TIME_RECORD_NAME,
self::METRIC_HITS_TIMED_RECORD_NAME,
);
}
/**
* @return string
*/
static public function getWhereClauseActionIsNotEvent()
{
return " AND log_link_visit_action.idaction_event_category IS NULL";
}
/**
* @param $select
* @param $from
*/
protected function updateQuerySelectFromForSiteSearch(&$select, &$from)
{
$selectFlagNoResultKeywords = ",
CASE WHEN (MAX(log_link_visit_action.custom_var_v" . ActionSiteSearch::CVAR_INDEX_SEARCH_COUNT . ") = 0
AND log_link_visit_action.custom_var_k" . ActionSiteSearch::CVAR_INDEX_SEARCH_COUNT . " = '" . ActionSiteSearch::CVAR_KEY_SEARCH_COUNT . "')
THEN 1 ELSE 0 END
AS `" . Metrics::INDEX_SITE_SEARCH_HAS_NO_RESULT . "`";
//we need an extra JOIN to know whether the referrer "idaction_name_ref" was a Site Search request
$from[] = array(
"table" => "log_action",
"tableAlias" => "log_action_name_ref",
"joinOn" => "log_link_visit_action.idaction_name_ref = log_action_name_ref.idaction"
);
$selectPageIsFollowingSiteSearch = ",
SUM( CASE WHEN log_action_name_ref.type = " . Action::TYPE_SITE_SEARCH . "
THEN 1 ELSE 0 END)
AS `" . Metrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS . "`";
$select .= $selectFlagNoResultKeywords
. $selectPageIsFollowingSiteSearch;
}
/**
* Initializes the DataTables created by the archiveDay function.
*/
private function initActionsTables()
{
$this->actionsTablesByType = array();
foreach (self::$actionTypes as $type) {
$dataTable = new DataTable();
$dataTable->setMaximumAllowedRows(ArchivingHelper::$maximumRowsInDataTableLevelZero);
if ($type == Action::TYPE_PAGE_URL
|| $type == Action::TYPE_PAGE_TITLE
) {
// for page urls and page titles, performance metrics exist and have to be aggregated correctly
$dataTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, self::$columnsAggregationOperation);
}
$this->actionsTablesByType[$type] = $dataTable;
}
}
protected function archiveDayActions($rankingQueryLimit)
{
$select = "log_action.name,
log_action.type,
log_action.idaction,
log_action.url_prefix,
count(distinct log_link_visit_action.idvisit) as `" . Metrics::INDEX_NB_VISITS . "`,
count(distinct log_link_visit_action.idvisitor) as `" . Metrics::INDEX_NB_UNIQ_VISITORS . "`,
count(*) as `" . Metrics::INDEX_PAGE_NB_HITS . "`,
sum(
case when " . Action::DB_COLUMN_CUSTOM_FLOAT . " is null
then 0
else " . Action::DB_COLUMN_CUSTOM_FLOAT . "
end
) / 1000 as `" . Metrics::INDEX_PAGE_SUM_TIME_GENERATION . "`,
sum(
case when " . Action::DB_COLUMN_CUSTOM_FLOAT . " is null
then 0
else 1
end
) as `" . Metrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION . "`,
min(" . Action::DB_COLUMN_CUSTOM_FLOAT . ") / 1000
as `" . Metrics::INDEX_PAGE_MIN_TIME_GENERATION . "`,
max(" . Action::DB_COLUMN_CUSTOM_FLOAT . ") / 1000
as `" . Metrics::INDEX_PAGE_MAX_TIME_GENERATION . "`
";
$from = array(
"log_link_visit_action",
array(
"table" => "log_action",
"joinOn" => "log_link_visit_action.%s = log_action.idaction"
)
);
$where = "log_link_visit_action.server_time >= ?
AND log_link_visit_action.server_time <= ?
AND log_link_visit_action.idsite = ?
AND log_link_visit_action.%s IS NOT NULL"
. $this->getWhereClauseActionIsNotEvent();
$groupBy = "log_action.idaction";
$orderBy = "`" . Metrics::INDEX_PAGE_NB_HITS . "` DESC, name ASC";
$rankingQuery = false;
if ($rankingQueryLimit > 0) {
$rankingQuery = new RankingQuery($rankingQueryLimit);
$rankingQuery->setOthersLabel(DataTable::LABEL_SUMMARY_ROW);
$rankingQuery->addLabelColumn(array('idaction', 'name'));
$rankingQuery->addColumn(array('url_prefix', Metrics::INDEX_NB_UNIQ_VISITORS));
$rankingQuery->addColumn(array(Metrics::INDEX_PAGE_NB_HITS, Metrics::INDEX_NB_VISITS), 'sum');
if ($this->isSiteSearchEnabled()) {
$rankingQuery->addColumn(Metrics::INDEX_SITE_SEARCH_HAS_NO_RESULT, 'min');
$rankingQuery->addColumn(Metrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS, 'sum');
}
$rankingQuery->addColumn(Metrics::INDEX_PAGE_SUM_TIME_GENERATION, 'sum');
$rankingQuery->addColumn(Metrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION, 'sum');
$rankingQuery->addColumn(Metrics::INDEX_PAGE_MIN_TIME_GENERATION, 'min');
$rankingQuery->addColumn(Metrics::INDEX_PAGE_MAX_TIME_GENERATION, 'max');
$rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($this->actionsTablesByType));
}
// Special Magic to get
// 1) No result Keywords
// 2) For each page view, count number of times the referrer page was a Site Search
if ($this->isSiteSearchEnabled()) {
$this->updateQuerySelectFromForSiteSearch($select, $from);
}
$this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "idaction_name", $rankingQuery);
$this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "idaction_url", $rankingQuery);
}
protected function isSiteSearchEnabled()
{
return $this->isSiteSearchEnabled;
}
protected function archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, $sprintfField, $rankingQuery = false)
{
$select = sprintf($select, $sprintfField);
// get query with segmentation
$query = $this->getLogAggregator()->generateQuery($select, $from, $where, $groupBy, $orderBy);
// replace the rest of the %s
$querySql = str_replace("%s", $sprintfField, $query['sql']);
// apply ranking query
if ($rankingQuery) {
$querySql = $rankingQuery->generateQuery($querySql);
}
// get result
$resultSet = $this->getLogAggregator()->getDb()->query($querySql, $query['bind']);
$modified = ArchivingHelper::updateActionsTableWithRowQuery($resultSet, $sprintfField, $this->actionsTablesByType);
return $modified;
}
/**
* Entry actions for Page URLs and Page names
*/
protected function archiveDayEntryActions($rankingQueryLimit)
{
$rankingQuery = false;
if ($rankingQueryLimit > 0) {
$rankingQuery = new RankingQuery($rankingQueryLimit);
$rankingQuery->setOthersLabel(DataTable::LABEL_SUMMARY_ROW);
$rankingQuery->addLabelColumn('idaction');
$rankingQuery->addColumn(Metrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS);
$rankingQuery->addColumn(array(Metrics::INDEX_PAGE_ENTRY_NB_VISITS,
Metrics::INDEX_PAGE_ENTRY_NB_ACTIONS,
Metrics::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH,
Metrics::INDEX_PAGE_ENTRY_BOUNCE_COUNT), 'sum');
$rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($this->actionsTablesByType));
$extraSelects = 'log_action.type, log_action.name,';
$from = array(
"log_visit",
array(
"table" => "log_action",
"joinOn" => "log_visit.%s = log_action.idaction"
)
);
$orderBy = "`" . Metrics::INDEX_PAGE_ENTRY_NB_ACTIONS . "` DESC, log_action.name ASC";
} else {
$extraSelects = false;
$from = "log_visit";
$orderBy = false;
}
$select = "log_visit.%s as idaction, $extraSelects
count(distinct log_visit.idvisitor) as `" . Metrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS . "`,
count(*) as `" . Metrics::INDEX_PAGE_ENTRY_NB_VISITS . "`,
sum(log_visit.visit_total_actions) as `" . Metrics::INDEX_PAGE_ENTRY_NB_ACTIONS . "`,
sum(log_visit.visit_total_time) as `" . Metrics::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH . "`,
sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as `" . Metrics::INDEX_PAGE_ENTRY_BOUNCE_COUNT . "`";
$where = "log_visit.visit_last_action_time >= ?
AND log_visit.visit_last_action_time <= ?
AND log_visit.idsite = ?
AND log_visit.%s > 0";
$groupBy = "log_visit.%s, idaction";
$this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "visit_entry_idaction_url", $rankingQuery);
$this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "visit_entry_idaction_name", $rankingQuery);
}
/**
* Exit actions
*/
protected function archiveDayExitActions($rankingQueryLimit)
{
$rankingQuery = false;
if ($rankingQueryLimit > 0) {
$rankingQuery = new RankingQuery($rankingQueryLimit);
$rankingQuery->setOthersLabel(DataTable::LABEL_SUMMARY_ROW);
$rankingQuery->addLabelColumn('idaction');
$rankingQuery->addColumn(Metrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS);
$rankingQuery->addColumn(Metrics::INDEX_PAGE_EXIT_NB_VISITS, 'sum');
$rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($this->actionsTablesByType));
$extraSelects = 'log_action.type, log_action.name,';
$from = array(
"log_visit",
array(
"table" => "log_action",
"joinOn" => "log_visit.%s = log_action.idaction"
)
);
$orderBy = "`" . Metrics::INDEX_PAGE_EXIT_NB_VISITS . "` DESC, log_action.name ASC";
} else {
$extraSelects = false;
$from = "log_visit";
$orderBy = false;
}
$select = "log_visit.%s as idaction, $extraSelects
count(distinct log_visit.idvisitor) as `" . Metrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS . "`,
count(*) as `" . Metrics::INDEX_PAGE_EXIT_NB_VISITS . "`";
$where = "log_visit.visit_last_action_time >= ?
AND log_visit.visit_last_action_time <= ?
AND log_visit.idsite = ?
AND log_visit.%s > 0";
$groupBy = "log_visit.%s, idaction";
$this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "visit_exit_idaction_url", $rankingQuery);
$this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "visit_exit_idaction_name", $rankingQuery);
return array($rankingQuery, $extraSelects, $from, $orderBy, $select, $where, $groupBy);
}
/**
* Time per action
*/
protected function archiveDayActionsTime($rankingQueryLimit)
{
$rankingQuery = false;
if ($rankingQueryLimit > 0) {
$rankingQuery = new RankingQuery($rankingQueryLimit);
$rankingQuery->setOthersLabel(DataTable::LABEL_SUMMARY_ROW);
$rankingQuery->addLabelColumn('idaction');
$rankingQuery->addColumn(Metrics::INDEX_PAGE_SUM_TIME_SPENT, 'sum');
$rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($this->actionsTablesByType));
$extraSelects = "log_action.type, log_action.name, count(*) as `" . Metrics::INDEX_PAGE_NB_HITS . "`,";
$from = array(
"log_link_visit_action",
array(
"table" => "log_action",
"joinOn" => "log_link_visit_action.%s = log_action.idaction"
)
);
$orderBy = "`" . Metrics::INDEX_PAGE_NB_HITS . "` DESC, log_action.name ASC";
} else {
$extraSelects = false;
$from = "log_link_visit_action";
$orderBy = false;
}
$select = "log_link_visit_action.%s as idaction, $extraSelects
sum(log_link_visit_action.time_spent_ref_action) as `" . Metrics::INDEX_PAGE_SUM_TIME_SPENT . "`";
$where = "log_link_visit_action.server_time >= ?
AND log_link_visit_action.server_time <= ?
AND log_link_visit_action.idsite = ?
AND log_link_visit_action.time_spent_ref_action > 0
AND log_link_visit_action.%s > 0"
. $this->getWhereClauseActionIsNotEvent();
$groupBy = "log_link_visit_action.%s, idaction";
$this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "idaction_url_ref", $rankingQuery);
$this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "idaction_name_ref", $rankingQuery);
}
/**
* Records in the DB the archived reports for Page views, Downloads, Outlinks, and Page titles
*/
protected function insertDayReports()
{
ArchivingHelper::clearActionsCache();
$this->insertPageUrlsReports();
$this->insertDownloadsReports();
$this->insertOutlinksReports();
$this->insertPageTitlesReports();
$this->insertSiteSearchReports();
}
protected function insertPageUrlsReports()
{
$dataTable = $this->getDataTable(Action::TYPE_PAGE_URL);
$this->insertTable($dataTable, self::PAGE_URLS_RECORD_NAME);
$records = array(
self::METRIC_PAGEVIEWS_RECORD_NAME => array_sum($dataTable->getColumn(Metrics::INDEX_PAGE_NB_HITS)),
self::METRIC_UNIQ_PAGEVIEWS_RECORD_NAME => array_sum($dataTable->getColumn(Metrics::INDEX_NB_VISITS)),
self::METRIC_SUM_TIME_RECORD_NAME => array_sum($dataTable->getColumn(Metrics::INDEX_PAGE_SUM_TIME_GENERATION)),
self::METRIC_HITS_TIMED_RECORD_NAME => array_sum($dataTable->getColumn(Metrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION))
);
$this->getProcessor()->insertNumericRecords($records);
}
/**
* @param $typeId
* @return DataTable
*/
protected function getDataTable($typeId)
{
return $this->actionsTablesByType[$typeId];
}
protected function insertTable(DataTable $dataTable, $recordName)
{
ArchivingHelper::deleteInvalidSummedColumnsFromDataTable($dataTable);
$report = $dataTable->getSerialized(ArchivingHelper::$maximumRowsInDataTableLevelZero, ArchivingHelper::$maximumRowsInSubDataTable, ArchivingHelper::$columnToSortByBeforeTruncation);
$this->getProcessor()->insertBlobRecord($recordName, $report);
}
protected function insertDownloadsReports()
{
$dataTable = $this->getDataTable(Action::TYPE_DOWNLOAD);
$this->insertTable($dataTable, self::DOWNLOADS_RECORD_NAME);
$this->getProcessor()->insertNumericRecord(self::METRIC_DOWNLOADS_RECORD_NAME, array_sum($dataTable->getColumn(Metrics::INDEX_PAGE_NB_HITS)));
$this->getProcessor()->insertNumericRecord(self::METRIC_UNIQ_DOWNLOADS_RECORD_NAME, array_sum($dataTable->getColumn(Metrics::INDEX_NB_VISITS)));
}
protected function insertOutlinksReports()
{
$dataTable = $this->getDataTable(Action::TYPE_OUTLINK);
$this->insertTable($dataTable, self::OUTLINKS_RECORD_NAME);
$this->getProcessor()->insertNumericRecord(self::METRIC_OUTLINKS_RECORD_NAME, array_sum($dataTable->getColumn(Metrics::INDEX_PAGE_NB_HITS)));
$this->getProcessor()->insertNumericRecord(self::METRIC_UNIQ_OUTLINKS_RECORD_NAME, array_sum($dataTable->getColumn(Metrics::INDEX_NB_VISITS)));
}
protected function insertPageTitlesReports()
{
$dataTable = $this->getDataTable(Action::TYPE_PAGE_TITLE);
$this->insertTable($dataTable, self::PAGE_TITLES_RECORD_NAME);
}
protected function insertSiteSearchReports()
{
$dataTable = $this->getDataTable(Action::TYPE_SITE_SEARCH);
$this->deleteUnusedColumnsFromKeywordsDataTable($dataTable);
$this->insertTable($dataTable, self::SITE_SEARCH_RECORD_NAME);
$this->getProcessor()->insertNumericRecord(self::METRIC_SEARCHES_RECORD_NAME, array_sum($dataTable->getColumn(Metrics::INDEX_NB_VISITS)));
$this->getProcessor()->insertNumericRecord(self::METRIC_KEYWORDS_RECORD_NAME, $dataTable->getRowsCount());
}
protected function deleteUnusedColumnsFromKeywordsDataTable(DataTable $dataTable)
{
$columnsToDelete = array(
Metrics::INDEX_NB_UNIQ_VISITORS,
Metrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS,
Metrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS,
Metrics::INDEX_PAGE_ENTRY_NB_ACTIONS,
Metrics::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH,
Metrics::INDEX_PAGE_ENTRY_NB_VISITS,
Metrics::INDEX_PAGE_ENTRY_BOUNCE_COUNT,
Metrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS,
);
$dataTable->deleteColumns($columnsToDelete);
}
public function aggregateMultipleReports()
{
ArchivingHelper::reloadConfig();
$dataTableToSum = array(
self::PAGE_TITLES_RECORD_NAME,
self::PAGE_URLS_RECORD_NAME,
);
$this->getProcessor()->aggregateDataTableRecords($dataTableToSum,
ArchivingHelper::$maximumRowsInDataTableLevelZero,
ArchivingHelper::$maximumRowsInSubDataTable,
ArchivingHelper::$columnToSortByBeforeTruncation,
self::$columnsAggregationOperation,
self::$columnsToRenameAfterAggregation
);
$dataTableToSum = array(
self::DOWNLOADS_RECORD_NAME,
self::OUTLINKS_RECORD_NAME,
self::SITE_SEARCH_RECORD_NAME,
);
$aggregation = null;
$nameToCount = $this->getProcessor()->aggregateDataTableRecords($dataTableToSum,
ArchivingHelper::$maximumRowsInDataTableLevelZero,
ArchivingHelper::$maximumRowsInSubDataTable,
ArchivingHelper::$columnToSortByBeforeTruncation,
$aggregation,
self::$columnsToRenameAfterAggregation
);
$this->getProcessor()->aggregateNumericMetrics($this->getMetricNames());
// Unique Keywords can't be summed, instead we take the RowsCount() of the keyword table
$this->getProcessor()->insertNumericRecord(self::METRIC_KEYWORDS_RECORD_NAME, $nameToCount[self::SITE_SEARCH_RECORD_NAME]['level0']);
}
}

View file

@ -0,0 +1,612 @@
<?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\Plugins\Actions;
use PDOStatement;
use Piwik\Config;
use Piwik\DataTable\Manager;
use Piwik\DataTable\Row;
use Piwik\DataTable;
use Piwik\DataTable\Row\DataTableSummaryRow;
use Piwik\Metrics;
use Piwik\Piwik;
use Piwik\Tracker\Action;
use Piwik\Tracker\PageUrl;
use Zend_Db_Statement;
/**
* This static class provides:
* - logic to parse/cleanup Action names,
* - logic to efficiently process aggregate the array data during Archiving
*
*/
class ArchivingHelper
{
const OTHERS_ROW_KEY = '';
/**
* Ideally this should use the DataArray object instead of custom data structure
*
* @param Zend_Db_Statement|PDOStatement $query
* @param string|bool $fieldQueried
* @param array $actionsTablesByType
* @return int
*/
static public function updateActionsTableWithRowQuery($query, $fieldQueried, & $actionsTablesByType)
{
$rowsProcessed = 0;
while ($row = $query->fetch()) {
if (empty($row['idaction'])) {
$row['type'] = ($fieldQueried == 'idaction_url' ? Action::TYPE_PAGE_URL : Action::TYPE_PAGE_TITLE);
// This will be replaced with 'X not defined' later
$row['name'] = '';
// Yes, this is kind of a hack, so we don't mix 'page url not defined' with 'page title not defined' etc.
$row['idaction'] = -$row['type'];
}
if ($row['type'] != Action::TYPE_SITE_SEARCH) {
unset($row[Metrics::INDEX_SITE_SEARCH_HAS_NO_RESULT]);
}
// This will appear as <url /> in the API, which is actually very important to keep
// eg. When there's at least one row in a report that does not have a URL, not having this <url/> would break HTML/PDF reports.
$url = '';
if ($row['type'] == Action::TYPE_SITE_SEARCH
|| $row['type'] == Action::TYPE_PAGE_TITLE
) {
$url = null;
} elseif (!empty($row['name'])
&& $row['name'] != DataTable::LABEL_SUMMARY_ROW) {
$url = PageUrl::reconstructNormalizedUrl((string)$row['name'], $row['url_prefix']);
}
if (isset($row['name'])
&& isset($row['type'])
) {
$actionName = $row['name'];
$actionType = $row['type'];
$urlPrefix = $row['url_prefix'];
$idaction = $row['idaction'];
// in some unknown case, the type field is NULL, as reported in #1082 - we ignore this page view
if (empty($actionType)) {
if ($idaction != DataTable::LABEL_SUMMARY_ROW) {
self::setCachedActionRow($idaction, $actionType, false);
}
continue;
}
$actionRow = self::getActionRow($actionName, $actionType, $urlPrefix, $actionsTablesByType);
self::setCachedActionRow($idaction, $actionType, $actionRow);
} else {
$actionRow = self::getCachedActionRow($row['idaction'], $row['type']);
// Action processed as "to skip" for some reasons
if ($actionRow === false) {
continue;
}
}
if (is_null($actionRow)) {
continue;
}
// Here we do ensure that, the Metadata URL set for a given row, is the one from the Pageview with the most hits.
// This is to ensure that when, different URLs are loaded with the same page name.
// For example http://piwik.org and http://id.piwik.org are reported in Piwik > Actions > Pages with /index
// But, we must make sure http://piwik.org is used to link & for transitions
// Note: this code is partly duplicated from Row->sumRowMetadata()
if (!is_null($url)
&& !$actionRow->isSummaryRow()
) {
if (($existingUrl = $actionRow->getMetadata('url')) !== false) {
if (!empty($row[Metrics::INDEX_PAGE_NB_HITS])
&& $row[Metrics::INDEX_PAGE_NB_HITS] > $actionRow->maxVisitsSummed
) {
$actionRow->setMetadata('url', $url);
$actionRow->maxVisitsSummed = $row[Metrics::INDEX_PAGE_NB_HITS];
}
} else {
$actionRow->setMetadata('url', $url);
$actionRow->maxVisitsSummed = !empty($row[Metrics::INDEX_PAGE_NB_HITS]) ? $row[Metrics::INDEX_PAGE_NB_HITS] : 0;
}
}
if ($row['type'] != Action::TYPE_PAGE_URL
&& $row['type'] != Action::TYPE_PAGE_TITLE
) {
// only keep performance metrics when they're used (i.e. for URLs and page titles)
if (array_key_exists(Metrics::INDEX_PAGE_SUM_TIME_GENERATION, $row)) {
unset($row[Metrics::INDEX_PAGE_SUM_TIME_GENERATION]);
}
if (array_key_exists(Metrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION, $row)) {
unset($row[Metrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION]);
}
if (array_key_exists(Metrics::INDEX_PAGE_MIN_TIME_GENERATION, $row)) {
unset($row[Metrics::INDEX_PAGE_MIN_TIME_GENERATION]);
}
if (array_key_exists(Metrics::INDEX_PAGE_MAX_TIME_GENERATION, $row)) {
unset($row[Metrics::INDEX_PAGE_MAX_TIME_GENERATION]);
}
}
unset($row['name']);
unset($row['type']);
unset($row['idaction']);
unset($row['url_prefix']);
foreach ($row as $name => $value) {
// in some edge cases, we have twice the same action name with 2 different idaction
// - this happens when 2 visitors visit the same new page at the same time, and 2 actions get recorded for the same name
// - this could also happen when 2 URLs end up having the same label (eg. 2 subdomains get aggregated to the "/index" page name)
if (($alreadyValue = $actionRow->getColumn($name)) !== false) {
$newValue = self::getColumnValuesMerged($name, $alreadyValue, $value);
$actionRow->setColumn($name, $newValue);
} else {
$actionRow->addColumn($name, $value);
}
}
// if the exit_action was not recorded properly in the log_link_visit_action
// there would be an error message when getting the nb_hits column
// we must fake the record and add the columns
if ($actionRow->getColumn(Metrics::INDEX_PAGE_NB_HITS) === false) {
// to test this code: delete the entries in log_link_action_visit for
// a given exit_idaction_url
foreach (self::getDefaultRow()->getColumns() as $name => $value) {
$actionRow->addColumn($name, $value);
}
}
$rowsProcessed++;
}
// just to make sure php copies the last $actionRow in the $parentTable array
$actionRow =& $actionsTablesByType;
return $rowsProcessed;
}
public static function removeEmptyColumns($dataTable)
{
// Delete all columns that have a value of zero
$dataTable->filter('ColumnDelete', array(
$columnsToRemove = array(Metrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS),
$columnsToKeep = array(),
$deleteIfZeroOnly = true
));
}
/**
* For rows which have subtables (eg. directories with sub pages),
* deletes columns which don't make sense when all values of sub pages are summed.
*
* @param $dataTable DataTable
*/
public static function deleteInvalidSummedColumnsFromDataTable($dataTable)
{
foreach ($dataTable->getRows() as $id => $row) {
if (($idSubtable = $row->getIdSubDataTable()) !== null
|| $id === DataTable::ID_SUMMARY_ROW
) {
if ($idSubtable !== null) {
$subtable = Manager::getInstance()->getTable($idSubtable);
self::deleteInvalidSummedColumnsFromDataTable($subtable);
}
if ($row instanceof DataTableSummaryRow) {
$row->recalculate();
}
foreach (Archiver::$columnsToDeleteAfterAggregation as $name) {
$row->deleteColumn($name);
}
}
}
// And this as well
ArchivingHelper::removeEmptyColumns($dataTable);
}
/**
* Returns the limit to use with RankingQuery for this plugin.
*
* @return int
*/
public static function getRankingQueryLimit()
{
$configGeneral = Config::getInstance()->General;
$configLimit = $configGeneral['archiving_ranking_query_row_limit'];
$limit = $configLimit == 0 ? 0 : max(
$configLimit,
$configGeneral['datatable_archiving_maximum_rows_actions'],
$configGeneral['datatable_archiving_maximum_rows_subtable_actions']
);
// FIXME: This is a quick fix for #3482. The actual cause of the bug is that
// the site search & performance metrics additions to
// ArchivingHelper::updateActionsTableWithRowQuery expect every
// row to have 'type' data, but not all of the SQL queries that are run w/o
// ranking query join on the log_action table and thus do not select the
// log_action.type column.
//
// NOTES: Archiving logic can be generalized as follows:
// 0) Do SQL query over log_link_visit_action & join on log_action to select
// some metrics (like visits, hits, etc.)
// 1) For each row, cache the action row & metrics. (This is done by
// updateActionsTableWithRowQuery for result set rows that have
// name & type columns.)
// 2) Do other SQL queries for metrics we can't put in the first query (like
// entry visits, exit vists, etc.) w/o joining log_action.
// 3) For each row, find the cached row by idaction & add the new metrics to
// it. (This is done by updateActionsTableWithRowQuery for result set rows
// that DO NOT have name & type columns.)
//
// The site search & performance metrics additions expect a 'type' all the time
// which breaks the original pre-rankingquery logic. Ranking query requires a
// join, so the bug is only seen when ranking query is disabled.
if ($limit === 0) {
$limit = 100000;
}
return $limit;
}
/**
* @param $columnName
* @param $alreadyValue
* @param $value
* @return mixed
*/
private static function getColumnValuesMerged($columnName, $alreadyValue, $value)
{
if ($columnName == Metrics::INDEX_PAGE_MIN_TIME_GENERATION) {
if (empty($alreadyValue)) {
$newValue = $value;
} else if (empty($value)) {
$newValue = $alreadyValue;
} else {
$newValue = min($alreadyValue, $value);
}
return $newValue;
}
if ($columnName == Metrics::INDEX_PAGE_MAX_TIME_GENERATION) {
$newValue = max($alreadyValue, $value);
return $newValue;
}
$newValue = $alreadyValue + $value;
return $newValue;
}
static public $maximumRowsInDataTableLevelZero;
static public $maximumRowsInSubDataTable;
static public $columnToSortByBeforeTruncation;
static protected $actionUrlCategoryDelimiter = null;
static protected $actionTitleCategoryDelimiter = null;
static protected $defaultActionName = null;
static protected $defaultActionNameWhenNotDefined = null;
static protected $defaultActionUrlWhenNotDefined = null;
static public function reloadConfig()
{
// for BC, we read the old style delimiter first (see #1067)Row
$actionDelimiter = @Config::getInstance()->General['action_category_delimiter'];
if (empty($actionDelimiter)) {
self::$actionUrlCategoryDelimiter = Config::getInstance()->General['action_url_category_delimiter'];
self::$actionTitleCategoryDelimiter = Config::getInstance()->General['action_title_category_delimiter'];
} else {
self::$actionUrlCategoryDelimiter = self::$actionTitleCategoryDelimiter = $actionDelimiter;
}
self::$defaultActionName = Config::getInstance()->General['action_default_name'];
self::$columnToSortByBeforeTruncation = Metrics::INDEX_NB_VISITS;
self::$maximumRowsInDataTableLevelZero = Config::getInstance()->General['datatable_archiving_maximum_rows_actions'];
self::$maximumRowsInSubDataTable = Config::getInstance()->General['datatable_archiving_maximum_rows_subtable_actions'];
DataTable::setMaximumDepthLevelAllowedAtLeast(self::getSubCategoryLevelLimit() + 1);
}
/**
* The default row is used when archiving, if data is inconsistent in the DB,
* there could be pages that have exit/entry hits, but don't yet
* have a record in the table (or the record was truncated).
*
* @return Row
*/
static private function getDefaultRow()
{
static $row = false;
if ($row === false) {
// This row is used in the case where an action is know as an exit_action
// but this action was not properly recorded when it was hit in the first place
// so we add this fake row information to make sure there is a nb_hits, etc. column for every action
$row = new Row(array(
Row::COLUMNS => array(
Metrics::INDEX_NB_VISITS => 1,
Metrics::INDEX_NB_UNIQ_VISITORS => 1,
Metrics::INDEX_PAGE_NB_HITS => 1,
)));
}
return $row;
}
/**
* Given a page name and type, builds a recursive datatable where
* each level of the tree is a category, based on the page name split by a delimiter (slash / by default)
*
* @param string $actionName
* @param int $actionType
* @param int $urlPrefix
* @param array $actionsTablesByType
* @return DataTable
*/
private static function getActionRow($actionName, $actionType, $urlPrefix = null, &$actionsTablesByType)
{
// we work on the root table of the given TYPE (either ACTION_URL or DOWNLOAD or OUTLINK etc.)
/* @var DataTable $currentTable */
$currentTable =& $actionsTablesByType[$actionType];
if(is_null($currentTable)) {
throw new \Exception("Action table for type '$actionType' was not found during Actions archiving.");
}
// check for ranking query cut-off
if ($actionName == DataTable::LABEL_SUMMARY_ROW) {
$summaryRow = $currentTable->getRowFromId(DataTable::ID_SUMMARY_ROW);
if ($summaryRow === false) {
$summaryRow = $currentTable->addSummaryRow(self::createSummaryRow());
}
return $summaryRow;
}
// go to the level of the subcategory
$actionExplodedNames = self::getActionExplodedNames($actionName, $actionType, $urlPrefix);
list($row, $level) = $currentTable->walkPath(
$actionExplodedNames, self::getDefaultRowColumns(), self::$maximumRowsInSubDataTable);
return $row;
}
/**
* Returns the configured sub-category level limit.
*
* @return int
*/
public static function getSubCategoryLevelLimit()
{
return Config::getInstance()->General['action_category_level_limit'];
}
/**
* Returns default label for the action type
*
* @param $type
* @return string
*/
static public function getUnknownActionName($type)
{
if (empty(self::$defaultActionNameWhenNotDefined)) {
self::$defaultActionNameWhenNotDefined = Piwik::translate('General_NotDefined', Piwik::translate('Actions_ColumnPageName'));
self::$defaultActionUrlWhenNotDefined = Piwik::translate('General_NotDefined', Piwik::translate('Actions_ColumnPageURL'));
}
if ($type == Action::TYPE_PAGE_TITLE) {
return self::$defaultActionNameWhenNotDefined;
}
return self::$defaultActionUrlWhenNotDefined;
}
/**
* Explodes action name into an array of elements.
*
* NOTE: before calling this function make sure ArchivingHelper::reloadConfig(); is called
*
* for downloads:
* we explode link http://piwik.org/some/path/piwik.zip into an array( 'piwik.org', '/some/path/piwik.zip' );
*
* for outlinks:
* we explode link http://dev.piwik.org/some/path into an array( 'dev.piwik.org', '/some/path' );
*
* for action urls:
* we explode link http://piwik.org/some/path into an array( 'some', 'path' );
*
* for action names:
* we explode name 'Piwik / Category 1 / Category 2' into an array('Piwik', 'Category 1', 'Category 2');
*
* @param string $name action name
* @param int $type action type
* @param int $urlPrefix url prefix (only used for TYPE_PAGE_URL)
* @return array of exploded elements from $name
*/
static public function getActionExplodedNames($name, $type, $urlPrefix = null)
{
// Site Search does not split Search keywords
if ($type == Action::TYPE_SITE_SEARCH) {
return array($name);
}
$name = str_replace("\n", "", $name);
$name = self::parseNameFromPageUrl($name, $type, $urlPrefix);
// outlinks and downloads
if(is_array($name)) {
return $name;
}
$split = self::splitNameByDelimiter($name, $type);
if (empty($split)) {
$defaultName = self::getUnknownActionName($type);
return array(trim($defaultName));
}
$lastPageName = end($split);
// we are careful to prefix the page URL / name with some value
// so that if a page has the same name as a category
// we don't merge both entries
if ($type != Action::TYPE_PAGE_TITLE) {
$lastPageName = '/' . $lastPageName;
} else {
$lastPageName = ' ' . $lastPageName;
}
$split[count($split) - 1] = $lastPageName;
return array_values($split);
}
/**
* Gets the key for the cache of action rows from an action ID and type.
*
* @param int $idAction
* @param int $actionType
* @return string|int
*/
private static function getCachedActionRowKey($idAction, $actionType)
{
return $idAction == DataTable::LABEL_SUMMARY_ROW
? $actionType . '_others'
: $idAction;
}
/**
* Static cache to store Rows during processing
*/
static protected $cacheParsedAction = array();
public static function clearActionsCache()
{
self::$cacheParsedAction = array();
}
/**
* Get cached action row by id & type. If $idAction is set to -1, the 'Others' row
* for the specific action type will be returned.
*
* @param int $idAction
* @param int $actionType
* @return Row|false
*/
private static function getCachedActionRow($idAction, $actionType)
{
$cacheLabel = self::getCachedActionRowKey($idAction, $actionType);
if (!isset(self::$cacheParsedAction[$cacheLabel])) {
// This can happen when
// - We select an entry page ID that was only seen yesterday, so wasn't selected in the first query
// - We count time spent on a page, when this page was only seen yesterday
return false;
}
return self::$cacheParsedAction[$cacheLabel];
}
/**
* Set cached action row for an id & type.
*
* @param int $idAction
* @param int $actionType
* @param \DataTable\Row
*/
private static function setCachedActionRow($idAction, $actionType, $actionRow)
{
$cacheLabel = self::getCachedActionRowKey($idAction, $actionType);
self::$cacheParsedAction[$cacheLabel] = $actionRow;
}
/**
* Returns the default columns for a row in an Actions DataTable.
*
* @return array
*/
private static function getDefaultRowColumns()
{
return array(Metrics::INDEX_NB_VISITS => 0,
Metrics::INDEX_NB_UNIQ_VISITORS => 0,
Metrics::INDEX_PAGE_NB_HITS => 0,
Metrics::INDEX_PAGE_SUM_TIME_SPENT => 0);
}
/**
* Creates a summary row for an Actions DataTable.
*
* @return Row
*/
private static function createSummaryRow()
{
return new Row(array(
Row::COLUMNS =>
array('label' => DataTable::LABEL_SUMMARY_ROW) + self::getDefaultRowColumns()
));
}
private static function splitNameByDelimiter($name, $type)
{
if(is_array($name)) {
return $name;
}
if ($type == Action::TYPE_PAGE_TITLE) {
$categoryDelimiter = self::$actionTitleCategoryDelimiter;
} else {
$categoryDelimiter = self::$actionUrlCategoryDelimiter;
}
if (empty($categoryDelimiter)) {
return array(trim($name));
}
$split = explode($categoryDelimiter, $name, self::getSubCategoryLevelLimit());
// trim every category and remove empty categories
$split = array_map('trim', $split);
$split = array_filter($split, 'strlen');
// forces array key to start at 0
$split = array_values($split);
return $split;
}
private static function parseNameFromPageUrl($name, $type, $urlPrefix)
{
$urlRegexAfterDomain = '([^/]+)[/]?([^#]*)[#]?(.*)';
if ($urlPrefix === null) {
// match url with protocol (used for outlinks / downloads)
$urlRegex = '@^http[s]?://' . $urlRegexAfterDomain . '$@i';
} else {
// the name is a url that does not contain protocol and www anymore
// we know that normalization has been done on db level because $urlPrefix is set
$urlRegex = '@^' . $urlRegexAfterDomain . '$@i';
}
$matches = array();
preg_match($urlRegex, $name, $matches);
if (!count($matches)) {
return $name;
}
$urlHost = $matches[1];
$urlPath = $matches[2];
$urlFragment = $matches[3];
if (in_array($type, array(Action::TYPE_DOWNLOAD, Action::TYPE_OUTLINK))) {
return array(trim($urlHost), '/' . trim($urlPath));
}
$name = $urlPath;
if ($name === '' || substr($name, -1) == '/') {
$name .= self::$defaultActionName;
}
$urlFragment = PageUrl::processUrlFragment($urlFragment);
if (!empty($urlFragment)) {
$name .= '#' . $urlFragment;
}
return $name;
}
}

View file

@ -0,0 +1,151 @@
<?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\Plugins\Actions;
use Piwik\Piwik;
use Piwik\View;
use Piwik\ViewDataTable\Factory;
/**
* Actions controller
*
*/
class Controller extends \Piwik\Plugin\Controller
{
//
// Actions that render whole pages
//
public function indexPageUrls()
{
return View::singleReport(
Piwik::translate('General_Pages'),
$this->getPageUrls(true));
}
public function indexEntryPageUrls()
{
return View::singleReport(
Piwik::translate('Actions_SubmenuPagesEntry'),
$this->getEntryPageUrls(true));
}
public function indexExitPageUrls()
{
return View::singleReport(
Piwik::translate('Actions_SubmenuPagesExit'),
$this->getExitPageUrls(true));
}
public function indexSiteSearch()
{
$view = new View('@Actions/indexSiteSearch');
$view->keywords = $this->getSiteSearchKeywords(true);
$view->noResultKeywords = $this->getSiteSearchNoResultKeywords(true);
$view->pagesUrlsFollowingSiteSearch = $this->getPageUrlsFollowingSiteSearch(true);
$categoryTrackingEnabled = \Piwik\Plugin\Manager::getInstance()->isPluginActivated('CustomVariables');
if ($categoryTrackingEnabled) {
$view->categories = $this->getSiteSearchCategories(true);
}
return $view->render();
}
public function indexPageTitles()
{
return View::singleReport(
Piwik::translate('Actions_SubmenuPageTitles'),
$this->getPageTitles(true));
}
public function indexDownloads()
{
return View::singleReport(
Piwik::translate('General_Downloads'),
$this->getDownloads(true));
}
public function indexOutlinks()
{
return View::singleReport(
Piwik::translate('General_Outlinks'),
$this->getOutlinks(true));
}
//
// Actions that render individual reports
//
public function getPageUrls()
{
return $this->renderReport(__FUNCTION__);
}
public function getEntryPageUrls()
{
return $this->renderReport(__FUNCTION__);
}
public function getExitPageUrls()
{
return $this->renderReport(__FUNCTION__);
}
public function getSiteSearchKeywords()
{
return $this->renderReport(__FUNCTION__);
}
public function getSiteSearchNoResultKeywords()
{
return $this->renderReport(__FUNCTION__);
}
public function getSiteSearchCategories()
{
return $this->renderReport(__FUNCTION__);
}
public function getPageUrlsFollowingSiteSearch()
{
return $this->renderReport(__FUNCTION__);
}
public function getPageTitlesFollowingSiteSearch()
{
return $this->renderReport(__FUNCTION__);
}
public function getPageTitles()
{
return $this->renderReport(__FUNCTION__);
}
public function getEntryPageTitles()
{
return $this->renderReport(__FUNCTION__);
}
public function getExitPageTitles()
{
return $this->renderReport(__FUNCTION__);
}
public function getDownloads()
{
return $this->renderReport(__FUNCTION__);
}
public function getOutlinks()
{
return $this->renderReport(__FUNCTION__);
}
}

View file

@ -0,0 +1,328 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function ($, require) {
var exports = require('piwik/UI'),
DataTable = exports.DataTable,
dataTablePrototype = DataTable.prototype;
// helper function for ActionDataTable
function getLevelFromClass(style) {
if (!style || typeof style == "undefined") return 0;
var currentLevel = 0;
var currentLevelIndex = style.indexOf('level');
if (currentLevelIndex >= 0) {
currentLevel = Number(style.substr(currentLevelIndex + 5, 1));
}
return currentLevel;
}
// helper function for ActionDataTable
function setImageMinus(domElem) {
$('img.plusMinus', domElem).attr('src', 'plugins/Zeitgeist/images/minus.png');
}
// helper function for ActionDataTable
function setImagePlus(domElem) {
$('img.plusMinus', domElem).attr('src', 'plugins/Zeitgeist/images/plus.png');
}
/**
* UI control that handles extra functionality for Actions datatables.
*
* @constructor
*/
exports.ActionsDataTable = function (element) {
this.parentAttributeParent = '';
this.parentId = '';
this.disabledRowDom = {}; // to handle double click on '+' row
DataTable.call(this, element);
};
$.extend(exports.ActionsDataTable.prototype, dataTablePrototype, {
//see dataTable::bindEventsAndApplyStyle
bindEventsAndApplyStyle: function (domElem, rows) {
var self = this;
self.cleanParams();
if (!rows) {
rows = $('tr', domElem);
}
// we dont display the link on the row with subDataTable when we are already
// printing all the subTables (case of recursive search when the content is
// including recursively all the subtables
if (!self.param.filter_pattern_recursive) {
self.numberOfSubtables = rows.filter('.subDataTable').click(function () {
self.onClickActionSubDataTable(this)
}).size();
}
self.applyCosmetics(domElem, rows);
self.handleColumnHighlighting(domElem);
self.handleRowActions(domElem, rows);
self.handleLimit(domElem);
self.handleAnnotationsButton(domElem);
self.handleExportBox(domElem);
self.handleSort(domElem);
self.handleOffsetInformation(domElem);
if (self.workingDivId != undefined) {
var dataTableLoadedProxy = function (response) {
self.dataTableLoaded(response, self.workingDivId);
};
self.handleSearchBox(domElem, dataTableLoadedProxy);
self.handleConfigurationBox(domElem, dataTableLoadedProxy);
}
self.handleColumnDocumentation(domElem);
self.handleRelatedReports(domElem);
self.handleTriggeredEvents(domElem);
self.handleCellTooltips(domElem);
self.handleExpandFooter(domElem);
self.setFixWidthToMakeEllipsisWork(domElem);
},
//see dataTable::applyCosmetics
applyCosmetics: function (domElem, rows) {
var self = this;
var rowsWithSubtables = rows.filter('.subDataTable');
rowsWithSubtables.css('font-weight', 'bold');
$("th:first-child", domElem).addClass('label');
var imagePlusMinusWidth = 12;
var imagePlusMinusHeight = 12;
$('td:first-child', rowsWithSubtables)
.each(function () {
$(this).prepend('<img width="' + imagePlusMinusWidth + '" height="' + imagePlusMinusHeight + '" class="plusMinus" src="" />');
if (self.param.filter_pattern_recursive) {
setImageMinus(this);
}
else {
setImagePlus(this);
}
});
var rootRow = rows.first().prev();
// we look at the style of the row before the new rows to determine the rows'
// level
var level = rootRow.length ? getLevelFromClass(rootRow.attr('class')) + 1 : 0;
rows.each(function () {
var currentStyle = $(this).attr('class') || '';
if (currentStyle.indexOf('level') == -1) {
$(this).addClass('level' + level);
}
// we add an attribute parent that contains the ID of all the parent categories
// this ID is used when collapsing a parent row, it searches for all children rows
// which 'parent' attribute's value contains the collapsed row ID
$(this).prop('parent', function () {
return self.parentAttributeParent + ' ' + self.parentId;
});
});
self.addOddAndEvenClasses(domElem);
},
addOddAndEvenClasses: function(domElem) {
// Add some styles on the cells even/odd
// label (first column of a data row) or not
$("tr:not(.hidden):odd td:first-child", domElem)
.removeClass('labeleven').addClass('label labelodd');
$("tr:not(.hidden):even td:first-child", domElem)
.removeClass('labelodd').addClass('label labeleven');
$("tr:not(.hidden):odd td", domElem).slice(1)
.removeClass('columneven').addClass('column columnodd');
$("tr:not(.hidden):even td", domElem).slice(1)
.removeClass('columnodd').addClass('column columneven');
},
handleRowActions: function (domElem, rows) {
this.doHandleRowActions(rows);
},
// Called when the user click on an actionDataTable row
onClickActionSubDataTable: function (domElem) {
var self = this;
// get the idSubTable
var idSubTable = $(domElem).attr('id');
var divIdToReplaceWithSubTable = 'subDataTable_' + idSubTable;
var NextStyle = $(domElem).next().attr('class');
var CurrentStyle = $(domElem).attr('class');
var currentRowLevel = getLevelFromClass(CurrentStyle);
var nextRowLevel = getLevelFromClass(NextStyle);
// if the row has not been clicked
// which is the same as saying that the next row level is equal or less than the current row
// because when we click a row the level of the next rows is higher (level2 row gives level3 rows)
if (currentRowLevel >= nextRowLevel) {
//unbind click to avoid double click problem
$(domElem).off('click');
self.disabledRowDom = $(domElem);
var numberOfColumns = $(domElem).children().length;
$(domElem).after('\
<tr id="' + divIdToReplaceWithSubTable + '" class="cellSubDataTable">\
<td colspan="' + numberOfColumns + '">\
<span class="loadingPiwik" style="display:inline"><img src="plugins/Zeitgeist/images/loading-blue.gif" /> Loading...</span>\
</td>\
</tr>\
');
var savedActionVariable = self.param.action;
// reset all the filters from the Parent table
var filtersToRestore = self.resetAllFilters();
// Do not reset the sorting filters that must be applied to sub tables
this.param['filter_sort_column'] = filtersToRestore['filter_sort_column'];
this.param['filter_sort_order'] = filtersToRestore['filter_sort_order'];
this.param['enable_filter_excludelowpop'] = filtersToRestore['enable_filter_excludelowpop'];
self.param.idSubtable = idSubTable;
self.param.action = self.props.subtable_controller_action;
self.reloadAjaxDataTable(false, function (resp) {
self.actionsSubDataTableLoaded(resp, idSubTable);
self.repositionRowActions($(domElem));
});
self.param.action = savedActionVariable;
self.restoreAllFilters(filtersToRestore);
delete self.param.idSubtable;
}
// else we toggle all these rows
else {
var plusDetected = $('td img.plusMinus', domElem).attr('src').indexOf('plus') >= 0;
var stripingNeeded = false;
$(domElem).siblings().each(function () {
var parents = $(this).prop('parent').split(' ');
if (parents) {
if (parents.indexOf(idSubTable) >= 0
|| parents.indexOf('subDataTable_' + idSubTable) >= 0) {
if (plusDetected) {
$(this).css('display', '').removeClass('hidden');
stripingNeeded = !stripingNeeded;
//unroll everything and display '-' sign
//if the row is already opened
var NextStyle = $(this).next().attr('class');
var CurrentStyle = $(this).attr('class');
var currentRowLevel = getLevelFromClass(CurrentStyle);
var nextRowLevel = getLevelFromClass(NextStyle);
if (currentRowLevel < nextRowLevel)
setImageMinus(this);
}
else {
$(this).css('display', 'none').addClass('hidden');
stripingNeeded = !stripingNeeded;
}
self.repositionRowActions($(domElem));
}
}
});
var table = $(domElem);
if (!table.hasClass('dataTable')) {
table = table.closest('.dataTable');
}
if (stripingNeeded) {
self.addOddAndEvenClasses(table);
}
self.$element.trigger('piwik:actionsSubTableToggled');
}
// toggle the +/- image
var plusDetected = $('td img.plusMinus', domElem).attr('src').indexOf('plus') >= 0;
if (plusDetected) {
setImageMinus(domElem);
}
else {
setImagePlus(domElem);
}
},
//called when the full table actions is loaded
dataTableLoaded: function (response, workingDivId) {
var content = $(response);
var idToReplace = workingDivId || $(content).attr('id');
//reset parents id
self.parentAttributeParent = '';
self.parentId = '';
var dataTableSel = $('#' + idToReplace);
// keep the original list of related reports
var oldReportsElem = $('.datatableRelatedReports', dataTableSel);
$('.datatableRelatedReports', content).replaceWith(oldReportsElem);
dataTableSel.replaceWith(content);
content.trigger('piwik:dataTableLoaded');
piwikHelper.lazyScrollTo(content[0], 400);
return content;
},
// Called when a set of rows for a category of actions is loaded
actionsSubDataTableLoaded: function (response, idSubTable) {
var self = this;
var idToReplace = 'subDataTable_' + idSubTable;
var root = $('#' + self.workingDivId);
var response = $(response);
self.parentAttributeParent = $('tr#' + idToReplace).prev().prop('parent');
self.parentId = idToReplace;
$('tr#' + idToReplace, root).after(response).remove();
var missingColumns = (response.prev().find('td').size() - response.find('td').size());
for (var i = 0; i < missingColumns; i++) {
// if the subtable has fewer columns than the parent table, add some columns.
// this happens for example, when the parent table has performance metrics and the subtable doesn't.
response.append('<td>-</td>');
}
var re = /subDataTable_(\d+)/;
var ok = re.exec(self.parentId);
if (ok) {
self.parentId = ok[1];
}
// we execute the bindDataTableEvent function for the new DIV
self.bindEventsAndApplyStyle($('#' + self.workingDivId), response);
self.$element.trigger('piwik:actionsSubDataTableLoaded');
//bind back the click event (disabled to avoid double-click problem)
self.disabledRowDom.click(
function () {
self.onClickActionSubDataTable(this)
});
}
});
})(jQuery, require);

View file

@ -0,0 +1,4 @@
.dataTableActions > .dataTableWrapper {
width: 500px;
min-height: 1px;
}

View file

@ -0,0 +1,17 @@
<div id='leftcolumn'>
<h2 piwik-enriched-headline>{{ 'Actions_WidgetSearchKeywords'|translate }}</h2>
{{ keywords|raw }}
<h2 piwik-enriched-headline>{{ 'Actions_WidgetSearchNoResultKeywords'|translate }}</h2>
{{ noResultKeywords|raw }}
{% if categories is defined %}
<h2 piwik-enriched-headline>{{ 'Actions_WidgetSearchCategories'|translate }}</h2>
{{ categories|raw }}
{% endif %}
</div>
<div id='rightcolumn'>
<h2 piwik-enriched-headline>{{ 'Actions_WidgetPageUrlsFollowingSearch'|translate }}</h2>
{{ pagesUrlsFollowingSiteSearch|raw }}
</div>

View file

@ -0,0 +1,371 @@
<?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\Plugins\Annotations;
use Exception;
use Piwik\Date;
use Piwik\Period;
use Piwik\Period\Range;
use Piwik\Piwik;
use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution as EvolutionViz;
/**
* @see plugins/Annotations/AnnotationList.php
*/
require_once PIWIK_INCLUDE_PATH . '/plugins/Annotations/AnnotationList.php';
/**
* API for annotations plugin. Provides methods to create, modify, delete & query
* annotations.
*
* @method static \Piwik\Plugins\Annotations\API getInstance()
*/
class API extends \Piwik\Plugin\API
{
/**
* Create a new annotation for a site.
*
* @param string $idSite The site ID to add the annotation to.
* @param string $date The date the annotation is attached to.
* @param string $note The text of the annotation.
* @param int $starred Either 0 or 1. Whether the annotation should be starred.
* @return array Returns an array of two elements. The first element (indexed by
* 'annotation') is the new annotation. The second element (indexed
* by 'idNote' is the new note's ID).
*/
public function add($idSite, $date, $note, $starred = 0)
{
$this->checkSingleIdSite($idSite, $extraMessage = "Note: Cannot add one note to multiple sites.");
$this->checkDateIsValid($date);
$this->checkUserCanAddNotesFor($idSite);
// add, save & return a new annotation
$annotations = new AnnotationList($idSite);
$newAnnotation = $annotations->add($idSite, $date, $note, $starred);
$annotations->save($idSite);
return $newAnnotation;
}
/**
* Modifies an annotation for a site and returns the modified annotation
* and its ID.
*
* If the current user is not allowed to modify an annotation, an exception
* will be thrown. A user can modify a note if:
* - the user has admin access for the site, OR
* - the user has view access, is not the anonymous user and is the user that
* created the note
*
* @param string $idSite The site ID to add the annotation to.
* @param string $idNote The ID of the note.
* @param string|null $date The date the annotation is attached to. If null, the annotation's
* date is not modified.
* @param string|null $note The text of the annotation. If null, the annotation's text
* is not modified.
* @param string|null $starred Either 0 or 1. Whether the annotation should be starred.
* If null, the annotation is not starred/un-starred.
* @return array Returns an array of two elements. The first element (indexed by
* 'annotation') is the new annotation. The second element (indexed
* by 'idNote' is the new note's ID).
*/
public function save($idSite, $idNote, $date = null, $note = null, $starred = null)
{
$this->checkSingleIdSite($idSite, $extraMessage = "Note: Cannot modify more than one note at a time.");
$this->checkDateIsValid($date, $canBeNull = true);
// get the annotations for the site
$annotations = new AnnotationList($idSite);
// check permissions
$this->checkUserCanModifyOrDelete($idSite, $annotations->get($idSite, $idNote));
// modify the annotation, and save the whole list
$annotations->update($idSite, $idNote, $date, $note, $starred);
$annotations->save($idSite);
return $annotations->get($idSite, $idNote);
}
/**
* Removes an annotation from a site's list of annotations.
*
* If the current user is not allowed to delete the annotation, an exception
* will be thrown. A user can delete a note if:
* - the user has admin access for the site, OR
* - the user has view access, is not the anonymous user and is the user that
* created the note
*
* @param string $idSite The site ID to add the annotation to.
* @param string $idNote The ID of the note to delete.
*/
public function delete($idSite, $idNote)
{
$this->checkSingleIdSite($idSite, $extraMessage = "Note: Cannot delete annotations from multiple sites.");
$annotations = new AnnotationList($idSite);
// check permissions
$this->checkUserCanModifyOrDelete($idSite, $annotations->get($idSite, $idNote));
// remove the note & save the list
$annotations->remove($idSite, $idNote);
$annotations->save($idSite);
}
/**
* Removes all annotations for a single site. Only super users can use this method.
*
* @param string $idSite The ID of the site to remove annotations for.
*/
public function deleteAll($idSite)
{
$this->checkSingleIdSite($idSite, $extraMessage = "Note: Cannot delete annotations from multiple sites.");
Piwik::checkUserHasSuperUserAccess();
$annotations = new AnnotationList($idSite);
// remove the notes & save the list
$annotations->removeAll($idSite);
$annotations->save($idSite);
}
/**
* Returns a single note for one site.
*
* @param string $idSite The site ID to add the annotation to.
* @param string $idNote The ID of the note to get.
* @return array The annotation. It will contain the following properties:
* - date: The date the annotation was recorded for.
* - note: The note text.
* - starred: Whether the note is starred or not.
* - user: The user that created the note.
* - canEditOrDelete: Whether the user that called this method can edit or
* delete the annotation returned.
*/
public function get($idSite, $idNote)
{
$this->checkSingleIdSite($idSite, $extraMessage = "Note: Specify only one site ID when getting ONE note.");
Piwik::checkUserHasViewAccess($idSite);
// get single annotation
$annotations = new AnnotationList($idSite);
return $annotations->get($idSite, $idNote);
}
/**
* Returns every annotation for a specific site within a specific date range.
* The date range is specified by a date, the period type (day/week/month/year)
* and an optional number of N periods in the past to include.
*
* @param string $idSite The site ID to add the annotation to. Can be one ID or
* a list of site IDs.
* @param bool|string $date The date of the period.
* @param string $period The period type.
* @param bool|int $lastN Whether to include the last N number of periods in the
* date range or not.
* @return array An array that indexes arrays of annotations by site ID. ie,
* array(
* 5 => array(
* array(...), // annotation #1
* array(...), // annotation #2
* ),
* 8 => array(...)
* )
*/
public function getAll($idSite, $date = false, $period = 'day', $lastN = false)
{
Piwik::checkUserHasViewAccess($idSite);
$annotations = new AnnotationList($idSite);
// if date/period are supplied, determine start/end date for search
list($startDate, $endDate) = self::getDateRangeForPeriod($date, $period, $lastN);
return $annotations->search($startDate, $endDate);
}
/**
* Returns the count of annotations for a list of periods, including the count of
* starred annotations.
*
* @param string $idSite The site ID to add the annotation to.
* @param string|bool $date The date of the period.
* @param string $period The period type.
* @param int|bool $lastN Whether to get counts for the last N number of periods or not.
* @param bool $getAnnotationText
* @return array An array mapping site IDs to arrays holding dates & the count of
* annotations made for those dates. eg,
* array(
* 5 => array(
* array('2012-01-02', array('count' => 4, 'starred' => 2)),
* array('2012-01-03', array('count' => 0, 'starred' => 0)),
* array('2012-01-04', array('count' => 2, 'starred' => 0)),
* ),
* 6 => array(
* array('2012-01-02', array('count' => 1, 'starred' => 0)),
* array('2012-01-03', array('count' => 4, 'starred' => 3)),
* array('2012-01-04', array('count' => 2, 'starred' => 0)),
* ),
* ...
* )
*/
public function getAnnotationCountForDates($idSite, $date, $period, $lastN = false, $getAnnotationText = false)
{
Piwik::checkUserHasViewAccess($idSite);
// get start & end date for request. lastN is ignored if $period == 'range'
list($startDate, $endDate) = self::getDateRangeForPeriod($date, $period, $lastN);
if ($period == 'range') {
$period = 'day';
}
// create list of dates
$dates = array();
for (; $startDate->getTimestamp() <= $endDate->getTimestamp(); $startDate = $startDate->addPeriod(1, $period)) {
$dates[] = $startDate;
}
// we add one for the end of the last period (used in for loop below to bound annotation dates)
$dates[] = $startDate;
// get annotations for the site
$annotations = new AnnotationList($idSite);
// create result w/ 0-counts
$result = array();
for ($i = 0; $i != count($dates) - 1; ++$i) {
$date = $dates[$i];
$nextDate = $dates[$i + 1];
$strDate = $date->toString();
foreach ($annotations->getIdSites() as $idSite) {
$result[$idSite][$strDate] = $annotations->count($idSite, $date, $nextDate);
// if only one annotation, return the one annotation's text w/ the counts
if ($getAnnotationText
&& $result[$idSite][$strDate]['count'] == 1
) {
$annotationsForSite = $annotations->search(
$date, Date::factory($nextDate->getTimestamp() - 1), $idSite);
$annotation = reset($annotationsForSite[$idSite]);
$result[$idSite][$strDate]['note'] = $annotation['note'];
}
}
}
// convert associative array into array of pairs (so it can be traversed by index)
$pairResult = array();
foreach ($result as $idSite => $counts) {
foreach ($counts as $date => $count) {
$pairResult[$idSite][] = array($date, $count);
}
}
return $pairResult;
}
/**
* Throws if the current user is not allowed to modify or delete an annotation.
*
* @param int $idSite The site ID the annotation belongs to.
* @param array $annotation The annotation.
* @throws Exception if the current user is not allowed to modify/delete $annotation.
*/
private function checkUserCanModifyOrDelete($idSite, $annotation)
{
if (!$annotation['canEditOrDelete']) {
throw new Exception(Piwik::translate('Annotations_YouCannotModifyThisNote'));
}
}
/**
* Throws if the current user is not allowed to create annotations for a site.
*
* @param int $idSite The site ID.
* @throws Exception if the current user is anonymous or does not have view access
* for site w/ id=$idSite.
*/
private static function checkUserCanAddNotesFor($idSite)
{
if (!AnnotationList::canUserAddNotesFor($idSite)) {
throw new Exception("The current user is not allowed to add notes for site #$idSite.");
}
}
/**
* Returns start & end dates for the range described by a period and optional lastN
* argument.
*
* @param string|bool $date The start date of the period (or the date range of a range
* period).
* @param string $period The period type ('day', 'week', 'month', 'year' or 'range').
* @param bool|int $lastN Whether to include the last N periods in the range or not.
* Ignored if period == range.
*
* @return Date[] array of Date objects or array(false, false)
* @ignore
*/
public static function getDateRangeForPeriod($date, $period, $lastN = false)
{
if ($date === false) {
return array(false, false);
}
// if the range is just a normal period (or the period is a range in which case lastN is ignored)
if ($lastN === false
|| $period == 'range'
) {
if ($period == 'range') {
$oPeriod = new Range('day', $date);
} else {
$oPeriod = Period::factory($period, Date::factory($date));
}
$startDate = $oPeriod->getDateStart();
$endDate = $oPeriod->getDateEnd();
} else // if the range includes the last N periods
{
list($date, $lastN) = EvolutionViz::getDateRangeAndLastN($period, $date, $lastN);
list($startDate, $endDate) = explode(',', $date);
$startDate = Date::factory($startDate);
$endDate = Date::factory($endDate);
}
return array($startDate, $endDate);
}
/**
* Utility function, makes sure idSite string has only one site ID and throws if
* otherwise.
*/
private function checkSingleIdSite($idSite, $extraMessage)
{
// can only add a note to one site
if (!is_numeric($idSite)) {
throw new Exception("Invalid idSite: '$idSite'. $extraMessage");
}
}
/**
* Utility function, makes sure date string is valid date, and throws if
* otherwise.
*/
private function checkDateIsValid($date, $canBeNull = false)
{
if ($date === null
&& $canBeNull
) {
return;
}
Date::factory($date);
}
}

View file

@ -0,0 +1,455 @@
<?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\Plugins\Annotations;
use Exception;
use Piwik\Date;
use Piwik\Option;
use Piwik\Piwik;
use Piwik\Site;
/**
* This class can be used to query & modify annotations for multiple sites
* at once.
*
* Example use:
* $annotations = new AnnotationList($idSites = "1,2,5");
* $annotation = $annotations->get($idSite = 1, $idNote = 4);
* // do stuff w/ annotation
* $annotations->update($idSite = 2, $idNote = 4, $note = "This is the new text.");
* $annotations->save($idSite);
*
* Note: There is a concurrency issue w/ this code. If two users try to save
* an annotation for the same site, it's possible one of their changes will
* never get made (as it will be overwritten by the other's).
*
*/
class AnnotationList
{
const ANNOTATION_COLLECTION_OPTION_SUFFIX = '_annotations';
/**
* List of site IDs this instance holds annotations for.
*
* @var array
*/
private $idSites;
/**
* Array that associates lists of annotations with site IDs.
*
* @var array
*/
private $annotations;
/**
* Constructor. Loads annotations from the database.
*
* @param string|int $idSites The list of site IDs to load annotations for.
*/
public function __construct($idSites)
{
$this->idSites = Site::getIdSitesFromIdSitesString($idSites);
$this->annotations = $this->getAnnotationsForSite();
}
/**
* Returns the list of site IDs this list contains annotations for.
*
* @return array
*/
public function getIdSites()
{
return $this->idSites;
}
/**
* Creates a new annotation for a site. This method does not perist the result.
* To save the new annotation in the database, call $this->save.
*
* @param int $idSite The ID of the site to add an annotation to.
* @param string $date The date the annotation is in reference to.
* @param string $note The text of the new annotation.
* @param int $starred Either 1 or 0. If 1, the new annotation has been starred,
* otherwise it will start out unstarred.
* @return array The added annotation.
* @throws Exception if $idSite is not an ID that was supplied upon construction.
*/
public function add($idSite, $date, $note, $starred = 0)
{
$this->checkIdSiteIsLoaded($idSite);
$this->annotations[$idSite][] = self::makeAnnotation($date, $note, $starred);
// get the id of the new annotation
end($this->annotations[$idSite]);
$newNoteId = key($this->annotations[$idSite]);
return $this->get($idSite, $newNoteId);
}
/**
* Persists the annotations list for a site, overwriting whatever exists.
*
* @param int $idSite The ID of the site to save annotations for.
* @throws Exception if $idSite is not an ID that was supplied upon construction.
*/
public function save($idSite)
{
$this->checkIdSiteIsLoaded($idSite);
$optionName = self::getAnnotationCollectionOptionName($idSite);
Option::set($optionName, serialize($this->annotations[$idSite]));
}
/**
* Modifies an annotation in this instance's collection of annotations.
*
* Note: This method does not perist the change in the DB. The save method must
* be called for that.
*
* @param int $idSite The ID of the site whose annotation will be updated.
* @param int $idNote The ID of the note.
* @param string|null $date The new date of the annotation, eg '2012-01-01'. If
* null, no change is made.
* @param string|null $note The new text of the annotation. If null, no change
* is made.
* @param int|null $starred Either 1 or 0, whether the annotation should be
* starred or not. If null, no change is made.
* @throws Exception if $idSite is not an ID that was supplied upon construction.
* @throws Exception if $idNote does not refer to valid note for the site.
*/
public function update($idSite, $idNote, $date = null, $note = null, $starred = null)
{
$this->checkIdSiteIsLoaded($idSite);
$this->checkNoteExists($idSite, $idNote);
$annotation =& $this->annotations[$idSite][$idNote];
if ($date !== null) {
$annotation['date'] = $date;
}
if ($note !== null) {
$annotation['note'] = $note;
}
if ($starred !== null) {
$annotation['starred'] = $starred;
}
}
/**
* Removes a note from a site's collection of annotations.
*
* Note: This method does not perist the change in the DB. The save method must
* be called for that.
*
* @param int $idSite The ID of the site whose annotation will be updated.
* @param int $idNote The ID of the note.
* @throws Exception if $idSite is not an ID that was supplied upon construction.
* @throws Exception if $idNote does not refer to valid note for the site.
*/
public function remove($idSite, $idNote)
{
$this->checkIdSiteIsLoaded($idSite);
$this->checkNoteExists($idSite, $idNote);
unset($this->annotations[$idSite][$idNote]);
}
/**
* Removes all notes for a single site.
*
* Note: This method does not perist the change in the DB. The save method must
* be called for that.
*
* @param int $idSite The ID of the site to get an annotation for.
* @throws Exception if $idSite is not an ID that was supplied upon construction.
*/
public function removeAll($idSite)
{
$this->checkIdSiteIsLoaded($idSite);
$this->annotations[$idSite] = array();
}
/**
* Retrieves an annotation by ID.
*
* This function returns an array with the following elements:
* - idNote: The ID of the annotation.
* - date: The date of the annotation.
* - note: The text of the annotation.
* - starred: 1 or 0, whether the annotation is stared;
* - user: (unless current user is anonymous) The user that created the annotation.
* - canEditOrDelete: True if the user can edit/delete the annotation.
*
* @param int $idSite The ID of the site to get an annotation for.
* @param int $idNote The ID of the note to get.
* @throws Exception if $idSite is not an ID that was supplied upon construction.
* @throws Exception if $idNote does not refer to valid note for the site.
*/
public function get($idSite, $idNote)
{
$this->checkIdSiteIsLoaded($idSite);
$this->checkNoteExists($idSite, $idNote);
$annotation = $this->annotations[$idSite][$idNote];
$this->augmentAnnotationData($idSite, $idNote, $annotation);
return $annotation;
}
/**
* Returns all annotations within a specific date range. The result is
* an array that maps site IDs with arrays of annotations within the range.
*
* Note: The date range is inclusive.
*
* @see self::get for info on what attributes stored within annotations.
*
* @param Date|bool $startDate The start of the date range.
* @param Date|bool $endDate The end of the date range.
* @param array|bool|int|string $idSite IDs of the sites whose annotations to
* search through.
* @return array Array mapping site IDs with arrays of annotations, eg:
* array(
* '5' => array(
* array(...), // annotation
* array(...), // annotation
* ...
* ),
* '6' => array(
* array(...), // annotation
* array(...), // annotation
* ...
* ),
* )
*/
public function search($startDate, $endDate, $idSite = false)
{
if ($idSite) {
$idSites = Site::getIdSitesFromIdSitesString($idSite);
} else {
$idSites = array_keys($this->annotations);
}
// collect annotations that are within the right date range & belong to the right
// site
$result = array();
foreach ($idSites as $idSite) {
if (!isset($this->annotations[$idSite])) {
continue;
}
foreach ($this->annotations[$idSite] as $idNote => $annotation) {
if ($startDate !== false) {
$annotationDate = Date::factory($annotation['date']);
if ($annotationDate->getTimestamp() < $startDate->getTimestamp()
|| $annotationDate->getTimestamp() > $endDate->getTimestamp()
) {
continue;
}
}
$this->augmentAnnotationData($idSite, $idNote, $annotation);
$result[$idSite][] = $annotation;
}
// sort by annotation date
if (!empty($result[$idSite])) {
uasort($result[$idSite], array($this, 'compareAnnotationDate'));
}
}
return $result;
}
/**
* Counts annotations & starred annotations within a date range and returns
* the counts. The date range includes the start date, but not the end date.
*
* @param int $idSite The ID of the site to count annotations for.
* @param string|false $startDate The start date of the range or false if no
* range check is desired.
* @param string|false $endDate The end date of the range or false if no
* range check is desired.
* @return array eg, array('count' => 5, 'starred' => 2)
*/
public function count($idSite, $startDate, $endDate)
{
$this->checkIdSiteIsLoaded($idSite);
// search includes end date, and count should not, so subtract one from the timestamp
$annotations = $this->search($startDate, Date::factory($endDate->getTimestamp() - 1));
// count the annotations
$count = $starred = 0;
if (!empty($annotations[$idSite])) {
$count = count($annotations[$idSite]);
foreach ($annotations[$idSite] as $annotation) {
if ($annotation['starred']) {
++$starred;
}
}
}
return array('count' => $count, 'starred' => $starred);
}
/**
* Utility function. Creates a new annotation.
*
* @param string $date
* @param string $note
* @param int $starred
* @return array
*/
private function makeAnnotation($date, $note, $starred = 0)
{
return array('date' => $date,
'note' => $note,
'starred' => (int)$starred,
'user' => Piwik::getCurrentUserLogin());
}
/**
* Retrieves annotations from the database for the sites supplied to the
* constructor.
*
* @return array Lists of annotations mapped by site ID.
*/
private function getAnnotationsForSite()
{
$result = array();
foreach ($this->idSites as $id) {
$optionName = self::getAnnotationCollectionOptionName($id);
$serialized = Option::get($optionName);
if ($serialized !== false) {
$result[$id] = @unserialize($serialized);
if(empty($result[$id])) {
// in case unserialize failed
$result[$id] = array();
}
} else {
$result[$id] = array();
}
}
return $result;
}
/**
* Utility function that checks if a site ID was supplied and if not,
* throws an exception.
*
* We can only modify/read annotations for sites that we've actually
* loaded the annotations for.
*
* @param int $idSite
* @throws Exception
*/
private function checkIdSiteIsLoaded($idSite)
{
if (!in_array($idSite, $this->idSites)) {
throw new Exception("This AnnotationList was not initialized with idSite '$idSite'.");
}
}
/**
* Utility function that checks if a note exists for a site, and if not,
* throws an exception.
*
* @param int $idSite
* @param int $idNote
* @throws Exception
*/
private function checkNoteExists($idSite, $idNote)
{
if (empty($this->annotations[$idSite][$idNote])) {
throw new Exception("There is no note with id '$idNote' for site with id '$idSite'.");
}
}
/**
* Returns true if the current user can modify or delete a specific annotation.
*
* A user can modify/delete a note if the user has admin access for the site OR
* the user has view access, is not the anonymous user and is the user that
* created the note in question.
*
* @param int $idSite The site ID the annotation belongs to.
* @param array $annotation The annotation.
* @return bool
*/
public static function canUserModifyOrDelete($idSite, $annotation)
{
// user can save if user is admin or if has view access, is not anonymous & is user who wrote note
$canEdit = Piwik::isUserHasAdminAccess($idSite)
|| (!Piwik::isUserIsAnonymous()
&& Piwik::getCurrentUserLogin() == $annotation['user']);
return $canEdit;
}
/**
* Adds extra data to an annotation, including the annotation's ID and whether
* the current user can edit or delete it.
*
* Also, if the current user is anonymous, the user attribute is removed.
*
* @param int $idSite
* @param int $idNote
* @param array $annotation
*/
private function augmentAnnotationData($idSite, $idNote, &$annotation)
{
$annotation['idNote'] = $idNote;
$annotation['canEditOrDelete'] = self::canUserModifyOrDelete($idSite, $annotation);
// we don't supply user info if the current user is anonymous
if (Piwik::isUserIsAnonymous()) {
unset($annotation['user']);
}
}
/**
* Utility function that compares two annotations.
*
* @param array $lhs An annotation.
* @param array $rhs An annotation.
* @return int -1, 0 or 1
*/
public function compareAnnotationDate($lhs, $rhs)
{
if ($lhs['date'] == $rhs['date']) {
return $lhs['idNote'] <= $rhs['idNote'] ? -1 : 1;
}
return $lhs['date'] < $rhs['date'] ? -1 : 1; // string comparison works because date format should be YYYY-MM-DD
}
/**
* Returns true if the current user can add notes for a specific site.
*
* @param int $idSite The site to add notes to.
* @return bool
*/
public static function canUserAddNotesFor($idSite)
{
return Piwik::isUserHasViewAccess($idSite)
&& !Piwik::isUserIsAnonymous($idSite);
}
/**
* Returns the option name used to store annotations for a site.
*
* @param int $idSite The site ID.
* @return string
*/
public static function getAnnotationCollectionOptionName($idSite)
{
return $idSite . self::ANNOTATION_COLLECTION_OPTION_SUFFIX;
}
}

View file

@ -0,0 +1,44 @@
<?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\Plugins\Annotations;
/**
* Annotations plugins. Provides the ability to attach text notes to
* dates for each sites. Notes can be viewed, modified, deleted or starred.
*
*/
class Annotations extends \Piwik\Plugin
{
/**
* @see Piwik\Plugin::getListHooksRegistered
*/
public function getListHooksRegistered()
{
return array(
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
);
}
/**
* Adds css files for this plugin to the list in the event notification.
*/
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = "plugins/Annotations/stylesheets/annotations.less";
}
/**
* Adds js files for this plugin to the list in the event notification.
*/
public function getJsFiles(&$jsFiles)
{
$jsFiles[] = "plugins/Annotations/javascripts/annotations.js";
}
}

View file

@ -0,0 +1,215 @@
<?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\Plugins\Annotations;
use Piwik\API\Request;
use Piwik\Common;
use Piwik\Piwik;
use Piwik\View;
/**
* Controller for the Annotations plugin.
*
*/
class Controller extends \Piwik\Plugin\Controller
{
/**
* Controller action that returns HTML displaying annotations for a site and
* specific date range.
*
* Query Param Input:
* - idSite: The ID of the site to get annotations for. Only one allowed.
* - date: The date to get annotations for. If lastN is not supplied, this is the start date,
* otherwise the start date in the last period.
* - period: The period type.
* - lastN: If supplied, the last N # of periods will be included w/ the range specified
* by date + period.
*
* Output:
* - HTML displaying annotations for a specific range.
*
* @param bool $fetch True if the annotation manager should be returned as a string,
* false if it should be echo-ed.
* @param bool|string $date Override for 'date' query parameter.
* @param bool|string $period Override for 'period' query parameter.
* @param bool|string $lastN Override for 'lastN' query parameter.
* @return string|void
*/
public function getAnnotationManager($fetch = false, $date = false, $period = false, $lastN = false)
{
$idSite = Common::getRequestVar('idSite');
if ($date === false) {
$date = Common::getRequestVar('date', false);
}
if ($period === false) {
$period = Common::getRequestVar('period', 'day');
}
if ($lastN === false) {
$lastN = Common::getRequestVar('lastN', false);
}
// create & render the view
$view = new View('@Annotations/getAnnotationManager');
$allAnnotations = Request::processRequest(
'Annotations.getAll', array('date' => $date, 'period' => $period, 'lastN' => $lastN));
$view->annotations = empty($allAnnotations[$idSite]) ? array() : $allAnnotations[$idSite];
$view->period = $period;
$view->lastN = $lastN;
list($startDate, $endDate) = API::getDateRangeForPeriod($date, $period, $lastN);
$view->startDate = $startDate->toString();
$view->endDate = $endDate->toString();
$dateFormat = Piwik::translate('CoreHome_ShortDateFormatWithYear');
$view->startDatePretty = $startDate->getLocalized($dateFormat);
$view->endDatePretty = $endDate->getLocalized($dateFormat);
$view->canUserAddNotes = AnnotationList::canUserAddNotesFor($idSite);
return $view->render();
}
/**
* Controller action that modifies an annotation and returns HTML displaying
* the modified annotation.
*
* Query Param Input:
* - idSite: The ID of the site the annotation belongs to. Only one ID is allowed.
* - idNote: The ID of the annotation.
* - date: The new date value for the annotation. (optional)
* - note: The new text for the annotation. (optional)
* - starred: Either 1 or 0. Whether the note should be starred or not. (optional)
*
* Output:
* - HTML displaying modified annotation.
*
* If an optional query param is not supplied, that part of the annotation is
* not modified.
*/
public function saveAnnotation()
{
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$this->checkTokenInUrl();
$view = new View('@Annotations/saveAnnotation');
// NOTE: permissions checked in API method
// save the annotation
$view->annotation = Request::processRequest("Annotations.save");
return $view->render();
}
}
/**
* Controller action that adds a new annotation for a site and returns new
* annotation manager HTML for the site and date range.
*
* Query Param Input:
* - idSite: The ID of the site to add an annotation to.
* - date: The date for the new annotation.
* - note: The text of the annotation.
* - starred: Either 1 or 0, whether the annotation should be starred or not.
* Defaults to 0.
* - managerDate: The date for the annotation manager. If a range is given, the start
* date is used for the new annotation.
* - managerPeriod: For rendering the annotation manager. @see self::getAnnotationManager
* for more info.
* - lastN: For rendering the annotation manager. @see self::getAnnotationManager
* for more info.
* Output:
* - @see self::getAnnotationManager
*/
public function addAnnotation()
{
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$this->checkTokenInUrl();
// the date used is for the annotation manager HTML that gets echo'd. we
// use this date for the new annotation, unless it is a date range, in
// which case we use the first date of the range.
$date = Common::getRequestVar('date');
if (strpos($date, ',') !== false) {
$date = reset(explode(',', $date));
}
// add the annotation. NOTE: permissions checked in API method
Request::processRequest("Annotations.add", array('date' => $date));
$managerDate = Common::getRequestVar('managerDate', false);
$managerPeriod = Common::getRequestVar('managerPeriod', false);
return $this->getAnnotationManager($fetch = true, $managerDate, $managerPeriod);
}
}
/**
* Controller action that deletes an annotation and returns new annotation
* manager HTML for the site & date range.
*
* Query Param Input:
* - idSite: The ID of the site this annotation belongs to.
* - idNote: The ID of the annotation to delete.
* - date: For rendering the annotation manager. @see self::getAnnotationManager
* for more info.
* - period: For rendering the annotation manager. @see self::getAnnotationManager
* for more info.
* - lastN: For rendering the annotation manager. @see self::getAnnotationManager
* for more info.
*
* Output:
* - @see self::getAnnotationManager
*/
public function deleteAnnotation()
{
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$this->checkTokenInUrl();
// delete annotation. NOTE: permissions checked in API method
Request::processRequest("Annotations.delete");
return $this->getAnnotationManager($fetch = true);
}
}
/**
* Controller action that echo's HTML that displays marker icons for an
* evolution graph's x-axis. The marker icons still need to be positioned
* by the JavaScript.
*
* Query Param Input:
* - idSite: The ID of the site this annotation belongs to. Only one is allowed.
* - date: The date to check for annotations. If lastN is not supplied, this is
* the start of the date range used to check for annotations. If supplied,
* this is the start of the last period in the date range.
* - period: The period type.
* - lastN: If supplied, the last N # of periods are included in the date range
* used to check for annotations.
*
* Output:
* - HTML that displays marker icons for an evolution graph based on the
* number of annotations & starred annotations in the graph's date range.
*/
public function getEvolutionIcons()
{
// get annotation the count
$annotationCounts = Request::processRequest(
"Annotations.getAnnotationCountForDates", array('getAnnotationText' => 1));
// create & render the view
$view = new View('@Annotations/getEvolutionIcons');
$view->annotationCounts = reset($annotationCounts); // only one idSite allowed for this action
return $view->render();
}
}

View file

@ -0,0 +1,599 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function ($, piwik) {
var annotationsApi = {
// calls Annotations.getAnnotationManager
getAnnotationManager: function (idSite, date, period, lastN, callback) {
var ajaxParams =
{
module: 'Annotations',
action: 'getAnnotationManager',
idSite: idSite,
date: date,
period: period
};
if (lastN) {
ajaxParams.lastN = lastN;
}
var ajaxRequest = new ajaxHelper();
ajaxRequest.addParams(ajaxParams, 'get');
ajaxRequest.setCallback(callback);
ajaxRequest.setFormat('html');
ajaxRequest.send(false);
},
// calls Annotations.addAnnotation
addAnnotation: function (idSite, managerDate, managerPeriod, date, note, callback) {
var ajaxParams =
{
module: 'Annotations',
action: 'addAnnotation',
idSite: idSite,
date: date,
managerDate: managerDate,
managerPeriod: managerPeriod,
note: note
};
var ajaxRequest = new ajaxHelper();
ajaxRequest.addParams(ajaxParams, 'get');
ajaxRequest.setCallback(callback);
ajaxRequest.setFormat('html');
ajaxRequest.send(false);
},
// calls Annotations.saveAnnotation
saveAnnotation: function (idSite, idNote, date, noteData, callback) {
var ajaxParams =
{
module: 'Annotations',
action: 'saveAnnotation',
idSite: idSite,
idNote: idNote,
date: date
};
for (var key in noteData) {
ajaxParams[key] = noteData[key];
}
var ajaxRequest = new ajaxHelper();
ajaxRequest.addParams(ajaxParams, 'get');
ajaxRequest.setCallback(callback);
ajaxRequest.setFormat('html');
ajaxRequest.send(false);
},
// calls Annotations.deleteAnnotation
deleteAnnotation: function (idSite, idNote, managerDate, managerPeriod, callback) {
var ajaxParams =
{
module: 'Annotations',
action: 'deleteAnnotation',
idSite: idSite,
idNote: idNote,
date: managerDate,
period: managerPeriod
};
var ajaxRequest = new ajaxHelper();
ajaxRequest.addParams(ajaxParams, 'get');
ajaxRequest.setCallback(callback);
ajaxRequest.setFormat('html');
ajaxRequest.send(false);
},
// calls Annotations.getEvolutionIcons
getEvolutionIcons: function (idSite, date, period, lastN, callback) {
var ajaxParams =
{
module: 'Annotations',
action: 'getEvolutionIcons',
idSite: idSite,
date: date,
period: period
};
if (lastN) {
ajaxParams.lastN = lastN;
}
var ajaxRequest = new ajaxHelper();
ajaxRequest.addParams(ajaxParams, 'get');
ajaxRequest.setFormat('html');
ajaxRequest.setCallback(callback);
ajaxRequest.send(false);
}
};
var today = new Date();
/**
* Returns options to configure an annotation's datepicker shown in edit mode.
*
* @param {Element} annotation The annotation element.
*/
var getDatePickerOptions = function (annotation) {
var annotationDateStr = annotation.attr('data-date'),
parts = annotationDateStr.split('-'),
annotationDate = new Date(parts[0], parts[1] - 1, parts[2]);
var result = piwik.getBaseDatePickerOptions(annotationDate);
// make sure days before site start & after today cannot be selected
var piwikMinDate = result.minDate;
result.beforeShowDay = function (date) {
var valid = true;
// if date is after today or before date of site creation, it cannot be selected
if (date > today
|| date < piwikMinDate) {
valid = false;
}
return [valid, ''];
};
// on select a date, change the text of the edit date link
result.onSelect = function (dateText) {
$('.annotation-period-edit>a', annotation).text(dateText);
$('.datepicker', annotation).hide();
};
return result;
};
/**
* Switches the current mode of an annotation between the view/edit modes.
*
* @param {Element} inAnnotationElement An element within the annotation to toggle the mode of.
* Should be two levels nested in the .annotation-value
* element.
* @return {Element} The .annotation-value element.
*/
var toggleAnnotationMode = function (inAnnotationElement) {
var annotation = $(inAnnotationElement).closest('.annotation');
$('.annotation-period,.annotation-period-edit,.delete-annotation,' +
'.annotation-edit-mode,.annotation-view-mode', annotation).toggle();
return $(inAnnotationElement).find('.annotation-value');
};
/**
* Creates the datepicker for an annotation element.
*
* @param {Element} annotation The annotation element.
*/
var createDatePicker = function (annotation) {
$('.datepicker', annotation).datepicker(getDatePickerOptions(annotation)).hide();
};
/**
* Creates datepickers for every period edit in an annotation manager.
*
* @param {Element} manager The annotation manager element.
*/
var createDatePickers = function (manager) {
$('.annotation-period-edit', manager).each(function () {
createDatePicker($(this).parent().parent());
});
};
/**
* Replaces the HTML of an annotation manager element, and resets date/period
* attributes.
*
* @param {Element} manager The annotation manager.
* @param {string} html The HTML of the new annotation manager.
*/
var replaceAnnotationManager = function (manager, html) {
var newManager = $(html);
manager.html(newManager.html())
.attr('data-date', newManager.attr('data-date'))
.attr('data-period', newManager.attr('data-period'));
createDatePickers(manager);
};
/**
* Returns true if an annotation element is starred, false if otherwise.
*
* @param {Element} annotation The annotation element.
* @return {boolean}
*/
var isAnnotationStarred = function (annotation) {
return !!(+$('.annotation-star', annotation).attr('data-starred') == 1);
};
/**
* Replaces the HTML of an annotation element with HTML returned from Piwik, and
* makes sure the data attributes are correct.
*
* @param {Element} annotation The annotation element.
* @param {string} html The replacement HTML (or alternatively, the replacement
* element/jQuery object).
*/
var replaceAnnotationHtml = function (annotation, html) {
var newHtml = $(html);
annotation.html(newHtml.html()).attr('data-date', newHtml.attr('data-date'));
createDatePicker(annotation);
};
/**
* Binds events to an annotation manager element.
*
* @param {Element} manager The annotation manager.
* @param {int} idSite The site ID the manager is showing annotations for.
* @param {function} onAnnotationCountChange Callback that is called when there is a change
* in the number of annotations and/or starred annotations,
* eg, when a user adds a new one or deletes an existing one.
*/
var bindAnnotationManagerEvents = function (manager, idSite, onAnnotationCountChange) {
if (!onAnnotationCountChange) {
onAnnotationCountChange = function () {};
}
// show new annotation row if create new annotation link is clicked
manager.on('click', '.add-annotation', function (e) {
e.preventDefault();
$('.new-annotation-row', manager).show();
$(this).hide();
return false;
});
// hide new annotation row if cancel button clicked
manager.on('click', '.new-annotation-cancel', function () {
var newAnnotationRow = $(this).parent().parent();
newAnnotationRow.hide();
$('.add-annotation', newAnnotationRow.closest('.annotation-manager')).show();
});
// save new annotation when new annotation row save is clicked
manager.on('click', '.new-annotation-save', function () {
var addRow = $(this).parent().parent(),
addNoteInput = addRow.find('.new-annotation-edit'),
noteDate = addRow.find('.annotation-period-edit>a').text();
// do nothing if input is empty
if (!addNoteInput.val()) {
return;
}
// disable input & link
addNoteInput.attr('disabled', 'disabled');
$(this).attr('disabled', 'disabled');
// add a new annotation for the site, date & period
annotationsApi.addAnnotation(
idSite,
manager.attr('data-date'),
manager.attr('data-period'),
noteDate,
addNoteInput.val(),
function (response) {
replaceAnnotationManager(manager, response);
// increment annotation count for this date
onAnnotationCountChange(noteDate, 1, 0);
}
);
});
// add new annotation when enter key pressed on new annotation input
manager.on('keypress', '.new-annotation-edit', function (e) {
if (e.which == 13) {
$(this).parent().find('.new-annotation-save').click();
}
});
// show annotation editor if edit link, annotation text or period text is clicked
manager.on('click', '.annotation-enter-edit-mode', function (e) {
e.preventDefault();
var annotationContent = toggleAnnotationMode(this);
annotationContent.find('.annotation-edit').focus();
return false;
});
// hide annotation editor if cancel button is clicked
manager.on('click', '.annotation-cancel', function () {
toggleAnnotationMode(this);
});
// save annotation if save button clicked
manager.on('click', '.annotation-edit-mode .annotation-save', function () {
var annotation = $(this).parent().parent().parent(),
input = $('.annotation-edit', annotation),
dateEditText = $('.annotation-period-edit>a', annotation).text();
// if annotation value/date has not changed, just show the view mode instead of edit
if (input[0].defaultValue == input.val()
&& dateEditText == annotation.attr('data-date')) {
toggleAnnotationMode(this);
return;
}
// disable input while ajax is happening
input.attr('disabled', 'disabled');
$(this).attr('disabled', 'disabled');
// save the note w/ the new note text & date
annotationsApi.saveAnnotation(
idSite,
annotation.attr('data-id'),
dateEditText,
{
note: input.val()
},
function (response) {
response = $(response);
var newDate = response.attr('data-date'),
isStarred = isAnnotationStarred(response),
originalDate = annotation.attr('data-date');
replaceAnnotationHtml(annotation, response);
// if the date has been changed, update the evolution icon counts to reflect the change
if (originalDate != newDate) {
// reduce count for original date
onAnnotationCountChange(originalDate, -1, isStarred ? -1 : 0);
// increase count for new date
onAnnotationCountChange(newDate, 1, isStarred ? 1 : 0);
}
}
);
});
// save annotation if 'enter' pressed on input
manager.on('keypress', '.annotation-value input', function (e) {
if (e.which == 13) {
$(this).parent().find('.annotation-save').click();
}
});
// delete annotation if delete link clicked
manager.on('click', '.delete-annotation', function (e) {
e.preventDefault();
var annotation = $(this).parent().parent();
$(this).attr('disabled', 'disabled');
// delete annotation by ajax
annotationsApi.deleteAnnotation(
idSite,
annotation.attr('data-id'),
manager.attr('data-date'),
manager.attr('data-period'),
function (response) {
replaceAnnotationManager(manager, response);
// update evolution icons
var isStarred = isAnnotationStarred(annotation);
onAnnotationCountChange(annotation.attr('data-date'), -1, isStarred ? -1 : 0);
}
);
return false;
});
// star/unstar annotation if star clicked
manager.on('click', '.annotation-star-changeable', function (e) {
var annotation = $(this).parent().parent(),
newStarredVal = $(this).attr('data-starred') == 0 ? 1 : 0 // flip existing 'starred' value
;
// perform ajax request to star annotation
annotationsApi.saveAnnotation(
idSite,
annotation.attr('data-id'),
annotation.attr('data-date'),
{
starred: newStarredVal
},
function (response) {
replaceAnnotationHtml(annotation, response);
// change starred count for this annotation in evolution graph based on what we're
// changing the starred value to
onAnnotationCountChange(annotation.attr('data-date'), 0, newStarredVal == 0 ? -1 : 1);
}
);
});
// when period edit is clicked, show datepicker
manager.on('click', '.annotation-period-edit>a', function (e) {
e.preventDefault();
$('.datepicker', $(this).parent()).toggle();
return false;
});
// make sure datepicker popups are closed if someone clicks elsewhere
$('body').on('mouseup', function (e) {
var container = $('.annotation-period-edit>.datepicker:visible').parent();
if (!container.has(e.target).length) {
container.find('.datepicker').hide();
}
});
};
// used in below function
var loadingAnnotationManager = false;
/**
* Shows an annotation manager under a report for a specific site & date range.
*
* @param {Element} domElem The element of the report to show the annotation manger
* under.
* @param {int} idSite The ID of the site to show the annotations of.
* @param {string} date The start date of the period.
* @param {string} period The period type.
* @param {int} lastN Whether to include the last N periods in the date range or not. Can
* be undefined.
* @param {function} [callback]
*/
var showAnnotationViewer = function (domElem, idSite, date, period, lastN, callback) {
var addToAnnotationCount = function (date, amt, starAmt) {
if (date.indexOf(',') != -1) {
date = date.split(',')[0];
}
$('.evolution-annotations>span', domElem).each(function () {
if ($(this).attr('data-date') == date) {
// get counts from attributes (and convert them to ints)
var starredCount = +$(this).attr('data-starred'),
annotationCount = +$(this).attr('data-count');
// modify the starred count & make sure the correct image is used
var newStarCount = starredCount + starAmt;
if (newStarCount > 0) {
var newImg = 'plugins/Zeitgeist/images/annotations_starred.png';
} else {
var newImg = 'plugins/Zeitgeist/images/annotations.png';
}
$(this).attr('data-starred', newStarCount).find('img').attr('src', newImg);
// modify the annotation count & hide/show based on new count
var newCount = annotationCount + amt;
$(this).attr('data-count', newCount).css('opacity', newCount > 0 ? 1 : 0);
return false;
}
});
};
var manager = $('.annotation-manager', domElem);
if (manager.length) {
// if annotations for the requested date + period are already loaded, then just toggle the
// visibility of the annotation viewer. otherwise, we reload the annotations.
if (manager.attr('data-date') == date
&& manager.attr('data-period') == period) {
// toggle manager view
if (manager.is(':hidden')) {
manager.slideDown('slow', function () { if (callback) callback(manager) });
}
else {
manager.slideUp('slow', function () { if (callback) callback(manager) });
}
}
else {
// show nothing but the loading gif
$('.annotations', manager).html('');
$('.loadingPiwik', manager).show();
// reload annotation manager for new date/period
annotationsApi.getAnnotationManager(idSite, date, period, lastN, function (response) {
replaceAnnotationManager(manager, response);
createDatePickers(manager);
// show if hidden
if (manager.is(':hidden')) {
manager.slideDown('slow', function () { if (callback) callback(manager) });
}
else {
if (callback) {
callback(manager);
}
}
});
}
}
else {
// if we are already loading the annotation manager, don't load it again
if (loadingAnnotationManager) {
return;
}
loadingAnnotationManager = true;
var isDashboard = !!$('#dashboardWidgetsArea').length;
if (isDashboard) {
$('.loadingPiwikBelow', domElem).insertAfter($('.evolution-annotations', domElem));
}
var loading = $('.loadingPiwikBelow', domElem).css({display: 'block'});
// the annotations for this report have not been retrieved yet, so do an ajax request
// & show the result
annotationsApi.getAnnotationManager(idSite, date, period, lastN, function (response) {
var manager = $(response).hide();
// if an error occurred (and response does not contain the annotation manager), do nothing
if (!manager.hasClass('annotation-manager')) {
return;
}
// create datepickers for each shown annotation
createDatePickers(manager);
bindAnnotationManagerEvents(manager, idSite, addToAnnotationCount);
loading.css('visibility', 'hidden');
// add & show annotation manager
if (isDashboard) {
manager.insertAfter($('.evolution-annotations', domElem));
} else {
$('.dataTableFeatures', domElem).append(manager);
}
manager.slideDown('slow', function () {
loading.hide().css('visibility', 'visible');
loadingAnnotationManager = false;
if (callback) callback(manager)
});
});
}
};
/**
* Determines the x-coordinates of a set of evolution annotation icons.
*
* @param {Element} annotations The '.evolution-annotations' element.
* @param {Element} graphElem The evolution graph's datatable element.
*/
var placeEvolutionIcons = function (annotations, graphElem) {
var canvases = $('.piwik-graph .jqplot-xaxis canvas', graphElem),
noteSize = 16;
// if no graph available, hide all icons
if (!canvases || canvases.length == 0) {
$('span', annotations).hide();
return true;
}
// set position of each individual icon
$('span', annotations).each(function (i) {
var canvas = $(canvases[i]),
canvasCenterX = canvas.position().left + (canvas.width() / 2);
$(this).css({
left: canvasCenterX - noteSize / 2,
// show if there are annotations for this x-axis tick
opacity: +$(this).attr('data-count') > 0 ? 1 : 0
});
});
};
// make showAnnotationViewer, placeEvolutionIcons & annotationsApi globally accessible
piwik.annotations = {
showAnnotationViewer: showAnnotationViewer,
placeEvolutionIcons: placeEvolutionIcons,
api: annotationsApi
};
}(jQuery, piwik));

View file

@ -0,0 +1,209 @@
.evolution-annotations {
position: relative;
height: 16px;
width: 100%;
margin-top: 12px;
margin-bottom: -28px;
cursor: pointer;
}
.evolution-annotations > span {
position: absolute;
top:10px;
}
#dashboard, .ui-dialog {
.evolution-annotations {
margin-top: -5px;
margin-bottom: -5px;
}
.evolution-annotations > span {
top: -1px;
position: absolute;
}
.annotation-manager {
margin-top: 15px;
}
}
.annotation-manager {
text-align: left;
margin-top: -18px;
}
.annotations-header {
display: inline-block;
width: 128px;
text-align: right;
font-size: 12px;
font-style: italic;
margin-bottom: 8px;
vertical-align: top;
color: #666;
}
.annotation-controls {
display: inline-block;
margin-left: 132px;
}
.annotation-controls>a {
font-size: 11px;
font-style: italic;
color: #666;
cursor: pointer;
padding: 3px 0 6px 0;
display: inline-block;
}
.annotation-controls>a:hover {
text-decoration: none;
}
.annotation-list {
margin-left: 8px;
}
.annotation-list table {
width: 100%;
}
.annotation-list-range {
display: inline-block;
font-size: 12px;
font-style: italic;
color: #666;
vertical-align: top;
margin: 0 0 8px 8px;
}
.empty-annotation-list, .annotation-list .loadingPiwik {
display: block;
font-style: italic;
color: #666;
margin: 0 0 12px 140px;
}
.annotation-meta {
width: 128px;
text-align: right;
vertical-align: top;
font-size: 14px;
}
.annotation-user {
font-style: italic;
font-size: 11px;
color: #444;
}
.annotation-user-cell {
vertical-align: top;
width: 92px;
}
.annotation-period {
display: inline-block;
font-style: italic;
margin: 0 8px 8px 8px;
vertical-align: top;
}
.annotation-value {
margin: 0 12px 12px 8px;
vertical-align: top;
position: relative;
font-size: 14px;
}
.annotation-enter-edit-mode {
cursor: pointer;
}
.annotation-edit, .new-annotation-edit {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
width: 98%;
}
.annotation-star {
display: inline-block;
margin: 0 8px 8px 0;
width: 16px;
}
.annotation-star-changeable {
cursor: pointer;
}
.delete-annotation {
font-size: 12px;
font-style: italic;
color: red;
text-decoration: none;
display: inline-block;
}
.delete-annotation:hover {
text-decoration: underline;
}
.annotation-manager .submit {
float: none;
}
.edit-annotation {
font-size: 10px;
color: #666;
font-style: italic;
}
.edit-annotation:hover {
text-decoration: none;
}
.annotationView {
float: right;
margin-left: 5px;
position: relative;
cursor: pointer;
}
.annotationView > span {
font-style: italic;
display: inline-block;
margin: 4px 4px 0 4px;
}
.annotation-period-edit {
display: inline-block;
background: white;
color: #444;
font-size: 12px;
border: 1px solid #e4e5e4;
padding: 5px 5px 6px 3px;
border-radius: 4px;
}
.annotation-period-edit:hover {
background: #f1f0eb;
border-color: #a9a399;
}
.annotation-period-edit>a {
text-decoration: none;
cursor: pointer;
display: block;
}
.annotation-period-edit>.datepicker {
position: absolute;
margin-top: 6px;
margin-left: -5px;
z-index: 15;
background: white;
border: 1px solid #e4e5e4;
border-radius: 4px;
}

View file

@ -0,0 +1,45 @@
<tr class="annotation" data-id="{{ annotation.idNote }}" data-date="{{ annotation.date }}">
<td class="annotation-meta">
<div class="annotation-star{% if annotation.canEditOrDelete %} annotation-star-changeable{% endif %}" data-starred="{{ annotation.starred }}"
{% if annotation.canEditOrDelete %}title="{{ 'Annotations_ClickToStarOrUnstar'|translate }}"{% endif %}>
{% if annotation.starred %}
<img src="plugins/Zeitgeist/images/star.png"/>
{% else %}
<img src="plugins/Zeitgeist/images/star_empty.png"/>
{% endif %}
</div>
<div class="annotation-period {% if annotation.canEditOrDelete %}annotation-enter-edit-mode{% endif %}">({{ annotation.date }})</div>
{% if annotation.canEditOrDelete %}
<div class="annotation-period-edit" style="display:none;">
<a href="#">{{ annotation.date }}</a>
<div class="datepicker" style="display:none;"/>
</div>
{% endif %}
</td>
<td class="annotation-value">
<div class="annotation-view-mode">
<span {% if annotation.canEditOrDelete %}title="{{ 'Annotations_ClickToEdit'|translate }}"
class="annotation-enter-edit-mode"{% endif %}>{{ annotation.note|raw }}</span>
{% if annotation.canEditOrDelete %}
<a href="#" class="edit-annotation annotation-enter-edit-mode" title="{{ 'Annotations_ClickToEdit'|translate }}">{{ 'General_Edit'|translate }}...</a>
{% endif %}
</div>
{% if annotation.canEditOrDelete %}
<div class="annotation-edit-mode" style="display:none;">
<input class="annotation-edit" type="text" value="{{ annotation.note|raw }}"/>
<br/>
<input class="annotation-save submit" type="button" value="{{ 'General_Save'|translate }}"/>
<input class="annotation-cancel submit" type="button" value="{{ 'General_Cancel'|translate }}"/>
</div>
{% endif %}
</td>
{% if annotation.user is defined and userLogin != 'anonymous' %}
<td class="annotation-user-cell">
<span class="annotation-user">{{ annotation.user }}</span><br/>
{% if annotation.canEditOrDelete %}
<a href="#" class="delete-annotation" style="display:none;" title="{{ 'Annotations_ClickToDelete'|translate }}">{{ 'General_Delete'|translate }}</a>
{% endif %}
</td>
{% endif %}
</tr>

View file

@ -0,0 +1,29 @@
<div class="annotations">
{% if annotations is empty %}
<div class="empty-annotation-list">{{ 'Annotations_NoAnnotations'|translate }}</div>
{% endif %}
<table>
{% for annotation in annotations %}
{% include "@Annotations/_annotation.twig" %}
{% endfor %}
<tr class="new-annotation-row" style="display:none;" data-date="{{ startDate }}">
<td class="annotation-meta">
<div class="annotation-star">&nbsp;</div>
<div class="annotation-period-edit">
<a href="#">{{ startDate }}</a>
<div class="datepicker" style="display:none;"/>
</div>
</td>
<td class="annotation-value">
<input type="text" value="" class="new-annotation-edit" placeholder="{{ 'Annotations_EnterAnnotationText'|translate }}"/><br/>
<input type="button" class="submit new-annotation-save" value="{{ 'General_Save'|translate }}"/>
<input type="button" class="submit new-annotation-cancel" value="{{ 'General_Cancel'|translate }}"/>
</td>
<td class="annotation-user-cell"><span class="annotation-user">{{ userLogin }}</span></td>
</tr>
</table>
</div>

View file

@ -0,0 +1,27 @@
<div class="annotation-manager"
{% if startDate != endDate %}data-date="{{ startDate }},{{ endDate }}" data-period="range"
{% else %}data-date="{{ startDate }}" data-period="{{ period }}"
{% endif %}>
<div class="annotations-header">
<span>{{ 'Annotations_Annotations'|translate }}</span>
</div>
<div class="annotation-list-range">{{ startDatePretty }}{% if startDate != endDate %} &mdash; {{ endDatePretty }}{% endif %}</div>
<div class="annotation-list">
{% include "@Annotations/_annotationList.twig" %}
<span class="loadingPiwik" style="display:none;"><img src="plugins/Zeitgeist/images/loading-blue.gif"/>{{ 'General_Loading'|translate }}</span>
</div>
<div class="annotation-controls">
{% if canUserAddNotes %}
<a href="#" class="add-annotation" title="{{ 'Annotations_CreateNewAnnotation'|translate }}">{{ 'Annotations_CreateNewAnnotation'|translate }}</a>
{% elseif userLogin == 'anonymous' %}
<a href="index.php?module=Login">{{ 'Annotations_LoginToAnnotate'|translate }}</a>
{% endif %}
</div>
</div>

View file

@ -0,0 +1,14 @@
<div class="evolution-annotations">
{% for dateCountPair in annotationCounts %}
{% set date=dateCountPair[0] %}
{% set counts=dateCountPair[1] %}
<span data-date="{{ date }}" data-count="{{ counts.count }}" data-starred="{{ counts.starred }}"
{% if counts.count == 0 %}title="{{ 'Annotations_AddAnnotationsFor'|translate(date) }}"
{% elseif counts.count == 1 %}title="{{ 'Annotations_AnnotationOnDate'|translate(date,
counts.note)|raw }}
{{ 'Annotations_ClickToEditOrAdd'|translate }}"
{% else %}}title="{{ 'Annotations_ViewAndAddAnnotations'|translate(date) }}"{% endif %}>
<img src="plugins/Zeitgeist/images/{% if counts.starred > 0 %}annotations_starred.png{% else %}annotations.png{% endif %}" width="16" height="16"/>
</span>
{% endfor %}
</div>

View file

@ -0,0 +1 @@
{% include "@Annotations/_annotation.twig" %}

View file

@ -0,0 +1,209 @@
<?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\Plugins\CoreAdminHome;
use Exception;
use Piwik\Config;
use Piwik\DataAccess\ArchiveTableCreator;
use Piwik\Date;
use Piwik\Db;
use Piwik\Option;
use Piwik\Period;
use Piwik\Period\Week;
use Piwik\Piwik;
use Piwik\Plugins\PrivacyManager\PrivacyManager;
use Piwik\Plugins\SitesManager\SitesManager;
use Piwik\SettingsPiwik;
use Piwik\Site;
use Piwik\TaskScheduler;
/**
* @method static \Piwik\Plugins\CoreAdminHome\API getInstance()
*/
class API extends \Piwik\Plugin\API
{
/**
* Will run all scheduled tasks due to run at this time.
*
* @return array
*/
public function runScheduledTasks()
{
Piwik::checkUserHasSuperUserAccess();
return TaskScheduler::runTasks();
}
/*
* stores the list of websites IDs to re-reprocess in archive.php
*/
const OPTION_INVALIDATED_IDSITES = 'InvalidatedOldReports_WebsiteIds';
/**
* When tracking data in the past (using Tracking API), this function
* can be used to invalidate reports for the idSites and dates where new data
* was added.
* DEV: If you call this API, the UI should display the data correctly, but will process
* in real time, which could be very slow after large data imports.
* After calling this function via REST, you can manually force all data
* to be reprocessed by visiting the script as the Super User:
* http://example.net/piwik/misc/cron/archive.php?token_auth=$SUPER_USER_TOKEN_AUTH_HERE
* REQUIREMENTS: On large piwik setups, you will need in PHP configuration: max_execution_time = 0
* We recommend to use an hourly schedule of the script at misc/cron/archive.php
* More information: http://piwik.org/setup-auto-archiving/
*
* @param string $idSites Comma separated list of idSite that have had data imported for the specified dates
* @param string $dates Comma separated list of dates to invalidate for all these websites
* @throws Exception
* @return array
*/
public function invalidateArchivedReports($idSites, $dates)
{
$idSites = Site::getIdSitesFromIdSitesString($idSites);
if (empty($idSites)) {
throw new Exception("Specify a value for &idSites= as a comma separated list of website IDs, for which your token_auth has 'admin' permission");
}
Piwik::checkUserHasAdminAccess($idSites);
// Ensure the specified dates are valid
$toInvalidate = $invalidDates = array();
$dates = explode(',', $dates);
$dates = array_unique($dates);
foreach ($dates as $theDate) {
try {
$date = Date::factory($theDate);
} catch (Exception $e) {
$invalidDates[] = $theDate;
continue;
}
if ($date->toString() == $theDate) {
$toInvalidate[] = $date;
} else {
$invalidDates[] = $theDate;
}
}
// If using the feature "Delete logs older than N days"...
$purgeDataSettings = PrivacyManager::getPurgeDataSettings();
$logsAreDeletedBeforeThisDate = $purgeDataSettings['delete_logs_schedule_lowest_interval'];
$logsDeleteEnabled = $purgeDataSettings['delete_logs_enable'];
$minimumDateWithLogs = false;
if ($logsDeleteEnabled
&& $logsAreDeletedBeforeThisDate
) {
$minimumDateWithLogs = Date::factory('today')->subDay($logsAreDeletedBeforeThisDate);
}
// Given the list of dates, process which tables they should be deleted from
$minDate = false;
$warningDates = $processedDates = array();
/* @var $date Date */
foreach ($toInvalidate as $date) {
// we should only delete reports for dates that are more recent than N days
if ($minimumDateWithLogs
&& $date->isEarlier($minimumDateWithLogs)
) {
$warningDates[] = $date->toString();
} else {
$processedDates[] = $date->toString();
}
$month = $date->toString('Y_m');
// For a given date, we must invalidate in the monthly archive table
$datesByMonth[$month][] = $date->toString();
// But also the year stored in January
$year = $date->toString('Y_01');
$datesByMonth[$year][] = $date->toString();
// but also weeks overlapping several months stored in the month where the week is starting
/* @var $week Week */
$week = Period::factory('week', $date);
$weekAsString = $week->getDateStart()->toString('Y_m');
$datesByMonth[$weekAsString][] = $date->toString();
// Keep track of the minimum date for each website
if ($minDate === false
|| $date->isEarlier($minDate)
) {
$minDate = $date;
}
}
// In each table, invalidate day/week/month/year containing this date
$archiveTables = ArchiveTableCreator::getTablesArchivesInstalled();
foreach ($archiveTables as $table) {
// Extract Y_m from table name
$suffix = ArchiveTableCreator::getDateFromTableName($table);
if (!isset($datesByMonth[$suffix])) {
continue;
}
// Dates which are to be deleted from this table
$datesToDeleteInTable = $datesByMonth[$suffix];
// Build one statement to delete all dates from the given table
$sql = $bind = array();
$datesToDeleteInTable = array_unique($datesToDeleteInTable);
foreach ($datesToDeleteInTable as $dateToDelete) {
$sql[] = '(date1 <= ? AND ? <= date2)';
$bind[] = $dateToDelete;
$bind[] = $dateToDelete;
}
$sql = implode(" OR ", $sql);
$query = "DELETE FROM $table " .
" WHERE ( $sql ) " .
" AND idsite IN (" . implode(",", $idSites) . ")";
Db::query($query, $bind);
}
\Piwik\Plugins\SitesManager\API::getInstance()->updateSiteCreatedTime($idSites, $minDate);
// Force to re-process data for these websites in the next archive.php cron run
$invalidatedIdSites = self::getWebsiteIdsToInvalidate();
$invalidatedIdSites = array_merge($invalidatedIdSites, $idSites);
$invalidatedIdSites = array_unique($invalidatedIdSites);
$invalidatedIdSites = array_values($invalidatedIdSites);
Option::set(self::OPTION_INVALIDATED_IDSITES, serialize($invalidatedIdSites));
Site::clearCache();
$output = array();
// output logs
if ($warningDates) {
$output[] = 'Warning: the following Dates have not been invalidated, because they are earlier than your Log Deletion limit: ' .
implode(", ", $warningDates) .
"\n The last day with logs is " . $minimumDateWithLogs . ". " .
"\n Please disable 'Delete old Logs' or set it to a higher deletion threshold (eg. 180 days or 365 years).'.";
}
$output[] = "Success. The following dates were invalidated successfully: " .
implode(", ", $processedDates);
return $output;
}
/**
* Returns array of idSites to force re-process next time archive.php runs
*
* @ignore
* @return mixed
*/
static public function getWebsiteIdsToInvalidate()
{
Piwik::checkUserHasSomeAdminAccess();
Option::clearCachedOption(self::OPTION_INVALIDATED_IDSITES);
$invalidatedIdSites = Option::get(self::OPTION_INVALIDATED_IDSITES);
if ($invalidatedIdSites
&& ($invalidatedIdSites = unserialize($invalidatedIdSites))
&& count($invalidatedIdSites)
) {
return $invalidatedIdSites;
}
return array();
}
}

View file

@ -0,0 +1,351 @@
<?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\Plugins\CoreAdminHome;
use Exception;
use Piwik\API\ResponseBuilder;
use Piwik\ArchiveProcessor\Rules;
use Piwik\Common;
use Piwik\Config;
use Piwik\DataTable\Renderer\Json;
use Piwik\Menu\MenuTop;
use Piwik\Nonce;
use Piwik\Option;
use Piwik\Piwik;
use Piwik\Plugins\CorePluginsAdmin\UpdateCommunication;
use Piwik\Plugins\CustomVariables\CustomVariables;
use Piwik\Plugins\LanguagesManager\API as APILanguagesManager;
use Piwik\Plugins\LanguagesManager\LanguagesManager;
use Piwik\Plugins\SitesManager\API as APISitesManager;
use Piwik\Settings\Manager as SettingsManager;
use Piwik\Site;
use Piwik\Tracker\IgnoreCookie;
use Piwik\Url;
use Piwik\View;
/**
*
*/
class Controller extends \Piwik\Plugin\ControllerAdmin
{
const SET_PLUGIN_SETTINGS_NONCE = 'CoreAdminHome.setPluginSettings';
public function index()
{
$this->redirectToIndex('UsersManager', 'userSettings');
return;
}
public function generalSettings()
{
Piwik::checkUserHasSomeAdminAccess();
$view = new View('@CoreAdminHome/generalSettings');
if (Piwik::hasUserSuperUserAccess()) {
$this->handleGeneralSettingsAdmin($view);
$view->trustedHosts = Url::getTrustedHostsFromConfig();
$logo = new CustomLogo();
$view->branding = array('use_custom_logo' => $logo->isEnabled());
$view->logosWriteable = $logo->isCustomLogoWritable();
$view->pathUserLogo = CustomLogo::getPathUserLogo();
$view->pathUserLogoSmall = CustomLogo::getPathUserLogoSmall();
$view->pathUserLogoSVG = CustomLogo::getPathUserSvgLogo();
$view->pathUserLogoDirectory = realpath(dirname($view->pathUserLogo) . '/');
}
$view->language = LanguagesManager::getLanguageCodeForCurrentUser();
$this->setBasicVariablesView($view);
return $view->render();
}
public function pluginSettings()
{
Piwik::checkUserIsNotAnonymous();
$settings = $this->getPluginSettings();
$view = new View('@CoreAdminHome/pluginSettings');
$view->nonce = Nonce::getNonce(static::SET_PLUGIN_SETTINGS_NONCE);
$view->pluginSettings = $settings;
$view->firstSuperUserSettingNames = $this->getFirstSuperUserSettingNames($settings);
$this->setBasicVariablesView($view);
return $view->render();
}
private function getPluginSettings()
{
$pluginsSettings = SettingsManager::getPluginSettingsForCurrentUser();
ksort($pluginsSettings);
return $pluginsSettings;
}
/**
* @param \Piwik\Plugin\Settings[] $pluginsSettings
* @return array array([pluginName] => [])
*/
private function getFirstSuperUserSettingNames($pluginsSettings)
{
$names = array();
foreach ($pluginsSettings as $pluginName => $pluginSettings) {
foreach ($pluginSettings->getSettingsForCurrentUser() as $setting) {
if ($setting instanceof \Piwik\Settings\SystemSetting) {
$names[$pluginName] = $setting->getName();
break;
}
}
}
return $names;
}
public function setPluginSettings()
{
Piwik::checkUserIsNotAnonymous();
Json::sendHeaderJSON();
$nonce = Common::getRequestVar('nonce', null, 'string');
if (!Nonce::verifyNonce(static::SET_PLUGIN_SETTINGS_NONCE, $nonce)) {
return json_encode(array(
'result' => 'error',
'message' => Piwik::translate('General_ExceptionNonceMismatch')
));
}
$pluginsSettings = SettingsManager::getPluginSettingsForCurrentUser();
try {
foreach ($pluginsSettings as $pluginName => $pluginSetting) {
foreach ($pluginSetting->getSettingsForCurrentUser() as $setting) {
$value = $this->findSettingValueFromRequest($pluginName, $setting->getKey());
if (!is_null($value)) {
$setting->setValue($value);
}
}
}
foreach ($pluginsSettings as $pluginSetting) {
$pluginSetting->save();
}
} catch (Exception $e) {
$message = html_entity_decode($e->getMessage(), ENT_QUOTES, 'UTF-8');
return json_encode(array('result' => 'error', 'message' => $message));
}
Nonce::discardNonce(static::SET_PLUGIN_SETTINGS_NONCE);
return json_encode(array('result' => 'success'));
}
private function findSettingValueFromRequest($pluginName, $settingKey)
{
$changedPluginSettings = Common::getRequestVar('settings', null, 'array');
if (!array_key_exists($pluginName, $changedPluginSettings)) {
return;
}
$settings = $changedPluginSettings[$pluginName];
foreach ($settings as $setting) {
if ($setting['name'] == $settingKey) {
return $setting['value'];
}
}
}
public function setGeneralSettings()
{
Piwik::checkUserHasSuperUserAccess();
$response = new ResponseBuilder(Common::getRequestVar('format'));
try {
$this->checkTokenInUrl();
$this->saveGeneralSettings();
$customLogo = new CustomLogo();
if (Common::getRequestVar('useCustomLogo', '0')) {
$customLogo->enable();
} else {
$customLogo->disable();
}
$toReturn = $response->getResponse();
} catch (Exception $e) {
$toReturn = $response->getResponseException($e);
}
return $toReturn;
}
/**
* Renders and echo's an admin page that lets users generate custom JavaScript
* tracking code and custom image tracker links.
*/
public function trackingCodeGenerator()
{
$view = new View('@CoreAdminHome/trackingCodeGenerator');
$this->setBasicVariablesView($view);
$view->topMenu = MenuTop::getInstance()->getMenu();
$viewableIdSites = APISitesManager::getInstance()->getSitesIdWithAtLeastViewAccess();
$defaultIdSite = reset($viewableIdSites);
$view->idSite = Common::getRequestVar('idSite', $defaultIdSite, 'int');
$view->defaultReportSiteName = Site::getNameFor($view->idSite);
$view->defaultSiteRevenue = \Piwik\MetricsFormatter::getCurrencySymbol($view->idSite);
$view->maxCustomVariables = CustomVariables::getMaxCustomVariables();
$allUrls = APISitesManager::getInstance()->getSiteUrlsFromId($view->idSite);
if (isset($allUrls[1])) {
$aliasUrl = $allUrls[1];
} else {
$aliasUrl = 'x.domain.com';
}
$view->defaultReportSiteAlias = $aliasUrl;
$mainUrl = Site::getMainUrlFor($view->idSite);
$view->defaultReportSiteDomain = @parse_url($mainUrl, PHP_URL_HOST);
// get currencies for each viewable site
$view->currencySymbols = APISitesManager::getInstance()->getCurrencySymbols();
$view->serverSideDoNotTrackEnabled = \Piwik\Plugins\PrivacyManager\DoNotTrackHeaderChecker::isActive();
return $view->render();
}
/**
* Shows the "Track Visits" checkbox.
*/
public function optOut()
{
$trackVisits = !IgnoreCookie::isIgnoreCookieFound();
$nonce = Common::getRequestVar('nonce', false);
$language = Common::getRequestVar('language', '');
if ($nonce !== false && Nonce::verifyNonce('Piwik_OptOut', $nonce)) {
Nonce::discardNonce('Piwik_OptOut');
IgnoreCookie::setIgnoreCookie();
$trackVisits = !$trackVisits;
}
$view = new View('@CoreAdminHome/optOut');
$view->trackVisits = $trackVisits;
$view->nonce = Nonce::getNonce('Piwik_OptOut', 3600);
$view->language = APILanguagesManager::getInstance()->isLanguageAvailable($language)
? $language
: LanguagesManager::getLanguageCodeForCurrentUser();
return $view->render();
}
public function uploadCustomLogo()
{
Piwik::checkUserHasSuperUserAccess();
$logo = new CustomLogo();
$success = $logo->copyUploadedLogoToFilesystem();
if($success) {
return '1';
}
return '0';
}
static public function isGeneralSettingsAdminEnabled()
{
return (bool) Config::getInstance()->General['enable_general_settings_admin'];
}
private function saveGeneralSettings()
{
if(!self::isGeneralSettingsAdminEnabled()) {
// General settings + Beta channel + SMTP settings is disabled
return;
}
// General Setting
$enableBrowserTriggerArchiving = Common::getRequestVar('enableBrowserTriggerArchiving');
$todayArchiveTimeToLive = Common::getRequestVar('todayArchiveTimeToLive');
Rules::setBrowserTriggerArchiving((bool)$enableBrowserTriggerArchiving);
Rules::setTodayArchiveTimeToLive($todayArchiveTimeToLive);
// update beta channel setting
$debug = Config::getInstance()->Debug;
$debug['allow_upgrades_to_beta'] = Common::getRequestVar('enableBetaReleaseCheck', '0', 'int');
Config::getInstance()->Debug = $debug;
// Update email settings
$mail = array();
$mail['transport'] = (Common::getRequestVar('mailUseSmtp') == '1') ? 'smtp' : '';
$mail['port'] = Common::getRequestVar('mailPort', '');
$mail['host'] = Common::unsanitizeInputValue(Common::getRequestVar('mailHost', ''));
$mail['type'] = Common::getRequestVar('mailType', '');
$mail['username'] = Common::unsanitizeInputValue(Common::getRequestVar('mailUsername', ''));
$mail['password'] = Common::unsanitizeInputValue(Common::getRequestVar('mailPassword', ''));
$mail['encryption'] = Common::getRequestVar('mailEncryption', '');
Config::getInstance()->mail = $mail;
// update trusted host settings
$trustedHosts = Common::getRequestVar('trustedHosts', false, 'json');
if ($trustedHosts !== false) {
Url::saveTrustedHostnameInConfig($trustedHosts);
}
Config::getInstance()->forceSave();
$pluginUpdateCommunication = new UpdateCommunication();
if (Common::getRequestVar('enablePluginUpdateCommunication', '0', 'int')) {
$pluginUpdateCommunication->enable();
} else {
$pluginUpdateCommunication->disable();
}
}
private function handleGeneralSettingsAdmin($view)
{
// Whether to display or not the general settings (cron, beta, smtp)
$view->isGeneralSettingsAdminEnabled = self::isGeneralSettingsAdminEnabled();
if($view->isGeneralSettingsAdminEnabled) {
$this->displayWarningIfConfigFileNotWritable();
}
$enableBrowserTriggerArchiving = Rules::isBrowserTriggerEnabled();
$todayArchiveTimeToLive = Rules::getTodayArchiveTimeToLive();
$showWarningCron = false;
if (!$enableBrowserTriggerArchiving
&& $todayArchiveTimeToLive < 3600
) {
$showWarningCron = true;
}
$view->showWarningCron = $showWarningCron;
$view->todayArchiveTimeToLive = $todayArchiveTimeToLive;
$view->enableBrowserTriggerArchiving = $enableBrowserTriggerArchiving;
$view->enableBetaReleaseCheck = Config::getInstance()->Debug['allow_upgrades_to_beta'];
$view->mail = Config::getInstance()->mail;
$pluginUpdateCommunication = new UpdateCommunication();
$view->canUpdateCommunication = $pluginUpdateCommunication->canBeEnabled();
$view->enableSendPluginUpdateCommunication = $pluginUpdateCommunication->isEnabled();
}
}

View file

@ -0,0 +1,129 @@
<?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\Plugins\CoreAdminHome;
use Piwik\DataAccess\ArchiveSelector;
use Piwik\DataAccess\ArchiveTableCreator;
use Piwik\Date;
use Piwik\Db;
use Piwik\Menu\MenuAdmin;
use Piwik\Piwik;
use Piwik\ScheduledTask;
use Piwik\ScheduledTime;
use Piwik\Settings\Manager as SettingsManager;
use Piwik\Settings\UserSetting;
/**
*
*/
class CoreAdminHome extends \Piwik\Plugin
{
/**
* @see Piwik\Plugin::getListHooksRegistered
*/
public function getListHooksRegistered()
{
return array(
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'Menu.Admin.addItems' => 'addMenu',
'TaskScheduler.getScheduledTasks' => 'getScheduledTasks',
'UsersManager.deleteUser' => 'cleanupUser'
);
}
public function cleanupUser($userLogin)
{
UserSetting::removeAllUserSettingsForUser($userLogin);
}
public function getScheduledTasks(&$tasks)
{
// general data purge on older archive tables, executed daily
$purgeArchiveTablesTask = new ScheduledTask ($this,
'purgeOutdatedArchives',
null,
ScheduledTime::factory('daily'),
ScheduledTask::HIGH_PRIORITY);
$tasks[] = $purgeArchiveTablesTask;
// lowest priority since tables should be optimized after they are modified
$optimizeArchiveTableTask = new ScheduledTask ($this,
'optimizeArchiveTable',
null,
ScheduledTime::factory('daily'),
ScheduledTask::LOWEST_PRIORITY);
$tasks[] = $optimizeArchiveTableTask;
}
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = "libs/jquery/themes/base/jquery-ui.css";
$stylesheets[] = "plugins/CoreAdminHome/stylesheets/menu.less";
$stylesheets[] = "plugins/Zeitgeist/stylesheets/base.less";
$stylesheets[] = "plugins/CoreAdminHome/stylesheets/generalSettings.less";
$stylesheets[] = "plugins/CoreAdminHome/stylesheets/pluginSettings.less";
}
public function getJsFiles(&$jsFiles)
{
$jsFiles[] = "libs/jquery/jquery.js";
$jsFiles[] = "libs/jquery/jquery-ui.js";
$jsFiles[] = "libs/jquery/jquery.browser.js";
$jsFiles[] = "libs/javascript/sprintf.js";
$jsFiles[] = "plugins/Zeitgeist/javascripts/piwikHelper.js";
$jsFiles[] = "plugins/Zeitgeist/javascripts/ajaxHelper.js";
$jsFiles[] = "libs/jquery/jquery.history.js";
$jsFiles[] = "plugins/CoreHome/javascripts/broadcast.js";
$jsFiles[] = "plugins/CoreAdminHome/javascripts/generalSettings.js";
$jsFiles[] = "plugins/CoreHome/javascripts/donate.js";
$jsFiles[] = "plugins/CoreAdminHome/javascripts/pluginSettings.js";
}
function addMenu()
{
MenuAdmin::getInstance()->add('CoreAdminHome_MenuManage', null, "", Piwik::isUserHasSomeAdminAccess(), $order = 1);
MenuAdmin::getInstance()->add('CoreAdminHome_MenuDiagnostic', null, "", Piwik::isUserHasSomeAdminAccess(), $order = 10);
MenuAdmin::getInstance()->add('General_Settings', null, "", Piwik::isUserHasSomeAdminAccess(), $order = 5);
MenuAdmin::getInstance()->add('General_Settings', 'CoreAdminHome_MenuGeneralSettings',
array('module' => 'CoreAdminHome', 'action' => 'generalSettings'),
Piwik::isUserHasSomeAdminAccess(),
$order = 6);
MenuAdmin::getInstance()->add('CoreAdminHome_MenuManage', 'CoreAdminHome_TrackingCode',
array('module' => 'CoreAdminHome', 'action' => 'trackingCodeGenerator'),
Piwik::isUserHasSomeAdminAccess(),
$order = 4);
MenuAdmin::getInstance()->add('General_Settings', 'CoreAdminHome_PluginSettings',
array('module' => 'CoreAdminHome', 'action' => 'pluginSettings'),
SettingsManager::hasPluginsSettingsForCurrentUser(),
$order = 7);
}
function purgeOutdatedArchives()
{
$archiveTables = ArchiveTableCreator::getTablesArchivesInstalled();
foreach ($archiveTables as $table) {
$date = ArchiveTableCreator::getDateFromTableName($table);
list($year, $month) = explode('_', $date);
// Somehow we may have archive tables created with older dates, prevent exception from being thrown
if($year > 1990) {
ArchiveSelector::purgeOutdatedArchives(Date::factory("$year-$month-15"));
}
}
}
function optimizeArchiveTable()
{
$archiveTables = ArchiveTableCreator::getTablesArchivesInstalled();
Db::optimizeTables($archiveTables);
}
}

View file

@ -0,0 +1,203 @@
<?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\Plugins\CoreAdminHome;
use Piwik\Config;
use Piwik\Filesystem;
use Piwik\Option;
use Piwik\SettingsPiwik;
class CustomLogo
{
const LOGO_HEIGHT = 300;
const LOGO_SMALL_HEIGHT = 100;
public function getLogoUrl($pathOnly = false)
{
$defaultLogo = 'plugins/Zeitgeist/images/logo.png';
$themeLogo = 'plugins/%s/images/logo.png';
$userLogo = CustomLogo::getPathUserLogo();
return $this->getPathToLogo($pathOnly, $defaultLogo, $themeLogo, $userLogo);
}
public function getHeaderLogoUrl($pathOnly = false)
{
$defaultLogo = 'plugins/Zeitgeist/images/logo-header.png';
$themeLogo = 'plugins/%s/images/logo-header.png';
$customLogo = CustomLogo::getPathUserLogoSmall();
return $this->getPathToLogo($pathOnly, $defaultLogo, $themeLogo, $customLogo);
}
public function getSVGLogoUrl($pathOnly = false)
{
$defaultLogo = 'plugins/Zeitgeist/images/logo.svg';
$themeLogo = 'plugins/%s/images/logo.svg';
$customLogo = CustomLogo::getPathUserSvgLogo();
$svg = $this->getPathToLogo($pathOnly, $defaultLogo, $themeLogo, $customLogo);
return $svg;
}
public function isEnabled()
{
return (bool) Option::get('branding_use_custom_logo');
}
public function enable()
{
Option::set('branding_use_custom_logo', '1', true);
}
public function disable()
{
Option::set('branding_use_custom_logo', '0', true);
}
public function hasSVGLogo()
{
if (!$this->isEnabled()) {
/* We always have our application logo */
return true;
}
if ($this->isEnabled()
&& file_exists(Filesystem::getPathToPiwikRoot() . '/' . CustomLogo::getPathUserSvgLogo())
) {
return true;
}
return false;
}
/**
* @return bool
*/
public function isCustomLogoWritable()
{
if(Config::getInstance()->General['enable_custom_logo_check'] == 0) {
return true;
}
$pathUserLogo = $this->getPathUserLogo();
$directoryWritingTo = PIWIK_DOCUMENT_ROOT . '/' . dirname($pathUserLogo);
// Create directory if not already created
Filesystem::mkdir($directoryWritingTo, $denyAccess = false);
$directoryWritable = is_writable($directoryWritingTo);
$logoFilesWriteable = is_writeable(PIWIK_DOCUMENT_ROOT . '/' . $pathUserLogo)
&& is_writeable(PIWIK_DOCUMENT_ROOT . '/' . $this->getPathUserSvgLogo())
&& is_writeable(PIWIK_DOCUMENT_ROOT . '/' . $this->getPathUserLogoSmall());;
$serverUploadEnabled = ini_get('file_uploads') == 1;
$isCustomLogoWritable = ($logoFilesWriteable || $directoryWritable) && $serverUploadEnabled;
return $isCustomLogoWritable;
}
protected function getPathToLogo($pathOnly, $defaultLogo, $themeLogo, $customLogo)
{
$pathToPiwikRoot = Filesystem::getPathToPiwikRoot();
$logo = $defaultLogo;
$themeName = \Piwik\Plugin\Manager::getInstance()->getThemeEnabled()->getPluginName();
$themeLogo = sprintf($themeLogo, $themeName);
if (file_exists($pathToPiwikRoot . '/' . $themeLogo)) {
$logo = $themeLogo;
}
if ($this->isEnabled()
&& file_exists($pathToPiwikRoot . '/' . $customLogo)
) {
$logo = $customLogo;
}
if (!$pathOnly) {
return SettingsPiwik::getPiwikUrl() . $logo;
}
return $pathToPiwikRoot . '/' . $logo;
}
public static function getPathUserLogo()
{
return self::rewritePath('misc/user/logo.png');
}
public static function getPathUserSvgLogo()
{
return self::rewritePath('misc/user/logo.svg');
}
public static function getPathUserLogoSmall()
{
return self::rewritePath('misc/user/logo-header.png');
}
protected static function rewritePath($path)
{
return SettingsPiwik::rewriteMiscUserPathWithHostname($path);
}
public function copyUploadedLogoToFilesystem()
{
if (empty($_FILES['customLogo'])
|| !empty($_FILES['customLogo']['error'])
) {
return false;
}
$file = $_FILES['customLogo']['tmp_name'];
if (!file_exists($file)) {
return false;
}
list($width, $height) = getimagesize($file);
switch ($_FILES['customLogo']['type']) {
case 'image/jpeg':
$image = imagecreatefromjpeg($file);
break;
case 'image/png':
$image = imagecreatefrompng($file);
break;
case 'image/gif':
$image = imagecreatefromgif($file);
break;
default:
return false;
}
$widthExpected = round($width * self::LOGO_HEIGHT / $height);
$smallWidthExpected = round($width * self::LOGO_SMALL_HEIGHT / $height);
$logo = imagecreatetruecolor($widthExpected, self::LOGO_HEIGHT);
$logoSmall = imagecreatetruecolor($smallWidthExpected, self::LOGO_SMALL_HEIGHT);
// Handle transparency
$background = imagecolorallocate($logo, 0, 0, 0);
$backgroundSmall = imagecolorallocate($logoSmall, 0, 0, 0);
imagecolortransparent($logo, $background);
imagecolortransparent($logoSmall, $backgroundSmall);
if ($_FILES['customLogo']['type'] == 'image/png') {
imagealphablending($logo, false);
imagealphablending($logoSmall, false);
imagesavealpha($logo, true);
imagesavealpha($logoSmall, true);
}
imagecopyresized($logo, $image, 0, 0, 0, 0, $widthExpected, self::LOGO_HEIGHT, $width, $height);
imagecopyresized($logoSmall, $image, 0, 0, 0, 0, $smallWidthExpected, self::LOGO_SMALL_HEIGHT, $width, $height);
imagepng($logo, PIWIK_DOCUMENT_ROOT . '/' . $this->getPathUserLogo(), 3);
imagepng($logoSmall, PIWIK_DOCUMENT_ROOT . '/' . $this->getPathUserLogoSmall(), 3);
return true;
}
}

View file

@ -0,0 +1,142 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
function sendGeneralSettingsAJAX() {
var enableBrowserTriggerArchiving = $('input[name=enableBrowserTriggerArchiving]:checked').val();
var enablePluginUpdateCommunication = $('input[name=enablePluginUpdateCommunication]:checked').val();
var enableBetaReleaseCheck = $('input[name=enableBetaReleaseCheck]:checked').val();
var todayArchiveTimeToLive = $('#todayArchiveTimeToLive').val();
var trustedHosts = [];
$('input[name=trusted_host]').each(function () {
trustedHosts.push($(this).val());
});
var ajaxHandler = new ajaxHelper();
ajaxHandler.setLoadingElement();
ajaxHandler.addParams({
format: 'json',
enableBrowserTriggerArchiving: enableBrowserTriggerArchiving,
enablePluginUpdateCommunication: enablePluginUpdateCommunication,
enableBetaReleaseCheck: enableBetaReleaseCheck,
todayArchiveTimeToLive: todayArchiveTimeToLive,
mailUseSmtp: isSmtpEnabled(),
mailPort: $('#mailPort').val(),
mailHost: $('#mailHost').val(),
mailType: $('#mailType').val(),
mailUsername: $('#mailUsername').val(),
mailPassword: $('#mailPassword').val(),
mailEncryption: $('#mailEncryption').val(),
useCustomLogo: isCustomLogoEnabled(),
trustedHosts: JSON.stringify(trustedHosts)
}, 'POST');
ajaxHandler.addParams({
module: 'CoreAdminHome',
action: 'setGeneralSettings'
}, 'GET');
ajaxHandler.redirectOnSuccess();
ajaxHandler.send(true);
}
function showSmtpSettings(value) {
$('#smtpSettings').toggle(value == 1);
}
function isSmtpEnabled() {
return $('input[name="mailUseSmtp"]:checked').val();
}
function showCustomLogoSettings(value) {
$('#logoSettings').toggle(value == 1);
}
function isCustomLogoEnabled() {
return $('input[name="useCustomLogo"]:checked').val();
}
function refreshCustomLogo() {
var imageDiv = $("#currentLogo");
if (imageDiv && imageDiv.attr("src")) {
var logoUrl = imageDiv.attr("src").split("?")[0];
imageDiv.attr("src", logoUrl + "?" + (new Date()).getTime());
}
}
$(document).ready(function () {
var originalTrustedHostCount = $('input[name=trusted_host]').length;
showSmtpSettings(isSmtpEnabled());
showCustomLogoSettings(isCustomLogoEnabled());
$('#generalSettingsSubmit').click(function () {
var doSubmit = function () {
sendGeneralSettingsAJAX();
};
var hasTrustedHostsChanged = false,
hosts = $('input[name=trusted_host]');
if (hosts.length != originalTrustedHostCount) {
hasTrustedHostsChanged = true;
}
else {
hosts.each(function () {
hasTrustedHostsChanged |= this.defaultValue != this.value;
});
}
// if trusted hosts have changed, make sure to ask for confirmation
if (hasTrustedHostsChanged) {
piwikHelper.modalConfirm('#confirmTrustedHostChange', {yes: doSubmit});
}
else {
doSubmit();
}
});
$('input[name=mailUseSmtp]').click(function () {
showSmtpSettings($(this).val());
});
$('input[name=useCustomLogo]').click(function () {
refreshCustomLogo();
showCustomLogoSettings($(this).val());
});
$('input').keypress(function (e) {
var key = e.keyCode || e.which;
if (key == 13) {
$('#generalSettingsSubmit').click();
}
}
);
$("#logoUploadForm").submit(function (data) {
var submittingForm = $(this);
var frameName = "upload" + (new Date()).getTime();
var uploadFrame = $("<iframe name=\"" + frameName + "\" />");
uploadFrame.css("display", "none");
uploadFrame.load(function (data) {
setTimeout(function () {
refreshCustomLogo();
uploadFrame.remove();
}, 1000);
});
$("body:first").append(uploadFrame);
submittingForm.attr("target", frameName);
});
$('#customLogo').change(function () {$("#logoUploadForm").submit()});
// trusted hosts event handling
var trustedHostSettings = $('#trustedHostSettings');
trustedHostSettings.on('click', '.remove-trusted-host', function (e) {
e.preventDefault();
$(this).parent('li').remove();
return false;
});
trustedHostSettings.find('.add-trusted-host').click(function (e) {
e.preventDefault();
// append new row to the table
trustedHostSettings.find('ul').append(trustedHostSettings.find('li:last').clone());
trustedHostSettings.find('li:last input').val('');
return false;
});
});

View file

@ -0,0 +1,310 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function ($, require) {
var piwikHost = window.location.host,
piwikPath = location.pathname.substring(0, location.pathname.lastIndexOf('/')),
exports = require('piwik/Tracking');
/**
* This class is deprecated. Use server-side events instead.
*
* @deprecated
*/
var TrackingCodeGenerator = function () {
// empty
};
var TrackingCodeGeneratorSingleton = exports.TrackingCodeGenerator = new TrackingCodeGenerator();
$(document).ready(function () {
// get preloaded server-side data necessary for code generation
var dataElement = $('#js-tracking-generator-data'),
currencySymbols = JSON.parse(dataElement.attr('data-currencies')),
maxCustomVariables = parseInt(dataElement.attr('max-custom-variables'), 10),
siteUrls = {},
siteCurrencies = {},
allGoals = {},
noneText = $('#image-tracker-goal').find('>option').text();
//
// utility methods
//
// returns JavaScript code for tracking custom variables based on an array of
// custom variable name-value pairs (so an array of 2-element arrays) and
// a scope (either 'visit' or 'page')
var getCustomVariableJS = function (customVariables, scope) {
var result = '';
for (var i = 0; i != 5; ++i) {
if (customVariables[i]) {
var key = customVariables[i][0],
value = customVariables[i][1];
result += ' _paq.push(["setCustomVariable", ' + (i + 1) + ', ' + JSON.stringify(key) + ', '
+ JSON.stringify(value) + ', ' + JSON.stringify(scope) + ']);\n';
}
}
return result;
};
// gets the list of custom variables entered by the user in a custom variable
// section
var getCustomVariables = function (sectionId) {
var customVariableNames = $('.custom-variable-name', '#' + sectionId),
customVariableValues = $('.custom-variable-value', '#' + sectionId);
var result = [];
if ($('.section-toggler-link', '#' + sectionId).is(':checked')) {
for (var i = 0; i != customVariableNames.length; ++i) {
var name = $(customVariableNames[i]).val();
result[i] = null;
if (name) {
result[i] = [name, $(customVariableValues[i]).val()];
}
}
}
return result;
};
// quickly gets the host + port from a url
var getHostNameFromUrl = function (url) {
var element = $('<a></a>')[0];
element.href = url;
return element.hostname;
};
// queries Piwik for needed site info for one site
var getSiteData = function (idSite, sectionSelect, callback) {
// if data is already loaded, don't do an AJAX request
if (siteUrls[idSite]
&& siteCurrencies[idSite]
&& typeof allGoals[idSite] !== 'undefined'
) {
callback();
return;
}
// disable section
$(sectionSelect).find('input,select,textarea').attr('disabled', 'disabled');
var ajaxRequest = new ajaxHelper();
ajaxRequest.setBulkRequests(
// get site info (for currency)
{
module: 'API',
method: 'SitesManager.getSiteFromId',
idSite: idSite
},
// get site urls
{
module: 'API',
method: 'SitesManager.getSiteUrlsFromId',
idSite: idSite
},
// get site goals
{
module: 'API',
method: 'Goals.getGoals',
idSite: idSite
}
);
ajaxRequest.setCallback(function (data) {
var currency = data[0][0].currency || '';
siteCurrencies[idSite] = currencySymbols[currency.toUpperCase()];
siteUrls[idSite] = data[1] || [];
allGoals[idSite] = data[2] || [];
// re-enable controls
$(sectionSelect).find('input,select,textarea').removeAttr('disabled');
callback();
});
ajaxRequest.setFormat('json');
ajaxRequest.send(false);
};
// resets the select options of a goal select using a site ID
var resetGoalSelectItems = function (idsite, id) {
var selectElement = $('#' + id).html('');
selectElement.append($('<option value=""></option>').text(noneText));
var goals = allGoals[idsite] || [];
for (var key in goals) {
var goal = goals[key];
selectElement.append($('<option/>').val(goal.idgoal).text(goal.name));
}
// set currency string
$('#' + id).parent().find('.currency').text(siteCurrencies[idsite]);
};
// function that generates JS code
var generateJsCodeAjax = null,
generateJsCode = function () {
// get params used to generate JS code
var params = {
piwikUrl: piwikHost + piwikPath,
groupPageTitlesByDomain: $('#javascript-tracking-group-by-domain').is(':checked') ? 1 : 0,
mergeSubdomains: $('#javascript-tracking-all-subdomains').is(':checked') ? 1 : 0,
mergeAliasUrls: $('#javascript-tracking-all-aliases').is(':checked') ? 1 : 0,
visitorCustomVariables: getCustomVariables('javascript-tracking-visitor-cv'),
pageCustomVariables: getCustomVariables('javascript-tracking-page-cv'),
customCampaignNameQueryParam: null,
customCampaignKeywordParam: null,
doNotTrack: $('#javascript-tracking-do-not-track').is(':checked') ? 1 : 0,
};
if ($('#custom-campaign-query-params-check').is(':checked')) {
params.customCampaignNameQueryParam = $('#custom-campaign-name-query-param').val();
params.customCampaignKeywordParam = $('#custom-campaign-keyword-query-param').val();
}
if (generateJsCodeAjax) {
generateJsCodeAjax.abort();
}
generateJsCodeAjax = new ajaxHelper();
generateJsCodeAjax.addParams({
module: 'API',
format: 'json',
method: 'SitesManager.getJavascriptTag',
idSite: $('#js-tracker-website').attr('siteid')
}, 'GET');
generateJsCodeAjax.addParams(params, 'POST');
generateJsCodeAjax.setCallback(function (response) {
generateJsCodeAjax = null;
$('#javascript-text').find('textarea').val(response.value);
});
generateJsCodeAjax.send();
};
// function that generates image tracker link
var generateImageTrackingAjax = null,
generateImageTrackerLink = function () {
// get data used to generate the link
var generateDataParams = {
piwikUrl: piwikHost + piwikPath,
actionName: $('#image-tracker-action-name').val(),
};
if ($('#image-tracking-goal-check').is(':checked')) {
generateDataParams.idGoal = $('#image-tracker-goal').val();
if (generateDataParams.idGoal) {
generateDataParams.revenue = $('#image-tracker-advanced-options').find('.revenue').val();
}
}
if (generateImageTrackingAjax) {
generateImageTrackingAjax.abort();
}
generateImageTrackingAjax = new ajaxHelper();
generateImageTrackingAjax.addParams({
module: 'API',
format: 'json',
method: 'SitesManager.getImageTrackingCode',
idSite: $('#image-tracker-website').attr('siteid')
}, 'GET');
generateImageTrackingAjax.addParams(generateDataParams, 'POST');
generateImageTrackingAjax.setCallback(function (response) {
generateImageTrackingAjax = null;
$('#image-tracking-text').find('textarea').val(response.value);
});
generateImageTrackingAjax.send();
};
// on image link tracker site change, change available goals
$('#image-tracker-website').bind('change', function (e, site) {
getSiteData(site.id, '#image-tracking-code-options', function () {
resetGoalSelectItems(site.id, 'image-tracker-goal');
generateImageTrackerLink();
});
});
// on js link tracker site change, change available goals
$('#js-tracker-website').bind('change', function (e, site) {
$('.current-site-name', '#optional-js-tracking-options').each(function () {
$(this).html(site.name);
});
getSiteData(site.id, '#js-code-options', function () {
var siteHost = getHostNameFromUrl(siteUrls[site.id][0]);
$('.current-site-host', '#optional-js-tracking-options').each(function () {
$(this).text(siteHost);
});
var defaultAliasUrl = 'x.' + siteHost;
$('.current-site-alias').text(siteUrls[site.id][1] || defaultAliasUrl);
resetGoalSelectItems(site.id, 'js-tracker-goal');
generateJsCode();
});
});
// on click 'add' link in custom variable section, add a new row, but only
// allow 5 custom variable entry rows
$('.add-custom-variable').click(function (e) {
e.preventDefault();
var newRow = '<tr>\
<td>&nbsp;</td>\
<td><input type="textbox" class="custom-variable-name"/></td>\
<td>&nbsp;</td>\
<td><input type="textbox" class="custom-variable-value"/></td>\
</tr>',
row = $(this).closest('tr');
row.before(newRow);
// hide add button if max # of custom variables has been reached
// (X custom variables + 1 row for add new row)
if ($('tr', row.parent()).length == (maxCustomVariables + 1)) {
$(this).hide();
}
return false;
});
// when any input in the JS tracking options section changes, regenerate JS code
$('#optional-js-tracking-options').on('change', 'input', function () {
generateJsCode();
});
// when any input/select in the image tracking options section changes, regenerate
// image tracker link
$('#image-tracking-section').on('change', 'input,select', function () {
generateImageTrackerLink();
});
// on click generated code textareas, select the text so it can be easily copied
$('#javascript-text>textarea,#image-tracking-text>textarea').click(function () {
$(this).select();
});
// initial generation
getSiteData(
$('#js-tracker-website').attr('siteid'),
'#js-code-options,#image-tracking-code-options',
function () {
var imageTrackerSiteId = $('#image-tracker-website').attr('siteid');
resetGoalSelectItems(imageTrackerSiteId, 'image-tracker-goal');
generateJsCode();
generateImageTrackerLink();
}
);
});
}(jQuery, require));

View file

@ -0,0 +1,82 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
$(document).ready(function () {
$submit = $('.pluginsSettingsSubmit');
if (!$submit) {
return;
}
$submit.click(updatePluginSettings);
function updatePluginSettings()
{
var $nonce = $('[name="setpluginsettingsnonce"]');
var nonceValue = '';
if ($nonce) {
nonceValue = $nonce.val();
}
var ajaxHandler = new ajaxHelper();
ajaxHandler.addParams({
module: 'CoreAdminHome',
action: 'setPluginSettings',
nonce: nonceValue
}, 'GET');
ajaxHandler.addParams({settings: getSettings()}, 'POST');
ajaxHandler.redirectOnSuccess();
ajaxHandler.setLoadingElement(getLoadingElement());
ajaxHandler.setErrorElement(getErrorElement());
ajaxHandler.send(true);
}
function getSettings()
{
var $pluginSections = $( "#pluginSettings[data-pluginname]" );
var values = {};
$pluginSections.each(function (index, pluginSection) {
$pluginSection = $(pluginSection);
var pluginName = $pluginSection.attr('data-pluginname');
var serialized = $('input, textarea, select:not([multiple])', $pluginSection).serializeArray();
// by default, it does not generate an array
var $multiSelects = $('select[multiple]', $pluginSection);
$multiSelects.each(function (index, multiSelect) {
var name = $(multiSelect).attr('name');
serialized.push({name: name, value: $(multiSelect).val()});
});
// by default, values of unchecked checkboxes are not send
var $uncheckedNodes = $('input[type=checkbox]:not(:checked)', $pluginSection);
$uncheckedNodes.each(function (index, uncheckedNode) {
var name = $(uncheckedNode).attr('name');
serialized.push({name: name, value: 0});
});
values[pluginName] = serialized;
});
return values;
}
function getErrorElement()
{
return $('#ajaxErrorPluginSettings');
}
function getLoadingElement()
{
return $('#ajaxLoadingPluginSettings');
}
});

View file

@ -0,0 +1,176 @@
.admin img {
vertical-align: middle;;
}
.admin a {
color: black;
}
#content.admin {
margin: 0 0 0 260px;
padding: 0 0 40px;
display: table;
font: 13px Arial, Helvetica, sans-serif;
}
.admin #header_message {
margin-top: 10px;
}
table.admin {
font-size: 0.9em;
font-family: Arial, Helvetica, verdana sans-serif;
background-color: #fff;
border-collapse: collapse;
}
table.admin thead th {
border-right: 1px solid #fff;
color: #fff;
text-align: center;
padding: 5px;
text-transform: uppercase;
height: 25px;
background-color: #a3c159;
font-weight: bold;
}
table.admin tbody tr {
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
}
table.admin tbody td {
color: #414141;
text-align: left;
vertical-align: top;
}
table.admin tbody th {
text-align: left;
padding: 2px;
}
table.admin tbody td, table.admin tbody th {
color: #536C2A;
text-decoration: none;
font-weight: normal;
padding: 10px;
}
table.admin tbody td:hover, table.admin tbody th:hover {
color: #009193;
text-decoration: none;
}
.warning {
border: 1px dotted gray;
padding: 15px;
font-size: .8em;
}
.warning ul {
margin-left: 50px;
}
.access_error {
font-size: .7em;
padding: 15px;
}
.admin h2 {
border-bottom: 1px solid #DADADA;
margin: 15px -15px 20px 0;
padding: 0 0 5px 0;
font-size: 24px;
width:100%;
}
.admin p, .admin section {
margin-top: 10px;
line-height: 140%;
padding-bottom: 20px;
}
.adminTable {
width: 100%;
clear: both;
margin: 0;
}
.adminTable a {
text-decoration: none;
color: #2B5C97;
}
.adminTable abbr {
white-space: nowrap;
}
.adminTable td {
font-size: 13px;
vertical-align: top;
padding: 7px 15px 9px 10px;
vertical-align: top;
}
.adminTable td.action-links {
text-align: right;
}
.adminTable .check-column {
text-align: right;
width: 1.5em;
padding: 0;
}
.adminTable .num {
text-align: center;
}
.adminTable .name {
font-weight: bold;
}
.adminTable .ui-inline-help {
margin-top: 0;
width: 100%;
}
/* other styles */
.form-description {
color: #666666;
font-style: italic;
margin-left: 10px;
}
#logoSettings, #smtpSettings {
margin-left: 50px;
}
/* to override .admin a */
.admin .sites_autocomplete a {
color: #255792;
}
/* trusted host styles */
#trustedHostSettings .adminTable {
width: 300px;
}
#trustedHostSettings .adminTable td {
vertical-align: middle;
padding-bottom: 0;
}
#trustedHostSettings .adminTable tr td:last-child {
padding: 0 0 0 0;
}
#trustedHostSettings input {
width: 238px;
}
#trustedHostSettings .add-trusted-host-container {
padding: 12px 24px;
}

View file

@ -0,0 +1,79 @@
#javascript-output-section textarea, #image-link-output-section textarea {
width: 100%;
display: block;
color: #111;
font-family: "Courier New", Courier, monospace;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
#javascript-output-section textarea {
height: 256px;
}
#image-link-output-section textarea {
height: 128px;
}
label {
margin-right: .35em;
display: inline-block;
}
#optional-js-tracking-options>tbody>tr>td, #image-tracking-section>tbody>tr>td {
width: 488px;
max-width: 488px;
}
.custom-variable-name, .custom-variable-value {
width: 100px;
}
h3 {
margin-top: 0;
}
.small-form-description {
color: #666;
font-size: 1em;
font-style: italic;
margin-left: 4em;
}
.tracking-option-section {
margin-bottom: 1.5em;
}
#javascript-output-section, #image-link-output-section {
padding-top: 1em;
}
#optional-js-tracking-options th, #image-tracking-section th {
text-align: left;
}
#js-visitor-cv-extra, #js-page-cv-extra, #js-campaign-query-param-extra {
margin-left: 1em;
}
#js-visitor-cv-extra td, #js-page-cv-extra td, #js-campaign-query-param-extra td {
vertical-align: middle;
}
.revenue {
width: 32px;
}
.goal-picker {
height: 1.2em;
}
.goal-picker select {
width: 128px;
}
#js-campaign-query-param-extra input {
width: 72px;
}

View file

@ -0,0 +1,78 @@
#container {
clear: left;
}
.Menu--admin {
padding: 0;
float: left;
width: 240px;
}
.Menu--admin > .Menu-tabList {
background-image: linear-gradient(top, #FECB00 0%, #FE9800 25%, #FE6702 50%, #CA0000 75%, #670002 100%);
background-image: -o-linear-gradient(top, #FECB00 0%, #FE9800 25%, #FE6702 50%, #CA0000 75%, #670002 100%);
background-image: -moz-linear-gradient(top, #FECB00 0%, #FE9800 25%, #FE6702 50%, #CA0000 75%, #670002 100%);
background-image: -webkit-linear-gradient(top, #FECB00 0%, #FE9800 25%, #FE6702 50%, #CA0000 75%, #670002 100%);
background-image: -ms-linear-gradient(top, #FECB00 0%, #FE9800 25%, #FE6702 50%, #CA0000 75%, #670002 100%);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #FECB00), color-stop(0.25, #FE9800), color-stop(0.5, #FE6702), color-stop(0.75, #CA0000), color-stop(1, #670002));
-moz-background-size: 5px 100%;
background-size: 5px 100%;
background-position: 0 0, 100% 0;
background-repeat: no-repeat;
}
.Menu--admin > .Menu-tabList {
padding-left: 5px;
margin-bottom: 0;
margin-top: 0.1em;
border: 1px solid #ddd;
border-radius: 5px;
}
.Menu--admin > .Menu-tabList li {
list-style: none;
margin: 0;
}
.Menu--admin > .Menu-tabList > li {
padding-bottom: 5px;
}
.Menu--admin > .Menu-tabList > li > a,
.Menu--admin > .Menu-tabList > li > span {
text-decoration: none;
border-bottom: 1px dotted #778;
display: block;
padding: 5px 10px;
font-size: 18px;
color: #7E7363;
}
.Menu--admin > .Menu-tabList li li a {
text-decoration: none;
padding: 0.6em 0.9em;
font: 14px Arial, Helvetica, sans-serif;
display: block;
}
.Menu--admin > .Menu-tabList li li a:link,
.Menu--admin > .Menu-tabList li li a:visited {
color: #000;
}
.Menu--admin > .Menu-tabList li li a:hover,
.Menu--admin > .Menu-tabList li li a.active {
color: #e87500;
background: #f1f1f1;
border-color: #000;
}
.Menu--admin > .Menu-tabList li li a:hover {
text-decoration: underline;
}
.Menu--admin > .Menu-tabList li li a.current {
background: #defdbb;
}

View file

@ -0,0 +1,40 @@
#pluginSettings {
width: 820px;
border-spacing: 0px 15px;
.columnTitle {
width:400px
}
.columnField {
width:220px
}
.columnHelp {
width:200px
}
.title {
font-weight: bold
}
.settingIntroduction {
padding-bottom: 0px;
}
.form-description {
font-style: normal;
margin-top: 3px;
display: block;
}
.superUserSettings {
margin-top: 1em;
}
}
#pluginsSettings {
.submitSeparator {
background-color: #DADADA;
height: 1px;
border: 0px;
}
}

View file

@ -0,0 +1,23 @@
{% if adminMenu|length > 1 %}
<div class="Menu Menu--admin">
<ul class="Menu-tabList">
{% for name,submenu in adminMenu %}
{% if submenu._hasSubmenu %}
<li>
<span>{{ name|translate }}</span>
<ul>
{% for sname,url in submenu %}
{% if sname|slice(0,1) != '_' %}
<li>
<a href='index.php{{ url._url|urlRewriteWithParameters }}'
{% if currentAdminMenuName is defined and sname==currentAdminMenuName %}class='active'{% endif %}>{{ sname|translate }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}

View file

@ -0,0 +1,324 @@
{% extends 'admin.twig' %}
{% block content %}
{# load macros #}
{% import 'macros.twig' as piwik %}
{% import 'ajaxMacros.twig' as ajax %}
{% if isSuperUser %}
{{ ajax.errorDiv() }}
{{ ajax.loadingDiv() }}
<h2>{{ 'CoreAdminHome_ArchivingSettings'|translate }}</h2>
<table class="adminTable" style='width:900px;'>
{% if isGeneralSettingsAdminEnabled %}
<tr>
<td style="width:400px;">{{ 'General_AllowPiwikArchivingToTriggerBrowser'|translate }}</td>
<td style="width:220px;">
<fieldset>
<input id="enableBrowserTriggerArchiving-yes" type="radio" value="1" name="enableBrowserTriggerArchiving"{% if enableBrowserTriggerArchiving==1 %} checked="checked"{% endif %} />
<label for="enableBrowserTriggerArchiving-yes">{{ 'General_Yes'|translate }}</label><br/>
<span class="form-description">{{ 'General_Default'|translate }}</span>
<br/><br/>
<input id="enableBrowserTriggerArchiving-no" type="radio" value="0" name="enableBrowserTriggerArchiving"{% if enableBrowserTriggerArchiving==0 %} checked="checked"{% endif %} />
<label for="enableBrowserTriggerArchiving-no">{{ 'General_No'|translate }}</label><br/>
<span class="form-description">{{ 'General_ArchivingTriggerDescription'|translate("<a href='?module=Proxy&action=redirect&url=http://piwik.org/docs/setup-auto-archiving/' target='_blank'>","</a>")|raw }}</span>
</fieldset>
<td>
{% set browserArchivingHelp %}
{{ 'General_ArchivingInlineHelp'|translate }}
<br/>
{{ 'General_SeeTheOfficialDocumentationForMoreInformation'|translate("<a href='?module=Proxy&action=redirect&url=http://piwik.org/docs/setup-auto-archiving/' target='_blank'>","</a>")|raw }}
{% endset %}
{{ piwik.inlineHelp(browserArchivingHelp) }}
</td>
</tr>
{% else %}
<tr>
<td style="width:400px;">{{ 'General_AllowPiwikArchivingToTriggerBrowser'|translate }}</td>
<td style="width:220px;">
<input id="enableBrowserTriggerArchiving-disabled" type="radio" checked="checked" disabled="disabled" />
<label for="enableBrowserTriggerArchiving-disabled">{% if enableBrowserTriggerArchiving==1 %}{{ 'General_Yes'|translate }}{% else %}{{ 'General_No'|translate }}{% endif %}</label><br/>
</td>
</tr>
{% endif %}
<tr>
<td width="400px">
<label for="todayArchiveTimeToLive">
{{ 'General_ReportsContainingTodayWillBeProcessedAtMostEvery'|translate }}
</label>
</td>
<td>
{% set timeOutInput %}
<input size='3' value='{{ todayArchiveTimeToLive }}' id='todayArchiveTimeToLive' {% if not isGeneralSettingsAdminEnabled %}disabled="disabled"{% endif %}/>
{% endset %}
{{ 'General_NSeconds'|translate(timeOutInput)|raw }}
</td>
{% if isGeneralSettingsAdminEnabled %}
<td width='450px'>
{% set archiveTodayTTLHelp %}
{% if showWarningCron %}
<strong>
{{ 'General_NewReportsWillBeProcessedByCron'|translate }}<br/>
{{ 'General_ReportsWillBeProcessedAtMostEveryHour'|translate }}
{{ 'General_IfArchivingIsFastYouCanSetupCronRunMoreOften'|translate }}<br/>
</strong>
{% endif %}
{{ 'General_SmallTrafficYouCanLeaveDefault'|translate(10) }}
<br/>
{{ 'General_MediumToHighTrafficItIsRecommendedTo'|translate(1800,3600) }}
{% endset %}
{{ piwik.inlineHelp(archiveTodayTTLHelp) }}
</td>
{% endif %}
</tr>
{% if isGeneralSettingsAdminEnabled %}
<tr>
<td colspan="3">
<h2>{{ 'CoreAdminHome_UpdateSettings'|translate }}</h2>
</td>
</tr>
<tr>
<td style="width:400px;">{{ 'CoreAdminHome_CheckReleaseGetVersion'|translate }}</td>
<td style="width:220px;">
<fieldset>
<input id="enableBetaReleaseCheck-0" type="radio" value="0" name="enableBetaReleaseCheck"{% if enableBetaReleaseCheck==0 %} checked="checked"{% endif %} />
<label for="enableBetaReleaseCheck-0">{{ 'CoreAdminHome_LatestStableRelease'|translate }}</label><br/>
<span class="form-description">{{ 'General_Recommended'|translate }}</span>
<br/><br/>
<input id="enableBetaReleaseCheck-1" type="radio" value="1" name="enableBetaReleaseCheck"{% if enableBetaReleaseCheck==1 %} checked="checked"{% endif %} />
<label for="enableBetaReleaseCheck-1">{{ 'CoreAdminHome_LatestBetaRelease'|translate }}</label><br/>
<span class="form-description">{{ 'CoreAdminHome_ForBetaTestersOnly'|translate }}</span>
</fieldset>
<td>
{% set checkReleaseHelp %}
{{ 'CoreAdminHome_DevelopmentProcess'|translate("<a href='?module=Proxy&action=redirect&url=http://piwik.org/participate/development-process/' target='_blank'>","</a>")|raw }}
<br/>
{{ 'CoreAdminHome_StableReleases'|translate("<a href='?module=Proxy&action=redirect&url=http://piwik.org/participate/user-feedback/' target='_blank'>","</a>")|raw }}
{% endset %}
{{ piwik.inlineHelp(checkReleaseHelp) }}
</td>
</tr>
{% if canUpdateCommunication %}
<tr>
<td style="width:400px;">{{ 'CoreAdminHome_SendPluginUpdateCommunication'|translate }}</td>
<td style="width:220px;">
<fieldset>
<input id="enablePluginUpdateCommunication-1" type="radio"
name="enablePluginUpdateCommunication" value="1"
{% if enableSendPluginUpdateCommunication==1 %} checked="checked"{% endif %}/>
<label for="enablePluginUpdateCommunication-1">{{ 'General_Yes'|translate }}</label>
<br />
<br />
<input class="indented-radio-button" id="enablePluginUpdateCommunication-0" type="radio"
name="enablePluginUpdateCommunication" value="0"
{% if enableSendPluginUpdateCommunication==0 %} checked="checked"{% endif %}/>
<label for="enablePluginUpdateCommunication-0">{{ 'General_No'|translate }}</label>
<br />
<span class="form-description">{{ 'General_Default'|translate }}</span>
</fieldset>
<td>
{{ piwik.inlineHelp('CoreAdminHome_SendPluginUpdateCommunicationHelp'|translate) }}
</td>
</tr>
{% endif %}
{% endif %}
</table>
{% if isGeneralSettingsAdminEnabled %}
<h2>{{ 'CoreAdminHome_EmailServerSettings'|translate }}</h2>
<div id='emailSettings'>
<table class="adminTable" style='width:600px;'>
<tr>
<td>{{ 'General_UseSMTPServerForEmail'|translate }}<br/>
<span class="form-description">{{ 'General_SelectYesIfYouWantToSendEmailsViaServer'|translate }}</span>
</td>
<td style="width:200px;">
<input id="mailUseSmtp-1" type="radio" name="mailUseSmtp" value="1" {% if mail.transport == 'smtp' %} checked {% endif %}/>
<label for="mailUseSmtp-1">{{ 'General_Yes'|translate }}</label>
<input class="indented-radio-button" id="mailUseSmtp-0" type="radio" name="mailUseSmtp" value="0"
{% if mail.transport == '' %} checked {% endif %}/>
<label for="mailUseSmtp-0">{{ 'General_No'|translate }}</label>
</td>
</tr>
</table>
</div>
<div id='smtpSettings'>
<table class="adminTable" style='width:550px;'>
<tr>
<td><label for="mailHost">{{ 'General_SmtpServerAddress'|translate }}</label></td>
<td style="width:200px;"><input type="text" id="mailHost" value="{{ mail.host }}"></td>
</tr>
<tr>
<td><label for="mailPort">{{ 'General_SmtpPort'|translate }}</label><br/>
<span class="form-description">{{ 'General_OptionalSmtpPort'|translate }}</span></td>
<td><input type="text" id="mailPort" value="{{ mail.port }}"></td>
</tr>
<tr>
<td><label for="mailType">{{ 'General_AuthenticationMethodSmtp'|translate }}</label><br/>
<span class="form-description">{{ 'General_OnlyUsedIfUserPwdIsSet'|translate }}</span>
</td>
<td>
<select id="mailType">
<option value="" {% if mail.type == '' %} selected="selected" {% endif %}></option>
<option id="plain" {% if mail.type == 'Plain' %} selected="selected" {% endif %} value="Plain">Plain</option>
<option id="login" {% if mail.type == 'Login' %} selected="selected" {% endif %} value="Login"> Login</option>
<option id="cram-md5" {% if mail.type == 'Crammd5' %} selected="selected" {% endif %} value="Crammd5"> Crammd5</option>
</select>
</td>
</tr>
<tr>
<td><label for="mailUsername">{{ 'General_SmtpUsername'|translate }}</label><br/>
<span class="form-description">{{ 'General_OnlyEnterIfRequired'|translate }}</span></td>
<td>
<input type="text" id="mailUsername" value="{{ mail.username }}"/>
</td>
</tr>
<tr>
<td><label for="mailPassword">{{ 'General_SmtpPassword'|translate }}</label><br/>
<span class="form-description">{{ 'General_OnlyEnterIfRequiredPassword'|translate }}<br/>
{{ 'General_WarningPasswordStored'|translate("<strong>","</strong>")|raw }}</span>
</td>
<td>
<input type="password" id="mailPassword" value="{{ mail.password }}"/>
</td>
</tr>
<tr>
<td><label for="mailEncryption">{{ 'General_SmtpEncryption'|translate }}</label><br/>
<span class="form-description">{{ 'General_EncryptedSmtpTransport'|translate }}</span></td>
<td>
<select id="mailEncryption">
<option value="" {% if mail.encryption == '' %} selected="selected" {% endif %}></option>
<option id="ssl" {% if mail.encryption == 'ssl' %} selected="selected" {% endif %} value="ssl">SSL</option>
<option id="tls" {% if mail.encryption == 'tls' %} selected="selected" {% endif %} value="tls">TLS</option>
</select>
</td>
</tr>
</table>
</div>
{% endif %}
<h2>{{ 'CoreAdminHome_BrandingSettings'|translate }}</h2>
<div id='brandSettings'>
{{ 'CoreAdminHome_CustomLogoHelpText'|translate }}
<table class="adminTable" style="width:900px;">
<tr>
<td style="width:200px;">{{ 'CoreAdminHome_UseCustomLogo'|translate }}</td>
<td style="width:200px;">
<input id="useCustomLogo-1" type="radio" name="useCustomLogo" value="1" {% if branding.use_custom_logo == 1 %} checked {% endif %}/>
<label for="useCustomLogo-1">{{ 'General_Yes'|translate }}</label>
<input class="indented-radio-button" id="useCustomLogo-0" type="radio" name="useCustomLogo" value="0" {% if branding.use_custom_logo == 0 %} checked {% endif %} />
<label for="useCustomLogo-0" class>{{ 'General_No'|translate }}</label>
</td>
<td id="inlineHelpCustomLogo">
{% set giveUsFeedbackText %}"{{ 'General_GiveUsYourFeedback'|translate }}"{% endset %}
{% set customLogoHelp %}
{{ 'CoreAdminHome_CustomLogoFeedbackInfo'|translate(giveUsFeedbackText,"<a href='?module=CorePluginsAdmin&action=plugins' target='_blank'>","</a>")|raw }}
{% endset %}
{{ piwik.inlineHelp(customLogoHelp) }}
</td>
</tr>
</table>
</div>
<div id='logoSettings'>
<form id="logoUploadForm" method="post" enctype="multipart/form-data" action="index.php?module=CoreAdminHome&format=json&action=uploadCustomLogo">
<table class="adminTable" style='width:550px;'>
<tr>
{% if logosWriteable %}
<td>
<label for="customLogo">{{ 'CoreAdminHome_LogoUpload'|translate }}:<br/>
<span class="form-description">{{ 'CoreAdminHome_LogoUploadHelp'|translate("JPG / PNG / GIF",110) }}</span>
</label>
</td>
<td style="width:200px;">
<input name="customLogo" type="file" id="customLogo"/>
<img src="{{ pathUserLogo }}?r={{ random() }}" id="currentLogo" height="150"/>
</td>
{% else %}
<td>
<div style="display:inline-block;margin-top:10px;" id="CoreAdminHome_LogoNotWriteable">
{{ 'CoreAdminHome_LogoNotWriteableInstruction'
|translate("<strong>"~pathUserLogoDirectory~"</strong><br/>", pathUserLogo ~", "~ pathUserLogoSmall ~", "~ pathUserLogoSVG ~"")
|notification({'placeAt': '#CoreAdminHome_LogoNotWriteable', 'noclear': true, 'context': 'warning', 'raw': true}) }}
</div>
</td>
{% endif %}
</tr>
</table>
</form>
</div>
<div class="ui-confirm" id="confirmTrustedHostChange">
<h2>{{ 'CoreAdminHome_TrustedHostConfirm'|translate }}</h2>
<input role="yes" type="button" value="{{ 'General_Yes'|translate }}"/>
<input role="no" type="button" value="{{ 'General_No'|translate }}"/>
</div>
<h2 id="trustedHostsSection">{{ 'CoreAdminHome_TrustedHostSettings'|translate }}</h2>
<div id='trustedHostSettings'>
{% include "@CoreHome/_warningInvalidHost.twig" %}
{% if not isGeneralSettingsAdminEnabled %}
{{ 'CoreAdminHome_PiwikIsInstalledAt'|translate }}: {{ trustedHosts|join(", ") }}
{% else %}
<p>{{ 'CoreAdminHome_PiwikIsInstalledAt'|translate }}:</p>
<strong>{{ 'CoreAdminHome_ValidPiwikHostname'|translate }}</strong>
<ul>
{% for hostIdx, host in trustedHosts %}
<li>
<input name="trusted_host" type="text" value="{{ host }}"/>
<a href="#" class="remove-trusted-host" title="{{ 'General_Delete'|translate }}">
<img alt="{{ 'General_Delete'|translate }}" src="plugins/Morpheus/images/ico_delete.png" />
</a>
</li>
{% endfor %}
</ul>
<div class="add-trusted-host-container">
<a href="#" class="add-trusted-host"><em>{{ 'General_Add'|translate }}</em></a>
</div>
{% endif %}
</div>
<input type="submit" value="{{ 'General_Save'|translate }}" id="generalSettingsSubmit" class="submit"/>
<br/>
<br/>
{% if isDataPurgeSettingsEnabled %}
{% set clickDeleteLogSettings %}{{ 'PrivacyManager_DeleteDataSettings'|translate }}{% endset %}
<h2>{{ 'PrivacyManager_DeleteDataSettings'|translate }}</h2>
<p>
{{ 'PrivacyManager_DeleteDataDescription'|translate }} {{ 'PrivacyManager_DeleteDataDescription2'|translate }}
<br/>
<a href='{{ linkTo({'module':"PrivacyManager", 'action':"privacySettings"}) }}#deleteLogsAnchor'>
{{ 'PrivacyManager_ClickHereSettings'|translate("'" ~ clickDeleteLogSettings ~ "'") }}
</a>
</p>
{% endif %}
{% endif %}
<h2>{{ 'CoreAdminHome_OptOutForYourVisitors'|translate }}</h2>
<p>{{ 'CoreAdminHome_OptOutExplanation'|translate }}
{% set optOutUrl %}{{ piwikUrl }}index.php?module=CoreAdminHome&action=optOut&language={{ language }}{% endset %}
{% set iframeOptOut %}
<iframe style="border: 0; height: 200px; width: 600px;" src="{{ optOutUrl }}"></iframe>
{% endset %}
<code>{{ iframeOptOut|escape }}</code>
<br/>
{{ 'CoreAdminHome_OptOutExplanationBis'|translate("<a href='" ~ optOutUrl ~ "' target='_blank'>","</a>")|raw }}
</p>
{% endblock %}

View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
{% if not trackVisits %}
{{ 'CoreAdminHome_OptOutComplete'|translate }}
<br/>
{{ 'CoreAdminHome_OptOutCompleteBis'|translate }}
{% else %}
{{ 'CoreAdminHome_YouMayOptOut'|translate }}
<br/>
{{ 'CoreAdminHome_YouMayOptOutBis'|translate }}
{% endif %}
<br/><br/>
<form method="post" action="?module=CoreAdminHome&amp;action=optOut{% if language %}&amp;language={{ language }}{% endif %}">
<input type="hidden" name="nonce" value="{{ nonce }}" />
<input type="hidden" name="fuzz" value="{{ "now"|date }}" />
<input onclick="this.form.submit()" type="checkbox" id="trackVisits" name="trackVisits" {% if trackVisits %}checked="checked"{% endif %} />
<label for="trackVisits"><strong>
{% if trackVisits %}
{{ 'CoreAdminHome_YouAreOptedIn'|translate }} {{ 'CoreAdminHome_ClickHereToOptOut'|translate }}
{% else %}
{{ 'CoreAdminHome_YouAreOptedOut'|translate }} {{ 'CoreAdminHome_ClickHereToOptIn'|translate }}
{% endif %}
</strong></label>
</form>
</body>
</html>

View file

@ -0,0 +1,173 @@
{% extends 'admin.twig' %}
{% block content %}
<div id="pluginsSettings">
{% import 'macros.twig' as piwik %}
{% import 'ajaxMacros.twig' as ajax %}
<p>
{{ 'CoreAdminHome_PluginSettingsIntro'|translate }}
{% for pluginName, settings in pluginSettings %}
<a href="#{{ pluginName|e('html_attr') }}">{{ pluginName }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
</p>
<input type="hidden" name="setpluginsettingsnonce" value="{{ nonce }}">
{% for pluginName, settings in pluginSettings %}
<h2 id="{{ pluginName|e('html_attr') }}">{{ pluginName }}</h2>
{% if settings.getIntroduction %}
<p class="pluginIntroduction">
{{ settings.getIntroduction }}
</p>
{% endif %}
<table class="adminTable" id="pluginSettings" data-pluginname="{{ pluginName|e('html_attr') }}">
{% for name, setting in settings.getSettingsForCurrentUser %}
{% set settingValue = setting.getValue %}
{% if pluginName in firstSuperUserSettingNames|keys and name == firstSuperUserSettingNames[pluginName] %}
<tr>
<td colspan="3">
<h3 class="superUserSettings">{{ 'MobileMessaging_Settings_SuperAdmin'|translate }}</h3>
</td>
</tr>
{% endif %}
{% if setting.introduction %}
<tr>
<td colspan="3">
<p class="settingIntroduction">
{{ setting.introduction }}
</p>
</td>
</tr>
{% endif %}
<tr>
<td class="columnTitle">
<span class="title">{{ setting.title }}</span>
<br />
<span class='form-description'>
{{ setting.description }}
</span>
</td>
<td class="columnField">
<fieldset>
<label>
{% if setting.uiControlType == 'select' or setting.uiControlType == 'multiselect' %}
<select
{% for attr, val in setting.uiControlAttributes %}
{{ attr|e('html_attr') }}="{{ val|e('html_attr') }}"
{% endfor %}
name="{{ setting.getKey|e('html_attr') }}"
{% if setting.uiControlType == 'multiselect' %}multiple{% endif %}>
{% for key, value in setting.availableValues %}
<option value='{{ key }}'
{% if settingValue is iterable and key in settingValue %}
selected='selected'
{% elseif settingValue==key %}
selected='selected'
{% endif %}>
{{ value }}
</option>
{% endfor %}
</select>
{% elseif setting.uiControlType == 'textarea' %}
<textarea style="width: 176px;"
{% for attr, val in setting.uiControlAttributes %}
{{ attr|e('html_attr') }}="{{ val|e('html_attr') }}"
{% endfor %}
name="{{ setting.getKey|e('html_attr') }}"
>
{{- settingValue -}}
</textarea>
{% elseif setting.uiControlType == 'radio' %}
{% for key, value in setting.availableValues %}
<input
id="name-value-{{ loop.index }}"
{% for attr, val in setting.uiControlAttributes %}
{{ attr|e('html_attr') }}="{{ val|e('html_attr') }}"
{% endfor %}
{% if settingValue==key %}
checked="checked"
{% endif %}
type="radio"
value="{{ key|e('html_attr') }}"
name="{{ setting.getKey|e('html_attr') }}" />
<label for="name-value-{{ loop.index }}">{{ value }}</label>
<br />
{% endfor %}
{% else %}
<input
{% for attr, val in setting.uiControlAttributes %}
{{ attr|e('html_attr') }}="{{ val|e('html_attr') }}"
{% endfor %}
{% if setting.uiControlType == 'checkbox' %}
value="1"
{% endif %}
{% if setting.uiControlType == 'checkbox' and settingValue %}
checked="checked"
{% endif %}
type="{{ setting.uiControlType|e('html_attr') }}"
name="{{ setting.getKey|e('html_attr') }}"
value="{{ settingValue|e('html_attr') }}"
>
{% endif %}
{% if setting.defaultValue and setting.uiControlType != 'checkbox' %}
<br/>
<span class='form-description'>
{{ 'General_Default'|translate }}
{% if setting.defaultValue is iterable %}
{{ setting.defaultValue|join(', ')|truncate(50) }}
{% else %}
{{ setting.defaultValue|truncate(50) }}
{% endif %}
</span>
{% endif %}
</label>
</fieldset>
</td>
<td class="columnHelp">
{% if setting.inlineHelp %}
<div class="ui-widget">
<div class="ui-inline-help">
{{ setting.inlineHelp }}
</div>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endfor %}
<hr class="submitSeparator"/>
{{ ajax.errorDiv('ajaxErrorPluginSettings') }}
{{ ajax.loadingDiv('ajaxLoadingPluginSettings') }}
<input type="submit" value="{{ 'General_Save'|translate }}" class="pluginsSettingsSubmit submit"/>
</div>
{% endblock %}

View file

@ -0,0 +1,272 @@
{% extends 'admin.twig' %}
{% block head %}
{{ parent() }}
<link rel="stylesheet" href="plugins/CoreAdminHome/stylesheets/jsTrackingGenerator.css" />
<script type="text/javascript" src="plugins/CoreAdminHome/javascripts/jsTrackingGenerator.js"></script>
{% endblock %}
{% block content %}
<div id="js-tracking-generator-data" max-custom-variables="{{ maxCustomVariables|e('html_attr') }}" data-currencies="{{ currencySymbols|json_encode }}"></div>
<h2 piwik-enriched-headline
feature-name="{{ 'CoreAdminHome_TrackingCode'|translate }}"
help-url="http://piwik.org/docs/tracking-api/">{{ 'CoreAdminHome_JavaScriptTracking'|translate }}</h2>
<div id="js-code-options" class="adminTable">
<p>
{{ 'CoreAdminHome_JSTrackingIntro1'|translate }}
<br/><br/>
{{ 'CoreAdminHome_JSTrackingIntro2'|translate }} {{ 'CoreAdminHome_JSTrackingIntro3'|translate('<a href="http://piwik.org/integrate/" target="_blank">','</a>')|raw }}
<br/><br/>
{{ 'CoreAdminHome_JSTrackingIntro4'|translate('<a href="#image-tracking-link">','</a>')|raw }}
<br/><br/>
{{ 'CoreAdminHome_JSTrackingIntro5'|translate('<a target="_blank" href="http://piwik.org/docs/javascript-tracking/">','</a>')|raw }}
</p>
<div>
{# website #}
<label class="website-label"><strong>{{ 'General_Website'|translate }}</strong></label>
<div piwik-siteselector
class="sites_autocomplete"
siteid="{{ idSite }}"
sitename="{{ defaultReportSiteName }}"
show-all-sites-item="false"
switch-site-on-select="false"
id="js-tracker-website"
show-selected-site="true"></div>
<br/><br/><br/>
</div>
<table id="optional-js-tracking-options" class="adminTable">
<tr>
<th>{{ 'General_Options'|translate }}</th>
<th>{{ 'Mobile_Advanced'|translate }}
<a href="#" class="section-toggler-link" data-section-id="javascript-advanced-options">({{ 'General_Show'|translate }})</a>
</th>
</tr>
<tr>
<td>
{# track across all subdomains #}
<div class="tracking-option-section">
<input type="checkbox" id="javascript-tracking-all-subdomains"/>
<label for="javascript-tracking-all-subdomains">{{ 'CoreAdminHome_JSTracking_MergeSubdomains'|translate }}
<span class='current-site-name'>{{ defaultReportSiteName|raw }}</span>
</label>
<div class="small-form-description">
{{ 'CoreAdminHome_JSTracking_MergeSubdomainsDesc'|translate("x.<span class='current-site-host'>"~defaultReportSiteDomain~"</span>","y.<span class='current-site-host'>"~defaultReportSiteDomain~"</span>")|raw }}
</div>
</div>
{# group page titles by site domain #}
<div class="tracking-option-section">
<input type="checkbox" id="javascript-tracking-group-by-domain"/>
<label for="javascript-tracking-group-by-domain">{{ 'CoreAdminHome_JSTracking_GroupPageTitlesByDomain'|translate }}</label>
<div class="small-form-description">
{{ 'CoreAdminHome_JSTracking_GroupPageTitlesByDomainDesc1'|translate("<span class='current-site-host'>" ~ defaultReportSiteDomain ~ "</span>")|raw }}
</div>
</div>
{# track across all site aliases #}
<div class="tracking-option-section">
<input type="checkbox" id="javascript-tracking-all-aliases"/>
<label for="javascript-tracking-all-aliases">{{ 'CoreAdminHome_JSTracking_MergeAliases'|translate }}
<span class='current-site-name'>{{ defaultReportSiteName|raw }}</span>
</label>
<div class="small-form-description">
{{ 'CoreAdminHome_JSTracking_MergeAliasesDesc'|translate("<span class='current-site-alias'>"~defaultReportSiteAlias~"</span>")|raw }}
</div>
</div>
</td>
<td>
<div id="javascript-advanced-options" style="display:none;">
{# visitor custom variable #}
<div class="custom-variable tracking-option-section" id="javascript-tracking-visitor-cv">
<input class="section-toggler-link" type="checkbox" id="javascript-tracking-visitor-cv-check" data-section-id="js-visitor-cv-extra"/>
<label for="javascript-tracking-visitor-cv-check">{{ 'CoreAdminHome_JSTracking_VisitorCustomVars'|translate }}</label>
<div class="small-form-description">
{{ 'CoreAdminHome_JSTracking_VisitorCustomVarsDesc'|translate }}
</div>
<table style="display:none;" id="js-visitor-cv-extra">
<tr>
<td><strong>{{ 'General_Name'|translate }}</strong></td>
<td><input type="textbox" class="custom-variable-name" placeholder="e.g. Type"/></td>
<td><strong>{{ 'General_Value'|translate }}</strong></td>
<td><input type="textbox" class="custom-variable-value" placeholder="e.g. Customer"/></td>
</tr>
<tr>
<td colspan="4" style="text-align:right;">
<a href="#" class="add-custom-variable">{{ 'General_Add'|translate }}</a>
</td>
</tr>
</table>
</div>
{# page view custom variable #}
<div class="custom-variable tracking-option-section" id="javascript-tracking-page-cv">
<input class="section-toggler-link" type="checkbox" id="javascript-tracking-page-cv-check" data-section-id="js-page-cv-extra"/>
<label for="javascript-tracking-page-cv-check">{{ 'CoreAdminHome_JSTracking_PageCustomVars'|translate }}</label>
<div class="small-form-description">
{{ 'CoreAdminHome_JSTracking_PageCustomVarsDesc'|translate }}
</div>
<table style="display:none;" id="js-page-cv-extra">
<tr>
<td><strong>{{ 'General_Name'|translate }}</strong></td>
<td><input type="textbox" class="custom-variable-name" placeholder="e.g. Category"/></td>
<td><strong>{{ 'General_Value'|translate }}</strong></td>
<td><input type="textbox" class="custom-variable-value" placeholder="e.g. White Papers"/></td>
</tr>
<tr>
<td colspan="4" style="text-align:right;">
<a href="#" class="add-custom-variable">{{ 'General_Add'|translate }}</a>
</td>
</tr>
</table>
</div>
{# do not track support #}
<div class="tracking-option-section">
<input type="checkbox" id="javascript-tracking-do-not-track"/>
<label for="javascript-tracking-do-not-track">{{ 'CoreAdminHome_JSTracking_EnableDoNotTrack'|translate }}</label>
<div class="small-form-description">
{{ 'CoreAdminHome_JSTracking_EnableDoNotTrackDesc'|translate }}
{% if serverSideDoNotTrackEnabled %}
<br/>
<br/>
{{ 'CoreAdminHome_JSTracking_EnableDoNotTrack_AlreadyEnabled'|translate }}
{% endif %}
</div>
</div>
{# custom campaign name/keyword query params #}
<div class="tracking-option-section">
<input class="section-toggler-link" type="checkbox" id="custom-campaign-query-params-check"
data-section-id="js-campaign-query-param-extra"/>
<label for="custom-campaign-query-params-check">{{ 'CoreAdminHome_JSTracking_CustomCampaignQueryParam'|translate }}</label>
<div class="small-form-description">
{{ 'CoreAdminHome_JSTracking_CustomCampaignQueryParamDesc'|translate('<a href="http://piwik.org/faq/general/#faq_119" target="_blank">','</a>')|raw }}
</div>
<table style="display:none;" id="js-campaign-query-param-extra">
<tr>
<td><strong>{{ 'CoreAdminHome_JSTracking_CampaignNameParam'|translate }}</strong></td>
<td><input type="text" id="custom-campaign-name-query-param"/></td>
</tr>
<tr>
<td><strong>{{ 'CoreAdminHome_JSTracking_CampaignKwdParam'|translate }}</strong></td>
<td><input type="text" id="custom-campaign-keyword-query-param"/></td>
</tr>
</table>
</div>
</div>
</td>
</tr>
</table>
</div>
<div id="javascript-output-section">
<h3>{{ 'General_JsTrackingTag'|translate }}</h3>
<p class="form-description">{{ 'CoreAdminHome_JSTracking_CodeNote'|translate("&lt;/body&gt;")|raw }}</p>
<div id="javascript-text">
<textarea> </textarea>
</div>
<br/>
</div>
<h2 id="image-tracking-link">{{ 'CoreAdminHome_ImageTracking'|translate }}</h2>
<div id="image-tracking-code-options" class="adminTable">
<p>
{{ 'CoreAdminHome_ImageTrackingIntro1'|translate }} {{ 'CoreAdminHome_ImageTrackingIntro2'|translate("<em>&lt;noscript&gt;&lt;/noscript&gt;</em>")|raw }}
<br/><br/>
{{ 'CoreAdminHome_ImageTrackingIntro3'|translate('<a href="http://piwik.org/docs/tracking-api/reference/" target="_blank">','</a>')|raw }}
</p>
<div>
{# website #}
<label class="website-label"><strong>{{ 'General_Website'|translate }}</strong></label>
<div piwik-siteselector
class="sites_autocomplete"
siteid="{{ idSite }}"
sitename="{{ defaultReportSiteName }}"
id="image-tracker-website"
show-all-sites-item="false"
switch-site-on-select="false"
show-selected-site="true"></div>
<br/><br/><br/>
</div>
<table id="image-tracking-section" class="adminTable">
<tr>
<th>{{ 'General_Options'|translate }}</th>
<th>{{ 'Mobile_Advanced'|translate }}
<a href="#" class="section-toggler-link" data-section-id="image-tracker-advanced-options">
({{ 'General_Show'|translate }})
</a>
</th>
</tr>
<tr>
<td>
{# action_name #}
<div class="tracking-option-section">
<label for="image-tracker-action-name">{{ 'Actions_ColumnPageName'|translate }}</label>
<input type="text" id="image-tracker-action-name"/>
</div>
</td>
<td>
<div id="image-tracker-advanced-options" style="display:none;">
{# goal #}
<div class="goal-picker tracking-option-section">
<input class="section-toggler-link" type="checkbox" id="image-tracking-goal-check" data-section-id="image-goal-picker-extra"/>
<label for="image-tracking-goal-check">{{ 'CoreAdminHome_TrackAGoal'|translate }}</label>
<div style="display:none;" id="image-goal-picker-extra">
<select id="image-tracker-goal">
<option value="">{{ 'UserCountryMap_None'|translate }}</option>
</select>
<span>{{ 'CoreAdminHome_WithOptionalRevenue'|translate }}</span>
<span class="currency">{{ defaultSiteRevenue }}</span>
<input type="text" class="revenue" value=""/>
</div>
</div>
</div>
</td>
</tr>
</table>
<div id="image-link-output-section" width="560px">
<h3>{{ 'CoreAdminHome_ImageTrackingLink'|translate }}</h3><br/><br/>
<div id="image-tracking-text">
<textarea> </textarea>
</div>
<br/>
</div>
</div>
<h2>{{ 'CoreAdminHome_ImportingServerLogs'|translate }}</h2>
<p>
{{ 'CoreAdminHome_ImportingServerLogsDesc'|translate('<a href="http://piwik.org/log-analytics/" target="_blank">','</a>')|raw }}
</p>
{% endblock %}

View file

@ -0,0 +1,90 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class CodeCoverage extends ConsoleCommand
{
protected function configure()
{
$this->setName('tests:coverage');
$this->setDescription('Run all phpunit tests and generate a combined code coverage');
$this->addArgument('group', InputArgument::OPTIONAL, 'Run only a specific test group. Separate multiple groups by comma, for instance core,integration', '');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$phpCovPath = trim(shell_exec('which phpcov'));
if (empty($phpCovPath)) {
$output->writeln('phpcov not installed. please install pear.phpunit.de/phpcov.');
return;
}
$command = $this->getApplication()->find('tests:run');
$arguments = array(
'command' => 'tests:run',
'--options' => sprintf('--coverage-php %s/tests/results/logs/%%group%%.cov', PIWIK_DOCUMENT_ROOT),
);
$groups = $input->getArgument('group');
if (!empty($groups)) {
$arguments['group'] = $groups;
} else {
shell_exec(sprintf('rm %s/tests/results/logs/*.cov', PIWIK_DOCUMENT_ROOT));
}
$inputObject = new ArrayInput($arguments);
$inputObject->setInteractive($input->isInteractive());
$command->run($inputObject, $output);
$command = 'phpcov';
// force xdebug usage for coverage options
if (!extension_loaded('xdebug')) {
$output->writeln('<info>xdebug extension required for code coverage.</info>');
$output->writeln('<info>searching for xdebug extension...</info>');
$extensionDir = shell_exec('php-config --extension-dir');
$xdebugFile = trim($extensionDir) . DIRECTORY_SEPARATOR . 'xdebug.so';
if (!file_exists($xdebugFile)) {
$dialog = $this->getHelperSet()->get('dialog');
$xdebugFile = $dialog->askAndValidate($output, 'xdebug not found. Please provide path to xdebug.so', function($xdebugFile) {
return file_exists($xdebugFile);
});
} else {
$output->writeln('<info>xdebug extension found in extension path.</info>');
}
$output->writeln("<info>using $xdebugFile as xdebug extension.</info>");
$command = sprintf('php -d zend_extension=%s %s', $xdebugFile, $phpCovPath);
}
shell_exec(sprintf('rm -rf %s/tests/results/coverage/*', PIWIK_DOCUMENT_ROOT));
passthru(sprintf('cd %1$s && %2$s --merge --html tests/results/coverage/ --whitelist ./core/ --whitelist ./plugins/ --add-uncovered %1$s/tests/results/logs/', PIWIK_DOCUMENT_ROOT, $command));
}
}

View file

@ -0,0 +1,55 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\CronArchive;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class CoreArchiver extends ConsoleCommand
{
protected function configure()
{
$this->configureArchiveCommand($this);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
if ($input->getOption('piwik-domain') && !$input->getOption('url')) {
$_SERVER['argv'][] = '--url=' . $input->getOption('piwik-domain');
}
include PIWIK_INCLUDE_PATH . '/misc/cron/archive.php';
}
// This is reused by another console command
static public function configureArchiveCommand(ConsoleCommand $command)
{
$command->setName('core:archive');
$command->setDescription("Runs the CLI archiver. It is an important tool for general maintenance and to keep Piwik very fast.");
$command->setHelp("* It is recommended to run the script with the option --piwik-domain=[piwik-server-url] only. Other options are not required.
* This script should be executed every hour via crontab, or as a daemon.
* You can also run it via http:// by specifying the Super User &token_auth=XYZ as a parameter ('Web Cron'),
but it is recommended to run it via command line/CLI instead.
* If you have any suggestion about this script, please let the team know at hello@piwik.org
* Enjoy!");
$command->addOption('url', null, InputOption::VALUE_REQUIRED, "Mandatory option as an alternative to '--piwik-domain'. Must be set to the Piwik base URL.\nFor example: --url=http://analytics.example.org/ or --url=https://example.org/piwik/");
$command->addOption('force-all-websites', null, InputOption::VALUE_NONE, "If specified, the script will trigger archiving on all websites and all past dates.\nYou may use --force-all-periods=[seconds] to trigger archiving on those websites\nthat had visits in the last [seconds] seconds.");
$command->addOption('force-all-periods', null, InputOption::VALUE_OPTIONAL, "Limits archiving to websites with some traffic in the last [seconds] seconds. \nFor example --force-all-periods=86400 will archive websites that had visits in the last 24 hours. \nIf [seconds] is not specified, all websites with visits in the last " . CronArchive::ARCHIVE_SITES_WITH_TRAFFIC_SINCE . "\n seconds (" . round(CronArchive::ARCHIVE_SITES_WITH_TRAFFIC_SINCE / 86400) . " days) will be archived.");
$command->addOption('force-timeout-for-periods', null, InputOption::VALUE_OPTIONAL, "The current week/ current month/ current year will be processed at most every [seconds].\nIf not specified, defaults to " . CronArchive::SECONDS_DELAY_BETWEEN_PERIOD_ARCHIVES . ".");
$command->addOption('force-date-last-n', null, InputOption::VALUE_REQUIRED, "This script calls the API with period=lastN. You can force the N in lastN by specifying this value.");
$command->addOption('force-idsites', null, InputOption::VALUE_OPTIONAL, 'If specified, archiving will be processed only for these Sites Ids (comma separated)');
$command->addOption('skip-idsites', null, InputOption::VALUE_OPTIONAL, 'If specified, archiving will be skipped for these websites (in case these website ids would have been archived).');
$command->addOption('disable-scheduled-tasks', null, InputOption::VALUE_NONE, "Skips executing Scheduled tasks (sending scheduled reports, db optimization, etc.).");
$command->addOption('xhprof', null, InputOption::VALUE_NONE, "Enables XHProf profiler for this archive.php run. Requires XHPRof (see tests/README.xhprof.md).");
$command->addOption('accept-invalid-ssl-certificate', null, InputOption::VALUE_NONE, "It is _NOT_ recommended to use this argument. Instead, you should use a valid SSL certificate!\nIt can be useful if you specified --url=https://... or if you are using Piwik with force_ssl=1");
}
}

View file

@ -0,0 +1,58 @@
<?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\Plugins\CoreConsole\Commands;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class GenerateApi extends GeneratePluginBase
{
protected function configure()
{
$this->setName('generate:api')
->setDescription('Adds an API to an existing plugin')
->addOption('pluginname', null, InputOption::VALUE_REQUIRED, 'The name of an existing plugin which does not have an API yet');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$pluginName = $this->getPluginName($input, $output);
$exampleFolder = PIWIK_INCLUDE_PATH . '/plugins/ExamplePlugin';
$replace = array('ExamplePlugin' => $pluginName);
$whitelistFiles = array('/API.php');
$this->copyTemplateToPlugin($exampleFolder, $pluginName, $replace, $whitelistFiles);
$this->writeSuccessMessage($output, array(
sprintf('API.php for %s generated.', $pluginName),
'You can now start adding API methods',
'Enjoy!'
));
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return array
* @throws \RunTimeException
*/
protected function getPluginName(InputInterface $input, OutputInterface $output)
{
$pluginNames = $this->getPluginNamesHavingNotSpecificFile('API.php');
$invalidName = 'You have to enter the name of an existing plugin which does not already have an API';
return $this->askPluginNameAndValidate($input, $output, $pluginNames, $invalidName);
}
}

View file

@ -0,0 +1,89 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\Common;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class GenerateCommand extends GeneratePluginBase
{
protected function configure()
{
$this->setName('generate:command')
->setDescription('Adds a command to an existing plugin')
->addOption('pluginname', null, InputOption::VALUE_REQUIRED, 'The name of an existing plugin')
->addOption('command', null, InputOption::VALUE_REQUIRED, 'The name of the command you want to create');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$pluginName = $this->getPluginName($input, $output);
$commandName = $this->getCommandName($input, $output);
$exampleFolder = PIWIK_INCLUDE_PATH . '/plugins/ExampleCommand';
$replace = array(
'ExampleCommand' => $pluginName,
'examplecommand' => strtolower($pluginName),
'HelloWorld' => $commandName,
'helloworld' => strtolower($commandName)
);
$whitelistFiles = array('/Commands', '/Commands/HelloWorld.php');
$this->copyTemplateToPlugin($exampleFolder, $pluginName, $replace, $whitelistFiles);
$this->writeSuccessMessage($output, array(
sprintf('Command %s for plugin %s generated', $commandName, $pluginName)
));
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return string
* @throws \RunTimeException
*/
private function getCommandName(InputInterface $input, OutputInterface $output)
{
$testname = $input->getOption('command');
$validate = function ($testname) {
if (empty($testname)) {
throw new \InvalidArgumentException('You have to enter a command name');
}
return $testname;
};
if (empty($testname)) {
$dialog = $this->getHelperSet()->get('dialog');
$testname = $dialog->askAndValidate($output, 'Enter the name of the command: ', $validate);
} else {
$validate($testname);
}
$testname = ucfirst($testname);
return $testname;
}
protected function getPluginName(InputInterface $input, OutputInterface $output)
{
$pluginNames = $this->getPluginNames();
$invalidName = 'You have to enter the name of an existing plugin';
return $this->askPluginNameAndValidate($input, $output, $pluginNames, $invalidName);
}
}

View file

@ -0,0 +1,58 @@
<?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\Plugins\CoreConsole\Commands;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class GenerateController extends GeneratePluginBase
{
protected function configure()
{
$this->setName('generate:controller')
->setDescription('Adds a Controller to an existing plugin')
->addOption('pluginname', null, InputOption::VALUE_REQUIRED, 'The name of an existing plugin which does not have a Controller yet');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$pluginName = $this->getPluginName($input, $output);
$exampleFolder = PIWIK_INCLUDE_PATH . '/plugins/ExamplePlugin';
$replace = array('ExamplePlugin' => $pluginName);
$whitelistFiles = array('/Controller.php', '/templates', '/templates/index.twig');
$this->copyTemplateToPlugin($exampleFolder, $pluginName, $replace, $whitelistFiles);
$this->writeSuccessMessage($output, array(
sprintf('Controller for %s generated.', $pluginName),
'You can now start adding Controller actions',
'Enjoy!'
));
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return array
* @throws \RunTimeException
*/
protected function getPluginName(InputInterface $input, OutputInterface $output)
{
$pluginNames = $this->getPluginNamesHavingNotSpecificFile('Controller.php');
$invalidName = 'You have to enter the name of an existing plugin which does not already have a Controller';
return $this->askPluginNameAndValidate($input, $output, $pluginNames, $invalidName);
}
}

View file

@ -0,0 +1,223 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\Filesystem;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class GeneratePlugin extends GeneratePluginBase
{
protected function configure()
{
$this->setName('generate:plugin')
->setAliases(array('generate:theme'))
->setDescription('Generates a new plugin/theme including all needed files')
->addOption('name', null, InputOption::VALUE_REQUIRED, 'Plugin name ([a-Z0-9_-])')
->addOption('description', null, InputOption::VALUE_REQUIRED, 'Plugin description, max 150 characters')
->addOption('pluginversion', null, InputOption::VALUE_OPTIONAL, 'Plugin version')
->addOption('full', null, InputOption::VALUE_OPTIONAL, 'If a value is set, an API and a Controller will be created as well. Option is only available for creating plugins, not for creating themes.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$isTheme = $this->isTheme($input);
$pluginName = $this->getPluginName($input, $output);
$description = $this->getPluginDescription($input, $output);
$version = $this->getPluginVersion($input, $output);
$createFullPlugin = !$isTheme && $this->getCreateFullPlugin($input, $output);
$this->generatePluginFolder($pluginName);
if ($isTheme) {
$exampleFolder = PIWIK_INCLUDE_PATH . '/plugins/ExampleTheme';
$replace = array(
'ExampleTheme' => $pluginName,
'ExampleDescription' => $description,
'0.1.0' => $version
);
$whitelistFiles = array();
} else {
$exampleFolder = PIWIK_INCLUDE_PATH . '/plugins/ExamplePlugin';
$replace = array(
'ExamplePlugin' => $pluginName,
'ExampleDescription' => $description,
'0.1.0' => $version
);
$whitelistFiles = array(
'/ExamplePlugin.php',
'/plugin.json',
'/README.md',
'/.travis.yml',
'/screenshots',
'/screenshots/.gitkeep',
'/javascripts',
'/javascripts/plugin.js',
);
}
$this->copyTemplateToPlugin($exampleFolder, $pluginName, $replace, $whitelistFiles);
$this->writeSuccessMessage($output, array(
sprintf('%s %s %s generated.', $isTheme ? 'Theme' : 'Plugin', $pluginName, $version),
'Enjoy!'
));
if ($createFullPlugin) {
$this->executePluginCommand($output, 'generate:api', $pluginName);
$this->executePluginCommand($output, 'generate:controller', $pluginName);
}
}
private function executePluginCommand(OutputInterface $output, $commandName, $pluginName)
{
$command = $this->getApplication()->find($commandName);
$arguments = array(
'command' => $commandName,
'--pluginname' => $pluginName
);
$input = new ArrayInput($arguments);
$command->run($input, $output);
}
/**
* @param InputInterface $input
* @return bool
*/
private function isTheme(InputInterface $input)
{
$commandName = $input->getFirstArgument();
return false !== strpos($commandName, 'theme');
}
protected function generatePluginFolder($pluginName)
{
$pluginPath = $this->getPluginPath($pluginName);
Filesystem::mkdir($pluginPath, true);
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return array
* @throws \RunTimeException
*/
protected function getPluginName(InputInterface $input, OutputInterface $output)
{
$self = $this;
$validate = function ($pluginName) use ($self) {
if (empty($pluginName)) {
throw new \RunTimeException('You have to enter a plugin name');
}
if (!Filesystem::isValidFilename($pluginName)) {
throw new \RunTimeException(sprintf('The plugin name %s is not valid', $pluginName));
}
$pluginPath = $self->getPluginPath($pluginName);
if (file_exists($pluginPath)) {
throw new \RunTimeException('A plugin with this name already exists');
}
return $pluginName;
};
$pluginName = $input->getOption('name');
if (empty($pluginName)) {
$dialog = $this->getHelperSet()->get('dialog');
$pluginName = $dialog->askAndValidate($output, 'Enter a plugin name: ', $validate);
} else {
$validate($pluginName);
}
$pluginName = ucfirst($pluginName);
return $pluginName;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return mixed
* @throws \RunTimeException
*/
protected function getPluginDescription(InputInterface $input, OutputInterface $output)
{
$validate = function ($description) {
if (empty($description)) {
throw new \RunTimeException('You have to enter a description');
}
if (150 < strlen($description)) {
throw new \RunTimeException('Description is too long, max 150 characters allowed.');
}
return $description;
};
$description = $input->getOption('description');
if (empty($description)) {
$dialog = $this->getHelperSet()->get('dialog');
$description = $dialog->askAndValidate($output, 'Enter a plugin description: ', $validate);
} else {
$validate($description);
}
return $description;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return string
*/
protected function getPluginVersion(InputInterface $input, OutputInterface $output)
{
$version = $input->getOption('pluginversion');
if (is_null($version)) {
$dialog = $this->getHelperSet()->get('dialog');
$version = $dialog->ask($output, 'Enter a plugin version number (default to 0.1.0): ', '0.1.0');
}
return $version;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return mixed
*/
protected function getCreateFullPlugin(InputInterface $input, OutputInterface $output)
{
$full = $input->getOption('full');
if (is_null($full)) {
$dialog = $this->getHelperSet()->get('dialog');
$full = $dialog->askConfirmation($output, 'Shall we also create an API and a Controller? (y/N)', false);
}
return !empty($full);
}
}

View file

@ -0,0 +1,143 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\Filesystem;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
abstract class GeneratePluginBase extends ConsoleCommand
{
public function getPluginPath($pluginName)
{
return PIWIK_INCLUDE_PATH . '/plugins/' . ucfirst($pluginName);
}
private function createFolderWithinPluginIfNotExists($pluginName, $folder)
{
$pluginPath = $this->getPluginPath($pluginName);
if (!file_exists($pluginName . $folder)) {
Filesystem::mkdir($pluginPath . $folder, true);
}
}
protected function createFileWithinPluginIfNotExists($pluginName, $fileName, $content)
{
$pluginPath = $this->getPluginPath($pluginName);
if (!file_exists($pluginPath . $fileName)) {
file_put_contents($pluginPath . $fileName, $content);
}
}
/**
* @param string $templateFolder full path like /home/...
* @param string $pluginName
* @param array $replace array(key => value) $key will be replaced by $value in all templates
* @param array $whitelistFiles If not empty, only given files/directories will be copied.
* For instance array('/Controller.php', '/templates', '/templates/index.twig')
*/
protected function copyTemplateToPlugin($templateFolder, $pluginName, array $replace = array(), $whitelistFiles = array())
{
$replace['PLUGINNAME'] = $pluginName;
$files = array_merge(
Filesystem::globr($templateFolder, '*'),
// Also copy files starting with . such as .gitignore
Filesystem::globr($templateFolder, '.*')
);
foreach ($files as $file) {
$fileNamePlugin = str_replace($templateFolder, '', $file);
if (!empty($whitelistFiles) && !in_array($fileNamePlugin, $whitelistFiles)) {
continue;
}
if (is_dir($file)) {
$this->createFolderWithinPluginIfNotExists($pluginName, $fileNamePlugin);
} else {
$template = file_get_contents($file);
foreach ($replace as $key => $value) {
$template = str_replace($key, $value, $template);
}
foreach ($replace as $key => $value) {
$fileNamePlugin = str_replace($key, $value, $fileNamePlugin);
}
$this->createFileWithinPluginIfNotExists($pluginName, $fileNamePlugin, $template);
}
}
}
protected function getPluginNames()
{
$pluginDirs = \_glob(PIWIK_INCLUDE_PATH . '/plugins/*', GLOB_ONLYDIR);
$pluginNames = array();
foreach ($pluginDirs as $pluginDir) {
$pluginNames[] = basename($pluginDir);
}
return $pluginNames;
}
protected function getPluginNamesHavingNotSpecificFile($filename)
{
$pluginDirs = \_glob(PIWIK_INCLUDE_PATH . '/plugins/*', GLOB_ONLYDIR);
$pluginNames = array();
foreach ($pluginDirs as $pluginDir) {
if (!file_exists($pluginDir . '/' . $filename)) {
$pluginNames[] = basename($pluginDir);
}
}
return $pluginNames;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return array
* @throws \RunTimeException
*/
protected function askPluginNameAndValidate(InputInterface $input, OutputInterface $output, $pluginNames, $invalidArgumentException)
{
$validate = function ($pluginName) use ($pluginNames, $invalidArgumentException) {
if (!in_array($pluginName, $pluginNames)) {
throw new \InvalidArgumentException($invalidArgumentException);
}
return $pluginName;
};
$pluginName = $input->getOption('pluginname');
if (empty($pluginName)) {
$dialog = $this->getHelperSet()->get('dialog');
$pluginName = $dialog->askAndValidate($output, 'Enter the name of your plugin: ', $validate, false, null, $pluginNames);
} else {
$validate($pluginName);
}
$pluginName = ucfirst($pluginName);
return $pluginName;
}
}

View file

@ -0,0 +1,58 @@
<?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\Plugins\CoreConsole\Commands;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class GenerateSettings extends GeneratePluginBase
{
protected function configure()
{
$this->setName('generate:settings')
->setDescription('Adds a plugin setting class to an existing plugin')
->addOption('pluginname', null, InputOption::VALUE_REQUIRED, 'The name of an existing plugin which does not have settings yet');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$pluginName = $this->getPluginName($input, $output);
$exampleFolder = PIWIK_INCLUDE_PATH . '/plugins/ExampleSettingsPlugin';
$replace = array('ExampleSettingsPlugin' => $pluginName);
$whitelistFiles = array('/Settings.php');
$this->copyTemplateToPlugin($exampleFolder, $pluginName, $replace, $whitelistFiles);
$this->writeSuccessMessage($output, array(
sprintf('Settings.php for %s generated.', $pluginName),
'You can now start defining your plugin settings',
'Enjoy!'
));
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return array
* @throws \RunTimeException
*/
protected function getPluginName(InputInterface $input, OutputInterface $output)
{
$pluginNames = $this->getPluginNamesHavingNotSpecificFile('Settings.php');
$invalidName = 'You have to enter the name of an existing plugin which does not already have settings';
return $this->askPluginNameAndValidate($input, $output, $pluginNames, $invalidName);
}
}

View file

@ -0,0 +1,189 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\Common;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class GenerateTest extends GeneratePluginBase
{
protected function configure()
{
$this->setName('generate:test')
->setDescription('Adds a test to an existing plugin')
->addOption('pluginname', null, InputOption::VALUE_REQUIRED, 'The name of an existing plugin')
->addOption('testname', null, InputOption::VALUE_REQUIRED, 'The name of the test to create')
->addOption('testtype', null, InputOption::VALUE_REQUIRED, 'Whether you want to create a "unit", "integration" or "database" test');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$pluginName = $this->getPluginName($input, $output);
$testName = $this->getTestName($input, $output);
$testType = $this->getTestType($input, $output);
$exampleFolder = PIWIK_INCLUDE_PATH . '/plugins/ExamplePlugin';
$replace = array(
'ExamplePlugin' => $pluginName,
'SimpleTest' => $testName,
'SimpleIntegrationTest' => $testName,
'@group Plugins' => '@group ' . $testType
);
$testClass = $this->getTestClass($testType);
if(!empty($testClass)) {
$replace['\PHPUnit_Framework_TestCase'] = $testClass;
}
$whitelistFiles = $this->getTestFilesWhitelist($testType);
$this->copyTemplateToPlugin($exampleFolder, $pluginName, $replace, $whitelistFiles);
$this->writeSuccessMessage($output, array(
sprintf('Test %s for plugin %s generated.', $testName, $pluginName),
'You can now start writing beautiful tests!',
));
$this->writeSuccessMessage($output, array(
'To run all your plugin tests, execute the command: ',
sprintf('./console tests:run %s', $pluginName),
'To run only this test: ',
sprintf('./console tests:run %s', $testName),
'Enjoy!'
));
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return string
* @throws \RunTimeException
*/
private function getTestName(InputInterface $input, OutputInterface $output)
{
$testname = $input->getOption('testname');
$validate = function ($testname) {
if (empty($testname)) {
throw new \InvalidArgumentException('You have to enter a valid test name ');
}
return $testname;
};
if (empty($testname)) {
$dialog = $this->getHelperSet()->get('dialog');
$testname = $dialog->askAndValidate($output, 'Enter the name of the test: ', $validate);
} else {
$validate($testname);
}
if (!Common::stringEndsWith(strtolower($testname), 'test')) {
$testname = $testname . 'Test';
}
$testname = ucfirst($testname);
return $testname;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return array
* @throws \RunTimeException
*/
protected function getPluginName(InputInterface $input, OutputInterface $output)
{
$pluginNames = $this->getPluginNames();
$invalidName = 'You have to enter the name of an existing plugin';
return $this->askPluginNameAndValidate($input, $output, $pluginNames, $invalidName);
}
/**
* @param InputInterface $input
* @return string
*/
private function getTestClass($testType)
{
if ('Database' == $testType) {
return '\DatabaseTestCase';
}
if ('Unit' == $testType) {
return '\PHPUnit_Framework_TestCase';
}
return false;
}
public function getValidTypes()
{
return array('unit', 'integration', 'database');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return string Unit, Integration, Database
*/
private function getTestType(InputInterface $input, OutputInterface $output)
{
$testtype = $input->getOption('testtype');
$self = $this;
$validate = function ($testtype) use ($self) {
if (empty($testtype) || !in_array($testtype, $self->getValidTypes())) {
throw new \InvalidArgumentException('You have to enter a valid test type: ' . implode(" or ", $self->getValidTypes()));
}
return $testtype;
};
if (empty($testtype)) {
$dialog = $this->getHelperSet()->get('dialog');
$testtype = $dialog->askAndValidate($output, 'Enter the type of the test to generate ('. implode(", ", $this->getValidTypes()).'): ', $validate, false, null, $this->getValidTypes());
} else {
$validate($testtype);
}
$testtype = ucfirst($testtype);
return $testtype;
}
/**
* @return array
*/
protected function getTestFilesWhitelist($testType)
{
if('Integration' == $testType) {
return array(
'/.gitignore',
'/tests',
'/tests/SimpleIntegrationTest.php',
'/tests/expected',
'/tests/expected/test___API.get_day.xml',
'/tests/expected/test___Goals.getItemsSku_day.xml',
'/tests/processed',
'/tests/processed/.gitignore',
'/tests/fixtures',
'/tests/fixtures/SimpleFixtureTrackFewVisits.php'
);
}
return array(
'/tests',
'/tests/SimpleTest.php'
);
}
}

View file

@ -0,0 +1,96 @@
<?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\Plugins\CoreConsole\Commands;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class GenerateVisualizationPlugin extends GeneratePlugin
{
protected function configure()
{
$this->setName('generate:visualizationplugin')
->setDescription('Generates a new visualization plugin including all needed files')
->addOption('name', null, InputOption::VALUE_REQUIRED, 'Plugin name ([a-Z0-9_-])')
->addOption('visualizationname', null, InputOption::VALUE_REQUIRED, 'Visualization name ([a-Z0-9])')
->addOption('description', null, InputOption::VALUE_REQUIRED, 'Plugin description, max 150 characters')
->addOption('pluginversion', null, InputOption::VALUE_OPTIONAL, 'Plugin version')
->addOption('full', null, InputOption::VALUE_OPTIONAL, 'If a value is set, an API and a Controller will be created as well. Option is only available for creating plugins, not for creating themes.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$pluginName = $this->getPluginName($input, $output);
$description = $this->getPluginDescription($input, $output);
$version = $this->getPluginVersion($input, $output);
$visualizationName = $this->getVisualizationName($input, $output);
$this->generatePluginFolder($pluginName);
$exampleFolder = PIWIK_INCLUDE_PATH . '/plugins/ExampleVisualization';
$replace = array(
'SimpleTable' => $visualizationName,
'simpleTable' => lcfirst($visualizationName),
'Simple Table' => $visualizationName,
'ExampleVisualization' => $pluginName,
'ExampleVisualizationDescription' => $description
);
$this->copyTemplateToPlugin($exampleFolder, $pluginName, $replace, $whitelistFiles = array());
$this->writeSuccessMessage($output, array(
sprintf('Visualization plugin %s %s generated.', $pluginName, $version),
'Enjoy!'
));
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return string
* @throws \RunTimeException
*/
private function getVisualizationName(InputInterface $input, OutputInterface $output)
{
$self = $this;
$validate = function ($visualizationName) use ($self) {
if (empty($visualizationName)) {
throw new \RunTimeException('You have to enter a visualization name');
}
if (!ctype_alnum($visualizationName)) {
throw new \RunTimeException(sprintf('The visualization name %s is not valid', $visualizationName));
}
return $visualizationName;
};
$visualizationName = $input->getOption('visualizationname');
if (empty($visualizationName)) {
$dialog = $this->getHelperSet()->get('dialog');
$visualizationName = $dialog->askAndValidate($output, 'Enter a visualization name: ', $validate);
} else {
$validate($visualizationName);
}
$visualizationName = ucfirst($visualizationName);
return $visualizationName;
}
}

View file

@ -0,0 +1,142 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class GitCommit extends ConsoleCommand
{
protected function configure()
{
$this->setName('git:commit')
->setDescription('Commit')
->addOption('message', 'm', InputOption::VALUE_REQUIRED, 'Commit Message');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$submodules = $this->getSubmodulePaths();
foreach ($submodules as $submodule) {
if (empty($submodule)) {
continue;
}
$status = $this->getStatusOfSubmodule($submodule);
if (false !== strpos($status, '?? ')) {
$output->writeln(sprintf('<error>%s has untracked files or folders. Delete or add them and try again.</error>', $submodule));
$output->writeln('<error>Status:</error>');
$output->writeln(sprintf('<comment>%s</comment>', $status));
return;
}
}
$commitMessage = $input->getOption('message');
if (empty($commitMessage)) {
$output->writeln('No message specified. Use option -m or --message.');
return;
}
if (!$this->hasChangesToBeCommitted()) {
$dialog = $this->getHelperSet()->get('dialog');
$question = '<question>There are no changes to be commited in the super repo, do you just want to commit and converge submodules?</question>';
if (!$dialog->askConfirmation($output, $question, false)) {
$output->writeln('<info>Cool, nothing done. Stage files using "git add" and try again.</info>');
return;
}
}
foreach ($submodules as $submodule) {
if (empty($submodule)) {
continue;
}
$status = $this->getStatusOfSubmodule($submodule);
if (empty($status)) {
$output->writeln(sprintf('%s has no changes, will ignore', $submodule));
continue;
}
$cmd = sprintf('cd %s/%s && git pull && git add . && git commit -am "%s"', PIWIK_DOCUMENT_ROOT, $submodule, $commitMessage);
$this->passthru($cmd, $output);
}
if ($this->hasChangesToBeCommitted()) {
$cmd = sprintf('cd %s && git commit -m "%s"', PIWIK_DOCUMENT_ROOT, $commitMessage);
$this->passthru($cmd, $output);
}
foreach ($submodules as $submodule) {
if (empty($submodule)) {
continue;
}
$cmd = sprintf('cd %s && git add %s', PIWIK_DOCUMENT_ROOT, $submodule);
$this->passthru($cmd, $output);
}
if ($this->hasChangesToBeCommitted()) {
$cmd = sprintf('cd %s && git commit -m "Updating submodules"', PIWIK_DOCUMENT_ROOT);
$this->passthru($cmd, $output);
}
}
private function passthru($cmd, OutputInterface $output)
{
$output->writeln('Executing command: ' . $cmd);
passthru($cmd);
}
private function hasChangesToBeCommitted()
{
$cmd = sprintf('cd %s && git status --porcelain', PIWIK_DOCUMENT_ROOT);
$result = shell_exec($cmd);
$result = trim($result);
if (false !== strpos($result, 'M ')) {
// stages
return true;
}
if (false !== strpos($result, 'MM ')) {
// staged and modified
return true;
}
return false;
}
/**
* @return array
*/
private function getSubmodulePaths()
{
$cmd = sprintf("grep path .gitmodules | sed 's/.*= //'");
$submodules = shell_exec($cmd);
$submodules = explode("\n", $submodules);
return $submodules;
}
protected function getStatusOfSubmodule($submodule)
{
$cmd = sprintf('cd %s/%s && git status --porcelain', PIWIK_DOCUMENT_ROOT, $submodule);
$status = trim(shell_exec($cmd));
return $status;
}
}

View file

@ -0,0 +1,55 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class GitPull extends ConsoleCommand
{
protected function configure()
{
$this->setName('git:pull');
$this->setDescription('Pull Piwik repo and all submodules');
}
protected function getBranchName()
{
$cmd = sprintf('cd %s && git rev-parse --abbrev-ref HEAD', PIWIK_DOCUMENT_ROOT);
$branch = shell_exec($cmd);
return trim($branch);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
if ('master' != $this->getBranchName()) {
$output->writeln('<info>Doing nothing because you are not on the master branch in super repo.</info>');
return;
}
$cmd = sprintf('cd %s && git checkout master && git pull && git submodule update --init --recursive --remote', PIWIK_DOCUMENT_ROOT);
$this->passthru($cmd, $output);
$cmd = 'git submodule foreach "(git checkout master; git pull)&"';
$this->passthru($cmd, $output);
}
private function passthru($cmd, OutputInterface $output)
{
$output->writeln('Executing command: ' . $cmd);
passthru($cmd);
}
}

View file

@ -0,0 +1,43 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class GitPush extends ConsoleCommand
{
protected function configure()
{
$this->setName('git:push');
$this->setDescription('Push Piwik repo and all submodules');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$cmd = sprintf('cd %s && git push --recurse-submodules=on-demand', PIWIK_DOCUMENT_ROOT);
$output->writeln('Executing command: ' . $cmd);
passthru($cmd);
}
private function hasUnpushedCommits()
{
$cmd = sprintf('cd %s && git log @{u}..',PIWIK_DOCUMENT_ROOT);
$hasUnpushedCommits = shell_exec($cmd);
$hasUnpushedCommits = trim($hasUnpushedCommits);
return !empty($hasUnpushedCommits);
}
}

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\Plugins\CoreConsole\Commands;
use Piwik\Plugin\Manager;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* core:plugin console command.
*/
class ManagePlugin extends ConsoleCommand
{
private $operations = array();
protected function configure()
{
$this->setName('core:plugin');
$this->setDescription("Perform various actions regarding one or more plugins.");
$this->addArgument("operation", InputArgument::REQUIRED, "Operation to apply (can be 'activate' or 'deactivate').");
$this->addArgument("plugins", InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'Plugin name(s) to activate.');
$this->addOption('domain', null, InputOption::VALUE_REQUIRED, "The domain to activate the plugin for.");
$this->operations['activate'] = 'activatePlugin';
$this->operations['deactivate'] = 'deactivatePlugin';
}
/**
* Execute command like: ./console cloudadmin:plugin activate CustomAlerts --piwik-domain=testcustomer.piwik.pro
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$operation = $input->getArgument("operation");
$plugins = $input->getArgument('plugins');
if (empty($this->operations[$operation])) {
throw new Exception("Invalid operation '$operation'.");
}
$fn = $this->operations[$operation];
foreach ($plugins as $plugin) {
call_user_func(array($this, $fn), $input, $output, $plugin);
}
}
private function activatePlugin(InputInterface $input, OutputInterface $output, $plugin)
{
Manager::getInstance()->activatePlugin($plugin, $input, $output);
$output->writeln("Activated plugin <info>$plugin</info>");
}
private function deactivatePlugin(InputInterface $input, OutputInterface $output, $plugin)
{
Manager::getInstance()->deactivatePlugin($plugin, $input, $output);
$output->writeln("Deactivated plugin <info>$plugin</info>");
}
}

View file

@ -0,0 +1,60 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class ManageTestFiles extends ConsoleCommand
{
protected function configure()
{
$this->setName('development:test-files');
$this->setDescription("Manage test files.");
$this->addArgument('operation', InputArgument::REQUIRED, 'The operation to apply. Supported operations include: '
. '"copy"');
$this->addOption('file', null, InputOption::VALUE_REQUIRED, "The file (or files) to apply the operation to.");
// TODO: allow copying by regex pattern
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$operation = $input->getArgument('operation');
if ($operation == 'copy') {
$this->copy($input, $output);
} else {
throw new \Exception("Invalid operation '$operation'.");
}
}
private function copy($input, $output)
{
$file = $input->getOption('file');
$prefix = PIWIK_INCLUDE_PATH . '/tests/PHPUnit/Integration/processed/';
$guesses = array(
'/' . $file,
$prefix . $file,
$prefix . $file . '.xml'
);
foreach ($guesses as $guess) {
if (is_file($guess)) {
$file = $guess;
}
}
copy($file, PIWIK_INCLUDE_PATH . '/tests/PHPUnit/Integration/expected/' . basename($file));
}
}

View file

@ -0,0 +1,87 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class RunTests extends ConsoleCommand
{
protected function configure()
{
$this->setName('tests:run');
$this->setDescription('Run Piwik PHPUnit tests one group after the other');
$this->addArgument('group', InputArgument::OPTIONAL, 'Run only a specific test group. Separate multiple groups by comma, for instance core,integration', '');
$this->addOption('options', 'o', InputOption::VALUE_OPTIONAL, 'All options will be forwarded to phpunit', '');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$options = $input->getOption('options');
$groups = $input->getArgument('group');
$groups = explode(",", $groups);
$groups = array_map('ucfirst', $groups);
$groups = array_filter($groups, 'strlen');
$command = 'phpunit';
// force xdebug usage for coverage options
if (false !== strpos($options, '--coverage') && !extension_loaded('xdebug')) {
$output->writeln('<info>xdebug extension required for code coverage.</info>');
$output->writeln('<info>searching for xdebug extension...</info>');
$extensionDir = shell_exec('php-config --extension-dir');
$xdebugFile = trim($extensionDir) . DIRECTORY_SEPARATOR . 'xdebug.so';
if (!file_exists($xdebugFile)) {
$dialog = $this->getHelperSet()->get('dialog');
$xdebugFile = $dialog->askAndValidate($output, 'xdebug not found. Please provide path to xdebug.so', function($xdebugFile) {
return file_exists($xdebugFile);
});
} else {
$output->writeln('<info>xdebug extension found in extension path.</info>');
}
$output->writeln("<info>using $xdebugFile as xdebug extension.</info>");
$phpunitPath = trim(shell_exec('which phpunit'));
$command = sprintf('php -d zend_extension=%s %s', $xdebugFile, $phpunitPath);
}
if(empty($groups)) {
$groups = $this->getTestsGroups();
}
foreach($groups as $group) {
$params = '--group ' . $group . ' ' . str_replace('%group%', $group, $options);
$cmd = sprintf('cd %s/tests/PHPUnit && %s %s', PIWIK_DOCUMENT_ROOT, $command, $params);
$output->writeln('Executing command: <info>' . $cmd . '</info>');
passthru($cmd);
$output->writeln("");
}
}
private function getTestsGroups()
{
return array('Core', 'Plugins', 'Integration', 'UI');
}
}

View file

@ -0,0 +1,70 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class RunUITests extends ConsoleCommand
{
protected function configure()
{
$this->setName('tests:run-ui');
$this->setDescription('Run screenshot tests');
$this->addArgument('specs', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Run only a specific test spec. Separate multiple specs by comma, for instance core,integration', array());
$this->addOption("persist-fixture-data", null, InputOption::VALUE_NONE, "Persist test data in a database and do not execute tear down.");
$this->addOption('keep-symlinks', null, InputOption::VALUE_NONE, "Keep recursive directory symlinks so test pages can be viewed in a browser.");
$this->addOption('print-logs', null, InputOption::VALUE_NONE, "Print webpage logs even if tests succeed.");
$this->addOption('drop', null, InputOption::VALUE_NONE, "Drop the existing database and re-setup a persisted fixture.");
$this->addOption('plugin', null, InputOption::VALUE_REQUIRED, "Execute all tests for a plugin.");
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$specs = $input->getArgument('specs');
$persistFixtureData = $input->getOption("persist-fixture-data");
$keepSymlinks = $input->getOption('keep-symlinks');
$printLogs = $input->getOption('print-logs');
$drop = $input->getOption('drop');
$plugin = $input->getOption('plugin');
$options = array();
if ($persistFixtureData) {
$options[] = "--persist-fixture-data";
}
if ($keepSymlinks) {
$options[] = "--keep-symlinks";
}
if ($printLogs) {
$options[] = "--print-logs";
}
if ($drop) {
$options[] = "--drop";
}
if ($plugin) {
$options[] = "--plugin=" . $plugin;
}
$options = implode(" ", $options);
$specs = implode(" ", $specs);
$cmd = "phantomjs '" . PIWIK_INCLUDE_PATH . "/tests/lib/screenshot-testing/run-tests.js' $options $specs";
$output->writeln('Executing command: <info>' . $cmd . '</info>');
$output->writeln('');
passthru($cmd);
}
}

View file

@ -0,0 +1,156 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\Url;
use Piwik\Piwik;
use Piwik\Config;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Console commands that sets up a fixture either in a local MySQL database or a remote one.
*
* TODO: use this console command in UI tests instead of setUpDatabase.php/tearDownDatabase.php scripts
*/
class SetupFixture extends ConsoleCommand
{
protected function configure()
{
$this->setName('tests:setup-fixture');
$this->setDescription('Create a database and fill it with data using a Piwik test fixture.');
$this->addArgument('fixture', InputArgument::REQUIRED,
"The class name of the fixture to apply. Doesn't need to have a namespace if it exists in the " .
"Piwik\\Tests\\Fixtures namespace.");
$this->addOption('db-name', null, InputOption::VALUE_REQUIRED,
"The name of the database that will contain the fixture data. This option is required to be set.");
$this->addOption('file', null, InputOption::VALUE_REQUIRED,
"The file location of the fixture. If this option is included the file will be required explicitly.");
$this->addOption('db-host', null, InputOption::VALUE_REQUIRED,
"The hostname of the MySQL database to use. Uses the default config value if not specified.");
$this->addOption('db-user', null, InputOption::VALUE_REQUIRED,
"The name of the MySQL user to use. Uses the default config value if not specified.");
$this->addOption('db-pass', null, InputOption::VALUE_REQUIRED,
"The MySQL user password to use. Uses the default config value if not specified.");
$this->addOption('teardown', null, InputOption::VALUE_NONE,
"If specified, the fixture will be torn down and the database deleted. Won't work if the --db-name " .
"option isn't supplied.");
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$dbName = $input->getOption('db-name');
if (!$dbName) {
throw new \Exception("Required argument --db-name is not set.");
}
$this->requireFixtureFiles();
$this->setIncludePathAsInTestBootstrap();
$file = $input->getOption('file');
if ($file) {
if (is_file($file)) {
require_once $file;
} else if (is_file(PIWIK_INCLUDE_PATH . '/' . $file)) {
require_once PIWIK_INCLUDE_PATH . '/' . $file;
} else {
throw new \Exception("Cannot find --file option file '$file'.");
}
}
$host = Url::getHost();
if (empty($host)) {
Url::setHost('localhost');
}
// get the fixture class
$fixtureClass = $input->getArgument('fixture');
if (class_exists("Piwik\\Tests\\Fixtures\\" . $fixtureClass)) {
$fixtureClass = "Piwik\\Tests\\Fixtures\\" . $fixtureClass;
}
if (!class_exists($fixtureClass)) {
throw new \Exception("Cannot find fixture class '$fixtureClass'.");
}
// create the fixture
$fixture = new $fixtureClass();
$fixture->dbName = $dbName;
$fixture->printToScreen = true;
Config::getInstance()->setTestEnvironment();
$fixture->createConfig = false;
// setup database overrides
$testingEnvironment = $fixture->getTestEnvironment();
$optionsToOverride = array(
'dbname' => $dbName,
'host' => $input->getOption('db-host'),
'user' => $input->getOption('db-user'),
'password' => $input->getOption('db-pass')
);
foreach ($optionsToOverride as $configOption => $value) {
if ($value) {
$configOverride = $testingEnvironment->configOverride;
$configOverride['database_tests'][$configOption] = $configOverride['database'][$configOption] = $value;
$testingEnvironment->configOverride = $configOverride;
Config::getInstance()->database[$configOption] = $value;
}
}
// perform setup and/or teardown
if ($input->getOption('teardown')) {
$testingEnvironment->save();
$fixture->performTearDown();
} else {
$fixture->performSetUp();
}
$this->writeSuccessMessage($output, array("Fixture successfully setup!"));
}
private function requireFixtureFiles()
{
require_once "PHPUnit/Autoload.php";
require_once PIWIK_INCLUDE_PATH . '/libs/PiwikTracker/PiwikTracker.php';
require_once PIWIK_INCLUDE_PATH . '/tests/PHPUnit/FakeAccess.php';
require_once PIWIK_INCLUDE_PATH . '/tests/PHPUnit/TestingEnvironment.php';
require_once PIWIK_INCLUDE_PATH . '/tests/PHPUnit/Fixture.php';
$fixturesToLoad = array(
'/tests/PHPUnit/Fixtures/*.php',
'/tests/PHPUnit/UI/Fixtures/*.php',
);
foreach($fixturesToLoad as $fixturePath) {
foreach (glob(PIWIK_INCLUDE_PATH . $fixturePath) as $file) {
require_once $file;
}
}
}
private function setIncludePathAsInTestBootstrap()
{
if (!defined('PIWIK_INCLUDE_SEARCH_PATH')) {
define('PIWIK_INCLUDE_SEARCH_PATH', get_include_path()
. PATH_SEPARATOR . PIWIK_INCLUDE_PATH . '/core'
. PATH_SEPARATOR . PIWIK_INCLUDE_PATH . '/libs'
. PATH_SEPARATOR . PIWIK_INCLUDE_PATH . '/plugins');
}
@ini_set('include_path', PIWIK_INCLUDE_SEARCH_PATH);
@set_include_path(PIWIK_INCLUDE_SEARCH_PATH);
}
}

View file

@ -0,0 +1,81 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\Http;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class SyncUITestScreenshots extends ConsoleCommand
{
protected function configure()
{
$this->setName('development:sync-ui-test-screenshots');
$this->setDescription('For Piwik core devs. Copies screenshots '
. 'from travis artifacts to tests/PHPUnit/UI/expected-ui-screenshots/');
$this->addArgument('buildnumber', InputArgument::REQUIRED, 'Travis build number you want to sync.');
$this->addArgument('screenshotsRegex', InputArgument::OPTIONAL,
'A regex to use when selecting screenshots to copy. If not supplied all screenshots are copied.', '.*');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$buildNumber = $input->getArgument('buildnumber');
$screenshotsRegex = $input->getArgument('screenshotsRegex');
if (empty($buildNumber)) {
throw new \InvalidArgumentException('Missing build number.');
}
$urlBase = sprintf('http://builds-artifacts.piwik.org/ui-tests.master/%s', $buildNumber);
$diffviewer = Http::sendHttpRequest($urlBase . "/screenshot-diffs/diffviewer.html", $timeout = 60);
$dom = new \DOMDocument();
$dom->loadHTML($diffviewer);
foreach ($dom->getElementsByTagName("tr") as $row) {
$columns = $row->getElementsByTagName("td");
$nameColumn = $columns->item(0);
$processedColumn = $columns->item(2);
$testPlugin = null;
if ($nameColumn
&& preg_match("/\(for ([a-zA-Z_]+) plugin\)/", $dom->saveXml($nameColumn), $matches)
) {
$testPlugin = $matches[1];
}
$file = null;
if ($processedColumn
&& preg_match("/href=\".*\/(.*)\"/", $dom->saveXml($processedColumn), $matches)
) {
$file = $matches[1];
}
if ($file !== null
&& preg_match("/" . $screenshotsRegex . "/", $file)
) {
if ($testPlugin == null) {
$downloadTo = "tests/PHPUnit/UI/expected-ui-screenshots/$file";
} else {
$downloadTo = "plugins/$testPlugin/tests/UI/expected-ui-screenshots/$file";
}
$output->write("<info>Downloading $file to .$downloadTo...</info>\n");
Http::sendHttpRequest("$urlBase/processed-ui-screenshots/$file", $timeout = 60, $userAgent = null,
PIWIK_DOCUMENT_ROOT . "/" . $downloadTo);
}
}
}
}

View file

@ -0,0 +1,33 @@
<?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\Plugins\CoreConsole\Commands;
use Piwik\Plugin\ConsoleCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
*/
class WatchLog extends ConsoleCommand
{
protected function configure()
{
$this->setName('log:watch');
$this->setDescription('Outputs the last parts of the log files and follows as the log file grows. Does not work on Windows');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$cmd = sprintf('tail -f %s/tmp/logs/*.log', PIWIK_DOCUMENT_ROOT);
$output->writeln('Executing command: ' . $cmd);
passthru($cmd);
}
}

View file

@ -0,0 +1,242 @@
<?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\Plugins\CoreHome;
use Exception;
use Piwik\API\Request;
use Piwik\Common;
use Piwik\Date;
use Piwik\FrontController;
use Piwik\Menu\MenuMain;
use Piwik\Notification\Manager as NotificationManager;
use Piwik\Piwik;
use Piwik\Plugins\CoreHome\DataTableRowAction\MultiRowEvolution;
use Piwik\Plugins\CoreHome\DataTableRowAction\RowEvolution;
use Piwik\Plugins\CorePluginsAdmin\MarketplaceApiClient;
use Piwik\Plugins\Dashboard\DashboardManagerControl;
use Piwik\Plugins\UsersManager\API;
use Piwik\Site;
use Piwik\UpdateCheck;
use Piwik\Url;
use Piwik\View;
/**
*
*/
class Controller extends \Piwik\Plugin\Controller
{
function getDefaultAction()
{
return 'redirectToCoreHomeIndex';
}
function redirectToCoreHomeIndex()
{
$defaultReport = API::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), API::PREFERENCE_DEFAULT_REPORT);
$module = 'CoreHome';
$action = 'index';
// User preference: default report to load is the All Websites dashboard
if ($defaultReport == 'MultiSites'
&& \Piwik\Plugin\Manager::getInstance()->isPluginActivated('MultiSites')
) {
$module = 'MultiSites';
}
if ($defaultReport == Piwik::getLoginPluginName()) {
$module = Piwik::getLoginPluginName();
}
$idSite = Common::getRequestVar('idSite', false, 'int');
parent::redirectToIndex($module, $action, $idSite);
}
public function showInContext()
{
$controllerName = Common::getRequestVar('moduleToLoad');
$actionName = Common::getRequestVar('actionToLoad', 'index');
if ($actionName == 'showInContext') {
throw new Exception("Preventing infinite recursion...");
}
$view = $this->getDefaultIndexView();
$view->content = FrontController::getInstance()->fetchDispatch($controllerName, $actionName);
return $view->render();
}
public function markNotificationAsRead()
{
$notificationId = Common::getRequestVar('notificationId');
NotificationManager::cancel($notificationId);
}
protected function getDefaultIndexView()
{
$view = new View('@CoreHome/getDefaultIndexView');
$this->setGeneralVariablesView($view);
$view->menu = MenuMain::getInstance()->getMenu();
$view->dashboardSettingsControl = new DashboardManagerControl();
$view->content = '';
return $view;
}
protected function setDateTodayIfWebsiteCreatedToday()
{
$date = Common::getRequestVar('date', false);
if ($date == 'today'
|| Common::getRequestVar('period', false) == 'range'
) {
return;
}
$websiteId = Common::getRequestVar('idSite', false, 'int');
if ($websiteId) {
$website = new Site($websiteId);
$datetimeCreationDate = $website->getCreationDate()->getDatetime();
$creationDateLocalTimezone = Date::factory($datetimeCreationDate, $website->getTimezone())->toString('Y-m-d');
$todayLocalTimezone = Date::factory('now', $website->getTimezone())->toString('Y-m-d');
if ($creationDateLocalTimezone == $todayLocalTimezone) {
Piwik::redirectToModule('CoreHome', 'index',
array('date' => 'today',
'idSite' => $websiteId,
'period' => Common::getRequestVar('period'))
);
}
}
}
public function index()
{
$this->setDateTodayIfWebsiteCreatedToday();
$view = $this->getDefaultIndexView();
return $view->render();
}
// --------------------------------------------------------
// ROW EVOLUTION
// The following methods render the popover that shows the
// evolution of a singe or multiple rows in a data table
// --------------------------------------------------------
/** Render the entire row evolution popover for a single row */
public function getRowEvolutionPopover()
{
$rowEvolution = $this->makeRowEvolution($isMulti = false);
$view = new View('@CoreHome/getRowEvolutionPopover');
return $rowEvolution->renderPopover($this, $view);
}
/** Render the entire row evolution popover for multiple rows */
public function getMultiRowEvolutionPopover()
{
$rowEvolution = $this->makeRowEvolution($isMulti = true);
$view = new View('@CoreHome/getMultiRowEvolutionPopover');
return $rowEvolution->renderPopover($this, $view);
}
/** Generic method to get an evolution graph or a sparkline for the row evolution popover */
public function getRowEvolutionGraph($fetch = false, $rowEvolution = null)
{
if (empty($rowEvolution)) {
$label = Common::getRequestVar('label', '', 'string');
$isMultiRowEvolution = strpos($label, ',') !== false;
$rowEvolution = $this->makeRowEvolution($isMultiRowEvolution, $graphType = 'graphEvolution');
$rowEvolution->useAvailableMetrics();
}
$view = $rowEvolution->getRowEvolutionGraph();
return $this->renderView($view);
}
/** Utility function. Creates a RowEvolution instance. */
private function makeRowEvolution($isMultiRowEvolution, $graphType = null)
{
if ($isMultiRowEvolution) {
return new MultiRowEvolution($this->idSite, $this->date, $graphType);
} else {
return new RowEvolution($this->idSite, $this->date, $graphType);
}
}
/**
* Forces a check for updates and re-renders the header message.
*
* This will check piwik.org at most once per 10s.
*/
public function checkForUpdates()
{
Piwik::checkUserHasSomeAdminAccess();
$this->checkTokenInUrl();
// perform check (but only once every 10s)
UpdateCheck::check($force = false, UpdateCheck::UI_CLICK_CHECK_INTERVAL);
MarketplaceApiClient::clearAllCacheEntries();
$view = new View('@CoreHome/checkForUpdates');
$this->setGeneralVariablesView($view);
return $view->render();
}
/**
* Renders and echo's the in-app donate form w/ slider.
*/
public function getDonateForm()
{
$view = new View('@CoreHome/getDonateForm');
if (Common::getRequestVar('widget', false)
&& Piwik::hasUserSuperUserAccess()
) {
$view->footerMessage = Piwik::translate('CoreHome_OnlyForSuperUserAccess');
}
return $view->render();
}
/**
* Renders and echo's HTML that displays the Piwik promo video.
*/
public function getPromoVideo()
{
$view = new View('@CoreHome/getPromoVideo');
$view->shareText = Piwik::translate('CoreHome_SharePiwikShort');
$view->shareTextLong = Piwik::translate('CoreHome_SharePiwikLong');
$view->promoVideoUrl = 'http://www.youtube.com/watch?v=OslfF_EH81g';
return $view->render();
}
/**
* Redirects the user to a paypal so they can donate to Piwik.
*/
public function redirectToPaypal()
{
$parameters = Request::getRequestArrayFromString($request = null);
foreach ($parameters as $name => $param) {
if ($name == 'idSite'
|| $name == 'module'
|| $name == 'action'
) {
unset($parameters[$name]);
}
}
$url = "https://www.paypal.com/cgi-bin/webscr?" . Url::getQueryStringFromParameters($parameters);
header("Location: $url");
exit;
}
public function getSiteSelector()
{
return "<div piwik-siteselector class=\"sites_autocomplete\" switch-site-on-select=\"false\"></div>";
}
public function getPeriodSelector()
{
$view = new View("@CoreHome/_periodSelect");
$this->setGeneralVariablesView($view);
return $view->render();
}
}

View file

@ -0,0 +1,201 @@
<?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\Plugins\CoreHome;
use Piwik\WidgetsList;
/**
*
*/
class CoreHome extends \Piwik\Plugin
{
/**
* @see Piwik\Plugin::getListHooksRegistered
*/
public function getListHooksRegistered()
{
return array(
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'WidgetsList.addWidgets' => 'addWidgets',
'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys'
);
}
/**
* Adds the donate form widget.
*/
public function addWidgets()
{
WidgetsList::add('Example Widgets', 'CoreHome_SupportPiwik', 'CoreHome', 'getDonateForm');
WidgetsList::add('Example Widgets', 'Installation_Welcome', 'CoreHome', 'getPromoVideo');
}
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = "libs/jquery/themes/base/jquery-ui.css";
$stylesheets[] = "libs/jquery/stylesheets/jquery.jscrollpane.css";
$stylesheets[] = "libs/jquery/stylesheets/scroll.less";
$stylesheets[] = "plugins/Zeitgeist/stylesheets/base.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/coreHome.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/menu.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/dataTable.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/cloud.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/jquery.ui.autocomplete.css";
$stylesheets[] = "plugins/CoreHome/stylesheets/jqplotColors.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/sparklineColors.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/promo.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/color_manager.css";
$stylesheets[] = "plugins/CoreHome/stylesheets/sparklineColors.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/notification.less";
$stylesheets[] = "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline.less";
}
public function getJsFiles(&$jsFiles)
{
$jsFiles[] = "libs/jquery/jquery.js";
$jsFiles[] = "libs/jquery/jquery-ui.js";
$jsFiles[] = "libs/jquery/jquery.browser.js";
$jsFiles[] = "libs/jquery/jquery.truncate.js";
$jsFiles[] = "libs/jquery/jquery.scrollTo.js";
$jsFiles[] = "libs/jquery/jquery.history.js";
$jsFiles[] = "libs/jquery/jquery.jscrollpane.js";
$jsFiles[] = "libs/jquery/jquery.mousewheel.js";
$jsFiles[] = "libs/jquery/mwheelIntent.js";
$jsFiles[] = "libs/javascript/sprintf.js";
$jsFiles[] = "libs/angularjs/angular.min.js";
$jsFiles[] = "libs/angularjs/angular-sanitize.min.js";
$jsFiles[] = "libs/angularjs/angular-animate.min.js";
$jsFiles[] = "plugins/Zeitgeist/javascripts/piwikHelper.js";
$jsFiles[] = "plugins/Zeitgeist/javascripts/ajaxHelper.js";
$jsFiles[] = "plugins/CoreHome/javascripts/require.js";
$jsFiles[] = "plugins/CoreHome/javascripts/uiControl.js";
$jsFiles[] = "plugins/CoreHome/javascripts/dataTable.js";
$jsFiles[] = "plugins/CoreHome/javascripts/dataTable_rowactions.js";
$jsFiles[] = "plugins/CoreHome/javascripts/popover.js";
$jsFiles[] = "plugins/CoreHome/javascripts/broadcast.js";
$jsFiles[] = "plugins/CoreHome/javascripts/menu.js";
$jsFiles[] = "plugins/CoreHome/javascripts/menu_init.js";
$jsFiles[] = "plugins/CoreHome/javascripts/calendar.js";
$jsFiles[] = "plugins/CoreHome/javascripts/sparkline.js";
$jsFiles[] = "plugins/CoreHome/javascripts/corehome.js";
$jsFiles[] = "plugins/CoreHome/javascripts/top_controls.js";
$jsFiles[] = "plugins/CoreHome/javascripts/donate.js";
$jsFiles[] = "libs/jqplot/jqplot-custom.min.js";
$jsFiles[] = "plugins/CoreHome/javascripts/promo.js";
$jsFiles[] = "plugins/CoreHome/javascripts/color_manager.js";
$jsFiles[] = "plugins/CoreHome/javascripts/notification.js";
$jsFiles[] = "plugins/CoreHome/javascripts/notification_parser.js";
$jsFiles[] = "plugins/CoreHome/angularjs/piwikAppConfig.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/services/service.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/services/piwik.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/services/piwik-api.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/filters/filter.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/filters/translate.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/filters/startfrom.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/filters/evolution.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/autocomplete-matched.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/focus-anywhere-but-here.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/ignore-click.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/onenter.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/focusif.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/dialog.js";
$jsFiles[] = "plugins/CoreHome/angularjs/piwikApp.js";
$jsFiles[] = "plugins/CoreHome/angularjs/anchorLinkFix.js";
$jsFiles[] = "plugins/CoreHome/angularjs/siteselector/siteselector-model.js";
$jsFiles[] = "plugins/CoreHome/angularjs/siteselector/siteselector-controller.js";
$jsFiles[] = "plugins/CoreHome/angularjs/siteselector/siteselector-directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline-directive.js";
}
public function getClientSideTranslationKeys(&$translationKeys)
{
$translationKeys[] = 'General_InvalidDateRange';
$translationKeys[] = 'General_Loading';
$translationKeys[] = 'General_Show';
$translationKeys[] = 'General_Hide';
$translationKeys[] = 'General_YearShort';
$translationKeys[] = 'General_MultiSitesSummary';
$translationKeys[] = 'CoreHome_YouAreUsingTheLatestVersion';
$translationKeys[] = 'CoreHome_IncludeRowsWithLowPopulation';
$translationKeys[] = 'CoreHome_ExcludeRowsWithLowPopulation';
$translationKeys[] = 'CoreHome_DataTableIncludeAggregateRows';
$translationKeys[] = 'CoreHome_DataTableExcludeAggregateRows';
$translationKeys[] = 'CoreHome_Default';
$translationKeys[] = 'CoreHome_PageOf';
$translationKeys[] = 'CoreHome_FlattenDataTable';
$translationKeys[] = 'CoreHome_UnFlattenDataTable';
$translationKeys[] = 'CoreHome_ExternalHelp';
$translationKeys[] = 'SitesManager_NotFound';
$translationKeys[] = 'Annotations_ViewAndAddAnnotations';
$translationKeys[] = 'General_RowEvolutionRowActionTooltipTitle';
$translationKeys[] = 'General_RowEvolutionRowActionTooltip';
$translationKeys[] = 'Annotations_IconDesc';
$translationKeys[] = 'Annotations_IconDescHideNotes';
$translationKeys[] = 'Annotations_HideAnnotationsFor';
$translationKeys[] = 'General_LoadingPopover';
$translationKeys[] = 'General_LoadingPopoverFor';
$translationKeys[] = 'General_ShortMonth_1';
$translationKeys[] = 'General_ShortMonth_2';
$translationKeys[] = 'General_ShortMonth_3';
$translationKeys[] = 'General_ShortMonth_4';
$translationKeys[] = 'General_ShortMonth_5';
$translationKeys[] = 'General_ShortMonth_6';
$translationKeys[] = 'General_ShortMonth_7';
$translationKeys[] = 'General_ShortMonth_8';
$translationKeys[] = 'General_ShortMonth_9';
$translationKeys[] = 'General_ShortMonth_10';
$translationKeys[] = 'General_ShortMonth_11';
$translationKeys[] = 'General_ShortMonth_12';
$translationKeys[] = 'General_LongMonth_1';
$translationKeys[] = 'General_LongMonth_2';
$translationKeys[] = 'General_LongMonth_3';
$translationKeys[] = 'General_LongMonth_4';
$translationKeys[] = 'General_LongMonth_5';
$translationKeys[] = 'General_LongMonth_6';
$translationKeys[] = 'General_LongMonth_7';
$translationKeys[] = 'General_LongMonth_8';
$translationKeys[] = 'General_LongMonth_9';
$translationKeys[] = 'General_LongMonth_10';
$translationKeys[] = 'General_LongMonth_11';
$translationKeys[] = 'General_LongMonth_12';
$translationKeys[] = 'General_ShortDay_1';
$translationKeys[] = 'General_ShortDay_2';
$translationKeys[] = 'General_ShortDay_3';
$translationKeys[] = 'General_ShortDay_4';
$translationKeys[] = 'General_ShortDay_5';
$translationKeys[] = 'General_ShortDay_6';
$translationKeys[] = 'General_ShortDay_7';
$translationKeys[] = 'General_LongDay_1';
$translationKeys[] = 'General_LongDay_2';
$translationKeys[] = 'General_LongDay_3';
$translationKeys[] = 'General_LongDay_4';
$translationKeys[] = 'General_LongDay_5';
$translationKeys[] = 'General_LongDay_6';
$translationKeys[] = 'General_LongDay_7';
$translationKeys[] = 'General_DayMo';
$translationKeys[] = 'General_DayTu';
$translationKeys[] = 'General_DayWe';
$translationKeys[] = 'General_DayTh';
$translationKeys[] = 'General_DayFr';
$translationKeys[] = 'General_DaySa';
$translationKeys[] = 'General_DaySu';
$translationKeys[] = 'General_Search';
$translationKeys[] = 'General_MoreDetails';
$translationKeys[] = 'General_Help';
}
}

View file

@ -0,0 +1,71 @@
<?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\Plugins\CoreHome\DataTableRowAction;
use Piwik\Common;
use Piwik\Piwik;
/**
* MULTI ROW EVOLUTION
* The class handles the popover that shows the evolution of a multiple rows in a data table
*/
class MultiRowEvolution extends RowEvolution
{
/** The requested metric */
protected $metric;
/** Show all metrics in the evolution graph when the popover opens */
protected $initiallyShowAllMetrics = true;
/** The metrics available in the metrics select */
protected $metricsForSelect;
/**
* The constructor
* @param int $idSite
* @param \Piwik\Date $date ($this->date from controller)
*/
public function __construct($idSite, $date)
{
$this->metric = Common::getRequestVar('column', '', 'string');
parent::__construct($idSite, $date);
}
protected function loadEvolutionReport($column = false)
{
// set the "column" parameter for the API.getRowEvolution call
parent::loadEvolutionReport($this->metric);
}
protected function extractEvolutionReport($report)
{
$this->metric = $report['column'];
$this->dataTable = $report['reportData'];
$this->availableMetrics = $report['metadata']['metrics'];
$this->metricsForSelect = $report['metadata']['columns'];
$this->dimension = $report['metadata']['dimension'];
}
/**
* Render the popover
* @param \Piwik\Plugins\CoreHome\Controller $controller
* @param View (the popover_rowevolution template)
*/
public function renderPopover($controller, $view)
{
// add data for metric select box
$view->availableMetrics = $this->metricsForSelect;
$view->selectedMetric = $this->metric;
$view->availableRecordsText = $this->dimension . ': '
. Piwik::translate('RowEvolution_ComparingRecords', array(count($this->availableMetrics)));
return parent::renderPopover($controller, $view);
}
}

View file

@ -0,0 +1,342 @@
<?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\Plugins\CoreHome\DataTableRowAction;
use Exception;
use Piwik\API\Request;
use Piwik\API\ResponseBuilder;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Date;
use Piwik\Metrics;
use Piwik\Piwik;
use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution as EvolutionViz;
use Piwik\Url;
use Piwik\ViewDataTable\Factory;
/**
* ROW EVOLUTION
* The class handles the popover that shows the evolution of a singe row in a data table
*/
class RowEvolution
{
/** The current site id */
protected $idSite;
/** The api method to get the data. Format: Plugin.apiAction */
protected $apiMethod;
/** The label of the requested row */
protected $label;
/** The requested period */
protected $period;
/** The requested date */
protected $date;
/** The request segment */
protected $segment;
/** The metrics that are available for the requested report and period */
protected $availableMetrics;
/** The name of the dimension of the current report */
protected $dimension;
/**
* The data
* @var \Piwik\DataTable
*/
protected $dataTable;
/** The label of the current record */
protected $rowLabel;
/** The icon of the current record */
protected $rowIcon;
/** The type of graph that has been requested last */
protected $graphType;
/** The metrics for the graph that has been requested last */
protected $graphMetrics;
/** Whether or not to show all metrics in the evolution graph when to popover opens */
protected $initiallyShowAllMetrics = false;
/**
* The constructor
* Initialize some local variables from the request
* @param int $idSite
* @param Date $date ($this->date from controller)
* @param null|string $graphType
* @throws Exception
*/
public function __construct($idSite, $date, $graphType = null)
{
$this->apiMethod = Common::getRequestVar('apiMethod', '', 'string');
if (empty($this->apiMethod)) throw new Exception("Parameter apiMethod not set.");
$this->label = ResponseBuilder::getLabelFromRequest($_GET);
$this->label = $this->label[0];
if ($this->label === '') throw new Exception("Parameter label not set.");
$this->period = Common::getRequestVar('period', '', 'string');
if (empty($this->period)) throw new Exception("Parameter period not set.");
$this->idSite = $idSite;
$this->graphType = $graphType;
if ($this->period != 'range') {
// handle day, week, month and year: display last X periods
$end = $date->toString();
list($this->date, $lastN) = EvolutionViz::getDateRangeAndLastN($this->period, $end);
}
$this->segment = \Piwik\API\Request::getRawSegmentFromRequest();
$this->loadEvolutionReport();
}
/**
* Render the popover
* @param \Piwik\Plugins\CoreHome\Controller $controller
* @param View (the popover_rowevolution template)
*/
public function renderPopover($controller, $view)
{
// render main evolution graph
$this->graphType = 'graphEvolution';
$this->graphMetrics = $this->availableMetrics;
$view->graph = $controller->getRowEvolutionGraph($fetch = true, $rowEvolution = $this);
// render metrics overview
$view->metrics = $this->getMetricsToggles();
// available metrics text
$metricsText = Piwik::translate('RowEvolution_AvailableMetrics');
$popoverTitle = '';
if ($this->rowLabel) {
$icon = $this->rowIcon ? '<img src="' . $this->rowIcon . '" alt="">' : '';
$metricsText = sprintf(Piwik::translate('RowEvolution_MetricsFor'), $this->dimension . ': ' . $icon . ' ' . $this->rowLabel);
$popoverTitle = $icon . ' ' . $this->rowLabel;
}
$view->availableMetricsText = $metricsText;
$view->popoverTitle = $popoverTitle;
return $view->render();
}
protected function loadEvolutionReport($column = false)
{
list($apiModule, $apiAction) = explode('.', $this->apiMethod);
$parameters = array(
'method' => 'API.getRowEvolution',
'label' => $this->label,
'apiModule' => $apiModule,
'apiAction' => $apiAction,
'idSite' => $this->idSite,
'period' => $this->period,
'date' => $this->date,
'format' => 'original',
'serialize' => '0'
);
if (!empty($this->segment)) {
$parameters['segment'] = $this->segment;
}
if ($column !== false) {
$parameters['column'] = $column;
}
$url = Url::getQueryStringFromParameters($parameters);
$request = new Request($url);
$report = $request->process();
$this->extractEvolutionReport($report);
}
protected function extractEvolutionReport($report)
{
$this->dataTable = $report['reportData'];
$this->rowLabel = $this->extractPrettyLabel($report);
$this->rowIcon = !empty($report['logo']) ? $report['logo'] : false;
$this->availableMetrics = $report['metadata']['metrics'];
$this->dimension = $report['metadata']['dimension'];
}
/**
* Generic method to get an evolution graph or a sparkline for the row evolution popover.
* Do as much as possible from outside the controller.
* @param string|bool $graphType
* @param array|bool $metrics
* @return Factory
*/
public function getRowEvolutionGraph($graphType = false, $metrics = false)
{
// set up the view data table
$view = Factory::build($graphType ? : $this->graphType, $this->apiMethod,
$controllerAction = 'CoreHome.getRowEvolutionGraph', $forceDefault = true);
$view->setDataTable($this->dataTable);
if (!empty($this->graphMetrics)) { // In row Evolution popover, this is empty
$view->config->columns_to_display = array_keys($metrics ? : $this->graphMetrics);
}
$view->config->show_goals = false;
$view->config->show_all_views_icons = false;
$view->config->show_active_view_icon = false;
$view->config->show_related_reports = false;
$view->config->show_series_picker = false;
$view->config->show_footer_message = false;
foreach ($this->availableMetrics as $metric => $metadata) {
$view->config->translations[$metric] = $metadata['name'];
}
$view->config->external_series_toggle = 'RowEvolutionSeriesToggle';
$view->config->external_series_toggle_show_all = $this->initiallyShowAllMetrics;
return $view;
}
/**
* Prepare metrics toggles with spark lines
* @return array
*/
protected function getMetricsToggles()
{
$i = 0;
$metrics = array();
foreach ($this->availableMetrics as $metric => $metricData) {
$unit = Metrics::getUnit($metric, $this->idSite);
$change = isset($metricData['change']) ? $metricData['change'] : false;
list($first, $last) = $this->getFirstAndLastDataPointsForMetric($metric);
$details = Piwik::translate('RowEvolution_MetricBetweenText', array($first, $last));
if ($change !== false) {
$lowerIsBetter = Metrics::isLowerValueBetter($metric);
if (substr($change, 0, 1) == '+') {
$changeClass = $lowerIsBetter ? 'bad' : 'good';
$changeImage = $lowerIsBetter ? 'arrow_up_red' : 'arrow_up';
} else if (substr($change, 0, 1) == '-') {
$changeClass = $lowerIsBetter ? 'good' : 'bad';
$changeImage = $lowerIsBetter ? 'arrow_down_green' : 'arrow_down';
} else {
$changeClass = 'neutral';
$changeImage = false;
}
$change = '<span class="' . $changeClass . '">'
. ($changeImage ? '<img src="plugins/MultiSites/images/' . $changeImage . '.png" /> ' : '')
. $change . '</span>';
$details .= ', ' . Piwik::translate('RowEvolution_MetricChangeText', $change);
}
// set metric min/max text (used as tooltip for details)
$max = isset($metricData['max']) ? $metricData['max'] : 0;
$min = isset($metricData['min']) ? $metricData['min'] : 0;
$min .= $unit;
$max .= $unit;
$minmax = Piwik::translate('RowEvolution_MetricMinMax', array($metricData['name'], $min, $max));
$newMetric = array(
'label' => $metricData['name'],
'details' => $details,
'minmax' => $minmax,
'sparkline' => $this->getSparkline($metric),
);
// Multi Rows, each metric can be for a particular row and display an icon
if (!empty($metricData['logo'])) {
$newMetric['logo'] = $metricData['logo'];
}
$metrics[] = $newMetric;
$i++;
}
return $metrics;
}
/** Get the img tag for a sparkline showing a single metric */
protected function getSparkline($metric)
{
// sparkline is always echoed, so we need to buffer the output
$view = $this->getRowEvolutionGraph($graphType = 'sparkline', $metrics = array($metric => $metric));
ob_start();
$view->render();
$spark = ob_get_contents();
ob_end_clean();
// undo header change by sparkline renderer
header('Content-type: text/html');
// base64 encode the image and put it in an img tag
$spark = base64_encode($spark);
return '<img src="data:image/png;base64,' . $spark . '" />';
}
/** Use the available metrics for the metrics of the last requested graph. */
public function useAvailableMetrics()
{
$this->graphMetrics = $this->availableMetrics;
}
private function getFirstAndLastDataPointsForMetric($metric)
{
$first = 0;
$firstTable = $this->dataTable->getFirstRow();
if (!empty($firstTable)) {
$row = $firstTable->getFirstRow();
if (!empty($row)) {
$first = floatval($row->getColumn($metric));
}
}
$last = 0;
$lastTable = $this->dataTable->getLastRow();
if (!empty($lastTable)) {
$row = $lastTable->getFirstRow();
if (!empty($row)) {
$last = floatval($row->getColumn($metric));
}
}
return array($first, $last);
}
/**
* @param $report
* @return string
*/
protected function extractPrettyLabel($report)
{
// By default, use the specified label
$rowLabel = Common::sanitizeInputValue($report['label']);
$rowLabel = str_replace('/', '<wbr>/', str_replace('&', '<wbr>&', $rowLabel ));
// If the dataTable specifies a label_html, use this instead
/** @var $dataTableMap \Piwik\DataTable\Map */
$dataTableMap = $report['reportData'];
$labelPretty = $dataTableMap->getColumn('label_html');
$labelPretty = array_filter($labelPretty, 'strlen');
$labelPretty = current($labelPretty);
if(!empty($labelPretty)) {
return $labelPretty;
}
return $rowLabel;
}
}

View file

@ -0,0 +1,108 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* See http://dev.piwik.org/trac/ticket/4795 "linking to #hash tag does not work after merging AngularJS"
*/
(function () {
function scrollToAnchorNode($node)
{
$.scrollTo($node, 20);
}
function preventDefaultIfEventExists(event)
{
if (event) {
event.preventDefault();
}
}
function scrollToAnchorIfPossible(hash, event)
{
if (!hash) {
return;
}
if (-1 !== hash.indexOf('&')) {
return;
}
var $node = $('#' + hash);
if ($node && $node.length) {
scrollToAnchorNode($node);
preventDefaultIfEventExists(event);
return;
}
$node = $('a[name='+ hash + ']');
if ($node && $node.length) {
scrollToAnchorNode($node);
preventDefaultIfEventExists(event);
}
}
function isLinkWithinSamePage(location, newUrl)
{
if (location && location.origin && -1 === newUrl.indexOf(location.origin)) {
// link to different domain
return false;
}
if (location && location.pathname && -1 === newUrl.indexOf(location.pathname)) {
// link to different path
return false;
}
if (location && location.search && -1 === newUrl.indexOf(location.search)) {
// link with different search
return false;
}
return true;
}
function handleScrollToAnchorIfPresentOnPageLoad()
{
if (location.hash.substr(0, 2) == '#/') {
var hash = location.hash.substr(2);
scrollToAnchorIfPossible(hash, null);
}
}
function handleScrollToAnchorAfterPageLoad()
{
angular.module('piwikApp').run(['$rootScope', function ($rootScope) {
$rootScope.$on('$locationChangeStart', function (event, newUrl, oldUrl, $location) {
if (!newUrl) {
return;
}
var hashPos = newUrl.indexOf('#/');
if (-1 === hashPos) {
return;
}
if (!isLinkWithinSamePage(this.location, newUrl)) {
return;
}
var hash = newUrl.substr(hashPos + 2);
scrollToAnchorIfPossible(hash, event);
});
}]);
}
handleScrollToAnchorAfterPageLoad();
$(handleScrollToAnchorIfPresentOnPageLoad);
})();

View file

@ -0,0 +1,42 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* If the given text or resolved expression matches any text within the element, the matching text will be wrapped
* with a class.
*
* Example:
* <div piwik-autocomplete-matched="'text'">My text</div> ==> <div>My <span class="autocompleteMatched">text</span></div>
*
* <div piwik-autocomplete-matched="searchTerm">{{ name }}</div>
* <input type="text" ng-model="searchTerm">
*/
angular.module('piwikApp.directive').directive('piwikAutocompleteMatched', function() {
return function(scope, element, attrs) {
var searchTerm;
scope.$watch(attrs.piwikAutocompleteMatched, function(value) {
searchTerm = value;
updateText();
});
function updateText () {
if (!searchTerm || !element) {
return;
}
var content = element.html();
var startTerm = content.toLowerCase().indexOf(searchTerm.toLowerCase());
if (-1 !== startTerm) {
var word = content.substr(startTerm, searchTerm.length);
content = content.replace(word, '<span class="autocompleteMatched">' + word + '</span>');
element.html(content);
}
}
};
});

View file

@ -0,0 +1,43 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
describe('piwikAutocompleteMatchedDirective', function() {
var $compile;
var $rootScope;
beforeEach(module('piwikApp.directive'));
beforeEach(inject(function(_$compile_, _$rootScope_){
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
function assertRenderedContentIs(query, expectedResult) {
var template = '<div piwik-autocomplete-matched="\'' + query + '\'">My Content</div>';
var element = $compile(template)($rootScope);
$rootScope.$digest();
expect(element.html()).to.eql(expectedResult);
}
describe('#piwikAutocompleteMatched()', function() {
it('should not change anything if query does not match the text', function() {
assertRenderedContentIs('Whatever', 'My Content');
});
it('should wrap the matching part and find case insensitive', function() {
assertRenderedContentIs('y cont', 'M<span class="autocompleteMatched">y Cont</span>ent');
});
it('should be able to wrap the whole content', function() {
assertRenderedContentIs('my content', '<span class="autocompleteMatched">My Content</span>');
});
it('should find matching content case sensitive', function() {
assertRenderedContentIs('My Co', '<span class="autocompleteMatched">My Co</span>ntent');
});
});
});

View file

@ -0,0 +1,41 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Usage:
* <div piwik-dialog="showDialog">...</div>
* Will show dialog once showDialog evaluates to true.
*
* <div piwik-dialog="showDialog" yes="executeMyFunction();">
* ... <input type="button" role="yes" value="button">
* </div>
* Will execute the "executeMyFunction" function in the current scope once the yes button is pressed.
*/
angular.module('piwikApp.directive').directive('piwikDialog', function(piwik) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
element.css('display', 'none');
element.on( "dialogclose", function() {
scope.$eval(attrs.piwikDialog+'=false');
});
scope.$watch(attrs.piwikDialog, function(newValue, oldValue) {
if (newValue) {
piwik.helper.modalConfirm(element, {yes: function() {
if (attrs.yes) {
scope.$eval(attrs.yes);
}
}});
}
});
}
};
});

View file

@ -0,0 +1,8 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp.directive', []);

View file

@ -0,0 +1,40 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* The given expression will be executed when the user presses either escape or presses something outside
* of this element
*
* Example:
* <div piwik-focus-anywhere-but-here="closeDialog()">my dialog</div>
*/
angular.module('piwikApp.directive').directive('piwikFocusAnywhereButHere', function($document){
return {
restrict: 'A',
link: function(scope, element, attr, ctrl) {
function onClickOutsideElement (event) {
if (element.has(event.target).length === 0) {
scope.$apply(attr.piwikFocusAnywhereButHere);
}
}
function onEscapeHandler (event) {
if (event.which === 27) {
scope.$apply(attr.piwikFocusAnywhereButHere);
}
}
$document.on('keyup', onEscapeHandler);
$document.on('mouseup', onClickOutsideElement);
scope.$on('$destroy', function() {
$document.off('mouseup', onClickOutsideElement);
$document.off('keyup', onEscapeHandler);
});
}
};
});

View file

@ -0,0 +1,27 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* If the given expression evaluates to true the element will be focussed
*
* Example:
* <input type="text" piwik-focus-if="view.editName">
*/
angular.module('piwikApp.directive').directive('piwikFocusIf', function($timeout) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch(attrs.piwikFocusIf, function(newValue, oldValue) {
if (newValue) {
$timeout(function () {
element[0].focus();
}, 5);
}
});
}
};
});

View file

@ -0,0 +1,21 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Prevents the default behavior of the click. For instance useful if a link should only work in case the user
* does a "right click open in new window".
*
* Example
* <a piwik-ignore-click ng-click="doSomething()" href="/">my link</a>
*/
angular.module('piwikApp.directive').directive('piwikIgnoreClick', function() {
return function(scope, element, attrs) {
$(element).click(function(event) {
event.preventDefault();
});
};
});

View file

@ -0,0 +1,27 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Allows you to define any expression to be executed in case the user presses enter
*
* Example
* <div piwik-onenter="save()">
* <div piwik-onenter="showList=false">
*/
angular.module('piwikApp.directive').directive('piwikOnenter', function() {
return function(scope, element, attrs) {
element.bind("keydown keypress", function(event) {
if(event.which === 13) {
scope.$apply(function(){
scope.$eval(attrs.piwikOnenter, {'event': event});
});
event.preventDefault();
}
});
};
});

View file

@ -0,0 +1,44 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp.filter').filter('evolution', function() {
function calculateEvolution(currentValue, pastValue)
{
pastValue = parseInt(pastValue, 10);
currentValue = parseInt(currentValue, 10) - pastValue;
if (currentValue === 0 || isNaN(currentValue)) {
evolution = 0;
} else if (pastValue === 0 || isNaN(pastValue)) {
evolution = 100;
} else {
evolution = (currentValue / pastValue) * 100;
}
return evolution;
}
function formatEvolution(evolution)
{
evolution = Math.round(evolution);
if (evolution > 0) {
evolution = '+' + evolution;
}
evolution += '%';
return evolution;
}
return function(currentValue, pastValue) {
var evolution = calculateEvolution(currentValue, pastValue);
return formatEvolution(evolution);
};
});

View file

@ -0,0 +1,7 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp.filter', []);

View file

@ -0,0 +1,13 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp.filter').filter('startFrom', function() {
return function(input, start) {
start = +start; //parse to int
return input.slice(start);
};
});

View file

@ -0,0 +1,40 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
describe('startFromFilter', function() {
var startFrom;
beforeEach(module('piwikApp.filter'));
beforeEach(inject(function($injector) {
var $filter = $injector.get('$filter');
startFrom = $filter('startFrom');
}));
describe('#startFrom()', function() {
it('should return all entries if index is zero', function() {
var result = startFrom([1,2,3], 0);
expect(result).to.eql([1,2,3]);
});
it('should return only partial entries if filter is higher than zero', function() {
var result = startFrom([1,2,3], 2);
expect(result).to.eql([3]);
});
it('should return no entries if start is higher than input length', function() {
var result = startFrom([1,2,3], 11);
expect(result).to.eql([]);
});
});
});

View file

@ -0,0 +1,19 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp.filter').filter('translate', function() {
return function(key, value1, value2, value3) {
var values = [];
if (arguments && arguments.length > 1) {
for (var index = 1; index < arguments.length; index++) {
values.push(arguments[index]);
}
}
return _pk_translate(key, values);
};
});

View file

@ -0,0 +1,186 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp.service').factory('piwikApi', function ($http, $q, $rootScope, piwik, $window) {
var url = 'index.php';
var format = 'json';
var getParams = {};
var postParams = {};
var requestHandle = null;
var piwikApi = {};
/**
* Adds params to the request.
* If params are given more then once, the latest given value is used for the request
*
* @param {object} params
* @return {void}
*/
function addParams (params) {
if (typeof params == 'string') {
params = piwik.broadcast.getValuesFromUrl(params);
}
for (var key in params) {
getParams[key] = params[key];
}
}
function reset () {
getParams = {};
postParams = {};
}
/**
* Send the request
* @return $promise
*/
function send () {
var deferred = $q.defer();
var requestHandle = deferred;
var onError = function (message) {
deferred.reject(message);
requestHandle = null;
};
var onSuccess = function (response) {
if (response && response.result == 'error') {
if (response.message) {
onError(response.message);
var UI = require('piwik/UI');
var notification = new UI.Notification();
notification.show(response.message, {
context: 'error',
type: 'toast',
id: 'ajaxHelper'
});
notification.scrollToNotification();
} else {
onError(null);
}
} else {
deferred.resolve(response);
}
requestHandle = null;
};
var headers = {
'Content-Type': 'application/x-www-form-urlencoded',
// ie 8,9,10 caches ajax requests, prevent this
'cache-control': 'no-cache'
};
var ajaxCall = {
method: 'POST',
url: url,
responseType: format,
params: _mixinDefaultGetParams(getParams),
data: $.param(getPostParams(postParams)),
timeout: deferred.promise,
headers: headers
};
$http(ajaxCall).success(onSuccess).error(onError);
return deferred.promise;
}
/**
* Get the parameters to send as POST
*
* @param {object} params parameter object
* @return {object}
* @private
*/
function getPostParams () {
return {
token_auth: piwik.token_auth
};
}
/**
* Mixin the default parameters to send as GET
*
* @param {object} getParamsToMixin parameter object
* @return {object}
* @private
*/
function _mixinDefaultGetParams (getParamsToMixin) {
var defaultParams = {
idSite: piwik.idSite || piwik.broadcast.getValueFromUrl('idSite'),
period: piwik.period || piwik.broadcast.getValueFromUrl('period'),
segment: piwik.broadcast.getValueFromHash('segment', $window.location.href.split('#')[1])
};
// never append token_auth to url
if (getParamsToMixin.token_auth) {
getParamsToMixin.token_auth = null;
delete getParamsToMixin.token_auth;
}
for (var key in defaultParams) {
if (!getParamsToMixin[key] && !postParams[key] && defaultParams[key]) {
getParamsToMixin[key] = defaultParams[key];
}
}
// handle default date & period if not already set
if (!getParamsToMixin.date && !postParams.date) {
getParamsToMixin.date = piwik.currentDateString || piwik.broadcast.getValueFromUrl('date');
if (getParamsToMixin.period == 'range' && piwik.currentDateString) {
getParamsToMixin.date = piwik.startDateString + ',' + getParamsToMixin.date;
}
}
return getParamsToMixin;
}
piwikApi.abort = function () {
reset();
if (requestHandle) {
requestHandle.resolve();
requestHandle = null;
}
};
/**
* Perform a reading API request.
* @param getParams
*/
piwikApi.fetch = function (getParams) {
getParams.module = 'API';
getParams.format = 'JSON';
addParams(getParams, 'GET');
var promise = send();
reset();
return promise;
};
piwikApi.post = function (getParams, _postParams_) {
if (_postParams_) {
postParams = _postParams_;
}
return this.fetch(getParams);
};
return piwikApi;
});

View file

@ -0,0 +1,13 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp.service').service('piwik', function () {
piwik.helper = piwikHelper;
piwik.broadcast = broadcast;
return piwik;
});

View file

@ -0,0 +1,37 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
describe('piwikService', function() {
var piwikService;
beforeEach(module('piwikApp.service'));
beforeEach(inject(function($injector) {
piwikService = $injector.get('piwik');
}));
describe('#piwikService', function() {
it('should be the same as piwik global var', function() {
piwik.should.equal(piwikService);
});
it('should mixin broadcast', function() {
expect(piwikService.broadcast).to.be.an('object');
});
it('should mixin piwikHelper', function() {
expect(piwikService.helper).to.be.an('object');
});
});
describe('#piwik_url', function() {
it('should contain the piwik url', function() {
expect(piwikService.piwik_url).to.eql('http://localhost/');
});
});
});

View file

@ -0,0 +1,8 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp.service', []);

View file

@ -0,0 +1,66 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Usage:
*
* <h2 piwik-enriched-headline>All Websites Dashboard</h2>
* -> uses "All Websites Dashboard" as featurename
*
* <h2 piwik-enriched-headline feature-name="All Websites Dashboard">All Websites Dashboard (Total: 309 Visits)</h2>
* -> custom featurename
*
* <h2 piwik-enriched-headline help-url="http://piwik.org/guide">All Websites Dashboard</h2>
* -> shows help icon and links to external url
*
* <h2 piwik-enriched-headline>All Websites Dashboard
* <div class="inlineHelp>My <strong>inline help</strong></div>
* </h2>
* -> shows help icon to display inline help on click. Note: You can combine inlinehelp and help-url
*/
angular.module('piwikApp').directive('piwikEnrichedHeadline', function($document, piwik, $filter){
var defaults = {
helpUrl: ''
};
return {
transclude: true,
restrict: 'A',
scope: {
helpUrl: '@',
featureName: '@'
},
templateUrl: 'plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline.html?cb=' + piwik.cacheBuster,
compile: function (element, attrs) {
for (var index in defaults) {
if (!attrs[index]) { attrs[index] = defaults[index]; }
}
return function (scope, element, attrs) {
var helpNode = $('[ng-transclude] .inlineHelp', element);
if ((!helpNode || !helpNode.length) && element.next()) {
// hack for reports :(
helpNode = element.next().find('.reportDocumentation');
}
if (helpNode && helpNode.length) {
if ($.trim(helpNode.text())) {
scope.inlineHelp = $.trim(helpNode.html());
}
helpNode.remove();
}
if (!attrs.featureName) {
attrs.featureName = $.trim(element.text());
}
};
}
};
});

View file

@ -0,0 +1,29 @@
<div class="enrichedHeadline"
ng-mouseenter="view.showIcons=true" ng-mouseleave="view.showIcons=false">
<span ng-transclude></span>
<span ng-show="view.showIcons">
<a ng-if="helpUrl && !inlineHelp"
target="_blank"
href="{{ helpUrl }}"
title="{{ 'CoreHome_ExternalHelp'|translate }}"
class="helpIcon"></a>
<a ng-if="inlineHelp"
title="{{ 'General_Help'|translate }}"
ng-click="view.showInlineHelp=!view.showInlineHelp"
class="helpIcon"></a>
<div class="ratingIcons"
piwik-rate-feature
title="{{ featureName }}"></div>
</span>
<div class="inlineHelp" ng-show="view.showIcons && view.showInlineHelp">
<div ng-bind-html="inlineHelp"></div>
<a ng-if="helpUrl"
target="_blank"
href="{{ helpUrl }}"
class="readMore">{{ 'General_MoreDetails'|translate }}</a>
</div>
</div>

View file

@ -0,0 +1,44 @@
.inlineHelp {
display: none;
}
.enrichedHeadline {
min-height: 22px;
.inlineHelp {
display:block;
background: #F7F7F7;
font-size: 12px;
font-weight: normal;
border: 1px solid #E4E5E4;
margin: 10px 0 10px 0;
padding: 10px;
border-radius: 4px;
max-width: 500px;
.readMore {
margin-top: 10px;
display: inline-block;
font-weight: bold;
}
}
.ratingIcons {
display:inline-block;
vertical-align: bottom;
}
.helpIcon:hover {
opacity: 0.9;
}
.helpIcon {
cursor: pointer;
display:inline-block;
margin: 0px 0px -1px 4px;
width: 16px;
opacity: 0.3;
height: 16px;
background: url(plugins/CoreHome/angularjs/enrichedheadline/help.png) no-repeat;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

View file

@ -0,0 +1,16 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp', [
'ngSanitize',
'ngAnimate',
'piwikApp.config',
'piwikApp.service',
'piwikApp.directive',
'piwikApp.filter'
]);
angular.module('app', []);

View file

@ -0,0 +1,9 @@
angular.module('piwikApp.config', []);
(function () {
var piwikAppConfig = angular.module('piwikApp.config');
// we probably want this later as a separate config file, till then it serves as a "bridge"
for (var index in piwik.config) {
piwikAppConfig.constant(index.toUpperCase(), piwik.config[index]);
}
})();

View file

@ -0,0 +1,49 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp').controller('SiteSelectorController', function($scope, siteSelectorModel, piwik, AUTOCOMPLETE_MIN_SITES){
$scope.model = siteSelectorModel;
$scope.autocompleteMinSites = AUTOCOMPLETE_MIN_SITES;
$scope.selectedSite = {id: '', name: ''};
$scope.activeSiteId = piwik.idSite;
$scope.switchSite = function (site) {
$scope.selectedSite.id = site.idsite;
if (site.name === $scope.allSitesText) {
$scope.selectedSite.name = $scope.allSitesText;
} else {
$scope.selectedSite.name = site.name.replace(/[\u0000-\u2666]/g, function(c) {
return '&#'+c.charCodeAt(0)+';';
});
}
if (!$scope.switchSiteOnSelect || $scope.activeSiteId == site.idsite) {
return;
}
if (site.idsite == 'all') {
piwik.broadcast.propagateNewPage('module=MultiSites&action=index');
} else {
piwik.broadcast.propagateNewPage('segment=&idSite=' + site.idsite, false);
}
};
$scope.getUrlAllSites = function () {
var newParameters = 'module=MultiSites&action=index';
return piwik.helper.getCurrentQueryStringWithParametersModified(newParameters);
};
$scope.getUrlForSiteId = function (idSite) {
var idSiteParam = 'idSite=' + idSite;
var newParameters = 'segment=&' + idSiteParam;
var hash = piwik.broadcast.isHashExists() ? piwik.broadcast.getHashFromUrl() : "";
return piwik.helper.getCurrentQueryStringWithParametersModified(newParameters) +
'#' + piwik.helper.getQueryStringWithParametersModified(hash.substring(1), newParameters);
};
});

View file

@ -0,0 +1,80 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Usage:
* <div piwik-siteselector>
*
* More advanced example
* <div piwik-siteselector
* show-selected-site="true" show-all-sites-item="true" switch-site-on-select="true"
* all-sites-location="top|bottom" all-sites-text="test" show-selected-site="true"
* show-all-sites-item="true">
*
* Within a form
* <div piwik-siteselector input-name="siteId">
*
* Events:
* Triggers a `change` event on any change
* <div piwik-siteselector id="mySelector">
* $('#mySelector').on('change', function (event) { event.id/event.name })
*/
angular.module('piwikApp').directive('piwikSiteselector', function($document, piwik, $filter){
var defaults = {
name: '',
siteid: piwik.idSite,
sitename: piwik.siteName,
allSitesLocation: 'bottom',
allSitesText: $filter('translate')('General_MultiSitesSummary'),
showSelectedSite: 'false',
showAllSitesItem: 'true',
switchSiteOnSelect: 'true'
};
return {
restrict: 'A',
scope: {
showSelectedSite: '=',
showAllSitesItem: '=',
switchSiteOnSelect: '=',
inputName: '@name',
allSitesText: '@',
allSitesLocation: '@'
},
templateUrl: 'plugins/CoreHome/angularjs/siteselector/siteselector.html?cb=' + piwik.cacheBuster,
controller: 'SiteSelectorController',
compile: function (element, attrs) {
for (var index in defaults) {
if (!attrs[index]) { attrs[index] = defaults[index]; }
}
return function (scope, element, attrs) {
// selectedSite.id|.name + model is hard-coded but actually the directive should not know about this
scope.selectedSite.id = attrs.siteid;
scope.selectedSite.name = attrs.sitename;
if (!attrs.siteid || !attrs.sitename) {
scope.model.loadInitialSites();
}
scope.$watch('selectedSite.id', function (newValue, oldValue, scope) {
if (newValue != oldValue) {
element.attr('siteid', newValue);
element.trigger('change', scope.selectedSite);
}
});
/** use observe to monitor attribute changes
attrs.$observe('maxsitenamewidth', function(val) {
// for instance trigger a function or whatever
}) */
};
}
};
});

View file

@ -0,0 +1,75 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp').factory('siteSelectorModel', function (piwikApi, $filter) {
var model = {};
model.sites = [];
model.hasMultipleWebsites = false;
model.isLoading = false;
model.firstSiteName = '';
var initialSites = null;
model.updateWebsitesList = function (sites) {
if (!sites || !sites.length) {
model.sites = [];
return [];
}
angular.forEach(sites, function (site) {
if (site.group) site.name = '[' + site.group + '] ' + site.name;
});
model.sites = $filter('orderBy')(sites, '+name');
if (!model.firstSiteName) {
model.firstSiteName = model.sites[0].name;
}
model.hasMultipleWebsites = model.hasMultipleWebsites || sites.length > 1;
return model.sites;
};
model.searchSite = function (term) {
if (!term) {
model.loadInitialSites();
return;
}
if (model.isLoading) {
piwikApi.abort();
}
model.isLoading = true;
return piwikApi.fetch({
method: 'SitesManager.getPatternMatchSites',
pattern: term
}).then(function (response) {
return model.updateWebsitesList(response);
})['finally'](function () { // .finally() is not IE8 compatible see https://github.com/angular/angular.js/commit/f078762d48d0d5d9796dcdf2cb0241198677582c
model.isLoading = false;
});
};
model.loadInitialSites = function () {
if (initialSites) {
model.sites = initialSites;
return;
}
this.searchSite('%').then(function (websites) {
initialSites = websites;
});
};
return model;
});

View file

@ -0,0 +1,61 @@
<div piwik-focus-anywhere-but-here="view.showSitesList=false" class="custom_select"
ng-class="{'sites_autocomplete--dropdown': (model.hasMultipleWebsites || showAllSitesItem || !model.sites.length)}">
<script type="text/ng-template" id="siteselector_allsiteslink.html">
<div ng-click="switchSite({idsite: 'all', name: allSitesText});view.showSitesList=false;"
class="custom_select_all">
<a href="{{ getUrlAllSites() }}"
piwik-ignore-click
ng-bind-html="allSitesText"></a>
</div>
</script>
<input ng-if="inputName" type="hidden" name="{{ inputName }}" ng-value="selectedSite.id"/>
<a ng-click="view.showSitesList=!view.showSitesList; view.showSitesList && model.loadInitialSites()"
href="javascript:void(0)"
class="custom_select_main_link"
ng-class="{'loading': model.isLoading}">
<span ng-bind-html="selectedSite.name || model.firstSiteName">?</span>
</a>
<div ng-show="view.showSitesList" class="custom_select_block">
<div ng-if="allSitesLocation=='top' && showAllSitesItem"
ng-include="'siteselector_allsiteslink.html'"></div>
<div class="custom_select_container">
<ul class="custom_select_ul_list" ng-click="view.showSitesList=false;">
<li ng-click="switchSite(site)"
ng-repeat="site in model.sites"
ng-hide="!showSelectedSite && activeSiteId==site.idsite">
<a piwik-ignore-click href="{{ getUrlForSiteId(site.idsite) }}"
piwik-autocomplete-matched="view.searchTerm">{{ site.name }}</a>
</li>
</ul>
<ul ng-show="!model.sites.length && view.searchTerm" class="ui-autocomplete ui-front ui-menu ui-widget ui-widget-content ui-corner-all siteSelect">
<li class="ui-menu-item">
<a class="ui-corner-all" tabindex="-1">{{ ('SitesManager_NotFound' | translate) + ' ' + view.searchTerm }}</a>
</li>
</ul>
</div>
<div ng-if="allSitesLocation=='bottom' && showAllSitesItem"
ng-include="'siteselector_allsiteslink.html'"></div>
<div class="custom_select_search" ng-show="autocompleteMinSites <= model.sites.length || view.searchTerm">
<input type="text"
ng-click="view.searchTerm=''"
ng-model="view.searchTerm"
ng-change="model.searchSite(view.searchTerm)"
class="websiteSearch inp"/>
<input type="submit"
ng-click="model.searchSite(view.searchTerm)"
value="{{ 'General_Search' | translate }}" class="but"/>
<img title="Clear"
ng-show="view.searchTerm"
ng-click="view.searchTerm=''; model.loadInitialSites()"
class="reset"
src="plugins/CoreHome/images/reset_search.png"/>
</div>
</div>
</div>

View file

@ -0,0 +1,177 @@
/*sites_autocomplete*/
.sites_autocomplete {
position: absolute;
font-size: 12px;
display: inline-block;
height: 30px; /* Hack to not push the dashboard widget below */
}
.sites_selector_in_dashboard {
margin-top:10px;
}
.top_bar_sites_selector {
float: right
}
.top_bar_sites_selector > label {
display: inline-block;
padding: 7px 0 6px 0;
float: left;
font-size: 12px;
}
.top_bar_sites_selector > .sites_autocomplete {
position: static;
padding-left: 12px;
}
.autocompleteMatched {
color: #5256BE;
font-weight: bold;
}
.sites_autocomplete .custom_select {
float: left;
position: relative;
z-index: 19;
background: #fff url(plugins/Zeitgeist/images/sites_selection.png) repeat-x 0 0;
border: 1px solid #d4d4d4;
color: #255792;
border-radius: 4px;
cursor: pointer;
min-width: 165px;
padding: 5px 6px 4px;
}
.sites_autocomplete .custom_select_main_link {
display: block;
text-decoration: none;
background: none;
cursor: default;
height:1.4em;
}
.sites_autocomplete .custom_select_ul_list li a,
.sites_autocomplete .custom_select_all a,
.sites_autocomplete .custom_select_main_link > span {
display: inline-block;
max-width: 140px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 20px 0 4px;
}
.sites_autocomplete--dropdown .custom_select_main_link:not(.loading):before {
content: " \25BC";
position: absolute;
right: 0;
font-size: 0.8em;
margin-top: 0.2em;
color: #444;
}
.sites_autocomplete--dropdown .custom_select_main_link {
cursor: pointer;
position: relative;
}
.sites_autocomplete .custom_select_main_link.loading {
background: url(plugins/Zeitgeist/images/loading-blue.gif) no-repeat right 3px;
}
.sites_autocomplete .custom_select_ul_list,
.sites_autocomplete ul.ui-autocomplete {
position: relative;
list-style: none;
line-height: 18px;
padding: 0 0 15px 0;
}
.sites_autocomplete .custom_select_ul_list li a,
.sites_autocomplete .custom_select_all a {
line-height: 18px;
height: auto;
display: block;
text-decoration: none;
}
.sites_autocomplete .custom_select_ul_list li a:hover,
.sites_autocomplete .custom_select_all a:hover {
background: #ebeae6;
}
.sites_autocomplete .custom_select_all a {
text-decoration: none;
margin: 0 0 5px 0;
}
.sites_autocomplete .custom_select_search {
margin: 0 0 0 4px;
height: 26px;
display: block;
white-space: nowrap;
background: url(plugins/Zeitgeist/images/search_bg.png) no-repeat 0 0;
}
.sites_autocomplete .custom_select_search .inp {
vertical-align: top;
width: 114px;
padding: 2px 6px;
border: 0;
background: transparent;
font-size: 10px;
color: #454545;
}
.sites_autocomplete {
width: 165px;
}
.sites_autocomplete .custom_select_search .but {
vertical-align: top;
font-size: 10px;
border: 0;
background: transparent;
width: 21px;
height: 17px;
overflow: hidden;
opacity: 0;
cursor: pointer;
}
.sites_selector_container>.sites_autocomplete {
padding-left: 12px;
}
.custom_selector_container .ui-menu-item,
.custom_selector_container .ui-menu-item a {
float:none;position:static
}
.custom_select_search .reset {
position: relative; top: 4px; left: -44px; cursor: pointer;
}
.custom_select_block {
overflow: hidden;
max-width: inherit;
visibility: visible;
}
.custom_select_block_show {
height: auto;
overflow: visible;
max-width:inherit;
}
.sites_selector_container {
padding-top: 5px;
}
.siteSelect a {
white-space: normal;
text-align: left;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Some files were not shown because too many files have changed in this diff Show more