PDF rausgenommen

This commit is contained in:
aschwarz
2023-01-23 11:03:31 +01:00
parent 82d562a322
commit a6523903eb
28078 changed files with 4247552 additions and 2 deletions

View File

@ -0,0 +1,804 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @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\Cache;
use Piwik\CacheId;
use Piwik\Category\CategoryList;
use Piwik\Common;
use Piwik\Config;
use Piwik\Container\StaticContainer;
use Piwik\DataTable;
use Piwik\DataTable\Filter\ColumnDelete;
use Piwik\DataTable\Row;
use Piwik\Date;
use Piwik\IP;
use Piwik\Metrics;
use Piwik\Period;
use Piwik\Period\Range;
use Piwik\Piwik;
use Piwik\Plugin\SettingsProvider;
use Piwik\Plugins\API\DataTable\MergeDataTables;
use Piwik\Plugins\CoreAdminHome\CustomLogo;
use Piwik\Plugins\CorePluginsAdmin\SettingsMetadata;
use Piwik\Site;
use Piwik\Translation\Translator;
use Piwik\Measurable\Type\TypeManager;
use Piwik\Version;
use Piwik\Widget\WidgetsList;
require_once PIWIK_INCLUDE_PATH . '/core/Config.php';
/**
* This API is the <a href='http://matomo.org/docs/analytics-api/metadata/' rel='noreferrer' 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 Matomo Mobile App to automatically display all Matomo reports, with translated report & columns names and nicely formatted values.
* More information on the <a href='http://matomo.org/docs/analytics-api/metadata/' rel='noreferrer' target='_blank'>Metadata API documentation page</a>
*
* @method static \Piwik\Plugins\API\API getInstance()
*/
class API extends \Piwik\Plugin\API
{
/**
* @var SettingsProvider
*/
private $settingsProvider;
/**
* @var ProcessedReport
*/
private $processedReport;
/**
* For Testing purpose only
* @var int
*/
public static $_autoSuggestLookBack = 60;
public function __construct(SettingsProvider $settingsProvider, ProcessedReport $processedReport)
{
$this->settingsProvider = $settingsProvider;
$this->processedReport = $processedReport;
}
/**
* Get Matomo version
* @return string
*/
public function getMatomoVersion()
{
Piwik::checkUserHasSomeViewAccess();
return Version::VERSION;
}
/**
* Get PHP version
* @return array
*/
public function getPhpVersion()
{
Piwik::checkUserHasSuperUserAccess();
return array(
'version' => PHP_VERSION,
'major' => PHP_MAJOR_VERSION,
'minor' => PHP_MINOR_VERSION,
'release' => PHP_RELEASE_VERSION,
'versionId' => PHP_VERSION_ID,
'extra' => PHP_EXTRA_VERSION,
);
}
/**
* Get Matomo version
* @return string
* @deprecated
*/
public function getPiwikVersion()
{
return $this->getMatomoVersion();
}
/**
* Returns the most accurate IP address available for the current user, in
* IPv4 format. This could be the proxy client's IP address.
*
* @return string IP address in presentation format.
*/
public function getIpFromHeader()
{
Piwik::checkUserHasSomeViewAccess();
return IP::getIpFromHeader();
}
/**
* 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
* @deprecated since Matomo 2.15.1
*/
public function getDefaultMetricTranslations()
{
return Metrics::getDefaultMetricTranslations();
}
/**
* Returns all available measurable types.
* Marked as deprecated so it won't appear in API page. It won't be a public API for now.
* @deprecated
* @return array
*/
public function getAvailableMeasurableTypes()
{
Piwik::checkUserHasSomeViewAccess();
$typeManager = new TypeManager();
$types = $typeManager->getAllTypes();
$available = array();
foreach ($types as $type) {
$measurableSettings = $this->settingsProvider->getAllMeasurableSettings($idSite = 0, $type->getId());
$settingsMetadata = new SettingsMetadata();
$available[] = array(
'id' => $type->getId(),
'name' => Piwik::translate($type->getName()),
'description' => Piwik::translate($type->getDescription()),
'howToSetupUrl' => $type->getHowToSetupUrl(),
'settings' => $settingsMetadata->formatSettings($measurableSettings)
);
}
return $available;
}
public function getSegmentsMetadata($idSites = array(), $_hideImplementationData = true)
{
if (empty($idSites)) {
Piwik::checkUserHasSomeViewAccess();
} else {
$idSites = Site::getIdSitesFromIdSitesString($idSites);
Piwik::checkUserHasViewAccess($idSites);
}
$isNotAnonymous = !Piwik::isUserIsAnonymous();
$sites = (is_array($idSites) ? implode('.', $idSites) : (int) $idSites);
$cache = Cache::getTransientCache();
$cacheKey = 'API.getSegmentsMetadata' . $sites . '_' . (int) $_hideImplementationData . '_' . (int) $isNotAnonymous;
$cacheKey = CacheId::pluginAware($cacheKey);
if ($cache->contains($cacheKey)) {
return $cache->fetch($cacheKey);
}
$metadata = new SegmentMetadata();
$segments = $metadata->getSegmentsMetadata($idSites, $_hideImplementationData, $isNotAnonymous);
$cache->save($cacheKey, $segments);
return $segments;
}
/**
* @param $segmentName
* @param $table
* @return array
*/
protected function getSegmentValuesFromVisitorLog($segmentName, $table)
{
// 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);
return $values;
}
/**
* Returns the url to application logo (~280x110px)
*
* @param bool $pathOnly If true, returns path relative to doc root. Otherwise, returns a URL.
* @return string
* @deprecated since Matomo 2.15.1
*/
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
* @deprecated since Matomo 2.15.1
*/
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)
{
Piwik::checkUserHasViewAccess($idSite);
if ($language) {
/** @var Translator $translator */
$translator = StaticContainer::get('Piwik\Translation\Translator');
$translator->setCurrentLanguage($language);
}
$metadata = $this->processedReport->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 THIS PARAMETER IS DEPRECATED AND WILL BE REMOVED IN PIWIK 4
* @param bool|string $period
* @param bool|Date $date
* @param bool $hideMetricsDoc
* @param bool $showSubtableReports
* @param int $idSite
* @return array
*/
public function getReportMetadata($idSites = '', $period = false, $date = false, $hideMetricsDoc = false,
$showSubtableReports = false, $idSite = false)
{
if (empty($idSite) && !empty($idSites)) {
if (is_array($idSites)) {
$idSite = array_shift($idSites);
} else {
$idSite = $idSites;
}
} elseif (empty($idSite) && empty($idSites)) {
throw new \Exception('Calling API.getReportMetadata without any idSite is no longer supported since Matomo 3.0.0. Please specifiy at least one idSite via the "idSite" parameter.');
}
Piwik::checkUserHasViewAccess($idSite);
$metadata = $this->processedReport->getReportMetadata($idSite, $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,
$format_metrics = null, $idDimension = false)
{
Piwik::checkUserHasViewAccess($idSite);
$processed = $this->processedReport->getProcessedReport($idSite, $period, $date, $apiModule, $apiAction, $segment,
$apiParameters, $idGoal, $language, $showTimer, $hideMetricsDoc, $idSubtable, $showRawMetrics, $format_metrics, $idDimension);
return $processed;
}
/**
* Get a list of all pages that shall be shown in a Matomo UI including a list of all widgets that shall
* be shown within each page.
*
* @param int $idSite
* @return array
*/
public function getReportPagesMetadata($idSite)
{
Piwik::checkUserHasViewAccess($idSite);
$widgetsList = WidgetsList::get();
$categoryList = CategoryList::get();
$metadata = new WidgetMetadata();
return $metadata->getPagesMetadata($categoryList, $widgetsList);
}
/**
* Get a list of all widgetizable widgets.
*
* @param int $idSite
* @return array
*/
public function getWidgetMetadata($idSite)
{
Piwik::checkUserHasViewAccess($idSite);
$widgetsList = WidgetsList::get();
$categoryList = CategoryList::get();
$metadata = new WidgetMetadata();
return $metadata->getWidgetMetadata($categoryList, $widgetsList);
}
/**
* Get a combined report of the *.get API methods.
*/
public function get($idSite, $period, $date, $segment = false, $columns = false)
{
Piwik::checkUserHasViewAccess($idSite);
$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'];
$allMetrics = array_merge($reportMeta['metrics'], @$reportMeta['processedMetrics'] ?: array());
foreach ($allMetrics 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');
foreach ($columnsByPlugin as $plugin => $columns) {
// load the data
$className = Request::getClassNameAPI($plugin);
$params['columns'] = implode(',', $columns);
$dataTable = Proxy::getInstance()->call($className, 'get', $params);
$dataTable->filter(function (DataTable $table) {
$table->clearQueuedFilters();
});
// merge reports
if ($mergedDataTable === false) {
$mergedDataTable = $dataTable;
} else {
$merger = new MergeDataTables();
$merger->mergeDataTables($mergedDataTable, $dataTable);
}
}
if (!empty($columnsMap)
&& !empty($mergedDataTable)
) {
$mergedDataTable->queueFilter('ColumnDelete', array(false, array_keys($columnsMap)));
}
return $mergedDataTable;
}
/**
* 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
* @param bool|int $idDimension
* @return array
*/
public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true, $idDimension = false)
{
// check if site exists
$idSite = (int) $idSite;
$site = new Site($idSite);
Piwik::checkUserHasViewAccess($idSite);
$apiParameters = array();
$entityNames = StaticContainer::get('entities.idNames');
foreach ($entityNames as $entityName) {
if ($entityName === 'idGoal' && is_numeric($idGoal)) {
$apiParameters['idGoal'] = $idGoal;
} elseif ($entityName === 'idDimension' && $idDimension) {
$apiParameters['idDimension'] = $idDimension;
} else {
// ideally it would get the value from API params but dynamic params is not possible yet in API. If this
// method is called eg in Request::processRequest, it could in theory pick up a param from the original request
// and not from the API request within the original request.
$idEntity = Common::getRequestVar($entityName, 0, 'int');
if ($idEntity > 0) {
$apiParameters[$entityName] = $idEntity;
}
}
}
$rowEvolution = new RowEvolution();
return $rowEvolution->getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label, $segment, $column,
$language, $apiParameters, $legendAppendMetric, $labelUseAbsoluteUrl);
}
/**
* 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) {
$params = Request::getRequestArrayFromString($url . '&format=php&serialize=0');
if (isset($params['urls']) && $params['urls'] == $urls) {
// by default 'urls' is added to $params as Request::getRequestArrayFromString adds all $_GET/$_POST
// default parameters
unset($params['urls']);
}
if (!empty($params['segment']) && strpos($url, 'segment=') > -1) {
// only unsanitize input when segment is actually present in URL, not when it was used from
// $defaultRequest in Request::getRequestArrayFromString from $_GET/$_POST
$params['segment'] = urlencode(Common::unsanitizeInputValue($params['segment']));
}
$req = new Request($params);
$result[] = $req->process();
}
return $result;
}
/**
* Return true if plugin is activated, false otherwise
*
* @param string $pluginName
* @return bool
*/
public function isPluginActivated($pluginName)
{
Piwik::checkUserHasSomeViewAccess();
return \Piwik\Plugin\Manager::getInstance()->isPluginActivated($pluginName);
}
/**
* 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)
{
if (empty(Config::getInstance()->General['enable_segment_suggested_values'])) {
return array();
}
Piwik::checkUserHasViewAccess($idSite);
$maxSuggestionsToReturn = 30;
$segment = $this->findSegment($segmentName, $idSite);
// if segment has suggested values callback then return result from it instead
$suggestedValuesCallbackRequiresTable = false;
if (isset($segment['suggestedValuesCallback'])) {
$suggestedValuesCallbackRequiresTable = $this->doesSuggestedValuesCallbackNeedData(
$segment['suggestedValuesCallback']);
if (!$suggestedValuesCallbackRequiresTable) {
return call_user_func($segment['suggestedValuesCallback'], $idSite, $maxSuggestionsToReturn);
}
}
// if period=range is disabled, do not proceed
if (!Period\Factory::isPeriodEnabledForAPI('range')) {
return array();
}
if (!empty($segment['unionOfSegments'])) {
$values = array();
foreach ($segment['unionOfSegments'] as $unionSegmentName) {
$unionSegment = $this->findSegment($unionSegmentName, $idSite);
try {
$result = $this->getSuggestedValuesForSegmentName($idSite, $unionSegment, $maxSuggestionsToReturn);
if (!empty($result)) {
$values = array_merge($result, $values);
}
} catch (\Exception $e) {
// we ignore if there was no data found for $unionSegmentName
}
}
if (empty($values)) {
throw new \Exception("There was no data to suggest for $segmentName");
}
} else {
$values = $this->getSuggestedValuesForSegmentName($idSite, $segment, $maxSuggestionsToReturn);
}
$values = $this->getMostFrequentValues($values);
$values = array_slice($values, 0, $maxSuggestionsToReturn);
$values = array_map(array('Piwik\Common', 'unsanitizeInputValue'), $values);
return $values;
}
private function findSegment($segmentName, $idSite)
{
$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 $segmentName not found.");
}
return $segmentFound;
}
private function getSuggestedValuesForSegmentName($idSite, $segment, $maxSuggestionsToReturn)
{
$startDate = Date::now()->subDay(self::$_autoSuggestLookBack)->toString();
$requestLastVisits = "method=Live.getLastVisitsDetails
&idSite=$idSite
&period=range
&date=$startDate,today
&format=original
&serialize=0
&flat=1";
$segmentName = $segment['segment'];
// 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=400";
} else {
$requestLastVisits .= "&doNotFetchActions=1";
$requestLastVisits .= "&filter_limit=800";
}
$request = new Request($requestLastVisits);
$table = $request->process();
if (empty($table)) {
throw new \Exception("There was no data to suggest for $segmentName");
}
if (isset($segment['suggestedValuesCallback']) &&
$this->doesSuggestedValuesCallbackNeedData($segment['suggestedValuesCallback'])) {
$values = call_user_func($segment['suggestedValuesCallback'], $idSite, $maxSuggestionsToReturn, $table);
} else {
$values = $this->getSegmentValuesFromVisitorLog($segmentName, $table);
}
return $values;
}
/**
* A glossary of all reports and their definition
*
* @param $idSite
* @return array
*/
public function getGlossaryReports($idSite)
{
$glossary = StaticContainer::get('Piwik\Plugins\API\Glossary');
return $glossary->reportsGlossary($idSite);
}
/**
* A glossary of all metrics and their definition
*
* @param $idSite
* @return array
*/
public function getGlossaryMetrics($idSite)
{
$glossary = StaticContainer::get('Piwik\Plugins\API\Glossary');
return $glossary->metricsGlossary($idSite);
}
/**
* @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',
'outlinkUrl', 'downloadUrl', 'eventUrl'
);
$isCustomVariablePage = stripos($segmentName, 'customVariablePage') !== false;
$isEventSegment = stripos($segmentName, 'event') !== false;
$isContentSegment = stripos($segmentName, 'content') !== false;
$doesSegmentNeedActionsInfo = in_array($segmentName, $segmentsNeedActionsInfo) || $isCustomVariablePage || $isEventSegment || $isContentSegment;
return $doesSegmentNeedActionsInfo;
}
/**
* @param $values
*
* @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);
// Sort this list by converting and sorting the array with custom method, so the result doesn't differ between PHP versions
$sortArray = [];
foreach ($values as $value => $count) {
$sortArray[] = [
'value' => $value,
'count' => $count
];
}
usort($sortArray, function($a, $b) {
if ($a['count'] == $b['count']) {
return strcmp($a['value'], $b['value']);
}
return $a['count'] > $b['count'] ? -1 : 1;
});
return array_column($sortArray, 'value');
}
private function doesSuggestedValuesCallbackNeedData($suggestedValuesCallback)
{
if (is_string($suggestedValuesCallback)
&& strpos($suggestedValuesCallback, '::') !== false
) {
$suggestedValuesCallback = explode('::', $suggestedValuesCallback);
}
if (is_array($suggestedValuesCallback)) {
$methodMetadata = new \ReflectionMethod($suggestedValuesCallback[0], $suggestedValuesCallback[1]);
} else {
$methodMetadata = new \ReflectionFunction($suggestedValuesCallback);
}
return $methodMetadata->getNumberOfParameters() >= 3;
}
}
/**
*/
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::registerEvents
*/
public function registerEvents()
{
return array(
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
'Platform.initialized' => 'detectIsApiRequest'
);
}
public function detectIsApiRequest()
{
Request::setIsRootRequestApiRequest(Request::getMethodIfApiRequest($request = null));
}
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = "plugins/API/stylesheets/listAllAPI.less";
$stylesheets[] = "plugins/API/stylesheets/glossary.less";
}
}

View File

@ -0,0 +1,198 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @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\Plugin\Report;
use Piwik\Url;
use Piwik\UrlHelper;
use Piwik\View;
/**
*
*/
class Controller extends \Piwik\Plugin\Controller
{
function index()
{
$token = 'token_auth=' . Common::getRequestVar('token_auth', 'anonymous', 'string');
// when calling the API through http, we limit the number of returned results
if (!isset($_GET['filter_limit'])) {
if (isset($_POST['filter_limit'])) {
$_GET['filter_limit'] = $_POST['filter_limit'];
} else {
$_GET['filter_limit'] = Config::getInstance()->General['API_datatable_default_limit'];
}
}
$request = new Request($token);
$response = $request->process();
if (is_array($response)) {
$response = var_export($response, true);
}
return $response;
}
public function listAllMethods()
{
Piwik::checkUserHasSomeViewAccess();
$ApiDocumentation = new DocumentationGenerator();
$prefixUrls = Common::getRequestVar('prefixUrl', 'https://demo.matomo.org/', 'string');
if (!UrlHelper::isLookLikeUrl($prefixUrls) || strpos($prefixUrls, 'http') !== 0) {
$prefixUrls = '';
}
return $ApiDocumentation->getApiDocumentationAsStringForDeveloperReference($outputExampleUrls = true, $prefixUrls);
}
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->getApiDocumentationAsString();
return $view->render();
}
public function listSegments()
{
$segments = API::getInstance()->getSegmentsMetadata($this->idSite);
$tableDimensions = $tableMetrics = '';
$customVariables = 0;
$lastCategory = array();
foreach ($segments as $segment) {
$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>
";
}
public function glossary()
{
Piwik::checkUserHasSomeViewAccess();
$reports = Request::processRequest('API', array('method' => 'API.getGlossaryReports', 'filter_limit' => -1));
$metrics = Request::processRequest('API', array('method' => 'API.getGlossaryMetrics', 'filter_limit' => -1));
$glossaryItems = array(
'metrics' => array(
'title' => Piwik::translate('General_Metrics'),
'entries' => $metrics
),
'reports' => array(
'title' => Piwik::translate('General_Reports'),
'entries' => $reports
)
);
/**
* Triggered to add or modify glossary items. You can either modify one of the existing core categories
* 'reports' and 'metrics' or add your own category.
*
* **Example**
*
* public function addGlossaryItems(&$glossaryItems)
* {
* $glossaryItems['users'] = array('title' => 'Users', 'entries' => array(
* array('name' => 'User1', 'documentation' => 'This user has ...'),
* array('name' => 'User2', 'documentation' => 'This user has ...'),
* ));
* $glossaryItems['reports']['entries'][] = array('name' => 'My Report', 'documentation' => 'This report has ...');
* }
*
* @param array &$glossaryItems An array containing all glossary items.
*/
Piwik::postEvent('API.addGlossaryItems', array(&$glossaryItems));
foreach ($glossaryItems as &$item) {
$item['letters'] = array();
foreach ($item['entries'] as &$entry) {
$cleanEntryName = preg_replace('/["\']/', '', $entry['name']);
$entry['letter'] = Common::mb_strtoupper(substr($cleanEntryName, 0, 1));
$item['letters'][] = $entry['letter'];
}
$item['letters'] = array_unique($item['letters']);
sort($item['letters']);
}
return $this->renderTemplate('glossary', array(
'glossaryItems' => $glossaryItems,
));
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API\DataTable;
use Piwik\DataTable\Row;
use Piwik\DataTable;
class MergeDataTables
{
/**
* Merge the columns of two data tables.
* Manipulates the first table.
*
* @param DataTable|DataTable\Map $table1 The table to eventually filter.
* @param DataTable|DataTable\Map $table2 Whether to delete rows with no visits or not.
*/
public 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) {
if (!array_key_exists($index, $subTables2)) {
// occurs when archiving starts on dayN and continues into dayN+1, see https://github.com/piwik/piwik/issues/5168#issuecomment-50959925
continue;
}
$subTable2 = $subTables2[$index];
$this->mergeDataTables($subTable1, $subTable2);
}
return;
}
$firstRow2 = $table2->getFirstRow();
if (!($firstRow2 instanceof Row)) {
return;
}
$firstRow1 = $table1->getFirstRow();
if (empty($firstRow1)) {
$firstRow1 = $table1->addRow(new Row());
}
foreach ($firstRow2->getColumns() as $metric => $value) {
$firstRow1->setColumn($metric, $value);
}
}
}

View File

