add icons for Character groups
This commit is contained in:
commit
2d9a41a5fe
3461 changed files with 594457 additions and 0 deletions
33
www/analytics/plugins/.htaccess
Normal file
33
www/analytics/plugins/.htaccess
Normal 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>
|
||||
723
www/analytics/plugins/API/API.php
Normal file
723
www/analytics/plugins/API/API.php
Normal 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";
|
||||
}
|
||||
}
|
||||
128
www/analytics/plugins/API/Controller.php
Normal file
128
www/analytics/plugins/API/Controller.php
Normal 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>
|
||||
";
|
||||
}
|
||||
}
|
||||
718
www/analytics/plugins/API/ProcessedReport.php
Normal file
718
www/analytics/plugins/API/ProcessedReport.php
Normal 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;
|
||||
}
|
||||
}
|
||||
529
www/analytics/plugins/API/RowEvolution.php
Normal file
529
www/analytics/plugins/API/RowEvolution.php
Normal 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;
|
||||
}
|
||||
}
|
||||
48
www/analytics/plugins/API/stylesheets/listAllAPI.less
Normal file
48
www/analytics/plugins/API/stylesheets/listAllAPI.less
Normal 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;
|
||||
}
|
||||
32
www/analytics/plugins/API/templates/listAllAPI.twig
Normal file
32
www/analytics/plugins/API/templates/listAllAPI.twig
Normal 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'>&token_auth=<strong>{{ token_auth }}</strong></span><br/>
|
||||
{{ 'API_KeepTokenSecret'|translate('<b>','</b>')|raw }}
|
||||
{{ list_api_methods_with_links|raw }}
|
||||
<br/>
|
||||
</div>
|
||||
{% endblock %}
|
||||
596
www/analytics/plugins/Actions/API.php
Normal file
596
www/analytics/plugins/Actions/API.php
Normal 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);
|
||||
}
|
||||
}
|
||||
937
www/analytics/plugins/Actions/Actions.php
Normal file
937
www/analytics/plugins/Actions/Actions.php
Normal 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("% ", "% ", 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);
|
||||
}
|
||||
}
|
||||
|
||||
548
www/analytics/plugins/Actions/Archiver.php
Normal file
548
www/analytics/plugins/Actions/Archiver.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
612
www/analytics/plugins/Actions/ArchivingHelper.php
Normal file
612
www/analytics/plugins/Actions/ArchivingHelper.php
Normal 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;
|
||||
}
|
||||
}
|
||||
151
www/analytics/plugins/Actions/Controller.php
Normal file
151
www/analytics/plugins/Actions/Controller.php
Normal 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__);
|
||||
}
|
||||
}
|
||||
328
www/analytics/plugins/Actions/javascripts/actionsDataTable.js
Normal file
328
www/analytics/plugins/Actions/javascripts/actionsDataTable.js
Normal 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);
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.dataTableActions > .dataTableWrapper {
|
||||
width: 500px;
|
||||
min-height: 1px;
|
||||
}
|
||||
17
www/analytics/plugins/Actions/templates/indexSiteSearch.twig
Normal file
17
www/analytics/plugins/Actions/templates/indexSiteSearch.twig
Normal 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>
|
||||
371
www/analytics/plugins/Annotations/API.php
Executable file
371
www/analytics/plugins/Annotations/API.php
Executable 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);
|
||||
}
|
||||
}
|
||||
455
www/analytics/plugins/Annotations/AnnotationList.php
Executable file
455
www/analytics/plugins/Annotations/AnnotationList.php
Executable 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;
|
||||
}
|
||||
}
|
||||
44
www/analytics/plugins/Annotations/Annotations.php
Executable file
44
www/analytics/plugins/Annotations/Annotations.php
Executable 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";
|
||||
}
|
||||
}
|
||||
215
www/analytics/plugins/Annotations/Controller.php
Executable file
215
www/analytics/plugins/Annotations/Controller.php
Executable 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();
|
||||
}
|
||||
}
|
||||
599
www/analytics/plugins/Annotations/javascripts/annotations.js
Executable file
599
www/analytics/plugins/Annotations/javascripts/annotations.js
Executable 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));
|
||||
209
www/analytics/plugins/Annotations/stylesheets/annotations.less
Executable file
209
www/analytics/plugins/Annotations/stylesheets/annotations.less
Executable 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;
|
||||
}
|
||||
45
www/analytics/plugins/Annotations/templates/_annotation.twig
Executable file
45
www/analytics/plugins/Annotations/templates/_annotation.twig
Executable 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>
|
||||
|
||||
29
www/analytics/plugins/Annotations/templates/_annotationList.twig
Executable file
29
www/analytics/plugins/Annotations/templates/_annotationList.twig
Executable 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"> </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>
|
||||
27
www/analytics/plugins/Annotations/templates/getAnnotationManager.twig
Executable file
27
www/analytics/plugins/Annotations/templates/getAnnotationManager.twig
Executable 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 %} — {{ 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>
|
||||
14
www/analytics/plugins/Annotations/templates/getEvolutionIcons.twig
Executable file
14
www/analytics/plugins/Annotations/templates/getEvolutionIcons.twig
Executable 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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
{% include "@Annotations/_annotation.twig" %}
|
||||
209
www/analytics/plugins/CoreAdminHome/API.php
Normal file
209
www/analytics/plugins/CoreAdminHome/API.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
351
www/analytics/plugins/CoreAdminHome/Controller.php
Normal file
351
www/analytics/plugins/CoreAdminHome/Controller.php
Normal 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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
129
www/analytics/plugins/CoreAdminHome/CoreAdminHome.php
Normal file
129
www/analytics/plugins/CoreAdminHome/CoreAdminHome.php
Normal 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);
|
||||
}
|
||||
}
|
||||
203
www/analytics/plugins/CoreAdminHome/CustomLogo.php
Normal file
203
www/analytics/plugins/CoreAdminHome/CustomLogo.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
|
@ -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> </td>\
|
||||
<td><input type="textbox" class="custom-variable-name"/></td>\
|
||||
<td> </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));
|
||||
|
|
@ -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');
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
78
www/analytics/plugins/CoreAdminHome/stylesheets/menu.less
Normal file
78
www/analytics/plugins/CoreAdminHome/stylesheets/menu.less
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
23
www/analytics/plugins/CoreAdminHome/templates/_menu.twig
Normal file
23
www/analytics/plugins/CoreAdminHome/templates/_menu.twig
Normal 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 %}
|
||||
|
|
@ -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 %}
|
||||
31
www/analytics/plugins/CoreAdminHome/templates/optOut.twig
Normal file
31
www/analytics/plugins/CoreAdminHome/templates/optOut.twig
Normal 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&action=optOut{% if language %}&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>
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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("</body>")|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><noscript></noscript></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 %}
|
||||
90
www/analytics/plugins/CoreConsole/Commands/CodeCoverage.php
Normal file
90
www/analytics/plugins/CoreConsole/Commands/CodeCoverage.php
Normal 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));
|
||||
}
|
||||
|
||||
}
|
||||
55
www/analytics/plugins/CoreConsole/Commands/CoreArchiver.php
Normal file
55
www/analytics/plugins/CoreConsole/Commands/CoreArchiver.php
Normal 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");
|
||||
}
|
||||
}
|
||||
58
www/analytics/plugins/CoreConsole/Commands/GenerateApi.php
Normal file
58
www/analytics/plugins/CoreConsole/Commands/GenerateApi.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
223
www/analytics/plugins/CoreConsole/Commands/GeneratePlugin.php
Normal file
223
www/analytics/plugins/CoreConsole/Commands/GeneratePlugin.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
189
www/analytics/plugins/CoreConsole/Commands/GenerateTest.php
Normal file
189
www/analytics/plugins/CoreConsole/Commands/GenerateTest.php
Normal 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'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
142
www/analytics/plugins/CoreConsole/Commands/GitCommit.php
Normal file
142
www/analytics/plugins/CoreConsole/Commands/GitCommit.php
Normal 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;
|
||||
}
|
||||
}
|
||||
55
www/analytics/plugins/CoreConsole/Commands/GitPull.php
Normal file
55
www/analytics/plugins/CoreConsole/Commands/GitPull.php
Normal 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);
|
||||
}
|
||||
}
|
||||
43
www/analytics/plugins/CoreConsole/Commands/GitPush.php
Normal file
43
www/analytics/plugins/CoreConsole/Commands/GitPush.php
Normal 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);
|
||||
}
|
||||
}
|
||||
68
www/analytics/plugins/CoreConsole/Commands/ManagePlugin.php
Normal file
68
www/analytics/plugins/CoreConsole/Commands/ManagePlugin.php
Normal 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>");
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
87
www/analytics/plugins/CoreConsole/Commands/RunTests.php
Normal file
87
www/analytics/plugins/CoreConsole/Commands/RunTests.php
Normal 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');
|
||||
}
|
||||
|
||||
}
|
||||
70
www/analytics/plugins/CoreConsole/Commands/RunUITests.php
Normal file
70
www/analytics/plugins/CoreConsole/Commands/RunUITests.php
Normal 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);
|
||||
}
|
||||
}
|
||||
156
www/analytics/plugins/CoreConsole/Commands/SetupFixture.php
Normal file
156
www/analytics/plugins/CoreConsole/Commands/SetupFixture.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
www/analytics/plugins/CoreConsole/Commands/WatchLog.php
Normal file
33
www/analytics/plugins/CoreConsole/Commands/WatchLog.php
Normal 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);
|
||||
}
|
||||
}
|
||||
242
www/analytics/plugins/CoreHome/Controller.php
Normal file
242
www/analytics/plugins/CoreHome/Controller.php
Normal 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();
|
||||
}
|
||||
}
|
||||
201
www/analytics/plugins/CoreHome/CoreHome.php
Normal file
201
www/analytics/plugins/CoreHome/CoreHome.php
Normal 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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
108
www/analytics/plugins/CoreHome/angularjs/anchorLinkFix.js
vendored
Normal file
108
www/analytics/plugins/CoreHome/angularjs/anchorLinkFix.js
vendored
Normal 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);
|
||||
|
||||
})();
|
||||
42
www/analytics/plugins/CoreHome/angularjs/common/directives/autocomplete-matched.js
vendored
Normal file
42
www/analytics/plugins/CoreHome/angularjs/common/directives/autocomplete-matched.js
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
43
www/analytics/plugins/CoreHome/angularjs/common/directives/autocomplete-matched_spec.js
vendored
Normal file
43
www/analytics/plugins/CoreHome/angularjs/common/directives/autocomplete-matched_spec.js
vendored
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
41
www/analytics/plugins/CoreHome/angularjs/common/directives/dialog.js
vendored
Normal file
41
www/analytics/plugins/CoreHome/angularjs/common/directives/dialog.js
vendored
Normal 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);
|
||||
}
|
||||
}});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
8
www/analytics/plugins/CoreHome/angularjs/common/directives/directive.js
vendored
Normal file
8
www/analytics/plugins/CoreHome/angularjs/common/directives/directive.js
vendored
Normal 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', []);
|
||||
40
www/analytics/plugins/CoreHome/angularjs/common/directives/focus-anywhere-but-here.js
vendored
Normal file
40
www/analytics/plugins/CoreHome/angularjs/common/directives/focus-anywhere-but-here.js
vendored
Normal 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);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
27
www/analytics/plugins/CoreHome/angularjs/common/directives/focusif.js
vendored
Normal file
27
www/analytics/plugins/CoreHome/angularjs/common/directives/focusif.js
vendored
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
21
www/analytics/plugins/CoreHome/angularjs/common/directives/ignore-click.js
vendored
Normal file
21
www/analytics/plugins/CoreHome/angularjs/common/directives/ignore-click.js
vendored
Normal 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();
|
||||
});
|
||||
};
|
||||
});
|
||||
27
www/analytics/plugins/CoreHome/angularjs/common/directives/onenter.js
vendored
Normal file
27
www/analytics/plugins/CoreHome/angularjs/common/directives/onenter.js
vendored
Normal 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();
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
44
www/analytics/plugins/CoreHome/angularjs/common/filters/evolution.js
vendored
Normal file
44
www/analytics/plugins/CoreHome/angularjs/common/filters/evolution.js
vendored
Normal 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);
|
||||
};
|
||||
});
|
||||
7
www/analytics/plugins/CoreHome/angularjs/common/filters/filter.js
vendored
Normal file
7
www/analytics/plugins/CoreHome/angularjs/common/filters/filter.js
vendored
Normal 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', []);
|
||||
13
www/analytics/plugins/CoreHome/angularjs/common/filters/startfrom.js
vendored
Normal file
13
www/analytics/plugins/CoreHome/angularjs/common/filters/startfrom.js
vendored
Normal 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);
|
||||
};
|
||||
});
|
||||
40
www/analytics/plugins/CoreHome/angularjs/common/filters/startfrom_spec.js
vendored
Normal file
40
www/analytics/plugins/CoreHome/angularjs/common/filters/startfrom_spec.js
vendored
Normal 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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
19
www/analytics/plugins/CoreHome/angularjs/common/filters/translate.js
vendored
Normal file
19
www/analytics/plugins/CoreHome/angularjs/common/filters/translate.js
vendored
Normal 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);
|
||||
};
|
||||
});
|
||||
186
www/analytics/plugins/CoreHome/angularjs/common/services/piwik-api.js
vendored
Normal file
186
www/analytics/plugins/CoreHome/angularjs/common/services/piwik-api.js
vendored
Normal 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;
|
||||
});
|
||||
13
www/analytics/plugins/CoreHome/angularjs/common/services/piwik.js
vendored
Normal file
13
www/analytics/plugins/CoreHome/angularjs/common/services/piwik.js
vendored
Normal 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;
|
||||
});
|
||||
37
www/analytics/plugins/CoreHome/angularjs/common/services/piwik_spec.js
vendored
Normal file
37
www/analytics/plugins/CoreHome/angularjs/common/services/piwik_spec.js
vendored
Normal 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/');
|
||||
});
|
||||
});
|
||||
});
|
||||
8
www/analytics/plugins/CoreHome/angularjs/common/services/service.js
vendored
Normal file
8
www/analytics/plugins/CoreHome/angularjs/common/services/service.js
vendored
Normal 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', []);
|
||||
66
www/analytics/plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline-directive.js
vendored
Normal file
66
www/analytics/plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline-directive.js
vendored
Normal 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());
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 |
16
www/analytics/plugins/CoreHome/angularjs/piwikApp.js
vendored
Normal file
16
www/analytics/plugins/CoreHome/angularjs/piwikApp.js
vendored
Normal 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', []);
|
||||
9
www/analytics/plugins/CoreHome/angularjs/piwikAppConfig.js
vendored
Normal file
9
www/analytics/plugins/CoreHome/angularjs/piwikAppConfig.js
vendored
Normal 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]);
|
||||
}
|
||||
})();
|
||||
49
www/analytics/plugins/CoreHome/angularjs/siteselector/siteselector-controller.js
vendored
Normal file
49
www/analytics/plugins/CoreHome/angularjs/siteselector/siteselector-controller.js
vendored
Normal 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);
|
||||
};
|
||||
});
|
||||
80
www/analytics/plugins/CoreHome/angularjs/siteselector/siteselector-directive.js
vendored
Normal file
80
www/analytics/plugins/CoreHome/angularjs/siteselector/siteselector-directive.js
vendored
Normal 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
|
||||
}) */
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
75
www/analytics/plugins/CoreHome/angularjs/siteselector/siteselector-model.js
vendored
Normal file
75
www/analytics/plugins/CoreHome/angularjs/siteselector/siteselector-model.js
vendored
Normal 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;
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
BIN
www/analytics/plugins/CoreHome/images/bg_header.jpg
Normal file
BIN
www/analytics/plugins/CoreHome/images/bg_header.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
BIN
www/analytics/plugins/CoreHome/images/bullet1.gif
Normal file
BIN
www/analytics/plugins/CoreHome/images/bullet1.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 B |
BIN
www/analytics/plugins/CoreHome/images/bullet2.gif
Normal file
BIN
www/analytics/plugins/CoreHome/images/bullet2.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 B |
BIN
www/analytics/plugins/CoreHome/images/favicon.ico
Normal file
BIN
www/analytics/plugins/CoreHome/images/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
www/analytics/plugins/CoreHome/images/googleplay.png
Normal file
BIN
www/analytics/plugins/CoreHome/images/googleplay.png
Normal file
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
Loading…
Add table
Add a link
Reference in a new issue