@ -0,0 +1,122 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API;
use Piwik\Metrics;
class Glossary
{
protected $metadata = array();
public function __construct(API $api)
{
$this->api = $api;
}
public function reportsGlossary($idSite)
{
$metadata = $this->api->getReportMetadata($idSite);
$reports = array();
foreach ($metadata as $report) {
if (isset($report['documentation'])) {
$reports[] = array(
'name' => sprintf("%s (%s)", $report['name'], $report['category']),
'documentation' => $report['documentation']
);
}
}
usort($reports, function ($a, $b) {
return strcmp($a['name'], $b['name']);
});
return $reports;
}
public function metricsGlossary($idSite)
{
$metadata = $this->api->getReportMetadata($idSite);
$metrics = array();
foreach ($metadata as $report) {
if (!isset($report['metricsDocumentation'])) {
continue;
}
foreach ($report['metricsDocumentation'] as $metricId => $metricDocumentation) {
$metricKey = $metricId;
if(empty($report['metrics'][$metricId])
&& empty($report['processedMetrics'][$metricId])) {
continue;
}
$metricName = isset($report['metrics'][$metricId]) ? $report['metrics'][$metricId] : $report['processedMetrics'][$metricId];
// Already one metric with same name, but different documentation...
if (isset($metrics[$metricKey])
&& $metrics[$metricKey]['documentation'] !== $metricDocumentation) {
// Don't show nb_hits in glossary since it duplicates others, eg. nb_downloads,
if($metricKey == 'nb_hits') {
continue;
}
$metricName = sprintf("%s (%s)", $metricName, $report['category']);
$metricKey = $metricName;
if (isset($metrics[$metricKey]) && $metrics[$metricKey]['documentation'] !== $metricDocumentation) {
throw new \Exception(sprintf("Metric %s has two different documentations: \n(1) %s \n(2) %s",
$metricKey,
$metrics[$metricKey]['documentation'],
$metricDocumentation)
);
}
} else {
if (!isset($report['metrics'][$metricId])
&& !isset($report['processedMetrics'][$metricId])
) {
// $metricId metric name not found in $report['dimension'] report
// it will be set in another one
continue;
}
}
$metrics[$metricKey] = array(
'name' => $metricName,
'id' => $metricId,
'documentation' => $metricDocumentation
);
}
}
$metricsTranslations = Metrics::getDefaultMetricTranslations();
foreach (Metrics::getDefaultMetricsDocumentation() as $metric => $translation) {
if (!isset($metrics[$metric]) && isset($metricsTranslations[$metric])) {
$metrics[$metric] = array(
'name' => $metricsTranslations[$metric],
'id' => $metric,
'documentation' => $translation
);
}
}
usort($metrics, function ($a, $b) {
return strcmp($a['name'], $b['name']);
});
return $metrics;
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API;
use Piwik\DeviceDetectorCache;
use Piwik\Menu\MenuAdmin;
use Piwik\Menu\MenuTop;
use Piwik\Piwik;
use DeviceDetector\Parser\OperatingSystem;
class Menu extends \Piwik\Plugin\Menu
{
const DD_SHORT_NAME_ANDROID = 'AND';
const DD_SHORT_NAME_IOS = 'IOS';
public function configureTopMenu(MenuTop $menu)
{
$this->addTopMenuMobileApp($menu);
}
public function configureAdminMenu(MenuAdmin $menu)
{
$menu->addPlatformItem('General_API',
$this->urlForAction('listAllAPI', array('segment' => false)),
7,
Piwik::translate('API_TopLinkTooltip')
);
if(Piwik::isUserIsAnonymous()) {
$menu->addPlatformItem('API_Glossary',
$this->urlForAction('glossary', array('segment' => false)),
50
);
}
}
private function addTopMenuMobileApp(MenuTop $menu)
{
if (empty($_SERVER['HTTP_USER_AGENT'])) {
return;
}
if (!class_exists("DeviceDetector\\DeviceDetector")) {
throw new \Exception("DeviceDetector could not be found, maybe you are using Piwik from git and need to update Composer. Execute this command: php composer.phar update");
}
$ua = new OperatingSystem($_SERVER['HTTP_USER_AGENT']);
$ua->setCache(new DeviceDetectorCache(86400));
$parsedOS = $ua->parse();
if (!empty($parsedOS['short_name']) && in_array($parsedOS['short_name'], array(self::DD_SHORT_NAME_ANDROID, self::DD_SHORT_NAME_IOS))) {
$menu->addItem('Mobile_MatomoMobile', null, 'https://matomo.org/mobile/', 4);
}
}
}

View File

@ -0,0 +1,856 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @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\CacheId;
use Piwik\Cache as PiwikCache;
use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\DataTable\Simple;
use Piwik\Date;
use Piwik\Metrics;
use Piwik\Metrics\Formatter;
use Piwik\Period;
use Piwik\Piwik;
use Piwik\Plugin\Metric;
use Piwik\Plugin\ReportsProvider;
use Piwik\Site;
use Piwik\Timer;
use Piwik\Url;
class ProcessedReport
{
/**
* @var ReportsProvider
*/
private $reportsProvider;
public function __construct(ReportsProvider $reportsProvider)
{
$this->reportsProvider = $reportsProvider;
}
/**
* 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);
$entityNames = StaticContainer::get('entities.idNames');
foreach ($entityNames as $entityName) {
if ($entityName === 'idGoal' || $entityName === 'idDimension') {
continue; // idGoal and idDimension is passed directly but for other entities we need to "workaround" and
// check for eg idFoo from GET/POST because we cannot add parameters to API dynamically
}
$idEntity = Common::getRequestVar($entityName, 0, 'int');
if ($idEntity > 0) {
$apiParameters[$entityName] = $idEntity;
}
}
foreach ($reportsMetadata as $report) {
// See ArchiveProcessor/Aggregator.php - unique visitors are not processed for period != day
// todo: should use SettingsPiwik::isUniqueVisitorsEnabled instead
if (($period && $period != 'day') && !($apiModule == 'VisitsSummary' && $apiAction == 'get')) {
unset($report['metrics']['nb_uniq_visitors']);
unset($report['metrics']['nb_users']);
}
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($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 int $idSite
* @param bool|string $period
* @param bool|Date $date
* @param bool $hideMetricsDoc
* @param bool $showSubtableReports
* @return array
*/
public function getReportMetadata($idSite, $period = false, $date = false, $hideMetricsDoc = false, $showSubtableReports = false)
{
Piwik::checkUserHasViewAccess($idSite);
// as they cache key contains a lot of information there would be an even better cache result by caching parts of
// this huge method separately but that makes it also more complicated. leaving it like this for now.
$key = $this->buildReportMetadataCacheKey($idSite, $period, $date, $hideMetricsDoc, $showSubtableReports);
$key = CacheId::pluginAware($key);
$cache = PiwikCache::getTransientCache();
if ($cache->contains($key)) {
return $cache->fetch($key);
}
$parameters = array('idSite' => $idSite, 'period' => $period, 'date' => $date);
$availableReports = array();
foreach ($this->reportsProvider->getAllReports() as $report) {
$report->configureReportMetadata($availableReports, $parameters);
}
foreach ($availableReports as &$availableReport) {
if ($hideMetricsDoc) {
unset($availableReport['metricsDocumentation']);
}
}
/**
* 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:
*
* - **idSite**: The site ID 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, 'sortReports'));
$knownMetrics = array_merge(Metrics::getDefaultMetrics(), Metrics::getDefaultProcessedMetrics());
$columnsToKeep = $this->getColumnsToKeep();
$columnsToRemove = $this->getColumnsToRemove();
foreach ($availableReports as &$availableReport) {
$availableReport['category'] = Piwik::translate($availableReport['category']);
$availableReport['subcategory'] = Piwik::translate($availableReport['subcategory']);
// Ensure all metrics have a translation
$metrics = $availableReport['metrics'];
$cleanedMetrics = array();
// TODO we can remove this once we remove the getReportMetadata event, leaving it here for backwards compatibility
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->hideShowMetricsWithParams($availableReport['metrics'], $columnsToRemove, $columnsToKeep);
if (isset($availableReport['processedMetrics'])) {
$availableReport['processedMetrics'] = $this->hideShowMetricsWithParams($availableReport['processedMetrics'], $columnsToRemove, $columnsToKeep);
}
if (isset($availableReport['metricsDocumentation'])) {
$availableReport['metricsDocumentation'] =
$this->hideShowMetricsWithParams($availableReport['metricsDocumentation'], $columnsToRemove, $columnsToKeep);
}
// 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
// TODO we should remove this once we remove the getReportMetadata event, leaving it here for backwards compatibility
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) {
$value = urlencode($value);
$value = str_replace('%', '', $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]);
}
}
}
$actualReports = array_values($availableReports);
$cache->save($key, $actualReports);
return $actualReports; // 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 array $a
* @param array $b
* @return int
*/
public function sortReports($a, $b)
{
return $this->reportsProvider->compareCategories($a['category'], $a['subcategory'], $a['order'], $b['category'], $b['subcategory'], $b['order']);
}
public function getProcessedReport($idSite, $period, $date, $apiModule, $apiAction, $segment = false,
$apiParameters = false, $idGoal = false, $language = false,
$showTimer = true, $hideMetricsDoc = false, $idSubtable = false, $showRawMetrics = false,
$formatMetrics = null, $idDimension = false)
{
$timer = new Timer();
if (empty($apiParameters)) {
$apiParameters = array();
}
if (!empty($idGoal)
&& empty($apiParameters['idGoal'])
) {
$apiParameters['idGoal'] = $idGoal;
}
if (!empty($idDimension)
&& empty($apiParameters['idDimension'])
) {
$apiParameters['idDimension'] = (int) $idDimension;
}
// 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;
if (!empty($reportMetadata['processedMetrics'])
&& !empty($reportMetadata['metrics']['nb_visits'])
&& @$reportMetadata['category'] != Piwik::translate('Goals_Ecommerce')
&& $apiModule !== 'MultiSites'
) {
$deleteRowsWithNoVisits = empty($reportMetadata['constantRowsCount']) ? '1' : '0';
$parameters['filter_add_columns_when_show_all_columns'] = $deleteRowsWithNoVisits;
}
$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, $formatMetrics);
if (function_exists('mb_substr')) {
foreach ($columns as &$name) {
if (substr($name, 0, 1) === mb_substr($name, 0, 1)) {
$name = ucfirst($name);
}
}
}
$website = new Site($idSite);
$period = Period\Factory::build($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
* @param bool|null $formatMetrics
* @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, $formatMetrics = null)
{
$hasDimension = isset($reportMetadata['dimension']);
$columns = @$reportMetadata['metrics'] ?: array();
if ($hasDimension) {
$columns = array_merge(
array('label' => $reportMetadata['dimension']),
$columns
);
}
if (isset($reportMetadata['processedMetrics']) && is_array($reportMetadata['processedMetrics'])) {
$processedMetricsAdded = Metrics::getDefaultProcessedMetrics();
foreach ($reportMetadata['processedMetrics'] as $processedMetricId => $processedMetricTranslation) {
// this processed metric can be displayed for this report
if ($processedMetricTranslation && $processedMetricId !== $processedMetricTranslation) {
$columns[$processedMetricId] = $processedMetricTranslation;
} elseif (isset($processedMetricsAdded[$processedMetricId])) {
// for instance in case 'nb_visits' => 'nb_visits' we will translate it
$columns[$processedMetricId] = $processedMetricsAdded[$processedMetricId];
}
}
}
// 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];
}
}
}
$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 $simpleDataTable) {
$this->removeEmptyColumns($columns, $reportMetadata, $simpleDataTable);
list($enhancedSimpleDataTable, $rowMetadata) = $this->handleSimpleDataTable($idSite, $simpleDataTable, $columns, $hasDimension, $showRawMetrics, $formatMetrics);
$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, $formatMetrics);
$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;
}
$columnsToRemove = $this->getColumnsToRemove();
$columnsToKeep = $this->getColumnsToKeep();
$columns = $this->hideShowMetricsWithParams($columns, $columnsToRemove, $columnsToKeep, $emptyColumns);
if (isset($reportMetadata['metrics'])) {
$reportMetadata['metrics'] = $this->hideShowMetricsWithParams($reportMetadata['metrics'], $columnsToRemove, $columnsToKeep, $emptyColumns);
}
if (isset($reportMetadata['metricsDocumentation'])) {
$reportMetadata['metricsDocumentation'] = $this->hideShowMetricsWithParams($reportMetadata['metricsDocumentation'], $columnsToRemove, $columnsToKeep, $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 = $this->getColumnsToRemove();
// remove columns if showColumns query parameters exist
$columnsToKeep = $this->getColumnsToKeep();
return $this->hideShowMetricsWithParams($columns, $columnsToRemove, $columnsToKeep, $emptyColumns);
}
private function hideShowMetricsWithParams($columns, $columnsToRemove, $columnsToKeep, $emptyColumns = array())
{
if (!is_array($columns)) {
return $columns;
}
if (null !== $columnsToRemove) {
foreach ($columnsToRemove as $name) {
// if a column to remove is in the column list, remove it
if (isset($columns[$name])) {
unset($columns[$name]);
}
}
}
if (null !== $columnsToKeep) {
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
* @param bool|null $formatMetrics
* @return array DataTable $enhancedDataTable filtered metrics with human readable format & Simple $rowsMetadata
*/
private function handleSimpleDataTable($idSite, $simpleDataTable, $metadataColumns, $hasDimension, $returnRawMetrics = false, $formatMetrics = null)
{
// new DataTable to store metadata
$rowsMetadata = new DataTable();
// new DataTable to store 'human readable' values
if ($hasDimension) {
$enhancedDataTable = new DataTable();
} else {
$enhancedDataTable = new Simple();
}
$formatter = new Formatter();
$hasNonEmptyRowData = false;
foreach ($simpleDataTable->getRows() as $row) {
$rowMetrics = $row->getColumns();
// add missing metrics
foreach ($metadataColumns as $id => $name) {
if (!isset($rowMetrics[$id])) {
$row->setColumn($id, 0);
$rowMetrics[$id] = 0;
}
}
$enhancedRow = new Row();
$enhancedDataTable->addRow($enhancedRow);
foreach ($rowMetrics as $columnName => $columnValue) {
// filter metrics according to metadata definition
if (isset($metadataColumns[$columnName])) {
// generate 'human readable' metric values
// if we handle MultiSites.getAll we do not always have the same idSite but different ones for
// each site, see https://github.com/piwik/piwik/issues/5006
$idSiteForRow = $idSite;
$idSiteMetadata = $row->getMetadata('idsite');
if ($idSiteMetadata && is_numeric($idSiteMetadata)) {
$idSiteForRow = (int) $idSiteMetadata;
}
// format metrics manually here to maintain API.getProcessedReport BC if format_metrics query parameter is
// not supplied. TODO: should be removed for 3.0. should only rely on format_metrics query parameter.
if ($formatMetrics === null
|| $formatMetrics == 'bc'
) {
$prettyValue = self::getPrettyValue($formatter, $idSiteForRow, $columnName, $columnValue, $htmlAllowed = false);
} else {
$prettyValue = $columnValue;
}
$enhancedRow->addColumn($columnName, $prettyValue);
} // For example the Maps Widget requires the raw metrics to do advanced datavis
else if ($returnRawMetrics) {
if (!isset($columnValue)) {
$columnValue = 0;
}
$enhancedRow->addColumn($columnName, $columnValue);
}
}
// If report has a dimension, extract metadata into a distinct DataTable
if ($hasDimension) {
$rowMetadata = $row->getMetadata();
$idSubDataTable = $row->getIdSubDataTable();
// always add a metadata row - even if empty, so the number of rows and metadata are equal and can be matched directly
$metadataRow = new Row();
$rowsMetadata->addRow($metadataRow);
if (count($rowMetadata) > 0 || !is_null($idSubDataTable)) {
$hasNonEmptyRowData = true;
foreach ($rowMetadata as $metadataKey => $metadataValue) {
$metadataRow->addColumn($metadataKey, $metadataValue);
}
if (!is_null($idSubDataTable)) {
$metadataRow->addColumn('idsubdatatable', $idSubDataTable);
}
}
}
}
// reset $rowsMetadata to empty DataTable if no row had metadata
if ($hasNonEmptyRowData === false) {
$rowsMetadata = new DataTable();
}
return array(
$enhancedDataTable,
$rowsMetadata
);
}
private function aggregateReportTotalValues($simpleDataTable, $totals)
{
$metadataTotals = $simpleDataTable->getMetadata('totals');
if (empty($metadataTotals)) {
return $totals;
}
$simpleTotals = $this->hideShowMetrics($metadataTotals);
return $this->calculateTotals($simpleTotals, $totals);
}
private function calculateTotals($simpleTotals, $totals)
{
foreach ($simpleTotals as $metric => $value) {
if (0 === strpos($metric, 'avg_') || '_rate' === substr($metric, -5) || '_evolution' === substr($metric, -10)) {
continue; // skip average, rate and evolution metrics
}
if (!is_numeric($value) && !is_array($value)) {
continue;
}
if (is_array($value)) {
$currentValue = array_key_exists($metric, $totals) ? $totals[$metric] : [];
$newValue = $this->calculateTotals($value, $currentValue);
if (!empty($newValue)) {
$totals[$metric] = $newValue;
}
}
if (!array_key_exists($metric, $totals)) {
$totals[$metric] = $value;
} else if(0 === strpos($metric, 'min_')) {
$totals[$metric] = min($totals[$metric], $value);
} else if(0 === strpos($metric, 'max_')) {
$totals[$metric] = max($totals[$metric], $value);
} else if($value) {
$totals[$metric] += $value;
}
}
return $totals;
}
private function getColumnsToRemove()
{
$columnsToRemove = Common::getRequestVar('hideColumns', '');
if ($columnsToRemove != '') {
return explode(',', $columnsToRemove);
}
return null;
}
private function getColumnsToKeep()
{
$columnsToKeep = Common::getRequestVar('showColumns', '');
if ($columnsToKeep != '') {
$columnsToKeep = explode(',', $columnsToKeep);
$columnsToKeep[] = 'label';
return $columnsToKeep;
}
return null;
}
private function buildReportMetadataCacheKey($idSite, $period, $date, $hideMetricsDoc, $showSubtableReports)
{
if (isset($_GET) && isset($_POST) && is_array($_GET) && is_array($_POST)) {
$request = $_GET + $_POST;
} elseif (isset($_GET) && is_array($_GET)) {
$request = $_GET;
} elseif (isset($_POST) && is_array($_POST)) {
$request = $_POST;
} else {
$request = array();
}
$key = '';
foreach ($request as $k => $v) {
if (is_array($v)) {
$key .= $k . $this->getImplodedArray($v) . ',';
} else {
$key .= $k . $v . ',';
}
}
$key .= $idSite . 'x' . ($period === false ? 0 : $period) . 'x' . ($date === false ? 0 : $date);
$key .= (int)$hideMetricsDoc . (int)$showSubtableReports . Piwik::getCurrentUserLogin();
return 'reportMetadata' . md5($key);
}
/**
* @param $v
* @return string
*/
private function getImplodedArray($v)
{
return implode(', ', array_map(function ($entry) {
if (is_array($entry)) {
return implode(":", $entry);
}
return $entry;
}, $v));
}
/**
* Prettifies a metric value based on the column name.
*
* @param int $idSite The ID of the site the metric is for (used if the column value is an amount of money).
* @param string $columnName The metric name.
* @param mixed $value The metric value.
* @param bool $isHtml If true, replaces all spaces with `'&nbsp;'`.
* @return string
*/
public static function getPrettyValue(Formatter $formatter, $idSite, $columnName, $value)
{
if (!is_numeric($value)) {
return $value;
}
// Display time in human readable
if (strpos($columnName, 'time_generation') !== false) {
return $formatter->getPrettyTimeFromSeconds($value, true);
}
if (strpos($columnName, 'time') !== false) {
return $formatter->getPrettyTimeFromSeconds($value);
}
// Add revenue symbol to revenues
$isMoneyMetric = strpos($columnName, 'revenue') !== false || strpos($columnName, 'price') !== false;
if ($isMoneyMetric && strpos($columnName, 'evolution') === false) {
return $formatter->getPrettyMoney($value, $idSite);
}
// Add % symbol to rates
if (strpos($columnName, '_rate') !== false) {
if (strpos($value, "%") === false) {
return (100 * $value) . "%";
}
}
return $value;
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API\Renderer;
use Piwik\API\ApiRenderer;
use Piwik\Common;
class Console extends ApiRenderer
{
/**
* @param $message
* @param Exception|\Throwable $exception
* @return string
*/
public function renderException($message, $exception)
{
self::sendHeader();
return 'Error: ' . $message;
}
public function sendHeader()
{
Common::sendHeader('Content-Type: text/plain; charset=utf-8');
}
}

View File

@ -0,0 +1,63 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API\Renderer;
use Piwik\API\ApiRenderer;
use Piwik\Common;
use Piwik\ProxyHttp;
class Csv extends ApiRenderer
{
public function renderSuccess($message)
{
Common::sendHeader("Content-Disposition: attachment; filename=piwik-report-export.csv");
return "message\n" . $message;
}
/**
* @param $message
* @param Exception|\Throwable $exception
* @return string
*/
public function renderException($message, $exception)
{
Common::sendHeader('Content-Type: text/html; charset=utf-8', true);
return 'Error: ' . $message;
}
public function renderDataTable($dataTable)
{
$convertToUnicode = Common::getRequestVar('convertToUnicode', true, 'int', $this->request);
$idSite = Common::getRequestVar('idSite', false, 'int', $this->request);
/** @var \Piwik\DataTable\Renderer\Csv $tableRenderer */
$tableRenderer = $this->buildDataTableRenderer($dataTable);
$tableRenderer->setConvertToUnicode($convertToUnicode);
$method = Common::getRequestVar('method', '', 'string', $this->request);
$tableRenderer->setApiMethod($method);
$tableRenderer->setIdSite($idSite);
$tableRenderer->setTranslateColumnNames(Common::getRequestVar('translateColumnNames', false, 'int', $this->request));
return $tableRenderer->render();
}
public function renderArray($array)
{
return $this->renderDataTable($array);
}
public function sendHeader()
{
Common::sendHeader("Content-Type: application/vnd.ms-excel", true);
ProxyHttp::overrideCacheControlHeaders();
}
}

View File

@ -0,0 +1,58 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API\Renderer;
use Piwik\API\ApiRenderer;
use Piwik\Common;
class Html extends ApiRenderer
{
public function renderSuccess($message)
{
return "<!-- Success: $message -->";
}
/**
* @param $message
* @param Exception|\Throwable $exception
* @return string
*/
public function renderException($message, $exception)
{
Common::sendHeader('Content-Type: text/plain; charset=utf-8', true);
return nl2br($message);
}
public function renderDataTable($dataTable)
{
/** @var \Piwik\DataTable\Renderer\Html $tableRenderer */
$tableRenderer = $this->buildDataTableRenderer($dataTable);
$tableRenderer->setTableId($this->request['method']);
$method = Common::getRequestVar('method', '', 'string', $this->request);
$tableRenderer->setApiMethod($method);
$tableRenderer->setIdSite(Common::getRequestVar('idSite', false, 'int', $this->request));
$tableRenderer->setTranslateColumnNames(Common::getRequestVar('translateColumnNames', false, 'int', $this->request));
return $tableRenderer->render();
}
public function renderArray($array)
{
return $this->renderDataTable($array);
}
public function sendHeader()
{
Common::sendHeader('Content-Type: text/html; charset=utf-8', true);
}
}

View File

@ -0,0 +1,111 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API\Renderer;
use Piwik\API\ApiRenderer;
use Piwik\Common;
use Piwik\DataTable\Renderer;
use Piwik\Piwik;
use Piwik\ProxyHttp;
/**
* API output renderer for JSON.
* NOTE: This is the old JSON format. It includes bugs that are fixed in the JSON2 API output format.
* Please use json2 format instead of this.
*
* @deprecated
*/
class Json extends ApiRenderer
{
public function renderSuccess($message)
{
$result = json_encode(array('result' => 'success', 'message' => $message));
return $this->applyJsonpIfNeeded($result);
}
/**
* @param $message
* @param Exception|\Throwable $exception
* @param \Exception|\Throwable $exception
* @return string
*/
public function renderException($message, $exception)
{
$exceptionMessage = str_replace(array("\r\n", "\n"), "", $message);
$result = json_encode(array('result' => 'error', 'message' => $exceptionMessage));
return $this->applyJsonpIfNeeded($result);
}
public function renderDataTable($dataTable)
{
$result = parent::renderDataTable($dataTable);
return $this->applyJsonpIfNeeded($result);
}
public function renderArray($array)
{
if (Piwik::isMultiDimensionalArray($array)) {
$jsonRenderer = Renderer::factory('json');
$jsonRenderer->setTable($array);
$result = $jsonRenderer->render();
return $this->applyJsonpIfNeeded($result);
}
return $this->renderDataTable($array);
}
public function sendHeader()
{
if ($this->isJsonp()) {
Common::sendHeader('Content-Type: application/javascript; charset=utf-8');
} else {
Renderer\Json::sendHeaderJSON();
}
ProxyHttp::overrideCacheControlHeaders();
}
private function isJsonp()
{
$callback = $this->getJsonpCallback();
if (false === $callback) {
return false;
}
return preg_match('/^[0-9a-zA-Z_.]*$/D', $callback) > 0;
}
private function getJsonpCallback()
{
$jsonCallback = Common::getRequestVar('callback', false, null, $this->request);
if ($jsonCallback === false) {
$jsonCallback = Common::getRequestVar('jsoncallback', false, null, $this->request);
}
return $jsonCallback;
}
/**
* @param $str
* @return string
*/
private function applyJsonpIfNeeded($str)
{
if ($this->isJsonp()) {
$jsonCallback = $this->getJsonpCallback();
$str = $jsonCallback . "(" . $str . ")";
}
return $str;
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API\Renderer;
use Piwik\Piwik;
/**
* Correct API output renderer for JSON. Includes bug fixes for bugs in the old JSON API
* format.
*/
class Json2 extends Json
{
public function renderArray($array)
{
$result = parent::renderArray($array);
// if $array is a simple associative array, remove the JSON root array that is added by renderDataTable
if (!empty($array)
&& Piwik::isAssociativeArray($array)
&& !Piwik::isMultiDimensionalArray($array)
) {
$result = substr($result, 1, strlen($result) - 2);
}
return $result;
}
}

View File

@ -0,0 +1,101 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API\Renderer;
use Piwik\API\ApiRenderer;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\DataTable\DataTableInterface;
class Original extends ApiRenderer
{
public function renderSuccess($message)
{
return true;
}
/**
* @param $message
* @param \Exception|\Throwable $exception
* @throws \Exception|\Throwable
* @return string
*/
public function renderException($message, $exception)
{
if ($this->shouldSerialize()) {
return serialize([
'result' => 'error',
'message' => $message,
]);
}
throw $exception;
}
public function renderDataTable($dataTable)
{
return $this->serializeIfNeeded($dataTable);
}
public function renderArray($array)
{
return $this->serializeIfNeeded($array);
}
public function renderScalar($scalar)
{
return $this->serializeIfNeeded($scalar);
}
public function renderObject($object)
{
return $this->serializeIfNeeded($object);
}
public function renderResource($resource)
{
return $resource;
}
public function sendHeader()
{
if ($this->shouldSerialize()) {
Common::sendHeader('Content-Type: text/plain; charset=utf-8');
}
}
/**
* Returns true if the user requested to serialize the output data (&serialize=1 in the request)
*
* @return bool
*/
private function shouldSerialize()
{
$serialize = Common::getRequestVar('serialize', 0, 'int', $this->request);
return !empty($serialize);
}
private function serializeIfNeeded($response)
{
if ($this->shouldSerialize()) {
if ($response instanceof DataTableInterface) {
// remove COLUMN_AGGREGATION_OPS_METADATA_NAME metadata since it can have closures
$response->filter(function (DataTable $table) {
$allMetadata = $table->getAllTableMetadata();
unset($allMetadata[DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME]);
$table->setAllTableMetadata($allMetadata);
});
}
return serialize($response);
}
return $response;
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API\Renderer;
use Piwik\API\ApiRenderer;
use Piwik\Common;
use Piwik\Piwik;
class Php extends ApiRenderer
{
public function renderSuccess($message)
{
$success = array('result' => 'success', 'message' => $message);
return $this->serializeIfNeeded($success);
}
/**
* @param $message
* @param \Exception|\Throwable $exception
* @return string
*/
public function renderException($message, $exception)
{
$message = array('result' => 'error', 'message' => $message);
return $this->serializeIfNeeded($message);
}
public function renderDataTable($dataTable)
{
/** @var \Piwik\DataTable\Renderer\Php $tableRenderer */
$tableRenderer = $this->buildDataTableRenderer($dataTable);
$tableRenderer->setSerialize($this->shouldSerialize(1));
$tableRenderer->setPrettyDisplay(Common::getRequestVar('prettyDisplay', false, 'int', $this->request));
return $tableRenderer->render();
}
public function renderArray($array)
{
if (!Piwik::isMultiDimensionalArray($array)) {
return $this->renderDataTable($array);
}
if ($this->shouldSerialize(1)) {
return serialize($array);
}
return $array;
}
public function sendHeader()
{
Common::sendHeader('Content-Type: text/plain; charset=utf-8');
}
/**
* Returns true if the user requested to serialize the output data (&serialize=1 in the request)
*
* @param mixed $defaultSerializeValue Default value in case the user hasn't specified a value
* @return bool
*/
private function shouldSerialize($defaultSerializeValue)
{
$serialize = Common::getRequestVar('serialize', $defaultSerializeValue, 'int', $this->request);
return !empty($serialize);
}
private function serializeIfNeeded($response)
{
if ($this->shouldSerialize(1)) {
return serialize($response);
}
return $response;
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API\Renderer;
use Piwik\API\ApiRenderer;
use Piwik\Common;
class Rss extends ApiRenderer
{
/**
* @param $message
* @param \Exception|\Throwable $exception
* @return string
*/
public function renderException($message, $exception)
{
self::sendHeader('plain');
return 'Error: ' . $message;
}
public function renderDataTable($dataTable)
{
/** @var \Piwik\DataTable\Renderer\Rss $tableRenderer */
$tableRenderer = $this->buildDataTableRenderer($dataTable);
$method = Common::getRequestVar('method', '', 'string', $this->request);
$tableRenderer->setApiMethod($method);
$tableRenderer->setIdSite(Common::getRequestVar('idSite', false, 'int', $this->request));
$tableRenderer->setTranslateColumnNames(Common::getRequestVar('translateColumnNames', false, 'int', $this->request));
return $tableRenderer->render();
}
public function renderArray($array)
{
return $this->renderDataTable($array);
}
public function sendHeader($type = "xml")
{
Common::sendHeader('Content-Type: text/' . $type . '; charset=utf-8');
}
}

View File

@ -0,0 +1,22 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API\Renderer;
use Piwik\Common;
class Tsv extends Csv
{
public function renderSuccess($message)
{
Common::sendHeader("Content-Disposition: attachment; filename=piwik-report-export.csv");
return "message\t" . $message;
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API\Renderer;
use Piwik\API\ApiRenderer;
use Piwik\Common;
class Xml extends ApiRenderer
{
public function renderSuccess($message)
{
return "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" .
"<result>\n" .
"\t<success message=\"" . $message . "\" />\n" .
"</result>";
}
/**
* @param $message
* @param \Exception|\Throwable $exception
* @return string
*/
public function renderException($message, $exception)
{
return '<?xml version="1.0" encoding="utf-8" ?>' . "\n" .
"<result>\n" .
"\t<error message=\"" . $message . "\" />\n" .
"</result>";
}
public function sendHeader()
{
Common::sendHeader('Content-Type: text/xml; charset=utf-8');
}
}

View File

@ -0,0 +1,111 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API\Reports;
use Piwik\Piwik;
use Piwik\Plugin\Report;
use Piwik\Plugin\ReportsProvider;
class Get extends Report
{
/**
* List of Plugin.Get reports that are merged in this one.
*
* @var Report[]
*/
private $reportsToMerge = array();
protected function init()
{
parent::init();
$this->reportsToMerge = $this->getReportsToMerge();
$this->module = 'API';
$this->action = 'get';
$this->categoryId = 'API';
$this->name = Piwik::translate('General_MainMetrics');
$this->documentation = '';
$this->processedMetrics = array();
foreach ($this->reportsToMerge as $report) {
if (!is_array($report->processedMetrics)) {
continue;
}
$this->processedMetrics = array_merge($this->processedMetrics, $report->processedMetrics);
}
$this->metrics = array();
foreach ($this->reportsToMerge as $report) {
if (!is_array($report->metrics)) {
continue;
}
$this->metrics = array_merge($this->metrics, $report->metrics);
}
$this->order = 6;
}
public function getMetrics()
{
$metrics = array();
foreach ($this->reportsToMerge as $report) {
$metrics = array_merge($metrics, $report->getMetrics());
}
return $metrics;
}
public function getProcessedMetrics()
{
$processedMetrics = array();
foreach ($this->reportsToMerge as $report) {
$reportMetrics = $report->getProcessedMetrics();
if (is_array($reportMetrics)) {
$processedMetrics = array_merge($processedMetrics, $reportMetrics);
}
}
return $processedMetrics;
}
/**
* @return Report[]
*/
private function getReportsToMerge()
{
$reports = new ReportsProvider();
$result = array();
foreach ($reports->getAllReportClasses() as $reportClass) {
if ($reportClass == 'Piwik\\Plugins\\API\\Reports\\Get') {
continue;
}
/** @var Report $report */
$report = new $reportClass();
if ($report->getModule() == 'API'
|| $report->getAction() != 'get'
) {
continue;
}
$metrics = $report->getMetrics();
if (!empty($report->parameters)
|| empty($metrics)
) {
continue;
}
$result[] = $report;
}
return $result;
}
}

View File

@ -0,0 +1,548 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @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\DataTablePostProcessor;
use Piwik\API\Request;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\DataTable\Filter\CalculateEvolutionFilter;
use Piwik\DataTable\Filter\SafeDecodeLabel;
use Piwik\DataTable\Row;
use Piwik\Period;
use Piwik\Piwik;
use Piwik\Site;
use Piwik\Url;
/**
* 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, $apiParameters = array(), $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 = DataTablePostProcessor::unsanitizeLabelParameter($label);
$labels = Piwik::getArrayFromApiParameter($label);
$metadata = $this->getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $apiParameters);
$dataTable = $this->loadRowEvolutionDataFromAPI($metadata, $idSite, $period, $date, $apiModule, $apiAction, $labels, $segment, $apiParameters);
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 $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);
$result = $mainUrlHost . '/' . $cleanLabel . '/';
} else {
$result = str_replace(LabelFilter::SEPARATOR_RECURSIVE_LABEL, ' - ', $label);
}
// remove @ terminal operator occurrences
return str_replace(LabelFilter::TERMINAL_OPERATOR, '', $result);
}
/**
* @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 array $apiParameters
* @throws Exception
* @return DataTable\Map|DataTable
*/
private function loadRowEvolutionDataFromAPI($metadata, $idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $apiParameters)
{
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,
// 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,
);
if (!empty($apiParameters) && is_array($apiParameters)) {
foreach ($apiParameters as $param => $value) {
$parameters[$param] = $value;
}
}
// 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'])) {
$parameters['filter_add_columns_when_show_all_columns'] = '0';
}
$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 $apiParameters
* @throws Exception
* @return array
*/
private function getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $apiParameters)
{
$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'];
}
if (empty($reportMetadata['dimension'])) {
throw new Exception(sprintf('Reports like %s.%s which do not have a dimension are not supported by row evolution', $apiModule, $apiAction));
}
$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();
if (empty($subDataTables)) {
throw new \Exception("Unexpected state: row evolution API call returned empty DataTable\\Map.");
}
$firstDataTable = reset($subDataTables);
$this->checkDataTableInstance($firstDataTable);
$firstDataTableRow = $firstDataTable->getFirstRow();
$lastDataTable = end($subDataTables);
$this->checkDataTableInstance($lastDataTable);
$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;
}
private function checkDataTableInstance($lastDataTable)
{
if (!($lastDataTable instanceof DataTable)) {
throw new \Exception("Unexpected state: row evolution returned DataTable\\Map w/ incorrect child table type: " . get_class($lastDataTable));
}
}
}

View File

@ -0,0 +1,141 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API;
use Piwik\Category\CategoryList;
use Piwik\Columns\Dimension;
use Piwik\Piwik;
use Piwik\Plugin\Segment;
class SegmentMetadata
{
/**
* Map of category name to order
* @var array
*/
private $categoryOrder = array();
public function getSegmentsMetadata($idSites = array(), $_hideImplementationData = true, $isAuthenticatedWithViewAccess)
{
$segments = array();
/**
* Triggered to add custom segment definitions.
*
* **Example**
*
* public function addSegments(&$segments)
* {
* $segment = new Segment();
* $segment->setSegment('my_segment_name');
* $segment->setType(Segment::TYPE_DIMENSION);
* $segment->setName('My Segment Name');
* $segment->setSqlSegment('log_table.my_segment_name');
* $segments[] = $segment;
* }
*
* @param array &$segments An array containing a list of segment entries.
*/
Piwik::postEvent('Segment.addSegments', array(&$segments));
foreach (Dimension::getAllDimensions() as $dimension) {
foreach ($dimension->getSegments() as $segment) {
$segments[] = $segment;
}
}
/** @var Segment[] $dimensionSegments */
$dimensionSegments = $segments;
$segments = array();
foreach ($dimensionSegments as $segment) {
if ($segment->isRequiresAtLeastViewAccess()) {
$segment->setPermission($isAuthenticatedWithViewAccess);
}
$segments[] = $segment->toArray();
}
$categoryList = CategoryList::get();
foreach ($segments as &$segment) {
$categoryId = $segment['category'];
$segment['name'] = Piwik::translate($segment['name']);
$segment['category'] = Piwik::translate($categoryId);
if (!isset($this->categoryOrder[$segment['category']])) {
$category = $categoryList->getCategory($categoryId);
if (!empty($category)) {
$this->categoryOrder[$segment['category']] = $category->getOrder();
} else {
$this->categoryOrder[$segment['category']] = 999;
}
}
if ($_hideImplementationData) {
unset($segment['sqlFilter']);
unset($segment['sqlFilterValue']);
unset($segment['sqlSegment']);
if (isset($segment['suggestedValuesCallback'])
&& !is_string($segment['suggestedValuesCallback'])
) {
unset($segment['suggestedValuesCallback']);
}
}
}
usort($segments, array($this, 'sortSegments'));
return $segments;
}
private function sortSegments($row1, $row2)
{
$customVarCategory = Piwik::translate('CustomVariables_CustomVariables');
$columns = array('category', 'type', 'name', 'segment');
foreach ($columns as $column) {
// Keep segments ordered alphabetically inside categories..
$type = -1;
if ($column == 'name') {
$type = 1;
}
if ($column === 'category') {
$idOrder1 = $this->categoryOrder[$row1[$column]];
$idOrder2 = $this->categoryOrder[$row2[$column]];
if ($idOrder1 === $idOrder2) {
continue;
}
return $idOrder1 > $idOrder2 ? 1 : -1;
}
$compare = $type * strcmp($row1[$column], $row2[$column]);
// hack so that custom variables "page" are grouped together in the doc
if ($row1['category'] == $customVarCategory
&& $row1['category'] == $row2['category']
) {
$compare = strcmp($row1['segment'], $row2['segment']);
return $compare;
}
if ($compare != 0) {
return $compare;
}
}
return $compare;
}
}

View File

@ -0,0 +1,319 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\API;
use Piwik\Category\CategoryList;
use Piwik\Piwik;
use Piwik\Plugins\CoreHome\CoreHome;
use Piwik\Report\ReportWidgetConfig;
use Piwik\Category\Category;
use Piwik\Category\Subcategory;
use Piwik\Widget\WidgetContainerConfig;
use Piwik\Widget\WidgetConfig;
use Piwik\Widget\WidgetsList;
class WidgetMetadata
{
public function getPagesMetadata(CategoryList $categoryList, WidgetsList $widgetsList)
{
$this->createMissingCategoriesAndSubcategories($categoryList, $widgetsList->getWidgetConfigs());
return $this->buildPagesMetadata($categoryList, $widgetsList);
}
public function getWidgetMetadata(CategoryList $categoryList, WidgetsList $widgetsList)
{
$this->createMissingCategoriesAndSubcategories($categoryList, $widgetsList->getWidgetConfigs());
$flat = array();
foreach ($widgetsList->getWidgetConfigs() as $widgetConfig) {
$metadataOverrides = [];
/** @var WidgetConfig[] $widgets */
$widgets = array($widgetConfig);
if ($widgetConfig instanceof WidgetContainerConfig) {
// so far we go only one level down, in theory these widgetConfigs could have again containers containing configs
$widgets = array_merge($widgets, $widgetConfig->getWidgetConfigs());
}
foreach ($widgets as $widget) {
// make sure to include only widgetizable widgets
if (!$widget->isWidgetizeable() || !$widget->getName()) {
continue;
}
// widgets in containers with ByDimension layout have a special, unrecognized category/subcategory
// (eg, "Sales by Referrer Type"). we change it to the container's category/subcategory so the widget
// will appear in the dashboard manager.
if ($widgetConfig instanceof WidgetContainerConfig
&& $widgetConfig->getLayout() == CoreHome::WIDGET_CONTAINER_LAYOUT_BY_DIMENSION
) {
$metadataOverrides = [
'category' => $widgetConfig->getCategoryId(),
'subcategory' => $widgetConfig->getSubcategoryId(),
'name' => Piwik::translate($widget->getCategoryId()) . ': '
. Piwik::translate($widget->getName()),
];
}
$flat[] = $this->buildWidgetMetadata($widget, $categoryList, $metadataOverrides);
}
}
usort($flat, array($this, 'sortWidgets'));
return $flat;
}
/**
* @param WidgetConfig $widget
* @param CategoryList|null $categoryList If null, no category information will be added to the widgets in first
* level (they will be added to nested widgets as potentially needed eg for
* widgets in ByDimensionView where they are needed to build the left menu)
* @param array $metadataOverrides Overrides for data in `$widget`. Currently only 'name', 'category', 'subcategory'
* are recognized.
* @return array
*/
public function buildWidgetMetadata(WidgetConfig $widget, $categoryList = null, array $metadataOverrides = [])
{
$widgetName = !empty($metadataOverrides['name']) ? $metadataOverrides['name'] : $widget->getName();
$item = array(
'name' => Piwik::translate($widgetName),
);
if (isset($categoryList)) {
$widgetCategory = !empty($metadataOverrides['category'])
? $metadataOverrides['category'] : $widget->getCategoryId();
$widgetSubcategory = !empty($metadataOverrides['subcategory'])
? $metadataOverrides['subcategory'] : $widget->getSubcategoryId();
$category = $categoryList->getCategory($widgetCategory);
$subcategory = $category ? $category->getSubcategory($widgetSubcategory) : null;
$item['category'] = $this->buildCategoryMetadata($category);
$item['subcategory'] = $this->buildSubcategoryMetadata($subcategory);
}
$item['module'] = $widget->getModule();
$item['action'] = $widget->getAction();
$item['order'] = $widget->getOrder();
$item['parameters'] = $widget->getParameters();
$item['uniqueId'] = $widget->getUniqueId();
$item['isWide'] = $widget->isWide();
$middleware = $widget->getMiddlewareParameters();
if (!empty($middleware)) {
$item['middlewareParameters'] = $middleware;
}
if ($widget instanceof ReportWidgetConfig) {
$item['viewDataTable'] = $widget->getViewDataTable();
$item['isReport'] = true;
}
if ($widget instanceof WidgetContainerConfig) {
$item['layout'] = $widget->getLayout();
$item['isContainer'] = true;
// we do not want to create categories to the inital categoryList. Otherwise we'd maybe display more pages
// etc.
$subCategoryList = new CategoryList();
$this->createMissingCategoriesAndSubcategories($subCategoryList, $widget->getWidgetConfigs());
$children = array();
foreach ($widget->getWidgetConfigs() as $widgetConfig) {
$children[] = $this->buildWidgetMetadata($widgetConfig, $subCategoryList);
}
$item['widgets'] = $children;
}
return $item;
}
private function sortWidgets($widgetA, $widgetB) {
$orderA = $widgetA['category']['order'];
$orderB = $widgetB['category']['order'];
if ($orderA === $orderB) {
if (!empty($widgetA['subcategory']['order']) && !empty($widgetB['subcategory']['order'])) {
$subOrderA = $widgetA['subcategory']['order'];
$subOrderB = $widgetB['subcategory']['order'];
if ($subOrderA === $subOrderB) {
return $this->compareWidgetIds($widgetA, $widgetB);
}
return $subOrderA > $subOrderB ? 1 : -1;
} else if (empty($widgetA['subcategory']['order']) && empty($widgetB['subcategory']['order'])) {
return $this->compareWidgetIds($widgetA, $widgetB);
} elseif (!empty($widgetA['subcategory']['order'])) {
return 1;
}
return -1;
}
return $orderA > $orderB ? 1 : -1;
}
private function compareWidgetIds($widgetA, $widgetB)
{
if ($widgetA['uniqueId'] == $widgetB['uniqueId']) {
return 0;
}
return $widgetA['uniqueId'] > $widgetB['uniqueId'] ? 1 : -1;
}
/**
* @param Category|null $category
* @return array
*/
private function buildCategoryMetadata($category)
{
if (!isset($category)) {
return null;
}
return array(
'id' => (string) $category->getId(),
'name' => $category->getDisplayName(),
'order' => $category->getOrder(),
'icon' => $category->getIcon(),
);
}
/**
* @param Subcategory|null $subcategory
* @return array
*/
private function buildSubcategoryMetadata($subcategory)
{
if (!isset($subcategory)) {
return null;
}
return array(
'id' => (string) $subcategory->getId(),
'name' => Piwik::translate($subcategory->getName()),
'order' => $subcategory->getOrder(),
);
}
/**
* @param CategoryList $categoryList
* @param WidgetConfig[] $widgetConfigs
*/
private function createMissingCategoriesAndSubcategories($categoryList, $widgetConfigs)
{
// move reports into categories/subcategories and create missing ones if needed
foreach ($widgetConfigs as $widgetConfig) {
$categoryId = $widgetConfig->getCategoryId();
$subcategoryId = $widgetConfig->getSubcategoryId();
if (!$categoryId) {
continue;
}
if ($widgetConfig instanceof WidgetContainerConfig && !$widgetConfig->getWidgetConfigs()) {
// if a container does not contain any widgets, ignore it
continue;
}
if (!$categoryList->hasCategory($categoryId)) {
$categoryList->addCategory($this->createCategory($categoryId));
}
if (!$subcategoryId) {
continue;
}
$category = $categoryList->getCategory($categoryId);
if (!$category->hasSubcategory($subcategoryId)) {
$category->addSubcategory($this->createSubcategory($categoryId, $subcategoryId));
}
}
}
private function createCategory($categoryId)
{
$category = new Category();
$category->setId($categoryId);
return $category;
}
private function createSubcategory($categoryId, $subcategoryId)
{
$subcategory = new Subcategory();
$subcategory->setCategoryId($categoryId);
$subcategory->setId($subcategoryId);
return $subcategory;
}
/**
* @param CategoryList $categoryList
* @param WidgetsList $widgetsList
* @return array
*/
private function buildPagesMetadata(CategoryList $categoryList, WidgetsList $widgetsList)
{
$pages = array();
$widgets = array();
foreach ($widgetsList->getWidgetConfigs() as $config) {
$pageId = $this->buildPageId($config->getCategoryId(), $config->getSubcategoryId());
if (!isset($widgets[$pageId])) {
$widgets[$pageId] = array();
}
$widgets[$pageId][] = $config;
}
foreach ($categoryList->getCategories() as $category) {
foreach ($category->getSubcategories() as $subcategory) {
$pageId = $this->buildPageId($category->getId(), $subcategory->getId());
if (!empty($widgets[$pageId])) {
$pages[] = $this->buildPageMetadata($category, $subcategory, $widgets[$pageId]);
}
}
}
return $pages;
}
private function buildPageId($categoryId, $subcategoryId)
{
return $categoryId . '.' . $subcategoryId;
}
public function buildPageMetadata(Category $category, Subcategory $subcategory, $widgetConfigs)
{
$ca = array(
'uniqueId' => $this->buildPageId($category->getId(), $subcategory->getId()),
'category' => $this->buildCategoryMetadata($category),
'subcategory' => $this->buildSubcategoryMetadata($subcategory),
'widgets' => array()
);
foreach ($widgetConfigs as $config) {
$ca['widgets'][] = $this->buildWidgetMetadata($config);
}
return $ca;
}
}

View File

@ -0,0 +1,5 @@
{
"API": {
"LoadedAPIs": "ማስጋባት ተሳክቷል %s ኤፒአይዎች"
}
}

View File

@ -0,0 +1,14 @@
{
"API": {
"ChangeTokenHint": "فضلاً الذهاب إلى %1$sصفحة الإعدادات الشخصية%2$sعند رغبتك تغيير هذا الرمز المميَّز.",
"GenerateVisits": "إذا كنت لا تملك بيانات لليوم الحالي، فيمكنك أن تنشئ بعض البيانات باستخدام تطبيق %1$s. يمكنك تفعيل تطبيق %2$s، ثم النقر على قائمة \"مولد الزوا\" في لوحة إدارة Matomo.",
"KeepTokenSecret": "مفتاح المصادقة هذا سري كما هو الحال في اسم المستخدم ولكلمة المرور، %1$s لا تعطه لأحد قط%2$s!",
"LoadedAPIs": "تم تحميل %s واجهة تطبيقات.",
"MoreInformation": "لمزيد من المعلومات حول واجهة التطبيقات لبرنامج Matomo، الرجاء مراجعة %1$s مقدمة إلى واجهة تطبيقات Matomo %2$s وكذلك %3$sدليل واجهة تطبيقات Matomo %4$s.",
"PluginDescription": "يمكن الحصول على جميع بيانات Matomo عبر واجهات برمجية بسيطة. وهذه الإضافة هي نقطة بداية خدمة وب يمكنك استدعاؤها للحصول على بيانات تحليلات وب بصيغ xml ، json ، php ، csv وغيرها",
"TopLinkTooltip": "الوصول إلى تحليلات ويب الخاصة بك برمجياً عبر واجهة تطبيقات بسيطة API على شكل json, xml وغيرها.",
"UserAuthentication": "مصادقة المستخدم",
"UsingTokenAuth": "إذا كنت ترغب في %1$s طلب بيانات من خلال نص برمجي أو Crontab، أو غيرها %2$s فستحتاج إلى إضافة باراميتر %3$s في روابط طلبات API والتي تتطلب المصادقة.",
"Glossary": "المسرد"
}
}

View File

@ -0,0 +1,10 @@
{
"API": {
"GenerateVisits": "Калі ў вас няма дадзеных на сённяшні дзень, вы можаце стварыць некаторыя дадзеныя, выкарыстоўваючы даданы модуль %1$s. Уключыце даданы модуль %2$s, затым націсніце на \"Генератар наведванняў\" у адміністрацыйнай частцы Matomo.",
"KeepTokenSecret": "Гэта ідэнтыфікацыйны токэн, ён такі жа сакрэтны, як ваш лагін і пароль, %1$s не дзеліцеся ім ня з кім%2$s!",
"LoadedAPIs": "%s API паспяхова загружаны",
"MoreInformation": "Дадатковыя звесткі аб Matomo API, калі ласка, звярніце ўвагу на %1$s Уводзіны ў Matomo API %2$s і %3$s Matomo API спасылкі %4$s.",
"UserAuthentication": "Аўтэнтыфікацыя карыстальніка",
"UsingTokenAuth": "Калі вы жадаеце %1$s запытаць дадзеныя ў рамках скрыпта, кронтаба і г.д. %2$s. Вам патрабуецца дадаць параметр %3$s да API каб выклікаць URL-адрасоў, якія патрабуюць праверкі сапраўднасці."
}
}

View File

@ -0,0 +1,11 @@
{
"API": {
"GenerateVisits": "Ако не разполагате с данни за днес можете да генерирате такива с помощта на добавката %1$s. Вие можете да активирате добавката %2$s, след което натиснете на „Генератор на посещения“, намиращ се в менюто на администраторската среда на Matomo.",
"KeepTokenSecret": "Това token_auth е тайна, като Вашето потребителско име и парола, %1$s не го споделяйте%2$s!",
"LoadedAPIs": "Успешно заредени %s API-та",
"MoreInformation": "За повече информация за Matomo API-тата, моля погледнете %1$s Въведение в Matomo API%2$s и %3$s Matomo API Референт%4$s.",
"TopLinkTooltip": "Информацията за уеб анализите може да бъде достъпена чрез прост приложно-програмен интерфейс в json, xml и др. формат.",
"UserAuthentication": "Удостоверяване на потребителя",
"UsingTokenAuth": "Ако искате да %1$s вмъкнете данните със скрипт, crontab, др. %2$s трябва да добавите параметър %3$s на API кода за повиквания на URL адреси, които изискват удостоверяване."
}
}

View File

@ -0,0 +1,7 @@
{
"API": {
"GenerateVisits": "Ako nemate podataka za danas onda možete prvo generisati neke podatke sa dodatkom %1$s. Ovo možete uraditi ako uključite dodatak %2$s i zatim kliknete na 'Proizvođač posjetilaca' u meniju koja se nalazi u Matomo adminskom prostoru.",
"KeepTokenSecret": "Ovaj token_auth je povjerljiv podatak poput vašeg korisničkog imena i lozine. %1$s Nemojte ga dijeliti sa drugima%2$s!",
"LoadedAPIs": "Uspješno učitani %s API(-ovi)"
}
}

View File

@ -0,0 +1,11 @@
{
"API": {
"GenerateVisits": "Si no disposeu d'informació d'avui podeu generar informació utilitzant l'extensió: %1$s. Heu d'activar l'extensió %2$s i desprès anar al menú 'Generador de visites' de l'espai d'administració del Matomo.",
"KeepTokenSecret": "El token_auth es tan secret com el vostre usuari i la vostra contrasenya, %1$s no compartiu el seu %2$s!",
"LoadedAPIs": "S'ha carregat correctament un total de %s API",
"MoreInformation": "Per mes informació sobre les APIs de Matomo, siusplau reviseu %1$s Introducció a l'API de Matomo %2$s i %3$s la Referència de l'API de Matomo %4$s.",
"TopLinkTooltip": "Accediu a la vostra informació de l'anàlisis Web d'una forma programada a través d'una API simple en json, xml, etc.",
"UserAuthentication": "Autentificació de l'usuari",
"UsingTokenAuth": "Si voleu %1$s obtenir informació a través d'un script, un crontab, etc %2$s heu d'afegir el paràmetre %3$s a les crides a la APU per les URLs que requereixen autentificació."
}
}

View File

@ -0,0 +1,15 @@
{
"API": {
"ChangeTokenHint": "Pokud chcete tento token změnit, %1$spřejdtě na svou stránku s osobními nastaveními%2$s",
"GenerateVisits": "Pokud pro dnešek nemáte data, můžete je nejprve vygenerovat pomocí zásuvného modulu %1$s. Můžete povolit zásuvný modul %2$s a poté kliknout na 'Generátor návštěv' v menu v administrační části Matomou",
"KeepTokenSecret": "Tento token_auth je tajný jako vaše uživatelské jméno a heslo, %1$s neříkejte jej nikomu jinému %2$s!",
"LoadedAPIs": "Úspěšně načteno %s API",
"MoreInformation": "Pro více informací o API Matomou se podívejte na %1$s Úvod do API Matomou %2$s a %3$s Referenci API Matomou %4$s",
"PluginDescription": "Všechna data v Matomou jsou dostupná pomocí jednoduchých API. Tento zásuvný modul je vstupním bodem těchto webových služeb, který vám umožňuje získat vaše analytická data jako XML, JSON, CSV, PHP atd.",
"ReportingApiReference": "Reference API hlášení",
"TopLinkTooltip": "Zpřístupněte svoje Webové analýzy programově skrze jednoduché API pomocí json, xml a dalších.",
"UserAuthentication": "Autentifikace uživatele",
"UsingTokenAuth": "Pokud chcete %1$s načíst data ze skriptu, cronu, atd. %2$s Potřebujete přidat parametr %3$s k voláním API, které vyžadují přihlášení",
"Glossary": "Glosář"
}
}

View File

@ -0,0 +1,16 @@
{
"API": {
"ChangeTokenHint": "Hvis du ønsker at ændre dette token, så gå til din %1$sside for personlige indstillingere%2$s.",
"GenerateVisits": "Hvis du ikke har data for i dag ,kan du generere nogle data ved hjælp af %1$s programudvidelsen. Aktiver %2$s programudvidelsen, og klik derefter på 'Besøgsgenerator' i menuen under indstillinger.",
"KeepTokenSecret": "Token_auth er ligeså hemmeligt som brugernavn og adgangskode, %1$sdel det ikke ud%2$s!",
"LoadedAPIs": "%s APIs indlæst",
"MoreInformation": "Mere information om Matomo API'er, findes på %1$sIntroduktion til Matomo API%2$s og %3$sMatomo API Reference%4$s.",
"PluginDescription": "Alle data i Matomo er tilgængelig gennem enkle API'er. Denne udvidelse er en webservice indgang, som kaldes for at få Web Analytics-data i xml, JSON, php, csv etc.",
"ReportingApiReference": "Rapporterings API Reference",
"TopLinkTooltip": "Få adgang til dine webanalyse data programmeringsmæssigt gennem en simpel API i JSON, XML, etc.",
"UserAuthentication": "Brugergodkendelse",
"UsingTokenAuth": "Hvis du ønsker at %1$s hente data i et script, med crontab, m.m. %2$s skal du tilføje parameteren %3$s til API-kaldes netadresse, som kræver godkendelse.",
"Glossary": "Ordliste",
"LearnAboutCommonlyUsedTerms2": "Forstå de hyppigst anvendte ord for at få mest muligt ud af Matomo."
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "Wenn Sie dieses Token ändern wollen, gehen Sie bitte zu Ihren %1$sBenutzereinstellungen%2$s.",
"GenerateVisits": "Sollten für heute keine Daten verfügbar sein, so können Sie mit dem %1$s Plugin Testdaten erzeugen. Aktivieren Sie das %2$s Plugin und klicken dann auf das 'Besuchergenerator'-Menu in der Matomo-Administration.",
"KeepTokenSecret": "Der token_auth ist so geheim wie Ihr Login und Passwort, %1$s teilen Sie es niemandem mit%2$s!",
"LoadedAPIs": "%s APIs erfolgreich geladen",
"MoreInformation": "Für weitere Informationen über die Matomo-APIs lesen Sie bitte %1$s Einführung in die Matomo-API %2$s und die %3$s Matomo-API-Referenz %4$s.",
"PluginDescription": "Alle Daten in Matomo sind über einfache APIs verfügbar. Dieses Plugin ist der Web Service Eingangspunkt, welchen Sie nutzen können um Daten der Webanalyse in XML, JSON, PHP, CSV usw. abzufragen.",
"ReportingApiReference": "Reporting-API-Referenz",
"TopLinkTooltip": "Greife auf die Webanalytikdaten über eine einfache API mit json, xml, usw. zu.",
"UserAuthentication": "Benutzerauthentifizierung",
"UsingTokenAuth": "Wenn Sie %1$s Daten mit einem Script, einem Crontab, etc. abrufen wollen, %2$s müssen Sie den Parameter %3$s an die URLs anhängen, deren API-Aufrufe eine Authentifizierung benötigen.",
"Glossary": "Glossar",
"LearnAboutCommonlyUsedTerms2": "Lernen Sie mehr über die häufig verwendeten Begriffe, um das Beste aus Ihren Matomo Analysen zu machen.",
"EvolutionMetricName": "%s Entwicklung"
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "Αν επιθυμείτε να αλλάξετε το σύμβολο, πηγαίνετε στη %1$sπροσωπική σας σελίδα ρυθμίσεων%2$s.",
"GenerateVisits": "Αν δεν έχετε δεδομένα για σήμερα μπορείτε να δημιουργήσετε χρησιμοποιώντας το πρόσθετο %1$s. Μπορείτε να ενεργοποιήσετε το πρόσθετο %2$s, μετά να επιλέξετε το μενού «Παραγωγή Επισκεπτών» στην περιοχή Διαχείρισης του Matomo.",
"KeepTokenSecret": "Αυτό το token_auth είναι μυστικό όπως και το όνομα χρήστη και συνθηματικό σας, %1$sγια το λόγο αυτό, μην το δίνετε πουθενά%2$s!",
"LoadedAPIs": "Φορτώθηκαν επιτυχώς %s APIs",
"MoreInformation": "Για περισσότερες πληροφορίες σχετικά με τα APIs του Matomo, δείτε στην %1$s Εισαγωγή για το Matomo API %2$s και την %3$s Παραπομπή για το Matomo API %4$s.",
"PluginDescription": "Όλα τα δεδομένα στο Matomo είναι διαθέσιμα μέσω απλών API. Το πρόσθετο αυτό είναι ένα σημείο εισόδου μιας υπηρεσίας ιστού, που μπορείτε να καλείτε για να λαμβάνετε τα δεδομένα αναλυτικών σας σε μορφή xml, json, php, csv, κτλ.",
"ReportingApiReference": "Παραπομπή API Αναφορών",
"TopLinkTooltip": "Προσπελάστε τα δεδομένα Στατιστικών Ιστού προγραμματιστικά μέσω μιας απλής εφαρμογής σε json, xml, κλπ.",
"UserAuthentication": "Πιστοποίηση χρήστη",
"UsingTokenAuth": "Αν θέλετε να %1$s ζητήσετε δεδομένα από ένα σενάριο, μια εργασία crontab, κτλ. %2$s χρειάζεται να προσθέσετε την παράμετρο %3$s στα URLs των κλήσεων API που απαιτούν αυθεντικοποίηση.",
"Glossary": "Γλωσσάρι",
"LearnAboutCommonlyUsedTerms2": "Μάθετε σχετικά με τους συχνότερα χρησιμοποιούμενους όρους για να εκμεταλλευτείτε στο έπακρον το Matomo Analytics.",
"EvolutionMetricName": "%s Πρόοδος"
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "If you want to change this token, please go to your %1$spersonal settings page%2$s.",
"GenerateVisits": "If you don't have data for today you can first generate some data using the %1$s plugin. You can enable the %2$s plugin, then click on the 'Visitor Generator' menu in the Matomo Admin area.",
"KeepTokenSecret": "This token_auth is as secret as your login and password, %1$s do not share it%2$s!",
"LoadedAPIs": "Successfully loaded %s APIs",
"MoreInformation": "For more information about the Matomo APIs, please have a look at the %1$s Introduction to Matomo API %2$s and the %3$s Matomo API Reference %4$s.",
"PluginDescription": "All the data in Matomo is available through simple APIs. This plugin is the web service entry point, that you can call to get your Web Analytics data in xml, json, php, csv, etc.",
"ReportingApiReference": "Reporting API Reference",
"TopLinkTooltip": "Access your Web Analytics data programmatically through a simple API in json, xml, etc.",
"UserAuthentication": "User authentication",
"UsingTokenAuth": "If you want to %1$s request data within a script, a crontab, etc. %2$s you need to add the parameter %3$s to the API calls URLs that require authentication.",
"Glossary": "Glossary",
"LearnAboutCommonlyUsedTerms2": "Learn about the commonly used terms to make the most of Matomo Analytics.",
"EvolutionMetricName": "%s Evolution"
}
}

View File

@ -0,0 +1,12 @@
{
"API": {
"ChangeTokenHint": "Si querés cambiar esta clave, por favor, andá a tu %1$spágina personal de configuración%2$s.",
"KeepTokenSecret": "Esta token_auth es tan secreta como tu nombre de usuario y contraseña, %1$s ¡no la compartás%2$s!",
"LoadedAPIs": "Se cargaron %s APIs exitosamente",
"ReportingApiReference": "Informe de referencia de API",
"TopLinkTooltip": "Accedé a tus datos de análisis web programáticamente, a través de una sencilla API en JSON, XML, etc.",
"UserAuthentication": "Autenticación de usuario",
"UsingTokenAuth": "Si querés %1$s solicitar datos dentro de un script, un crontab, etc %2$s tenés que agregar el parámetro %3$s a las direcciones web de llamadas a la API que requiere autenticación.",
"Glossary": "Glosario"
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "Si quieres cambiar este token, por favor ve hacia tu %1$s página de ajustes de usuario %2$s",
"GenerateVisits": "Si no tiene los datos de hoy puede generar primero algunos datos mediante el complemento %1$s. Puede habilitar el complemento %2$s, después haga clic en el menú 'Generador de Visitas' en el área Administración de Matomo.",
"KeepTokenSecret": "Este token_auth es tan secreto como su nombre de usuario y contraseña, %1$s no lo comparta %2$s!",
"LoadedAPIs": "Se han cargado %s APIs exitosamente",
"MoreInformation": "Para mayor información acerca de las APIs de Matomo, por favor lea la %1$s Introducción a las API Matomo %2$s y la %3$s Referencias de las API Matomo %4$s.",
"PluginDescription": "Toda la información en Matomo está disponible a través de simples APIs. Este complemento es el punto de entrada del servicio de internet, al que puede convocar para obtener la información analítica de internet sea en formato xml, json, php, csv, etc.",
"ReportingApiReference": "Informando Referencia API",
"TopLinkTooltip": "Acceda a sus datos de análisis de internet programáticamente, a través de una sencilla API en json, xml, etc.",
"UserAuthentication": "Autenticación de usuario",
"UsingTokenAuth": "Si quieres %1$s solicitar datos dentro de un script, un crontab, etc. %2$s tiene que agregar el parámetro %3$s a las llamadas a la URL de la API que requieren autenticación.",
"Glossary": "Glosario",
"LearnAboutCommonlyUsedTerms2": "Conozca más acerca de los términos más utilizados para maximizar sus analíticas Matomo.",
"EvolutionMetricName": "Evolución %s"
}
}

View File

@ -0,0 +1,8 @@
{
"API": {
"LoadedAPIs": "Edukalt laetud %s API-id",
"MoreInformation": "Et saada rohkem infot Matomou API-st, vaata %1$sMatomou API tutvustus%2$s ja %3$sMatomou API juhend%4$s.",
"UserAuthentication": "Kasutaja autentimine",
"Glossary": "Sõnastik"
}
}

View File

@ -0,0 +1,5 @@
{
"API": {
"LoadedAPIs": "%s API ondo kargatu dira"
}
}

View File

@ -0,0 +1,11 @@
{
"API": {
"ChangeTokenHint": "برای تغییرات این توکن، لطفا به %1$sصفحهی تنظیمات شخصی%2$s خود مراجعه کنید.",
"KeepTokenSecret": "این token_auth برای رمزعبور و ورود شما مثل یک راز است , %1$s آن را به کسی نگویید %2$s!",
"LoadedAPIs": "API های %s با موفقیت بارگزاری شدند",
"TopLinkTooltip": "با استفاده از یک API ساده به اطلاعات آماری وب خود از طریق کدنویسی در فرمت های json و xml و غیره دسترسی پیدا کنید.",
"UserAuthentication": "تأیید هویت کاربر",
"Glossary": "واژه‌ نامه",
"LearnAboutCommonlyUsedTerms2": "برای استفاده‌ی هر چه بیشتر از داده‌های تحلیلی متومو، درباره‌ی واژه‌های کاربردی مطالعه کنید."
}
}

View File

@ -0,0 +1,15 @@
{
"API": {
"ChangeTokenHint": "Jos haluat vaihtaa tämän avaimen, mene %1$shenkilökohtaiselle asetussivullesi%2$s.",
"GenerateVisits": "Jos sinulla ei ole yhtään tietoja tälle päivälle, voit generoida satunnaisia tietoja käyttämällä lisäosaa %1$s. Voit ottaa käyttöön %2$s-lisäosan ja painaa 'Vierailijoiden luominen'-linkkiä Matomon ylläpitosivulla.",
"KeepTokenSecret": "Tämä token_auth on yhtä salainen kuin salasanasi, %1$s älä jaa sitä%2$s!",
"LoadedAPIs": "Ladattiin %s APIa",
"MoreInformation": "Lisätietoa Matomon APIsta löytyy sivulta %1$sJohdatus Matomon APIin%2$s ja %3$sMatomon API%4$s.",
"PluginDescription": "Kaikki Matomossa olevat tiedot on saatavilla yksinkertaisten APIen kautta. Tämä lisäosa on API verkkosivuille. Tämän lisäosan kautta voit ladata tiedot xml:nä, json:na, php:nä, csv:nä jne.",
"ReportingApiReference": "Raportointi-API:n referenssi",
"TopLinkTooltip": "Hae analytiikkatietoja automaattisesti yksinkertaisella API:lla JSON:lla, XML:llä jne.",
"UserAuthentication": "Käyttäjän autentikointi",
"UsingTokenAuth": "Jos haluat %1$s hakea tietoja skriptillä, crontabista jne. %2$s sinun täytyy lisätä parametri %3$s API-kutsujen osoitteisiin, jos kutsu vaatii autentikoinnin.",
"Glossary": "Sanasto"
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "Si vous souhaitez modifier ce jeton, veuillez vous rendre sur votre %1$spage de paramètres personnels%2$s.",
"GenerateVisits": "Si vous n'avez pas de données pour aujourd'hui vous pouvez en générer en utilisant le plugin %1$s. Vous pouvez activer le plugin %2$s puis cliquer sur le menu 'Générateur de visites' dans la zone d'administration de Matomo.",
"KeepTokenSecret": "Cette clef partagée (tocken_auth) est aussi secrète que votre login et mot de passe, %1$s ne la partagez pas%2$s !",
"LoadedAPIs": "%s API chargées avec succès",
"MoreInformation": "Pour plus d'informations à propos de l'API de Matomo, merci de visiter %1$s l'introduction à Matomo %2$s et %3$s la référence de l'API Matomo %4$s.",
"PluginDescription": "Toutes les données de Matomo sont disponibles via des API simples. Ce composant est le service web que vous pouvez appeler afin d'obtenir vos données d'analyse web en xml, json, php, csv, etc",
"ReportingApiReference": "Référence de l'API de rapports",
"TopLinkTooltip": "Accédez à vos données de statistiques web depuis votre code via une API simple en JSON, XML, Etc.",
"UserAuthentication": "Authentification de l'utilisateur",
"UsingTokenAuth": "Si vous souhaitez %1$s récupérer des données depuis un script, un crontab, etc %2$s vous devez ajouter le paramètre %3$s aux appels d'URLs de l'API qui requièrent une authentification.",
"Glossary": "Glossaire",
"LearnAboutCommonlyUsedTerms2": "Apprenez en plus sur les termes utilisés communément pour tirer le meilleur parti des statistiques Matomo.",
"EvolutionMetricName": "%s Evolution"
}
}

View File

@ -0,0 +1,6 @@
{
"API": {
"LoadedAPIs": "%s APIs foro cargadas con éxito",
"UserAuthentication": "Autenticación do usuario"
}
}

View File

@ -0,0 +1,10 @@
{
"API": {
"GenerateVisits": "אם אין לך מידע להיום ניתן קודם כל לחולל מידע בעזרת התוסף %1$s. ניתן להפעיל את התוסף %2$s, ולאחר מכן לחולל מידע על מבקרים דרך פאנל הניהול של Matomo.",
"KeepTokenSecret": "הtoken_auth סודי ביותר כמו שם המשתמש והסיסמה, %1$s אין לשתף אף אחד בפרטים אלו%2$s!",
"LoadedAPIs": "%s APIים נטענו בהצלחה",
"MoreInformation": "למידע נוסף עבור הAPIים של Matomo, מומלץ להציץ ב%1$sהיכרות עם הAPI של Matomo%2$s וגם ב%3$sהעמקה אודות הAPI של Matomo%4$s.",
"UserAuthentication": "אימות משתמש",
"UsingTokenAuth": "אם ברצונך %1$s לדרוש מידע מתוך סקריטפ, עבודה כרונית וכד' %2$s יש צורך להוסיף את הפרמטר %3$s לכל קריאת API מהURLים שדורשים אימות."
}
}

View File

@ -0,0 +1,11 @@
{
"API": {
"GenerateVisits": "आप आज के लिए डेटा नहीं है, तो आप पहले %1$s प्लगइन का उपयोग कर कुछ डेटा उत्पन्न कर सकते हैं. आप %2$s प्लगइन सक्षम कर सकते हैं, तो Matomo व्यवस्थापक क्षेत्र में 'आगंतुक जेनरेटर' मेनू पर क्लिक करें.",
"KeepTokenSecret": "इस टोकन प्रमाणन अपने लॉगिन और पासवर्ड के रूप में गुप्त रूप है, %1$s %2$s यह साझा नहीं करते हैं!",
"LoadedAPIs": "सफलतापूर्वक लोड %s एपीआई",
"MoreInformation": "Matomo एपीआई के बारे में अधिक जानकारी के लिए,कृपया %1$s Matomo एपीआई %2$s और %3$s Matomo एपीआई संदर्भ %4$s के परिचय पर एक नजर डाले",
"TopLinkTooltip": "Json, xml, आदि में एक सरल एपीआई के माध्यम से प्रोग्राम के रूप में अपने वेब विश्लेषिकी डेटा का उपयोग करे",
"UserAuthentication": "प्रयोगकर्ता का प्रामाणीकरण",
"UsingTokenAuth": "आप एक स्क्रिप्ट के भीतर %1$s डेटा, एक crontab, आदि %2$s के लिए अनुरोध करना चाहते हैं तो प्रमाणीकरण की आवश्यकता है कि यूआरएल कॉल आप एपीआई के लिए पैरामीटर %3$s जोड़ने की जरूरत है."
}
}

View File

@ -0,0 +1,6 @@
{
"API": {
"GenerateVisits": "Ako ne postoje podaci za danas, možete ih kreirati pomoću %1$s plugina. Prvo uključite %2$s plugin, zatim koristite opciju 'Kreiraj posjetitelje' u izborniku Matomo administracije.",
"LoadedAPIs": "%s API je uspješno učitan"
}
}

View File

@ -0,0 +1,10 @@
{
"API": {
"GenerateVisits": "Ha nem volt még ma látogató, automatikusan generálhatsz mintaadatokat a %1$s kiegészítő segítségével. A %2$s kiegészítőt a Matomo adminisztrációs felületén megjelenő Látogatógenerálás menüpontra kattintva engedélyezheted.",
"KeepTokenSecret": "Ez a token_auth nevű kód pontosan annyira érzékeny adat mint a felhasználói neved és jelszavad, ezért %1$s ne oszd meg mindenkivel%2$s!",
"LoadedAPIs": "A(z) %s API sikeresen betöltődött.",
"MoreInformation": "További információkért a Matomo API-kkal kapcsolatban kérjük, tekintse meg a %1$s Introduction to Matomo API %2$s és a %3$s Matomo API Reference %4$s című leírásokat.",
"UserAuthentication": "Felhasználó autentikációja",
"UsingTokenAuth": "Ha azt szeretné, hogy a %1$s külső alkalmazás adatot kérjen a szkripten belül, a crontab segítségével, stb. %2$s hozzá kell adnia a %3$s paramétert az API hívásoknál használt URL-ekhez, ha autentikáció szükséges számukra."
}
}

View File

@ -0,0 +1,15 @@
{
"API": {
"ChangeTokenHint": "Jika Anda ingin mengganti token ini, silakan menuju %1$s pengaturan halaman Anda %2$s.",
"GenerateVisits": "Bila Anda tidak memiliki data untuk hari ini, Anda dapat membuat beberapa menggunakan pengaya %1$s. Anda dapat mengaktifkan pengaya %2$s, lalu klik menu 'Pembangkit Kunjungan' di halaman Pengurus Matomo.",
"KeepTokenSecret": "token_auth ini merupakan hal yang rahasia sebagaimana nama-id dan sandi Anda, %1$s sehingga jangan memberitahukannya%2$s!",
"LoadedAPIs": "Berhasil memuat API %s",
"MoreInformation": "Untuk informasi selengkapnya tentang API Matomo, silakan melihat %1$s Pengenalan API Matomo%2$s dan %3$sReferensi API Matomo%4$s.",
"PluginDescription": "Semua data di Matomo tersedia melalui API sederhana. Plugin ini adalah titik masuk layanan web, yang dapat Anda hubungkan untuk mendapatkan data Web Analytics Anda dalam bentuk xml, json, php, csv, dll.",
"ReportingApiReference": "Referensi Pelaporan API",
"TopLinkTooltip": "Akses data Analisis Ramatraya terprogram melalui API sederhana dalam json, xml, dan lain lain.",
"UserAuthentication": "Otentikasi pengguna",
"UsingTokenAuth": "Jika Anda ingin melakukan permintaan data %1$s menggunakan skrip, crontab, dll. %2$s Anda harus menambah parameter %3$s agar API memanggil URL yang membutuhkan otentikasi.",
"Glossary": "Glosarium"
}
}

View File

@ -0,0 +1,10 @@
{
"API": {
"GenerateVisits": "Ef þú hefur ekki gögn dagsins í dag verður þú fyrst að búa til gögn með að nota %1$s íbótina. Virkjaðu %2$s íbótina og smelltu síðan á valmyndinni 'Gesta framleiðni' í Matomo umsjónarmanns svæðinu.",
"KeepTokenSecret": "Þessi tókar auðkenning er eins leynd og þitt notendanafn og lykilorð %1$s deilið því ekki%2$s!",
"LoadedAPIs": "Tókst að hlaða %s API",
"MoreInformation": "Fyrir frekari upplýsingar um Matomo API, Vinsamlegast skoðið %1$s Inngangur að Matomo API %2$s og %3$s Matomo API Tilvísunir %4$s.",
"UserAuthentication": "Notenda auðkenning",
"UsingTokenAuth": "Ef þú vilt að %1$s biðji um gögn inn í skriftu eða crontab færslu osfv. %2$s verður þú að bæta við færibreytu %3$s til API kalls vefslóðir sem krefst auðkenningar."
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "Se desideri cambiare questo token, vai alla tua %1$spagina delle impostazioni personali%2$s.",
"GenerateVisits": "Se oggi non hai ricevuto dati, puoi generarli usando il plugin %1$s. Abilita il plugin %2$s, quindi clicca su \"Generatore di Visite\" nell'area amministrazione Matomo.",
"KeepTokenSecret": "Questo token_auth è segreto come il tuo login e la tua password, %1$s non condividerlo %2$s!",
"LoadedAPIs": "Sono state caricate con successo %s API",
"MoreInformation": "Per ulteriori informazioni sulle API di Matomo, si prega di dare un'occhiata alla %1$s Introduzione alle API di Matomo %2$s e alla %3$s Guida di Riferimento delle API di Matomo %4$s.",
"PluginDescription": "Tutti i dati di Matomo sono disponibili tramite semplici API. Questo plugin è la porta d'accesso al servizio web che puoi utilizzare per avere i dati delle tue statistiche web in xml, json, php, csv, ecc.",
"ReportingApiReference": "Segnalazione Riferimenti API",
"TopLinkTooltip": "Accedi ai tuoi dati di Web Analytics tramite le semplici API in JSON, XML, ecc",
"UserAuthentication": "Autenticazione utente",
"UsingTokenAuth": "Se si vogliono i dati di richiesta %1$s all'interno di uno script, un crontab, ecc %2$s è necessario aggiungere il parametro %3$s per le chiamate API URL che richiedono l'autenticazione.",
"Glossary": "Glossario",
"LearnAboutCommonlyUsedTerms2": "Scopri i termini comunemente utilizzati per ottenere il massimo da Matomo Analytics.",
"EvolutionMetricName": "%s Evoluzione"
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "このトークンを変更する場合は、%1$s個人設定%2$s ページに移動してください。",
"GenerateVisits": "今日のデータがない場合は、%1$s プラグインを使用して、はじめに若干のデータを生成することができます。 これを行うには、%2$s プラグインを有効にし、管理エリアで 'ビジタージェネレータ' メニューをクリックします。",
"KeepTokenSecret": "token_auth は、ログイン名とパスワードのように秘密にし、%1$s絶対に共有しないでください%2$s",
"LoadedAPIs": "%s API が正常に読み込まれました",
"MoreInformation": "Matomo API の詳細については、%1$sMatomo API 入門%2$sと%3$s Matomo API リファレンス%4$sをご覧ください。",
"PluginDescription": "Matomo のすべてのデータは、シンプルな API を介して利用できます。 このプラグインは、xml、json、php、csv などで Web Analytics データを取得するために呼び出すことのできる Web サービスのエントリポイントです。",
"ReportingApiReference": "API リファレンスレポート",
"TopLinkTooltip": "jsopn、xml 等シンプルな API を介して、プログラムで Web Analytics データにアクセスできます。",
"UserAuthentication": "ユーザー認証",
"UsingTokenAuth": "%1$sスクリプトcrontab 等)でリクエストデータを得たい場合%2$sは、API をコールする URL認証が必要にパラメータ %3$s を付加する必要があります。",
"Glossary": "用語集",
"LearnAboutCommonlyUsedTerms2": "Matomo Analytics を最大限に活用するためによく使われる用語について学びましょう。",
"EvolutionMetricName": "%s 推移"
}
}

View File

@ -0,0 +1,10 @@
{
"API": {
"GenerateVisits": "თუ დღეისთვის არ გაქვთ მონაცემები, შეგიძლიათ ჯერ %1$s პლაგინის გამოყენებით დააგენერიროთ რამდენიმე მონაცემი. შეგიძლიათ ჩართოთ პლაგინი %2$s, შემდეგ Matomo ადმინისტრირების ზონაში დააწკაპუნოთ \"ვიზიტორების გენერატორი\" მენიუზე.",
"KeepTokenSecret": "ეს token_auth ისევე საიდუმლოა, როგორც თქვენი მომხმარებლის სახელი და პაროლი, %1$s არავის გაუზიაროთ ის%2$s!",
"LoadedAPIs": "%s API ფუნქციები წარმატებით ჩაიტვირთა",
"MoreInformation": "Matomo API ფუნქციების შესახებ დამატებითი ინფორმაციისთვის გთხოვთ, გადახედოთ მასალებს %1$s Matomo API ფუნქციბის გამოყენების ინსტრუქცია %2$s და %3$s Matomo API ფუნქციების ცნობარი %4$s.",
"UserAuthentication": "მომხმარებლის აუტენთიფიკაცია",
"UsingTokenAuth": "თუ გსურთ %1$s გააკეთოთ მონაცემების მოთხოვნა სკრიპტიდან, crontab ფაილიდან და სხვ. %2$s თქვენ უნდა დაამატოთ %3$s პარამეტრი API ფუნქციის გამოძახების URLებს, რაც მოითხოვს აუტენთიფიკაციას."
}
}

View File

@ -0,0 +1,15 @@
{
"API": {
"ChangeTokenHint": "만약 이 토큰을 변경하고 싶으시다면, %1$s 에 있는 개인 설정페이지 %2$s 에서 변경하실 수 있습니다.",
"GenerateVisits": "오늘에 해당하는 데이터가 없는 경우는 %1$s 플러그인을 사용하여 처음 약간의 데이터를 생성할 수 있습니다. %2$s 플러그인을 활성화하고 관리 영역에서 '방문자 생성기'메뉴를 클릭하세요.",
"KeepTokenSecret": "token_auth는 외부 로그인을 위한 비밀키입니다. %1$s 절대로 공유하지 마세요 %2$s!",
"LoadedAPIs": "성공적으로 %s API를 불러옴",
"MoreInformation": "Matomo API에 대한 자세한 내용은 %1$sIntroduction to Matomo API %2$s 문서와 %3$sMatomo API Reference%4$s 문서를 참조하세요.",
"PluginDescription": "Matomo 내 모든 데이터는 간단한 API를 통해서 접근할 수 있습니다. 이 플러그인은 웹 분석 데이터를 xml, json, php, csv 등의 형태로 받을 수 있는 웹 서비스 진입로입니다.",
"ReportingApiReference": "API 레퍼런스 보고",
"TopLinkTooltip": "JSON, XML 등의 간단한 API를 통해 프로그래밍 방식으로 웹 로그 분석 데이터에 접근할 수 있습니다.",
"UserAuthentication": "사용자 인증",
"UsingTokenAuth": "%1$s 스크립트 (crontab 등)에서 요청 데이터를 얻고 싶다면 %2$s는 API를 호출하는 URL (인증 필요)에 매개 변수 %3$s를 추가해야합니다.",
"Glossary": "용어"
}
}

View File

@ -0,0 +1,10 @@
{
"API": {
"GenerateVisits": "Jei neturite duomenų šiandienai , galite sugeneruoti jų pasinaudoję %1$s papildiniu. Įjunkite šį %2$s papildinį, tada spauskite ant 'Apsilankymų generatorius' meniu Matomo administratoriaus srityje.",
"KeepTokenSecret": "Šis token_auth yra slaptas kaip ir naudotojo vardas bei slaptažodis, %1$s neviešinkite jo%2$s!",
"LoadedAPIs": "Sėkmingai įkrautos API sąsajos (%s).",
"MoreInformation": "Norinčius gauti daugiau informacijos apie Matomo API sąsajas, prašome žvilgtelėti į %1$s Įvadas į Matomo API %2$s ir %3$s Matomo API informacija %4$s.",
"UserAuthentication": "Naudotojo autentifikavimas",
"UsingTokenAuth": "Jei norite %1$s gauti duomenis scenarijų, crontab ir pan. įrankių pagalba, %2$s turite pridėti parametrus %3$s į API užklausų URLs, kurios reikalauja autentifikacijos."
}
}

View File

@ -0,0 +1,5 @@
{
"API": {
"UserAuthentication": "Lietotāja autentifikācija"
}
}

View File

@ -0,0 +1,14 @@
{
"API": {
"GenerateVisits": "Hvis du ikke har data for i dag kan du først generere noe data med utvidelsen %1$s. Du kan aktivere %2$s, så klikker du på «Besøksgenerator»-menyen i Matomo admin.",
"KeepTokenSecret": "Denne token_auth er like hemmelig som ditt brukernavn og passord. %1$sIkke del den!%2$s",
"LoadedAPIs": "Lastet %s API-er",
"MoreInformation": "For mer informasjon om Matomo API-er, vennligst se på %1$s Introduction to Matomo API %2$s og %3$s Matomo API Reference %4$s.",
"PluginDescription": "Alle data i Matomo er tilgjengelig gjennom enkle API-er. Denne utvidelsen er web-tjenestens inngangspunkt, som du kan kalle opp for å få ut dine data i xml, json, php, csv, etc.",
"ReportingApiReference": "Referanse for rapporterings-API",
"TopLinkTooltip": "Få tilgang til dine analysedata programmatisk gjennom en enkel API i json, xml, etc.",
"UserAuthentication": "Brukerautentisering",
"UsingTokenAuth": "Hvis du vil %1$s spørre etter data i et script, en crontab, etc. %2$s må du legge til parameteret %3$s til API-spørringer som krever autentisering.",
"Glossary": "Ordliste"
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "Als u deze token wilt wijzigen, gaat u naar uw %1$spersoonlijke instellingen-pagina%2$s.",
"GenerateVisits": "Als u geen data hebt voor vandaag, kunt u eerst wat data genereren met de %1$s plugin. U kunt de %2$s plugin inschakelen, en dan klikken op het 'Bezoekers Generator' menu in het Matomo beheer gedeelte.",
"KeepTokenSecret": "Deze token_auth is even geheim als uw login en wachtwoord, %1$sdeel het met niemand%2$s!",
"LoadedAPIs": "%s API's succesvol geladen",
"MoreInformation": "Voor meer informatie over de Matomo API's, lees de %1$s introductie van de Matomo API %2$s en de %3$s Matomo API referenties %4$s.",
"PluginDescription": "Alle data in Matomo is beschikbaar via eenvoudige API's. Deze plugin is het web service contactpunt, waarmee je je Web Analytics data in xml, json, php, csv, etc. kunt verkrijgen.",
"ReportingApiReference": "Rapportage API verwijzing",
"TopLinkTooltip": "Benader je Web Analyse data via een simpele API in json, xml, enz.",
"UserAuthentication": "Gebruikers authenticatie",
"UsingTokenAuth": "Als je data wil %1$s opvragen via een script, een cronjob, enz. waarvoor authenticatie vereist is, %2$s moet u de parameter %3$s aan de API aanvraag URL's toevoegen",
"Glossary": "Woordenlijst",
"LearnAboutCommonlyUsedTerms2": "Leer over de standaard gebruikte termen om het meeste uit Matomo Analytics te halen.",
"EvolutionMetricName": "%s Trends"
}
}

View File

@ -0,0 +1,5 @@
{
"API": {
"LoadedAPIs": "Lasta vellykka %s APIar"
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "W celu zmiany tokenu, proszę odwiedź %1$s strony ustawień osobistych %2$s.",
"GenerateVisits": "Jeżeli nie posiadasz żadnych danych do dnia dzisiejszego, możesz na początek wygenerować trochę symulowanych danych testowych używając wtyczki o nazwie %1$s. Możesz aktywować i uruchomić wtyczkę %2$s, a następnie kliknąć w menu przycisk Potwierdź w 'generatorze odwiedzin', w strefie menu Konfiguracja administratora systemu Matomo.",
"KeepTokenSecret": "Ten token_auth jest tak samo tajny jak twój login i hasło, %1$s nie upowszechniaj go%2$s!",
"LoadedAPIs": "Środowisko API załadowano %s pomyślnie",
"MoreInformation": "Aby uzyskać więcej informacji o interfejsie API statystyk Matomo, prosimy przeczytać %1$s Wprowadzenie do interfejsu API w Matomo%2$s, a także na stronie opracowania %3$s Matomo API Reference%4$s.",
"PluginDescription": "Wszystkie dane w Matomou są dostępne poprzez proste API. Ten plugin jest punktem końcowym serwisu web,",
"ReportingApiReference": "Referencje API Raportów",
"TopLinkTooltip": "Dostęp do twoich danych analitycznych programistycznie poprzez proste API w formatach json, xml itp.",
"UserAuthentication": "Uwierzytelnianie użytkownika",
"UsingTokenAuth": "Jeżeli wysuniesz żądanie %1$s ujęcia pewnych danych w statystyce za pomocą crontab, %2$s musisz dodać poniższy parametr %3$s do interfejsu API, wywołując URL-e które wymagają uwierzytelniania.",
"Glossary": "Słownik",
"LearnAboutCommonlyUsedTerms2": "Poznaj najczęściej wykorzystywane zwroty, aby jak najlepiej wykorzystać możliwości Statystyk Matomo.",
"EvolutionMetricName": "%s Zmiany"
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "Se você quiser alterar esse token, vá para a 1%1$s página de configurações pessoais 2%2$s.",
"GenerateVisits": "Se você não tem nenhum dado hoje, primeiro você pode gerar alguns dados usando o plugin %1$s. Basta ativar o plugin %2$s e clicar no menu \"Gerador de Visitantes\" na área de Admin do Matomo.",
"KeepTokenSecret": "Esse token_auth é tão secreto quanto seu login e sua senha, %1$s não compartilhe isso %2$s!",
"LoadedAPIs": "%s APIs carregadas com sucesso",
"MoreInformation": "Para mais informações sobre as APIs Matomo, por favor, dê uma olhada no %1$s Introdução para Matomo API %2$s e a %3$s API Matomo de Referência %4$s.",
"PluginDescription": "Todos os dados em Matomo estão disponíveis através da APIs simples. Este plugin é o ponto de entrada do serviço web, que você pode chamar para obter seus dados de Web Analytics em XML, JSON, php, CSV, etc.",
"ReportingApiReference": "Reportando Referência do API",
"TopLinkTooltip": "Acesse seus dados de Análise Web de forma programática através de uma API simples em json, xml, etc.",
"UserAuthentication": "Autenticação de usuário",
"UsingTokenAuth": "Se você quer %1$s requisitar dados dentro de um script, um crontab, etc. %2$s você precisa adicionar o parâmetro %3$s para a API chamar URLS que requerem autenticação.",
"Glossary": "Glossário",
"LearnAboutCommonlyUsedTerms2": "Saiba mais sobre os termos comumente usados para tirar o máximo do Matomo Analytics.",
"EvolutionMetricName": "%s Evolução"
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "Se quiser alterar este código, por favor, aceda à sua %1$spágina de definições pessoais%2$s.",
"GenerateVisits": "Se não tiver dados para hoje pode começar por gerar alguns através da extensão %1$s. Pode ativar a extensão %2$s e depois clicar no menu 'Gerador de visitantes' na área de Administração do Matomo.",
"KeepTokenSecret": "Este token_auth é tão secreto como o seu nome de utilizador e palavra-passe. %1$s não o partilhe%2$s!",
"LoadedAPIs": "%s APIs carregadas com sucesso",
"MoreInformation": "Para mais informação sobre as APIs do Matomo, por favor dê uma vista de olhos na %1$s Introdução à API do Matomo %2$s e a %3$s referência da API do Matomo %4$s.",
"PluginDescription": "Todos os dados no Matomo estão disponíveis através de APIs simples. Esta extensão é o ponto de entrada do web service que pode invocar para obter os seus dados em xml, json, php, csv, etc.",
"ReportingApiReference": "Referência da API de relatórios",
"TopLinkTooltip": "Aceda programaticamente à sua informação através de uma simples API em json, xml, etc.",
"UserAuthentication": "Autenticação do utilizador",
"UsingTokenAuth": "Se quer %1$s pedir dados através de um script, crontab, etc. %2$s tem que adicionar o parâmetro %3$s aos endereços das chamadas à API que necessitam de autenticação.",
"Glossary": "Glossário",
"LearnAboutCommonlyUsedTerms2": "Saber mais sobre os termos mais utilizados para obter o máximo do Matomo Analytics.",
"EvolutionMetricName": "Evolução de %s"
}
}

View File

@ -0,0 +1,14 @@
{
"API": {
"GenerateVisits": "Daca nu ai date pentru astazi, prima data poti genera date folosind pluginul %1$s. Poti activa pluginul %2$s, apoi apasa pe meniul 'Visitor Generator' in aria de administrare Matomo Admin.",
"KeepTokenSecret": "Acest token_auth este secret cum sunt login-ul si parola, %1$s nu le fa publice%2$s!",
"LoadedAPIs": "%s API-uri încarcate cu succes",
"MoreInformation": "Pentru mai multe informatii despre API-urile Matomo, va rugam sa va uitati la %1$s Introducere la Matomo API %2$s si la %3$s Referinte Matomo API %4$s.",
"PluginDescription": "Toate informaţiile din Matomo sunt disponibile prin simple API-uri. Acest plugin este punctul de intrare al serviciului web prin care poţi prelua date Web Analytics în format xml, json, php, csv, etc.",
"ReportingApiReference": "Referinţe API de raportare",
"TopLinkTooltip": "Acceseaza datele tale de Web Analytics in mod organizat prin intermediul unui simplu API in json, xml, etc.",
"UserAuthentication": "Identificare utilizator",
"UsingTokenAuth": "Daca vrei sa %1$s preiei date printr-un script, crontab etc. %2$s este necesar sa adaugi parametrul %3$s la URL-urile API care cer autentificare.",
"Glossary": "Glosar"
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "Если вы хотите изменить этот токен, пожалуйста, пройдите на вашу %1$sстраницу персональных настроек%2$s.",
"GenerateVisits": "Если у вас нет данных на сегодня, вы можете для начала сгенерировать немного статистики, используя плагин: %1$s. Включите плагин %2$s, кликните на Visitor Generator в панели администрирования Matomo.",
"KeepTokenSecret": "Этот token_auth является таким же секретным, как ваш логин и пароль. %1$s НЕ СООБЩАЙТЕ ЕГО НИКОМУ%2$s!",
"LoadedAPIs": "%s API успешно загружены",
"MoreInformation": "Чтобы узнать больше информации о Matomo API, пожалуйста, посмотрите раздел %1$s Introduction to Matomo API %2$s и %3$s Matomo API Reference %4$s.",
"PluginDescription": "Все данные из Matomo доступны через простые API. Этот плагин является входной точкой для веб сервиса, который можно вызвать чтобы получить данные веб аналитики в xml, json, php, csv и др.",
"ReportingApiReference": "Сообщение справочной информации по API",
"TopLinkTooltip": "Получайте доступ к вашей веб-аналитике с помощью простого API и использования json, xml и др.",
"UserAuthentication": "Аутентификация пользователя",
"UsingTokenAuth": "Если вам необходимо %1$s запрашивать данные в ваших скриптах, cron-задачах или другого источника, то вам %2$s необходимо добавить следующий ключ %3$s к URL API-вызова, который требует аутентификации.",
"Glossary": "Глоссарий",
"LearnAboutCommonlyUsedTerms2": "Узнайте о наиболее часто используемых терминах, чтобы максимально использовать Matomo Analytics.",
"EvolutionMetricName": "%s Динамика"
}
}

View File

@ -0,0 +1,6 @@
{
"API": {
"LoadedAPIs": "Úspešne načítané %s API",
"UserAuthentication": "Overenie užívateľa"
}
}

View File

@ -0,0 +1,10 @@
{
"API": {
"GenerateVisits": "Če nimate današnjih podatkov, potem lahko najprej ustvarite nekaj podatkov s %1$s vtičnikom. Vtičnik %2$s lahko omogočite in nato kliknete na 'Visitor Generator' meni v Matomo administraciji.",
"KeepTokenSecret": "Ta žeton je skriven, kot vaše uporabniško ime in geslo, %1$s ne delite ga z drugimi %2$s!",
"LoadedAPIs": "Uspešno naloženih %s API-jev",
"MoreInformation": "Za več informacij o Matomo API-ju, si oglejte %1$sNavodila za uporabo Matomo API-ja %2$s in %3$s Matomo API Reference %4$s.",
"UserAuthentication": "Overitev uporabnika",
"UsingTokenAuth": "Če želite %1$s zahtevati podatke preko skripte, crontaba, itd. %2$s potem morate dodati parameter %3$s v URL API klica za overitev."
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "Nëse doni ta ndryshoni këtë token, ju lutemi, shkoni te %1$sfaqja juaj e rregullimeve personale%2$s.",
"GenerateVisits": "Nëse skeni të dhëna për ditën e sotme, mund të prodhonit ca të dhëna duke përdorur shtojcën %1$s. Shtojcën %2$s mund ta aktivizoni, mandej klikoni mbi menunë 'Gjenerator Vizitorësh' te pjesa Përgjegjës Matomo-je.",
"KeepTokenSecret": "Ky token_auth është po aq i fshehtë sa të dhënat tuaja për hyrjen dhe fjalëkalimi juaj, %1$s mos ia jepni kujt%2$s!",
"LoadedAPIs": "U ngarkuan me sukses %s API",
"MoreInformation": "Për më tepër të dhëna rreth API-sh të Matomo-s, ju lutemi, hidhini një sy %1$s Hyrje në API-t e Matomo-s %2$s dhe %3$s Referencë API-sh Matomo %4$s.",
"PluginDescription": "Në Matomo krejt të dhënat janë të passhme përmes API-sh të thjeshta. Kjo shtojcë është pika e hyrjes për një shërbim web, të cilin mund ta vini në punë për ti pasur të dhënat tuaja të Analizave Web në format xml, json, php, csv, etj.",
"ReportingApiReference": "Referencë API Raportimesh",
"TopLinkTooltip": "Hyni programatikisht te të dhënat tuaja të Analizave Web përmes një API-je të thjeshtë json, xml, etj.",
"UserAuthentication": "Mirëfilltësim përdoruesi",
"UsingTokenAuth": "Nëse doni të %1$s kërkoni të dhëna brenda një programthi, një crontab-i, etj. %2$s lypset të shtoni parametrin %3$s te URL-të për thirrje API që lypin mirëfilltësim.",
"Glossary": "Fjalorth",
"LearnAboutCommonlyUsedTerms2": "Mësoni më tepër rreth termash të përdorur rëndom, që të përfitoni maksimumin prej Matomo Analytics.",
"EvolutionMetricName": "Ecuri e %s"
}
}

View File

@ -0,0 +1,15 @@
{
"API": {
"ChangeTokenHint": "Ukoliko želite da promenite ovaj token, molimo vas da to učinite na %1$sstranici sa ličnim podešavanjima%2$s.",
"GenerateVisits": "Ukoliko nema podataka za danas, onda ih prvo generišite pomoću dodatka %1$s. Aktivirajte dodatak %2$s a onda kliknite na 'Generator posetilaca' na stranici za Matomo administraciju.",
"KeepTokenSecret": "token_auth je poverljivi podatak poput vašeg korisničkog imena i lozinke, stoga ga %1$s nemojte nikome pokazivati%2$s!",
"LoadedAPIs": "Uspešno učitano API-ja: %s",
"MoreInformation": "Za više informacija o Matomo API-ju molimo vas da pogledate %1$s uvod u Matomo API %2$s i %3$s Matomo API referenc listu %4$s.",
"PluginDescription": "Svi podaci su u Matomo-u dostupni preko jednostavnih API-ja. Ovaj dodatak je ulazna tačka za veb servis koji možete pozivati kako biste dobili vaše analitičke podatke u XML, JSON, PHP, CSV i drugim formatima.",
"ReportingApiReference": "Referenca API-ja za izveštavanje",
"TopLinkTooltip": "Pristupite analitičkim podacima iz vašeg programa pomoću jednostavnog API-ja u json-u, xml-u itd.",
"UserAuthentication": "Autentifikacija korisnika",
"UsingTokenAuth": "Ukoliko želite da %1$s dođete do podataka putem skripta, crontaba i slično %2$s potrebno je da dodate parametar %3$s API pozivima.",
"Glossary": "Rečnik"
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "Om du vill ändra denna token gå till din %1$spersonliga inställningssida%2$s.",
"GenerateVisits": "Om du inte har data för idag så kan du först skapa några data med hjälp av %1$s plugin. Du kan aktivera %2$s plugin, klicka sedan på 'Besöksgeneratorn'-menyn i Matomo administrationsområdet.",
"KeepTokenSecret": "Detta token_auth är lika hemligt som ditt användarnamn och lösenord, %1$s dela inte med dig av detta! %2$s!",
"LoadedAPIs": "Laddade in %s API'er utan problem",
"MoreInformation": "För mer information om Matomos API'er, ta en titt i %1$s Introduction to Matomo API %2$s och %3$s Matomo API Reference %4$s.",
"PluginDescription": "Allt data i Matomo finns tillgängligt genom enkla API:er. Denna plugin är en web service entry point, vilken du kan använda för att hämta ditt Matomo-data i formaten xml, json, php, csv, etc.",
"ReportingApiReference": "Rapporterings-API referens",
"TopLinkTooltip": "Få åtkomst till webbanalysdata programmatiskt genom ett enkelt API i t.ex. json, xml etc.",
"UserAuthentication": "Användarautentisering",
"UsingTokenAuth": "Om du vill %1$s begära uppgifter inom ett skript, ett crontab-jobb, etc. %2$s måste du lägga till parametern %3$s till API-anrop webbadresser som kräver autentisering.",
"Glossary": "Ordbok",
"LearnAboutCommonlyUsedTerms2": "Lär dig mer om de vanligaste termerna för att få ut mest av Matomo Analytics.",
"EvolutionMetricName": "%s Utveckling"
}
}

View File

@ -0,0 +1,8 @@
{
"API": {
"KeepTokenSecret": "இந்த token_auth ஆனது உங்கள் கடவுச்சொல்லை போன்று இரகசியமானது. %1$s பகிர வேண்டாம்%2$s!",
"LoadedAPIs": "%s ஏபிஐ-கள் வெற்றிகரமாக ஏற்றப்பட்டன",
"TopLinkTooltip": "JSON, XML முதலிய எளிய பயன்பாட்டு நிரலாக்க இடைமுகம் மூலம் நிரலாக்கத்தின்படி உங்கள் இணைய பகுப்பாய்வு தரவு அணுக",
"UserAuthentication": "பயனர் உறுதிப்பாடு"
}
}

View File

@ -0,0 +1,5 @@
{
"API": {
"UserAuthentication": "వాడుకరి అధీకరణ"
}
}

View File

@ -0,0 +1,10 @@
{
"API": {
"GenerateVisits": "ถ้าคุณไม่มีข้อมูลสำหรับวันนี้ คุณสามารถสร้างข้อมูลบางอย่างใช้ปลั๊กอินการ %1$s เป็นครั้งแรก คุณสามารถเปิดใช้งานปลั๊กอิน %2$s แล้วคลิกที่เมนู 'ตัวสร้างผู้เข้าชม' ในพื้นที่จัดการ Matomo",
"KeepTokenSecret": "token_auth นี้จะเป็นความลับในการเข้าสู่ระบบและรหัสผ่านของคุณ %1$s ไม่แชร์ %2$s ได้",
"LoadedAPIs": "โหลด API %s สำเร็จแล้ว",
"MoreInformation": "สำหรับข้อมูลเพิ่มเติมเกี่ยวกับ Matomo APIs โปรดให้ดูที่ %1$s บทนำสู่การใช้ Matomo API %2$s และ %3$s แหล่งที่มาของ Matomo API %4$s.",
"UserAuthentication": "การรับรองความถูกต้องของผู้ใช้",
"UsingTokenAuth": "ถ้าคุณต้องการ ข้อมูลที่ร้องขอ %1$s ภายในสคริปต์ crontab ฯลฯ %2$s คุณต้องการเพิ่มพารามิเตอร์ %3$s API เรียก url ที่ต้องการการรับรองความถูกต้อง"
}
}

View File

@ -0,0 +1,11 @@
{
"API": {
"GenerateVisits": "Kung wala kang data para sa araw na ito maaari ka munang bumuo ng ilang data gamit ang %1$s na plugin. Maaari mong i-enable ang %2$s na plugin, at pagkatapos ay i-click ang 'Tagabuo ng Bisita' na menu sa Matomo Admin area.",
"KeepTokenSecret": "Ang token_auth na ito ay isang lihim gaya ng iyong login at password, %1$s huwag itong ibahagi sa iba %2$s!",
"LoadedAPIs": "Matagumpay na nai-load ang %s APIs",
"MoreInformation": "Para sa karagdagang impormasyon tungkol sa Matomo API, mangyaring tumingin sa %1$s Panimula sa Matomo API %2$s at ang %3$s Matomo API Reference %4$s.",
"TopLinkTooltip": "I-access ang iyong data ng Web Analytics programmatically sa pamamagitan ng isang simpleng API sa json, xml, atbp.",
"UserAuthentication": "Pagpapatunay sa User",
"UsingTokenAuth": "Kung nais mong %1$s mag-request ng data sa loob ng isang script, crontab, atbp. %2$s kailangan mong idagdag ang parameter na %3$s sa mga tumatawag ng API na URL na nangangailangan ng pagpapatunay."
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "Bu kodu değiştirmek isterseniz %1$skişisel ayarlar sayfanıza%2$s gidin.",
"GenerateVisits": "Bugün için bir veri yoksa %1$s uygulama ekini kullanarak sahte veriler oluşturabilirsiniz. %2$s uygulama ekini etkinleştirin. Ardından Matomo Yönetim bölümünden 'Ziyaretçi Üreteci' menüsüne tıklayın.",
"KeepTokenSecret": "Bu token_auth kodu kullanıcı adı ve parolanız kadar gizlidir. %1$s kimseyle paylaşmayın%2$s!",
"LoadedAPIs": "%s API yazılımı yüklendi",
"MoreInformation": "Matomo API yazılımı hakkında ayrıntılı bilgi almak için lütfen %1$s Matomo API Kullanımı %2$s ve %3$s Matomo API Referansı %4$s belgelerine bakın.",
"PluginDescription": "Tüm Matomo verilerine temel API kullanılarak erişilebilir. Bu uygulama eki web istatistiği verilerini xml, json, php, csv ve benzer şekillerde çağırabilen web hizmetinin başlangıç noktasıdır.",
"ReportingApiReference": "Rapor API Referansı",
"TopLinkTooltip": "Web istatistiği verilerine json, xml ve benzer yöntemlerle Temel API üzerinden erişilmesini sağlar.",
"UserAuthentication": "Kullanıcı kimlik doğrulaması",
"UsingTokenAuth": "Bir betik, zamanlanmış görev vb içerisinden %1$s veri almak isterseniz, %2$s kimlik doğrulaması gerektiren API bağlantılarına %3$s parametresini eklemelisiniz.",
"Glossary": "Sözlük",
"LearnAboutCommonlyUsedTerms2": "Matomo istatistiklerini daha iyi anlamak için kullanılan terimler hakkında bilgi alın.",
"EvolutionMetricName": "%s Gelişme"
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "Якщо ви хочете змінити цей токен, будь ласка, перейдіть в свою %1$sособисту сторінку налаштувань%2$s.",
"GenerateVisits": "Якщо на сьогодні ще немає даних то можна власноручно згенерувати дані використовуючи плагін %1$s . Щоб увімкнути плагін %2$s , клацніть \"Visitor Generator\" меню в Панелі Адміністратора Matomo.",
"KeepTokenSecret": "token_auth є секретним, на рівні з логіном та паролем, тож %1$s тримайте цю інформацію секреті%2$s!",
"LoadedAPIs": "%s API успішно завантажено",
"MoreInformation": "Для отримання детальнішої інформації про API Matomo, перегляньте %1$s Вступ до Matomo API %2$s та %3$s Довідковий матеріал по Matomo API %4$s.",
"PluginDescription": "Всі дані з Matomo доступні через прості API. Цей плагін є вхідною точкою для веб сервісу, який можна викликати щоб отримати дані веб аналітики в xml, json, php, csv та інше.",
"ReportingApiReference": "Повідомлення довідкової інформації по API",
"TopLinkTooltip": "Отримуйте доступ до вашої веб-аналітиці за допомогою простого API з використанням json, xml та іншого.",
"UserAuthentication": "Аутентифікація користоувача",
"UsingTokenAuth": "Для %1$sдоступу до інформації скриптом, програмою і т.д.%2$s треба додавати параметр %3$s до кожного виклику API якищо використовуваний URL вимагає аутентифікації.",
"Glossary": "Глосарій",
"LearnAboutCommonlyUsedTerms2": "Дізнайтеся про найбільш часто використовувані терміни, щоб максимально використати Matomo Analytics.",
"EvolutionMetricName": "%s Зміни"
}
}

View File

@ -0,0 +1,14 @@
{
"API": {
"GenerateVisits": "Nếu bạn không có dữ liệu cho ngày hôm nay, bạn có thể sinh một số dữ liệu (về người thăm website) bằng cách sử dụng plugin %1$s. Bạn có thể khởi động plugin %2$s, sau đó click vào menu \"Visitor Generator\" trên khu vực quản trị Matomo.",
"KeepTokenSecret": "token_auth này chặt chẽ như mật khẩu đăng nhập của bạn, %1$s không thể chia sẻ nó %2$s!",
"LoadedAPIs": "Nạp thành công API %s",
"MoreInformation": "Để biết thêm thông tin về các API của Matomo, vui lòng xem ở Giới thiệu %1$s về %2$s API Matomo và API %3$s Matomo Reference %4$s",
"PluginDescription": "Tất cả dữ liệu của Matomo đều tủy cập được thông qua API. Plugin này là 1 web service, từ đó bạn có thể gọi để truy cập dữ liệu dưới dạng xml, json, csv,...",
"ReportingApiReference": "Tham chiếu API báo cáo",
"TopLinkTooltip": "Truy cập dữ liệu lập trình Web Analytics của bạn thông qua một API đơn giản trong JSON, xml, vv",
"UserAuthentication": "Xác thực người dùng",
"UsingTokenAuth": "Nếu bạn muốn %1$s truy vấn dữ liệu trong script hoặc crontab, ... %2$s bạn cần phải thêm tham số %3$s vào các API call URL yêu cầu xác thực.",
"Glossary": "Thuật ngữ"
}
}

View File

@ -0,0 +1,16 @@
{
"API": {
"ChangeTokenHint": "如果你想改变这个标记,请到你的%1$s个人设置页面%2$s。",
"GenerateVisits": "如果您今天还沒有任何资料,可以先用 %1$s 插件来产生一些数据。您可以启用 %2$s 插件,然后点击在 Matomo 管理菜单下的'生成访客'选项。",
"KeepTokenSecret": "授权号 token_auth 与您的帐号和密码一样重要,%1$s请不要公开%2$s",
"LoadedAPIs": "已成功载入 %s 个 APIs",
"MoreInformation": "了解更多关于 Matomo APIs 的资讯,请访问 %1$sMatomo API 介绍%2$s 和 %3$s Matomo API 参考资料%4$s。",
"PluginDescription": "Matomo中的所有数据都可通过简单的API接口获取。这个插件是Web服务的切入点那你可以通过调用获取你想要的网站分析的数据比如XMLJSONPHPCSV等。",
"ReportingApiReference": "报告API参考",
"TopLinkTooltip": "通过一个简单的 API让您可以以 xml, json及其它格式取得网站分析的数据。",
"UserAuthentication": "身份验证",
"UsingTokenAuth": "如果您想在代码、定时任务中 %1$s 请求数据,%2$s 要在 API 调用需要认证的网址时增加参数 %3$s。",
"Glossary": "词汇表",
"LearnAboutCommonlyUsedTerms2": "了解常用术语以便更好的利用Matomo数据分析。"
}
}

View File

@ -0,0 +1,17 @@
{
"API": {
"ChangeTokenHint": "如果你要變更這個 Token請到你的%1$s個人設定頁面%2$s。",
"GenerateVisits": "如果你今天還沒有任何資料,你可以先使用 %1$s 外掛來產生一些數據。你可以啟用 %2$s 外掛,然後在 Matomo 後台管理選單中點擊「訪客產生器」。",
"KeepTokenSecret": "token_auth 如同你的帳號和密碼一樣重要,%1$s請不要公開它%2$s",
"LoadedAPIs": "已成功載入 %s 個 API",
"MoreInformation": "查看%1$sMatomo API 介紹%2$s和 %3$sMatomo API 參考資料%4$s以取得更多關於 Matomo API 的資訊。",
"PluginDescription": "Matomo 內的所有資料都能透過簡單的 API 來取得。這個外掛是網頁服務的進入點,你可以用來獲取 xml、json、php 或 csv 格式的網頁分析資料。",
"ReportingApiReference": "報表 API 參考資料",
"TopLinkTooltip": "透過簡單的 API 來取得 json、xml 等格式的網頁分析資料。",
"UserAuthentication": "使用者驗證",
"UsingTokenAuth": "如果你想%1$s經由程式碼或工作排程來請求資料。%2$s你需要新增參數 %3$s 至 API 呼叫網址來進行驗證。",
"Glossary": "詞彙表",
"LearnAboutCommonlyUsedTerms2": "瞭解更多最常使用的術語,讓 Matomo 分析發揮至極。",
"EvolutionMetricName": "%s發展趨勢"
}
}

View File

@ -0,0 +1,11 @@
.glossary .table-of-contents a {
box-sizing: content-box;
padding: 0 10px 5px;
&:hover, &.active {
text-decoration: none;
padding: 0 10px 3px;
border-left: none;
border-bottom: 2px solid @color-blue-piwik;
}
}

View File

@ -0,0 +1,34 @@
.example, .example A {
color: #9E9E9E;
}
.page_api {
padding: 0 15px 0 15px;
}
.page_api p {
line-height: 140%;
padding-bottom: 20px;
}
.apiFirstLine {
font-weight: bold;
padding-bottom: 10px;
}
.page_api ul {
list-style: disc outside none;
margin-left: 25px;
}
.apiDescription {
line-height: 1.5em;
padding-bottom: 1em;
}
.apiMethod {
margin-bottom: 5px;
margin-left: 20px;
}

View File

@ -0,0 +1,73 @@
{% set title %}{{ 'API_Glossary'|translate }}{% endset %}
{% set theContent %}
<div class="row">
<div class="col s12">
<div piwik-content-intro>
<h2>{{ title|e('html_attr') }}</h2>
<p>
{{ 'API_LearnAboutCommonlyUsedTerms2'|translate }}
<!-- {% for keyword, item in glossaryItems %}{{ item.entries|length }} {{ keyword }}{% if not loop.last %}, {% endif %}{% endfor %} -->
</p>
</div>
</div>
</div>
<div class="row glossary">
<div class="col s12">
<ul class="tabs">
{% for keyword, item in glossaryItems %}
<li class="tab col s3"><a {% if loop.index0 == 0 %}class="active"{% endif %} href="#{{ keyword }}">{{ item.title }}</a></li>
{% endfor %}
</ul>
</div>
{% for keyword, item in glossaryItems %}
<div id="{{ keyword }}" class="col s12">
<div class="card">
<div class="card-content">
<div style="background:#fff;width:100%" class="pushpin">
<h2 class="card-title">{{ item.title }}</h2>
<ul class="pagination table-of-contents">
{% for letter in item.letters %}
<li class="waves-effect"><a href="#{{ keyword }}{{ letter }}">{{ letter }}</a></li>
{% endfor %}
</ul></div>
{% set lastLetter = '' %}
{% for entry in item.entries %}
{% if not lastLetter or lastLetter != entry.letter %}
{% if lastLetter %}</div>{% endif %}
{% set lastLetter = entry.letter %}
<div class="section scrollspy" id="{{ keyword }}{{ lastLetter }}">
{% endif %}
<h3 style="color:#4183C4;font-weight: bold;">{{ entry.name }}</h3>
{% if entry.subtitle|default is not empty %}<p style="color:#999;text-transform:uppercase;font-weight:normal;margin-top:-16px;">{{ entry.subtitle|translate }}</p>{% endif %}
<p>{{ entry.documentation|raw }}
{% if entry.id is defined %}
<br/><span style="color: #bbb;">{{ entry.id }}{% if keyword == 'metrics' or entry.is_metric|default %} (API){% endif %}</span>
{% endif %}
</p>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<script type="text/javascript">
$(document).ready(function(){
$('.scrollspy').scrollSpy();
$('.pushpin').pushpin({ top: $('.pushpin').offset().top });
$('.tabs').tabs();
});
</script>
{% endset %}
{% if isWidget %}
{% include '@API/glossaryWidget.twig' %}
{% else %}
{% include '@API/glossaryAdmin.twig' %}
{% endif %}

View File

@ -0,0 +1,3 @@
{% extends 'admin.twig' %}
{% block content %}{{ theContent|raw }}{% endblock %}

View File

@ -0,0 +1,3 @@
{% extends 'layout.twig' %}
{% block root %}{{ theContent|raw }}{% endblock %}

View File

@ -0,0 +1,34 @@
{% extends 'admin.twig' %}
{% set title %}{{ 'API_ReportingApiReference'|translate }}{% endset %}
{% block topcontrols %}
{% include "@CoreHome/_siteSelectHeader.twig" %}
{% include "@CoreHome/_periodSelect.twig" %}
{% endblock %}
{% block content %}
<div class="api-list">
<div piwik-content-block content-title="{{ title }}" rate="true">
<p>{{ 'API_PluginDescription'|translate }}</p>
<p>
{{ 'API_MoreInformation'|translate("<a target='_blank' rel='noreferrer noopener' href='https://matomo.org/docs/analytics-api'>","</a>","<a target='_blank' rel='noreferrer' href='https://matomo.org/docs/analytics-api/reference'>","</a>")|raw }}
</p>
</div>
<div piwik-content-block content-title="{{ 'API_UserAuthentication'|translate|e('html_attr') }}">
<p>
{{ 'API_UsingTokenAuth'|translate('','',"")|raw }}<br/>
<pre piwik-select-on-focus id='token_auth'>&amp;token_auth=<strong piwik-show-sensitive-data="{{ token_auth }}" data-click-element-selector="#token_auth"></strong></pre><br/>
{{ 'API_KeepTokenSecret'|translate('<b>','</b>')|raw }}<br />
{{ 'API_ChangeTokenHint'|translate('<a href="' ~ linkTo({
'module': 'UsersManager',
'action': 'userSettings',
}) ~ '">', '</a>')|raw }}
</p>
</div>
{{ list_api_methods_with_links|raw }}
<br/>
</div>
{% endblock %}

View File

@ -0,0 +1,557 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @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\Archive;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Date;
use Piwik\Metrics as PiwikMetrics;
use Piwik\Piwik;
use Piwik\Plugin\Report;
use Piwik\Plugins\Actions\Columns\Metrics\AveragePageGenerationTime;
use Piwik\Plugins\Actions\Columns\Metrics\AverageTimeOnPage;
use Piwik\Plugins\Actions\Columns\Metrics\BounceRate;
use Piwik\Plugins\Actions\Columns\Metrics\ExitRate;
use Piwik\Plugins\CustomVariables\API as APICustomVariables;
use Piwik\Plugins\Actions\Actions\ActionSiteSearch;
use Piwik\Plugin\ReportsProvider;
use Piwik\Tracker\Action;
use Piwik\Tracker\PageUrl;
/**
* The Actions API lets you request reports for all your Visitor Actions: Page URLs, Page titles, Events, Content Tracking,
* File Downloads and Clicks on external websites.
*
* For example, "getPageTitles" will return all your page titles along with standard <a href='http://matomo.org/docs/analytics-api/reference/#toc-metric-definitions' rel='noreferrer' 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);
$report = ReportsProvider::factory("Actions", "get");
$archive = Archive::build($idSite, $period, $date, $segment);
$requestedColumns = Piwik::getArrayFromApiParameter($columns);
$columns = $report->getMetricsRequiredForReport($allColumns = null, $requestedColumns);
$inDbColumnNames = array_map(function ($value) { return 'Actions_' . $value; }, $columns);
$dataTable = $archive->getDataTableFromNumeric($inDbColumnNames);
$dataTable->deleteColumns(array_diff($requestedColumns, $columns));
$newNameMapping = array_combine($inDbColumnNames, $columns);
$dataTable->filter('ReplaceColumnNames', array($newNameMapping));
$columnsToShow = $requestedColumns ?: $report->getAllMetrics();
$dataTable->queueFilter('ColumnDelete', array($columnsToRemove = array(), $columnsToShow));
return $dataTable;
}
/**
* @param int $idSite
* @param string $period
* @param Date $date
* @param bool $segment
* @param bool $expanded
* @param bool|int $idSubtable
* @param bool|int $depth
* @param bool|int $flat
*
* @return DataTable|DataTable\Map
*/
public function getPageUrls($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false,
$depth = false, $flat = false)
{
Piwik::checkUserHasViewAccess($idSite);
$dataTable = Archive::createDataTableFromArchive('Actions_actions_url', $idSite, $period, $date, $segment, $expanded, $flat, $idSubtable, $depth);
$this->filterActionsDataTable($dataTable, Action::TYPE_PAGE_URL);
if ($flat) {
$dataTable->filter(function(DataTable $dataTable) {
foreach ($dataTable->getRows() as $row) {
$label = $row->getColumn('label');
if (substr($label, 0, 1) !== '/' && $label != Piwik::translate('General_NotDefined', Piwik::translate('Actions_ColumnPageURL'))) {
$row->setColumn('label', '/'.$label);
}
}
});
}
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)
{
Piwik::checkUserHasViewAccess($idSite);
$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)
{
Piwik::checkUserHasViewAccess($idSite);
$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(
PiwikMetrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS,
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, $flat = false)
{
Piwik::checkUserHasViewAccess($idSite);
$dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable, false, $flat);
$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, $flat = false)
{
Piwik::checkUserHasViewAccess($idSite);
$dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable, false, $flat);
$this->filterNonExitActions($dataTable);
return $dataTable;
}
public function getPageUrl($pageUrl, $idSite, $period, $date, $segment = false)
{
Piwik::checkUserHasViewAccess($idSite);
$callBackParameters = array('Actions_actions_url', $idSite, $period, $date, $segment, $expanded = false, $flat = false, $idSubtable = null);
$dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageUrl, Action::TYPE_PAGE_URL);
$this->addPageProcessedMetrics($dataTable);
$this->filterActionsDataTable($dataTable, Action::TYPE_PAGE_URL);
return $dataTable;
}
public function getPageTitles($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false, $flat = false)
{
Piwik::checkUserHasViewAccess($idSite);
$dataTable = Archive::createDataTableFromArchive('Actions_actions', $idSite, $period, $date, $segment, $expanded, $flat, $idSubtable);
$this->filterActionsDataTable($dataTable, Action::TYPE_PAGE_TITLE);
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, $flat = false)
{
Piwik::checkUserHasViewAccess($idSite);
$dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable, $flat);
$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, $flat = false)
{
Piwik::checkUserHasViewAccess($idSite);
$dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable, $flat);
$this->filterNonExitActions($dataTable);
return $dataTable;
}
public function getPageTitle($pageName, $idSite, $period, $date, $segment = false)
{
Piwik::checkUserHasViewAccess($idSite);
$callBackParameters = array('Actions_actions', $idSite, $period, $date, $segment, $expanded = false, $flat = false, $idSubtable = null);
$dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageName, Action::TYPE_PAGE_TITLE);
$this->addPageProcessedMetrics($dataTable);
$this->filterActionsDataTable($dataTable, Action::TYPE_PAGE_TITLE);
return $dataTable;
}
public function getDownloads($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false, $flat = false)
{
Piwik::checkUserHasViewAccess($idSite);
$dataTable = Archive::createDataTableFromArchive('Actions_downloads', $idSite, $period, $date, $segment, $expanded, $flat, $idSubtable);
$this->filterActionsDataTable($dataTable, Action::TYPE_DOWNLOAD);
return $dataTable;
}
public function getDownload($downloadUrl, $idSite, $period, $date, $segment = false)
{
Piwik::checkUserHasViewAccess($idSite);
$callBackParameters = array('Actions_downloads', $idSite, $period, $date, $segment, $expanded = false, $flat = false, $idSubtable = null);
$dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $downloadUrl, Action::TYPE_DOWNLOAD);
$this->filterActionsDataTable($dataTable, Action::TYPE_DOWNLOAD);
return $dataTable;
}
public function getOutlinks($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false, $flat = false)
{
Piwik::checkUserHasViewAccess($idSite);
$dataTable = Archive::createDataTableFromArchive('Actions_outlink', $idSite, $period, $date, $segment, $expanded, $flat, $idSubtable);
$this->filterActionsDataTable($dataTable, Action::TYPE_OUTLINK);
return $dataTable;
}
public function getOutlink($outlinkUrl, $idSite, $period, $date, $segment = false)
{
Piwik::checkUserHasViewAccess($idSite);
$callBackParameters = array('Actions_outlink', $idSite, $period, $date, $segment, $expanded = false, $flat = false, $idSubtable = null);
$dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $outlinkUrl, Action::TYPE_OUTLINK);
$this->filterActionsDataTable($dataTable, Action::TYPE_OUTLINK);
return $dataTable;
}
public function getSiteSearchKeywords($idSite, $period, $date, $segment = false)
{
Piwik::checkUserHasViewAccess($idSite);
$dataTable = $this->getSiteSearchKeywordsRaw($idSite, $period, $date, $segment);
$dataTable->deleteColumn(PiwikMetrics::INDEX_SITE_SEARCH_HAS_NO_RESULT);
$this->filterActionsDataTable($dataTable, Action::TYPE_SITE_SEARCH);
$dataTable->filter('ReplaceColumnNames');
$dataTable->filter('AddSegmentByLabel', array('siteSearchKeyword'));
$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 = Archive::createDataTableFromArchive('Actions_sitesearch', $idSite, $period, $date, $segment, $expanded = false);
return $dataTable;
}
public function getSiteSearchNoResultKeywords($idSite, $period, $date, $segment = false)
{
Piwik::checkUserHasViewAccess($idSite);
$dataTable = $this->getSiteSearchKeywordsRaw($idSite, $period, $date, $segment);
// Delete all rows that have some results
$dataTable->filter('ColumnCallbackDeleteRow',
array(
PiwikMetrics::INDEX_SITE_SEARCH_HAS_NO_RESULT,
function ($value) {
return $value < 1;
}
));
$dataTable->deleteRow(DataTable::ID_SUMMARY_ROW);
$dataTable->deleteColumn(PiwikMetrics::INDEX_SITE_SEARCH_HAS_NO_RESULT);
$this->filterActionsDataTable($dataTable, $isPageTitleType = false);
$dataTable->filter('AddSegmentByLabel', array('siteSearchKeyword'));
$dataTable->filter('ReplaceColumnNames');
$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)
{
Piwik::checkUserHasViewAccess($idSite);
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();
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, $isPageTitleType = false);
$dataTable->filter('ReplaceColumnNames');
$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('\Piwik\Archive::createDataTableFromArchive', $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 = $table->getEmptyClone();
$result->addRow($row);
$result->setAllTableMetadata($table->getAllTableMetadata());
return $result;
}
// match found on this level and more levels remaining: go deeper
$idSubTable = $row->getIdSubDataTable();
$callBackParameters[7] = $idSubTable;
/**
* @var \Piwik\Period $period
*/
$period = $table->getMetadata('period');
if (!empty($period)) {
$callBackParameters[3] = $period->getDateStart() . ',' . $period->getDateEnd();
}
$table = call_user_func_array('\Piwik\Archive::createDataTableFromArchive', $callBackParameters);
return $this->doFilterPageDatatableSearch($callBackParameters, $table, $searchTree);
}
throw new Exception("For this API function, DataTable " . get_class($table) . " is not supported");
}
/**
* Common filters for all Actions API
*
* @param DataTable|DataTable\Simple|DataTable\Map $dataTable
* @param bool $isPageTitleType Whether we are handling page title or regular URL
*/
private function filterActionsDataTable($dataTable, $isPageTitleType)
{
// 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('Piwik\Plugins\Actions\DataTable\Filter\Actions', array($isPageTitleType));
return $dataTable;
}
/**
* 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(PiwikMetrics::INDEX_PAGE_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(PiwikMetrics::INDEX_PAGE_EXIT_NB_VISITS,
function ($visits) {
return !strlen($visits);
})
);
}
private function addPageProcessedMetrics(DataTable\DataTableInterface $dataTable)
{
$dataTable->filter(function (DataTable $table) {
$extraProcessedMetrics = $table->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME);
$extraProcessedMetrics[] = new AverageTimeOnPage();
$extraProcessedMetrics[] = new BounceRate();
$extraProcessedMetrics[] = new ExitRate();
$extraProcessedMetrics[] = new AveragePageGenerationTime();
$table->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, $extraProcessedMetrics);
});
}
}

View File

@ -0,0 +1,187 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions;
use Piwik\Site;
use Piwik\Plugin\ViewDataTable;
use Piwik\Tracker\Action;
/**
* 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::registerEvents
*/
public function registerEvents()
{
return array(
'ViewDataTable.configure' => 'configureViewDataTable',
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'Insights.addReportToOverview' => 'addReportToInsightsOverview',
'Metrics.getDefaultMetricTranslations' => 'addMetricTranslations',
'Metrics.getDefaultMetricDocumentationTranslations' => 'addMetricDocumentationTranslations',
'Actions.addActionTypes' => 'addActionTypes'
);
}
public function addMetricTranslations(&$translations)
{
$metrics = array(
'nb_pageviews' => 'General_ColumnPageviews',
'nb_uniq_pageviews' => 'General_ColumnUniquePageviews',
'nb_downloads' => 'General_Downloads',
'nb_uniq_downloads' => 'Actions_ColumnUniqueDownloads',
'nb_outlinks' => 'General_Outlinks',
'nb_uniq_outlinks' => 'Actions_ColumnUniqueOutlinks',
'nb_searches' => 'Actions_ColumnSearches',
'nb_keywords' => 'Actions_ColumnSiteSearchKeywords',
'avg_time_generation' => 'General_ColumnAverageGenerationTime',
'exit_rate' => 'General_ColumnExitRate',
'entry_nb_visits' => 'General_ColumnEntrances',
'entry_bounce_count' => 'General_ColumnBounces',
'exit_nb_visits' => 'General_ColumnExits',
'nb_pages_per_search' => 'Actions_ColumnPagesPerSearch',
'nb_hits_following_search' => 'General_ColumnViewedAfterSearch',
);
$translations = array_merge($translations, $metrics);
}
public function addMetricDocumentationTranslations(&$translations)
{
$metrics = array(
'nb_pageviews' => 'General_ColumnPageviewsDocumentation',
'nb_uniq_pageviews' => 'General_ColumnUniquePageviewsDocumentation',
'nb_downloads' => 'Actions_ColumnClicksDocumentation',
'nb_uniq_downloads' => 'Actions_ColumnUniqueClicksDocumentation',
'nb_outlinks' => 'Actions_ColumnClicksDocumentation',
'nb_uniq_outlinks' => 'Actions_ColumnUniqueClicksDocumentation',
'nb_searches' => 'Actions_ColumnSearchesDocumentation',
'avg_time_generation' => 'General_ColumnAverageGenerationTimeDocumentation',
'entry_nb_visits' => 'General_ColumnEntrancesDocumentation',
'entry_bounce_count' => 'General_ColumnBouncesDocumentation',
'exit_nb_visits' => 'General_ColumnExitsDocumentation',
'exit_rate' => 'General_ColumnExitRateDocumentation'
);
$translations = array_merge($translations, $metrics);
}
public function addReportToInsightsOverview(&$reports)
{
$reports['Actions_getPageUrls'] = array();
$reports['Actions_getPageTitles'] = array();
$reports['Actions_getDownloads'] = array('flat' => 1);
}
public function getJsFiles(&$jsFiles)
{
$jsFiles[] = "plugins/Actions/javascripts/actionsDataTable.js";
$jsFiles[] = "plugins/Actions/javascripts/rowactions.js";
}
public function addActionTypes(&$types)
{
$types[] = [
'id' => Action::TYPE_PAGE_URL,
'name' => 'pageviews'
];
$types[] = [
'id' => Action::TYPE_CONTENT,
'name' => 'contents'
];
$types[] = [
'id' => Action::TYPE_SITE_SEARCH,
'name' => 'sitesearches'
];
$types[] = [
'id' => Action::TYPE_EVENT,
'name' => 'events'
];
$types[] = [
'id' => Action::TYPE_OUTLINK,
'name' => 'outlinks'
];
$types[] = [
'id' => Action::TYPE_DOWNLOAD,
'name' => 'downloads'
];
}
public function isSiteSearchEnabled($idSites, $idSite)
{
$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;
}
public static 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.");
}
}
public static function isCustomVariablesPluginsEnabled()
{
return \Piwik\Plugin\Manager::getInstance()->isPluginActivated('CustomVariables');
}
public function configureViewDataTable(ViewDataTable $view)
{
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;
}
}
}
/**
* @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);
}
}
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Actions;
use Piwik\Common;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visit;
/**
* This class represents an outlink.
* This is a particular type of Action: it has no 'name'
*
*/
class ActionClickUrl extends Action
{
public function __construct(Request $request)
{
parent::__construct(self::TYPE_OUTLINK, $request);
$this->setActionUrlWithoutExcludingParameters($request->getParam('link'));
}
public static function shouldHandle(Request $request)
{
$outlinkUrl = $request->getParam('link');
return !empty($outlinkUrl);
}
protected function getActionsToLookup()
{
return array(
// Note: we do not normalize outlink URL
'idaction_url' => array($this->getActionUrl(), $this->getActionType())
);
}
public function writeDebugInfo()
{
parent::writeDebugInfo();
if ($this->detectActionIsOutlinkOnAliasHost($this, $this->request->getIdSite())) {
Common::printDebug("INFO: The outlink URL host is one of the known host for this website. ");
}
}
/**
* Detect whether action is an outlink given host aliases
*
* @param Action $action
* @return bool true if the outlink the visitor clicked on points to one of the known hosts for this website
*/
protected function detectActionIsOutlinkOnAliasHost(Action $action, $idSite)
{
$decodedActionUrl = $action->getActionUrl();
$actionUrlParsed = @parse_url($decodedActionUrl);
if (!isset($actionUrlParsed['host'])) {
return false;
}
return Visit::isHostKnownAliasHost($actionUrlParsed['host'], $idSite);
}
}

View File

@ -0,0 +1,41 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Actions;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
/**
* This class represents a download.
* This is a particular type of Action: it has no 'name'
*/
class ActionDownloadUrl extends Action
{
public function __construct(Request $request)
{
parent::__construct(self::TYPE_DOWNLOAD, $request);
$this->setActionUrlWithoutExcludingParameters($request->getParam('download'));
}
public static function shouldHandle(Request $request)
{
$downloadUrl = $request->getParam('download');
return !empty($downloadUrl);
}
protected function getActionsToLookup()
{
return array(
// Note: we do not normalize download URL
'idaction_url' => array($this->getActionUrl(), $this->getActionType())
);
}
}

View File

@ -0,0 +1,300 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Actions;
use Piwik\Common;
use Piwik\Tracker\Action;
use Piwik\Tracker\PageUrl;
use Piwik\Tracker\Request;
use Piwik\Tracker\Cache;
use Piwik\UrlHelper;
/**
* This class represents a search on the site.
* - Its name is the search keyword
* - by default the URL is not recorded (since it's not used)
* - tracks site search result count and site search category as custom variables
*
*/
class ActionSiteSearch extends Action
{
private $searchCategory = false;
private $searchCount = false;
const CVAR_KEY_SEARCH_CATEGORY = '_pk_scat';
const CVAR_KEY_SEARCH_COUNT = '_pk_scount';
const CVAR_INDEX_SEARCH_CATEGORY = '4';
const CVAR_INDEX_SEARCH_COUNT = '5';
public function __construct(Request $request, $detect = true)
{
parent::__construct(Action::TYPE_SITE_SEARCH, $request);
$this->originalUrl = $request->getParam('url');
if ($detect) {
$this->isSearchDetected();
}
}
public static function shouldHandle(Request $request)
{
$search = new self($request, false);
return $search->detectSiteSearch($request->getParam('url'));
}
protected function getActionsToLookup()
{
return array(
'idaction_name' => array($this->getActionName(), Action::TYPE_SITE_SEARCH),
);
}
public function getIdActionUrl()
{
// Site Search, by default, will not track URL. We do not want URL to appear as "Page URL not defined"
// so we specifically set it to NULL in the table (the archiving query does IS NOT NULL)
return null;
}
public function getIdActionUrlForEntryAndExitIds()
{
return $this->getIdActionUrl();
}
public function getIdActionNameForEntryAndExitIds()
{
return $this->getIdActionName();
}
public function getCustomFloatValue()
{
return $this->request->getPageGenerationTime();
}
protected function isSearchDetected()
{
$siteSearch = $this->detectSiteSearch($this->originalUrl);
if (empty($siteSearch)) {
return false;
}
list($actionName, $url, $category, $count) = $siteSearch;
if (!empty($category)) {
$this->searchCategory = trim($category);
}
if ($count !== false) {
$this->searchCount = $count;
}
$this->setActionName($actionName);
$this->setActionUrl($url);
return true;
}
public function getCustomVariables()
{
$customVariables = parent::getCustomVariables();
// Enrich Site Search actions with Custom Variables, overwriting existing values
if (!empty($this->searchCategory)) {
if (!empty($customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_CATEGORY])) {
Common::printDebug("WARNING: Overwriting existing Custom Variable in slot " . self::CVAR_INDEX_SEARCH_CATEGORY . " for this page view");
}
$customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_CATEGORY] = self::CVAR_KEY_SEARCH_CATEGORY;
$customVariables['custom_var_v' . self::CVAR_INDEX_SEARCH_CATEGORY] = Request::truncateCustomVariable($this->searchCategory);
}
if ($this->searchCount !== false) {
if (!empty($customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_COUNT])) {
Common::printDebug("WARNING: Overwriting existing Custom Variable in slot " . self::CVAR_INDEX_SEARCH_COUNT . " for this page view");
}
$customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_COUNT] = self::CVAR_KEY_SEARCH_COUNT;
$customVariables['custom_var_v' . self::CVAR_INDEX_SEARCH_COUNT] = (int)$this->searchCount;
}
return $customVariables;
}
protected function detectSiteSearchFromUrl($website, $parsedUrl)
{
$doRemoveSearchParametersFromUrl = true;
$separator = '&';
$count = $actionName = $categoryName = false;
$keywordParameters = isset($website['sitesearch_keyword_parameters'])
? $website['sitesearch_keyword_parameters']
: array();
$queryString = !empty($parsedUrl['query']) ? $parsedUrl['query'] : '';
$fragment = !empty($parsedUrl['fragment']) ? $parsedUrl['fragment'] : '';
$parsedFragment = parse_url($fragment);
// check if fragment contains a separate query (beginning with ?) otherwise assume complete fragment as query
if ($fragment && strpos($fragment, '?') !== false && !empty($parsedFragment['query'])) {
$fragmentBeforeQuery = !empty($parsedFragment['path']) ? $parsedFragment['path'] : '';
$fragmentQuery = $parsedFragment['query'];
} else {
$fragmentQuery = $fragment;
$fragmentBeforeQuery = '';
}
$parametersRaw = UrlHelper::getArrayFromQueryString($queryString.$separator.$fragmentQuery);
// strtolower the parameter names for smooth site search detection
$parameters = array();
foreach ($parametersRaw as $k => $v) {
$parameters[Common::mb_strtolower($k)] = $v;
}
// decode values if they were sent from a client using another charset
$pageEncoding = $this->request->getParam('cs');
PageUrl::reencodeParameters($parameters, $pageEncoding);
// Detect Site Search keyword
foreach ($keywordParameters as $keywordParameterRaw) {
$keywordParameter = Common::mb_strtolower($keywordParameterRaw);
if (!empty($parameters[$keywordParameter])) {
$actionName = $parameters[$keywordParameter];
break;
}
}
if (empty($actionName)) {
return false;
}
$categoryParameters = isset($website['sitesearch_category_parameters'])
? $website['sitesearch_category_parameters']
: array();
foreach ($categoryParameters as $categoryParameterRaw) {
$categoryParameter = Common::mb_strtolower($categoryParameterRaw);
if (!empty($parameters[$categoryParameter])) {
$categoryName = $parameters[$categoryParameter];
break;
}
}
if (isset($parameters['search_count'])
&& $this->isValidSearchCount($parameters['search_count'])
) {
$count = $parameters['search_count'];
}
// Remove search kwd from URL
if ($doRemoveSearchParametersFromUrl) {
// @see excludeQueryParametersFromUrl()
// Excluded the detected parameters from the URL
$parametersToExclude = array($categoryParameterRaw, $keywordParameterRaw);
if (isset($parsedUrl['query'])) {
$parsedUrl['query'] = UrlHelper::getQueryStringWithExcludedParameters(UrlHelper::getArrayFromQueryString($parsedUrl['query']), $parametersToExclude);
}
if (isset($parsedUrl['fragment'])) {
$parsedUrl['fragment'] = UrlHelper::getQueryStringWithExcludedParameters(UrlHelper::getArrayFromQueryString($fragmentQuery), $parametersToExclude);
if ($fragmentBeforeQuery) {
if ($parsedUrl['fragment']) {
$parsedUrl['fragment'] = $fragmentBeforeQuery.'?'.$parsedUrl['fragment'];
} else {
$parsedUrl['fragment'] = $fragmentBeforeQuery;
}
}
}
}
$url = UrlHelper::getParseUrlReverse($parsedUrl);
if (is_array($actionName)) {
$actionName = reset($actionName);
}
$actionName = PageUrl::urldecodeValidUtf8($actionName);
$actionName = trim($actionName);
if (empty($actionName)) {
return false;
}
if (is_array($categoryName)) {
$categoryName = reset($categoryName);
}
$categoryName = PageUrl::urldecodeValidUtf8($categoryName);
$categoryName = trim($categoryName);
return array($url, $actionName, $categoryName, $count);
}
protected function isValidSearchCount($count)
{
return is_numeric($count) && $count >= 0;
}
public function detectSiteSearch($originalUrl)
{
$website = Cache::getCacheWebsiteAttributes($this->request->getIdSite());
if (empty($website['sitesearch'])) {
Common::printDebug("Internal 'Site Search' tracking is not enabled for this site. ");
return false;
}
$actionName = $url = $categoryName = $count = false;
$originalUrl = PageUrl::cleanupUrl($originalUrl);
// Detect Site search from Tracking API parameters rather than URL
$searchKwd = $this->request->getParam('search');
if (!empty($searchKwd)) {
$actionName = $searchKwd;
$isCategoryName = $this->request->getParam('search_cat');
if (!empty($isCategoryName)) {
$categoryName = $isCategoryName;
}
$isCount = $this->request->getParam('search_count');
if ($this->isValidSearchCount($isCount)) {
$count = $isCount;
}
}
if (empty($actionName)) {
$parsedUrl = @parse_url($originalUrl);
// Detect Site Search from URL query parameters
if (!empty($parsedUrl['query']) || !empty($parsedUrl['fragment'])) {
// array($url, $actionName, $categoryName, $count);
$searchInfo = $this->detectSiteSearchFromUrl($website, $parsedUrl);
if (!empty($searchInfo)) {
list ($url, $actionName, $categoryName, $count) = $searchInfo;
}
}
}
$actionName = trim($actionName);
$categoryName = trim($categoryName);
if (empty($actionName)) {
Common::printDebug("(this is not a Site Search request)");
return false;
}
Common::printDebug("Detected Site Search keyword '$actionName'. ");
if (!empty($categoryName)) {
Common::printDebug("- Detected Site Search Category '$categoryName'. ");
}
if ($count !== false) {
Common::printDebug("- Search Results Count was '$count'. ");
}
if ($url != $originalUrl) {
Common::printDebug("NOTE: The Page URL was changed / removed, during the Site Search detection, was '$originalUrl', now is '$url'");
}
return array(
$actionName,
$url,
$categoryName,
$count
);
}
}

View File

@ -0,0 +1,538 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions;
use Piwik\Config;
use Piwik\DataTable;
use Piwik\Metrics as PiwikMetrics;
use Piwik\RankingQuery;
use Piwik\Tracker\Action;
use Piwik\Plugins\Actions\Actions\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';
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
*/
public static 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 `" . PiwikMetrics::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 `" . PiwikMetrics::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 (Metrics::$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, Metrics::$columnsAggregationOperation);
}
$this->actionsTablesByType[$type] = $dataTable;
}
}
protected function archiveDayActions($rankingQueryLimit)
{
$metricsConfig = Metrics::getActionMetrics();
$select = "log_action.name,
log_action.type,
log_action.idaction,
log_action.url_prefix";
$select = $this->addMetricsToSelect($select, $metricsConfig);
$from = array(
"log_link_visit_action",
array(
"table" => "log_action",
"joinOn" => "log_link_visit_action.%s = log_action.idaction"
)
);
$where = $this->getLogAggregator()->getWhereStatement('log_link_visit_action', 'server_time');
$where .= " AND log_link_visit_action.%s IS NOT NULL"
. $this->getWhereClauseActionIsNotEvent();
$groupBy = "log_link_visit_action.%s";
$orderBy = "`" . PiwikMetrics::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('url_prefix');
if ($this->isSiteSearchEnabled()) {
$rankingQuery->addColumn(PiwikMetrics::INDEX_SITE_SEARCH_HAS_NO_RESULT, 'min');
$rankingQuery->addColumn(PiwikMetrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS, 'sum');
}
$this->addMetricsToRankingQuery($rankingQuery, $metricsConfig);
$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, $groupBy, $orderBy, "idaction_name", $rankingQuery, $metricsConfig);
$this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "idaction_url", $rankingQuery, $metricsConfig);
}
private function addMetricsToSelect($select, $metricsConfig)
{
if (!empty($metricsConfig)) {
foreach ($metricsConfig as $metric => $config) {
$select .= ', ' . $config['query'] . " as `" . $metric . "`";
}
}
return $select;
}
private function addMetricsToRankingQuery(RankingQuery $rankingQuery, $metricsConfig)
{
foreach ($metricsConfig as $metric => $config) {
if (!empty($config['aggregation'])) {
$rankingQuery->addColumn($metric, $config['aggregation']);
} else {
$rankingQuery->addColumn($metric);
}
}
}
protected function isSiteSearchEnabled()
{
return $this->isSiteSearchEnabled;
}
protected function archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, $sprintfField, RankingQuery $rankingQuery = null, $metricsConfig = array())
{
$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->generateRankingQuery($querySql);
}
// get result
$resultSet = $this->getLogAggregator()->getDb()->query($querySql, $query['bind']);
$modified = ArchivingHelper::updateActionsTableWithRowQuery($resultSet, $sprintfField, $this->actionsTablesByType, $metricsConfig);
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(PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS);
$rankingQuery->addColumn(array(PiwikMetrics::INDEX_PAGE_ENTRY_NB_VISITS,
PiwikMetrics::INDEX_PAGE_ENTRY_NB_ACTIONS,
PiwikMetrics::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH,
PiwikMetrics::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 = "`" . PiwikMetrics::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 `" . PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS . "`,
count(*) as `" . PiwikMetrics::INDEX_PAGE_ENTRY_NB_VISITS . "`,
sum(log_visit.visit_total_actions) as `" . PiwikMetrics::INDEX_PAGE_ENTRY_NB_ACTIONS . "`,
sum(log_visit.visit_total_time) as `" . PiwikMetrics::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 `" . PiwikMetrics::INDEX_PAGE_ENTRY_BOUNCE_COUNT . "`";
$where = $this->getLogAggregator()->getWhereStatement('log_visit', 'visit_last_action_time');
$where .= " AND log_visit.%s > 0";
$groupBy = "log_visit.%s";
$this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "visit_entry_idaction_url", $rankingQuery);
$this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "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(PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS);
$rankingQuery->addColumn(PiwikMetrics::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 = "`" . PiwikMetrics::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 `" . PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS . "`,
count(*) as `" . PiwikMetrics::INDEX_PAGE_EXIT_NB_VISITS . "`";
$where = $this->getLogAggregator()->getWhereStatement('log_visit', 'visit_last_action_time');
$where .= " AND log_visit.%s > 0";
$groupBy = "log_visit.%s";
$this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "visit_exit_idaction_url", $rankingQuery);
$this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "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(PiwikMetrics::INDEX_PAGE_SUM_TIME_SPENT, 'sum');
$rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($this->actionsTablesByType));
$extraSelects = "log_action.type, log_action.name, count(*) as `" . PiwikMetrics::INDEX_PAGE_NB_HITS . "`,";
$from = array(
"log_link_visit_action",
array(
"table" => "log_action",
"joinOn" => "log_link_visit_action.%s = log_action.idaction"
)
);
$orderBy = "`" . PiwikMetrics::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 `" . PiwikMetrics::INDEX_PAGE_SUM_TIME_SPENT . "`";
$where = $this->getLogAggregator()->getWhereStatement('log_link_visit_action', 'server_time');
$where .= " 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";
$this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "idaction_url_ref", $rankingQuery);
$this->archiveDayQueryProcess($select, $from, $where, $groupBy, $orderBy, "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);
$prefix = $this->getProcessor()->getParams()->getSite()->getMainUrl();
$prefix = rtrim($prefix, '/') . '/';
$this->setFolderPathMetadata($dataTable, $isUrl = true, $prefix);
$this->insertTable($dataTable, self::PAGE_URLS_RECORD_NAME);
$records = array(
self::METRIC_PAGEVIEWS_RECORD_NAME => array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS)),
self::METRIC_UNIQ_PAGEVIEWS_RECORD_NAME => array_sum($dataTable->getColumn(PiwikMetrics::INDEX_NB_VISITS)),
self::METRIC_SUM_TIME_RECORD_NAME => array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_SUM_TIME_GENERATION)),
self::METRIC_HITS_TIMED_RECORD_NAME => array_sum($dataTable->getColumn(PiwikMetrics::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(PiwikMetrics::INDEX_PAGE_NB_HITS)));
$this->getProcessor()->insertNumericRecord(self::METRIC_UNIQ_DOWNLOADS_RECORD_NAME, array_sum($dataTable->getColumn(PiwikMetrics::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(PiwikMetrics::INDEX_PAGE_NB_HITS)));
$this->getProcessor()->insertNumericRecord(self::METRIC_UNIQ_OUTLINKS_RECORD_NAME, array_sum($dataTable->getColumn(PiwikMetrics::INDEX_NB_VISITS)));
}
protected function insertPageTitlesReports()
{
$dataTable = $this->getDataTable(Action::TYPE_PAGE_TITLE);
$this->setFolderPathMetadata($dataTable, $isUrl = false);
$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(PiwikMetrics::INDEX_PAGE_NB_HITS)));
$this->getProcessor()->insertNumericRecord(self::METRIC_KEYWORDS_RECORD_NAME, $dataTable->getRowsCount());
}
protected function deleteUnusedColumnsFromKeywordsDataTable(DataTable $dataTable)
{
$columnsToDelete = array(
PiwikMetrics::INDEX_NB_UNIQ_VISITORS,
PiwikMetrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS,
PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS,
PiwikMetrics::INDEX_PAGE_ENTRY_NB_ACTIONS,
PiwikMetrics::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH,
PiwikMetrics::INDEX_PAGE_ENTRY_NB_VISITS,
PiwikMetrics::INDEX_PAGE_ENTRY_BOUNCE_COUNT,
PiwikMetrics::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,
Metrics::$columnsAggregationOperation,
Metrics::$columnsToRenameAfterAggregation,
$countRowsRecursive = array()
);
$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,
Metrics::$columnsToRenameAfterAggregation,
$countRowsRecursive = array()
);
$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']);
}
private function setFolderPathMetadata(DataTable $dataTable, $isUrl, $prefix = '')
{
$configGeneral = Config::getInstance()->General;
$separator = $isUrl ? '/' : $configGeneral['action_title_category_delimiter'];
$metadataName = $isUrl ? 'folder_url_start' : 'page_title_path';
foreach ($dataTable->getRows() as $row) {
$subtable = $row->getSubtable();
if (!$subtable) {
continue;
}
$metadataValue = $prefix . $row->getColumn('label');
$row->setMetadata($metadataName, $metadataValue);
$this->setFolderPathMetadata($subtable, $isUrl, $metadataValue . $separator);
}
}
}

View File

@ -0,0 +1,645 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @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\Row\DataTableSummaryRow;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\Metrics as PiwikMetrics;
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
*/
public static function updateActionsTableWithRowQuery($query, $fieldQueried, & $actionsTablesByType, $metricsConfig)
{
$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[PiwikMetrics::INDEX_SITE_SEARCH_HAS_NO_RESULT]);
}
if (in_array($row['type'], array(Action::TYPE_CONTENT, Action::TYPE_EVENT, Action::TYPE_EVENT_NAME, Action::TYPE_CONTENT_PIECE, Action::TYPE_CONTENT_TARGET))) {
continue;
}
$hasRowName = !empty($row['name']) && $row['name'] != DataTable::LABEL_SUMMARY_ROW;
// 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 = '';
$pageTitlePath = null;
if ($row['type'] == Action::TYPE_SITE_SEARCH
|| $row['type'] == Action::TYPE_PAGE_TITLE
) {
$url = null;
if ($hasRowName) {
$pageTitlePath = $row['name'];
}
} elseif ($hasRowName) {
$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[PiwikMetrics::INDEX_PAGE_NB_HITS])
&& $row[PiwikMetrics::INDEX_PAGE_NB_HITS] > $actionRow->maxVisitsSummed
) {
$actionRow->setMetadata('url', $url);
$actionRow->maxVisitsSummed = $row[PiwikMetrics::INDEX_PAGE_NB_HITS];
}
} else {
$actionRow->setMetadata('url', $url);
$actionRow->maxVisitsSummed = !empty($row[PiwikMetrics::INDEX_PAGE_NB_HITS]) ? $row[PiwikMetrics::INDEX_PAGE_NB_HITS] : 0;
}
}
if ($pageTitlePath !== null
&& !$actionRow->isSummaryRow()
) {
$actionRow->setMetadata('page_title_path', $pageTitlePath);
}
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(PiwikMetrics::INDEX_PAGE_SUM_TIME_GENERATION, $row)) {
unset($row[PiwikMetrics::INDEX_PAGE_SUM_TIME_GENERATION]);
}
if (array_key_exists(PiwikMetrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION, $row)) {
unset($row[PiwikMetrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION]);
}
if (array_key_exists(PiwikMetrics::INDEX_PAGE_MIN_TIME_GENERATION, $row)) {
unset($row[PiwikMetrics::INDEX_PAGE_MIN_TIME_GENERATION]);
}
if (array_key_exists(PiwikMetrics::INDEX_PAGE_MAX_TIME_GENERATION, $row)) {
unset($row[PiwikMetrics::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, $metricsConfig);
$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(PiwikMetrics::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(PiwikMetrics::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
) {
$subTable = $row->getSubtable();
if ($subTable) {
self::deleteInvalidSummedColumnsFromDataTable($subTable);
}
if ($row instanceof DataTableSummaryRow) {
$row->recalculate();
}
foreach (Metrics::$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, $metricsConfig)
{
if (array_key_exists($columnName, $metricsConfig)) {
$config = $metricsConfig[$columnName];
if (!empty($config['aggregation'])) {
if ($config['aggregation'] == 'min') {
if (empty($alreadyValue)) {
$newValue = $value;
} else if (empty($value)) {
$newValue = $alreadyValue;
} else {
$newValue = min($alreadyValue, $value);
}
return $newValue;
}
if ($config['aggregation'] == 'max') {
$newValue = max($alreadyValue, $value);
return $newValue;
}
}
}
$newValue = $alreadyValue + $value;
return $newValue;
}
public static $maximumRowsInDataTableLevelZero;
public static $maximumRowsInSubDataTable;
public static $columnToSortByBeforeTruncation;
protected static $actionUrlCategoryDelimiter = null;
protected static $actionTitleCategoryDelimiter = null;
protected static $defaultActionName = null;
protected static $defaultActionNameWhenNotDefined = null;
protected static $defaultActionUrlWhenNotDefined = null;
public static function reloadConfig()
{
// for BC, we read the old style delimiter first (see #1067)
$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 = PiwikMetrics::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
*/
private static 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(
PiwikMetrics::INDEX_NB_VISITS => 1,
PiwikMetrics::INDEX_NB_UNIQ_VISITORS => 1,
PiwikMetrics::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
*/
public 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
*/
public static 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('Matomo', '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
*/
public static 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);
if ($type == Action::TYPE_PAGE_TITLE && self::$actionTitleCategoryDelimiter === '') {
if ($name === '' || $name === false || $name === null || trim($name) === '') {
$name = self::getUnknownActionName($type);
}
return array(' ' . trim($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
*/
protected static $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(PiwikMetrics::INDEX_NB_VISITS => 0,
PiwikMetrics::INDEX_NB_UNIQ_VISITORS => 0,
PiwikMetrics::INDEX_PAGE_NB_HITS => 0,
PiwikMetrics::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))) {
$path = '/' . trim($urlPath);
if (!empty($urlFragment)) {
$path .= '#' . $urlFragment;
}
return array(trim($urlHost), $path);
}
$name = $urlPath;
if ($name === '' || substr($name, -1) == '/') {
$name .= self::$defaultActionName;
}
$urlFragment = PageUrl::processUrlFragment($urlFragment);
if (!empty($urlFragment)) {
$name .= '#' . $urlFragment;
}
return $name;
}
}

View File

@ -0,0 +1,19 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Categories;
use Piwik\Category\Subcategory;
class DownloadsSubcategory extends Subcategory
{
protected $categoryId = 'General_Actions';
protected $id = 'General_Downloads';
protected $order = 35;
}

View File

@ -0,0 +1,19 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Categories;
use Piwik\Category\Subcategory;
class EntryPagesSubcategory extends Subcategory
{
protected $categoryId = 'General_Actions';
protected $id = 'Actions_SubmenuPagesEntry';
protected $order = 10;
}

View File

@ -0,0 +1,19 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Categories;
use Piwik\Category\Subcategory;
class ExitPagesSubcategory extends Subcategory
{
protected $categoryId = 'General_Actions';
protected $id = 'Actions_SubmenuPagesExit';
protected $order = 15;
}

View File

@ -0,0 +1,19 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Categories;
use Piwik\Category\Subcategory;
class OutlinksSubcategory extends Subcategory
{
protected $categoryId = 'General_Actions';
protected $id = 'General_Outlinks';
protected $order = 30;
}

View File

@ -0,0 +1,19 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Categories;
use Piwik\Category\Subcategory;
class PageTitlesSubcategory extends Subcategory
{
protected $categoryId = 'General_Actions';
protected $id = 'Actions_SubmenuPageTitles';
protected $order = 20;
}

View File

@ -0,0 +1,19 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Categories;
use Piwik\Category\Subcategory;
class PagesSubcategory extends Subcategory
{
protected $categoryId = 'General_Actions';
protected $id = 'General_Pages';
protected $order = 5;
}

View File

@ -0,0 +1,19 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Categories;
use Piwik\Category\Subcategory;
class SiteSearchSubcategory extends Subcategory
{
protected $categoryId = 'General_Actions';
protected $id = 'Actions_SubmenuSitesearch';
protected $order = 25;
}

View File

@ -0,0 +1,81 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Columns;
use Piwik\Columns\DimensionMetricFactory;
use Piwik\Columns\MetricsList;
use Piwik\Development;
use Piwik\Piwik;
use Piwik\Plugin\Dimension\ActionDimension;
use Exception;
/**
* This example dimension only defines a name and does not track any data. It's supposed to be only used in reports.
*
* See {@link http://developer.piwik.org/api-reference/Piwik/Columns\Dimension} for more information.
*/
class ActionType extends ActionDimension
{
protected $columnName = 'type';
protected $dbTableName = 'log_action';
protected $segmentName = 'actionType';
protected $type = self::TYPE_ENUM;
protected $nameSingular = 'Actions_ActionType';
protected $namePlural = 'Actions_ActionTypes';
protected $category = 'General_Actions';
public function __construct()
{
$this->acceptValues = 'A type of action, such as: pageviews, contents, sitesearches, events, outlinks, downloads';
}
public function getEnumColumnValues()
{
$availableTypes = [];
/**
* Triggered to determine the available action types
*
* Plugin can use this event to add their own action types, so they are available in segmentation
* The array maps internal ids to readable action type names used in visitor details
*
* **Example**
*
* public function addActionTypes(&$availableTypes)
* {
* $availableTypes[] = array(
* 'id' => 76,
* 'name' => 'media_play'
* );
* }
*
* @param array $availableTypes
*/
Piwik::postEvent('Actions.addActionTypes', [&$availableTypes]);
$types = [];
foreach ($availableTypes as $type) {
if (empty($type['id']) || empty($type['name'])) {
throw new Exception("Invalid action added with event `Actions.addActionTypes`: " . var_export($type, true));
}
if (Development::isEnabled() && array_key_exists($type['id'], $types)) {
throw new Exception(sprintf("Action '%s' with id %s couldn't be added, as '%s' was already added for this id", $type['name'], $type['id'], $types[$type['id']]));
}
$types[$type['id']] = $type['name'];
}
return $types;
}
public function configureMetrics(MetricsList $metricsList, DimensionMetricFactory $dimensionMetricFactory)
{
// do not genereate any metric for this
}
}

View File

@ -0,0 +1,29 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Columns;
use Piwik\Piwik;
use Piwik\Plugin\Dimension\ActionDimension;
use Piwik\Plugins\Actions\Segment;
class ActionUrl extends ActionDimension
{
protected $nameSingular = 'Actions_ColumnActionURL';
protected function configureSegments()
{
$segment = new Segment();
$segment->setSegment('actionUrl');
$segment->setName('Actions_ColumnActionURL');
$segment->setUnionOfSegments(array('pageUrl', 'downloadUrl', 'outlinkUrl', 'eventUrl'));
$this->addSegment($segment);
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Columns;
use Piwik\Columns\Discriminator;
use Piwik\Columns\Join\ActionNameJoin;
use Piwik\Plugin\Dimension\ActionDimension;
use Piwik\Tracker\Action;
class ClickedUrl extends ActionDimension
{
protected $columnName = 'idaction_url';
protected $segmentName = 'outlinkUrl';
protected $nameSingular = 'Actions_ColumnClickedURL';
protected $namePlural = 'Actions_ColumnClickedURLs';
protected $category = 'General_Actions';
protected $sqlFilter = '\\Piwik\\Tracker\\TableLogAction::getIdActionFromSegment';
protected $type = self::TYPE_URL;
public function getDbColumnJoin()
{
return new ActionNameJoin();
}
public function getDbDiscriminator()
{
return new Discriminator('log_action', 'type', Action::TYPE_OUTLINK);
}
}

View File

@ -0,0 +1,18 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Columns;
use Piwik\Columns\Dimension;
use Piwik\Piwik;
class DestinationPage extends Dimension
{
protected $type = self::TYPE_TEXT;
protected $nameSingular = 'General_ColumnDestinationPage';
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Columns;
use Piwik\Columns\Discriminator;
use Piwik\Columns\Join\ActionNameJoin;
use Piwik\Plugin\Dimension\ActionDimension;
use Piwik\Tracker\Action;
class DownloadUrl extends ActionDimension
{
protected $segmentName = 'downloadUrl';
protected $nameSingular = 'Actions_ColumnDownloadURL';
protected $namePlural = 'Actions_ColumnDownloadURLs';
protected $columnName = 'idaction_url';
protected $category = 'General_Actions';
protected $sqlFilter = '\\Piwik\\Tracker\\TableLogAction::getIdActionFromSegment';
protected $type = self::TYPE_URL;
public function getDbColumnJoin()
{
return new ActionNameJoin();
}
public function getDbDiscriminator()
{
return new Discriminator('log_action', 'type', Action::TYPE_DOWNLOAD);
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Columns;
use Piwik\Columns\Discriminator;
use Piwik\Columns\Join\ActionNameJoin;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
class EntryPageTitle extends VisitDimension
{
protected $columnName = 'visit_entry_idaction_name';
protected $columnType = 'INTEGER(10) UNSIGNED NULL';
protected $type = self::TYPE_TEXT;
protected $segmentName = 'entryPageTitle';
protected $nameSingular = 'Actions_ColumnEntryPageTitle';
protected $namePlural = 'Actions_WidgetEntryPageTitles';
protected $category = 'General_Actions';
protected $sqlFilter = '\\Piwik\\Tracker\\TableLogAction::getIdActionFromSegment';
public function getDbColumnJoin()
{
return new ActionNameJoin();
}
public function getDbDiscriminator()
{
return new Discriminator('log_action', 'type', Action::TYPE_PAGE_TITLE);
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
$idActionName = false;
if (!empty($action)) {
$idActionName = $action->getIdActionNameForEntryAndExitIds();
}
return (int) $idActionName;
}
}

View File

@ -0,0 +1,80 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Actions\Columns;
use Piwik\Columns\Discriminator;
use Piwik\Columns\Join\ActionNameJoin;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
class EntryPageUrl extends VisitDimension
{
protected $columnName = 'visit_entry_idaction_url';
protected $columnType = 'INTEGER(11) UNSIGNED NULL DEFAULT NULL';
protected $segmentName = 'entryPageUrl';
protected $nameSingular = 'Actions_ColumnEntryPageURL';
protected $namePlural = 'Actions_ColumnEntryPageURLs';
protected $category = 'General_Actions';
protected $sqlFilter = '\\Piwik\\Tracker\\TableLogAction::getIdActionFromSegment';
protected $type = self::TYPE_URL;
public function getDbColumnJoin()
{
return new ActionNameJoin();
}
public function getDbDiscriminator()
{
return new Discriminator('log_action', 'type', Action::TYPE_PAGE_URL);
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
$idActionUrl = false;
if (!empty($action)) {
$idActionUrl = $action->getIdActionUrlForEntryAndExitIds();
}
if($idActionUrl === false) {
return false;
}
return (int) $idActionUrl;
}
/*
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onExistingVisit(Request $request, Visitor $visitor, $action)
{
$idAction = $visitor->getVisitorColumn('visit_entry_idaction_url');
if (is_null($idAction) && !empty($action)) {
$idAction = $action->getIdActionUrlForEntryAndExitIds();
if (!empty($idAction)) {
return $idAction;
}
}
return false;
}
}

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