Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Steur <thomas.steur@gmail.com>2015-07-03 03:54:27 +0300
committersgiehl <stefan@piwik.org>2015-10-06 18:25:13 +0300
commit9ba8f216fd7856ce5fef06bf82ecb8f8a2e7e630 (patch)
tree6ce07d18a85d00b39ab720abe042361c0775aead /plugins/API
parent8ccc9dc05da021325cdbf141a548637fa52f16b2 (diff)
generate pages instead of implementing them in each controller
Diffstat (limited to 'plugins/API')
-rw-r--r--plugins/API/API.php162
-rw-r--r--plugins/API/ProcessedReport.php86
-rw-r--r--plugins/API/Reports/Get.php6
-rw-r--r--plugins/API/SegmentMetadata.php167
-rw-r--r--plugins/API/WidgetMetadata.php282
-rw-r--r--plugins/API/tests/Unit/WidgetMetadataTest.php278
6 files changed, 799 insertions, 182 deletions
diff --git a/plugins/API/API.php b/plugins/API/API.php
index 4ea0cb4055..8245d0712e 100644
--- a/plugins/API/API.php
+++ b/plugins/API/API.php
@@ -10,7 +10,10 @@ namespace Piwik\Plugins\API;
use Piwik\API\Proxy;
use Piwik\API\Request;
-use Piwik\Columns\Dimension;
+use Piwik\Cache;
+use Piwik\CacheId;
+use Piwik\Category\CategoryList;
+use Piwik\Common;
use Piwik\Config;
use Piwik\Container\StaticContainer;
use Piwik\DataTable;
@@ -23,11 +26,13 @@ use Piwik\Period;
use Piwik\Period\Range;
use Piwik\Piwik;
use Piwik\Plugin\Dimension\VisitDimension;
+use Piwik\Plugin\Report;
use Piwik\Plugins\API\DataTable\MergeDataTables;
use Piwik\Plugins\CoreAdminHome\CustomLogo;
use Piwik\Translation\Translator;
use Piwik\Measurable\Type\TypeManager;
use Piwik\Version;
+use Piwik\Widget\WidgetsList;
require_once PIWIK_INCLUDE_PATH . '/core/Config.php';
@@ -122,83 +127,20 @@ class API extends \Piwik\Plugin\API
{
$isAuthenticatedWithViewAccess = Piwik::isUserHasViewAccess($idSites) && !Piwik::isUserIsAnonymous();
- $segments = array();
- foreach (Dimension::getAllDimensions() as $dimension) {
- foreach ($dimension->getSegments() as $segment) {
- if ($segment->isRequiresAtLeastViewAccess()) {
- $segment->setPermission($isAuthenticatedWithViewAccess);
- }
+ $sites = (is_array($idSites) ? implode('.', $idSites) : (int) $idSites);
+ $cache = Cache::getTransientCache();
+ $cachKey = 'API.getSegmentsMetadata' . $sites . '_' . (int) $_hideImplementationData . '_' . (int) $isAuthenticatedWithViewAccess;
+ $cachKey = CacheId::pluginAware($cachKey);
- $segments[] = $segment->toArray();
- }
+ if ($cache->contains($cachKey)) {
+ return $cache->fetch($cachKey);
}
- /**
- * Triggered when gathering all available segment dimensions.
- *
- * This event can be used to make new segment dimensions available.
- *
- * **Example**
- *
- * public function getSegmentsMetadata(&$segments, $idSites)
- * {
- * $segments[] = array(
- * 'type' => 'dimension',
- * 'category' => Piwik::translate('General_Visit'),
- * 'name' => 'General_VisitorIP',
- * 'segment' => 'visitIp',
- * 'acceptedValues' => '13.54.122.1, etc.',
- * 'sqlSegment' => 'log_visit.location_ip',
- * 'sqlFilter' => array('Piwik\IP', 'P2N'),
- * 'permission' => $isAuthenticatedWithViewAccess,
- * );
- * }
- *
- * @param array &$dimensions The list of available segment dimensions. Append to this list to add
- * new segments. Each element in this list must contain the
- * following information:
- *
- * - **type**: Either `'metric'` or `'dimension'`. `'metric'` means
- * the value is a numeric and `'dimension'` means it is
- * a string. Also, `'metric'` values will be displayed
- * under **Visit (metrics)** in the Segment Editor.
- * - **category**: The segment category name. This can be an existing
- * segment category visible in the segment editor.
- * - **name**: The pretty name of the segment. Can be a translation token.
- * - **segment**: The segment name, eg, `'visitIp'` or `'searches'`.
- * - **acceptedValues**: A string describing one or two exacmple values, eg
- * `'13.54.122.1, etc.'`.
- * - **sqlSegment**: The table column this segment will segment by.
- * For example, `'log_visit.location_ip'` for the
- * **visitIp** segment.
- * - **sqlFilter**: A PHP callback to apply to segment values before
- * they are used in SQL.
- * - **permission**: True if the current user has view access to this
- * segment, false if otherwise.
- * @param array $idSites The list of site IDs we're getting the available segments
- * for. Some segments (such as Goal segments) depend on the
- * site.
- */
- Piwik::postEvent('API.getSegmentDimensionMetadata', array(&$segments, $idSites));
-
- foreach ($segments as &$segment) {
- $segment['name'] = Piwik::translate($segment['name']);
- $segment['category'] = Piwik::translate($segment['category']);
-
- if ($_hideImplementationData) {
- unset($segment['sqlFilter']);
- unset($segment['sqlFilterValue']);
- unset($segment['sqlSegment']);
-
- if (isset($segment['suggestedValuesCallback'])
- && !is_string($segment['suggestedValuesCallback'])
- ) {
- unset($segment['suggestedValuesCallback']);
- }
- }
- }
+ $metadata = new SegmentMetadata();
+ $segments = $metadata->getSegmentsMetadata($idSites, $_hideImplementationData, $isAuthenticatedWithViewAccess);
+
+ $cache->save($cachKey, $segments);
- usort($segments, array($this, 'sortSegments'));
return $segments;
}
@@ -219,32 +161,6 @@ class API extends \Piwik\Plugin\API
return $values;
}
- private function sortSegments($row1, $row2)
- {
- $customVarCategory = Piwik::translate('CustomVariables_CustomVariables');
-
- $columns = array('type', 'category', 'name', 'segment');
- foreach ($columns as $column) {
- // Keep segments ordered alphabetically inside categories..
- $type = -1;
- if ($column == 'name') $type = 1;
-
- $compare = $type * strcmp($row1[$column], $row2[$column]);
-
- // hack so that custom variables "page" are grouped together in the doc
- if ($row1['category'] == $customVarCategory
- && $row1['category'] == $row2['category']
- ) {
- $compare = strcmp($row1['segment'], $row2['segment']);
- return $compare;
- }
- if ($compare != 0) {
- return $compare;
- }
- }
- return $compare;
- }
-
/**
* Returns the url to application logo (~280x110px)
*
@@ -343,6 +259,41 @@ class API extends \Piwik\Plugin\API
}
/**
+ * Get a list of all pages that shall be shown in a Piwik 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)
@@ -555,7 +506,12 @@ class API extends \Piwik\Plugin\API
if ($suggestedValuesCallbackRequiresTable) {
$values = call_user_func($segmentFound['suggestedValuesCallback'], $idSite, $maxSuggestionsToReturn, $table);
} else {
- $values = $this->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);
}
$values = $this->getMostFrequentValues($values);
@@ -600,9 +556,7 @@ class API extends \Piwik\Plugin\API
// If you update this, also update flattenVisitorDetailsArray
$segmentsNeedActionsInfo = array('visitConvertedGoalId',
'pageUrl', 'pageTitle', 'siteSearchKeyword',
- 'entryPageTitle', 'entryPageUrl', 'exitPageTitle', 'exitPageUrl',
- 'outlinkUrl', 'downloadUrl'
- );
+ 'entryPageTitle', 'entryPageUrl', 'exitPageTitle', 'exitPageUrl');
$isCustomVariablePage = stripos($segmentName, 'customVariablePage') !== false;
$isEventSegment = stripos($segmentName, 'event') !== false;
$isContentSegment = stripos($segmentName, 'content') !== false;
diff --git a/plugins/API/ProcessedReport.php b/plugins/API/ProcessedReport.php
index d9d315b4c0..65b59a2051 100644
--- a/plugins/API/ProcessedReport.php
+++ b/plugins/API/ProcessedReport.php
@@ -23,9 +23,11 @@ use Piwik\Metrics\Formatter;
use Piwik\Period;
use Piwik\Piwik;
use Piwik\Plugin\Report;
+use Piwik\Plugin\Reports;
use Piwik\Site;
use Piwik\Timer;
use Piwik\Url;
+use Piwik\Category\Category;
class ProcessedReport
{
@@ -166,77 +168,14 @@ class ProcessedReport
$availableReports = array();
- foreach (Report::getAllReports() as $report) {
+ $reports = new Reports();
+ foreach ($reports->getAllReports() as $report) {
$report->configureReportMetadata($availableReports, $parameters);
}
- /**
- * Triggered when gathering metadata for all available reports.
- *
- * Plugins that define new reports should use this event to make them available in via
- * the metadata API. By doing so, the report will become available in scheduled reports
- * as well as in the Piwik Mobile App. In fact, any third party app that uses the metadata
- * API will automatically have access to the new report.
- *
- * @param string &$availableReports The list of available reports. Append to this list
- * to make a report available.
- *
- * Every element of this array must contain the following
- * information:
- *
- * - **category**: A translated string describing the report's category.
- * - **name**: The translated display title of the report.
- * - **module**: The plugin of the report.
- * - **action**: The API method that serves the report.
- *
- * The following information is optional:
- *
- * - **dimension**: The report's [dimension](/guides/all-about-analytics-data#dimensions) if any.
- * - **metrics**: An array mapping metric names with their display names.
- * - **metricsDocumentation**: An array mapping metric names with their
- * translated documentation.
- * - **processedMetrics**: The array of metrics in the report that are
- * calculated using existing metrics. Can be set to
- * `false` if the report contains no processed
- * metrics.
- * - **order**: The order of the report in the list of reports
- * with the same category.
- *
- * @param array $parameters Contains the values of the sites and period we are
- * getting reports for. Some reports depend on this data.
- * For example, Goals reports depend on the site IDs being
- * requested. Contains the following information:
- *
- * - **idSites**: The array of site IDs we are getting reports for.
- * - **period**: The period type, eg, `'day'`, `'week'`, `'month'`,
- * `'year'`, `'range'`.
- * - **date**: A string date within the period or a date range, eg,
- * `'2013-01-01'` or `'2012-01-01,2013-01-01'`.
- *
- * TODO: put dimensions section in all about analytics data
- * @deprecated since 2.5.0 Use Report Classes instead.
- * @ignore
- */
- Piwik::postEvent('API.getReportMetadata', array(&$availableReports, $parameters));
-
- // TODO we can remove this one once we remove API.getReportMetadata event (except hideMetricsDoc)
foreach ($availableReports as &$availableReport) {
- // can be removed once we remove hook API.getReportMetadata
- if (!isset($availableReport['metrics'])) {
- $availableReport['metrics'] = Metrics::getDefaultMetrics();
- }
- // can be removed once we remove hook API.getReportMetadata
- if (!isset($availableReport['processedMetrics'])) {
- $availableReport['processedMetrics'] = Metrics::getDefaultProcessedMetrics();
- }
-
- if ($hideMetricsDoc) // remove metric documentation if it's not wanted
- {
+ if ($hideMetricsDoc) {
unset($availableReport['metricsDocumentation']);
- } else if (!isset($availableReport['metricsDocumentation'])) {
- // set metric documentation to default if it's not set
- // can be removed once we remove hook API.getReportMetadata
- $availableReport['metricsDocumentation'] = Metrics::getDefaultMetricsDocumentation();
}
}
@@ -270,6 +209,9 @@ class ProcessedReport
$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();
@@ -349,16 +291,8 @@ class ProcessedReport
*/
private static function sortReports($a, $b)
{
- static $order = null;
- if (is_null($order)) {
- $order = array();
- foreach (Report::$orderOfReports as $category) {
- $order[] = Piwik::translate($category);
- }
- }
- return ($category = strcmp(array_search($a['category'], $order), array_search($b['category'], $order))) == 0
- ? (@$a['order'] < @$b['order'] ? -1 : 1)
- : $category;
+ $reports = new Reports();
+ return $reports->compareCategories($a['category'], $a['subcategory'], $a['order'], $b['category'], $b['subcategory'], $b['order']);
}
public function getProcessedReport($idSite, $period, $date, $apiModule, $apiAction, $segment = false,
diff --git a/plugins/API/Reports/Get.php b/plugins/API/Reports/Get.php
index de087d0a86..61cded3856 100644
--- a/plugins/API/Reports/Get.php
+++ b/plugins/API/Reports/Get.php
@@ -10,6 +10,7 @@ namespace Piwik\Plugins\API\Reports;
use Piwik\Piwik;
use Piwik\Plugin\Report;
+use Piwik\Plugin\Reports;
class Get extends Report
{
@@ -29,7 +30,7 @@ class Get extends Report
$this->module = 'API';
$this->action = 'get';
- $this->category = 'API';
+ $this->categoryId = 'API';
$this->name = Piwik::translate('General_MainMetrics');
$this->documentation = '';
@@ -80,8 +81,9 @@ class Get extends Report
*/
private function getReportsToMerge()
{
+ $reports = new Reports();
$result = array();
- foreach (Report::getAllReportClasses() as $reportClass) {
+ foreach ($reports->getAllReportClasses() as $reportClass) {
if ($reportClass == 'Piwik\\Plugins\\API\\Reports\\Get') {
continue;
}
diff --git a/plugins/API/SegmentMetadata.php b/plugins/API/SegmentMetadata.php
new file mode 100644
index 0000000000..9d7e563bfb
--- /dev/null
+++ b/plugins/API/SegmentMetadata.php
@@ -0,0 +1,167 @@
+<?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\Columns\Dimension;
+use Piwik\Piwik;
+
+class SegmentMetadata
+{
+ public function getSegmentsMetadata($idSites = array(), $_hideImplementationData = true, $isAuthenticatedWithViewAccess)
+ {
+ $segments = array();
+
+ foreach (Dimension::getAllDimensions() as $dimension) {
+ foreach ($dimension->getSegments() as $segment) {
+ $segments[] = $segment->toArray();
+ }
+ }
+
+ /**
+ * Triggered when gathering all available segment dimensions.
+ *
+ * This event can be used to make new segment dimensions available.
+ *
+ * **Example**
+ *
+ * public function getSegmentsMetadata(&$segments, $idSites)
+ * {
+ * $segments[] = array(
+ * 'type' => 'dimension',
+ * 'category' => Piwik::translate('General_Visit'),
+ * 'name' => 'General_VisitorIP',
+ * 'segment' => 'visitIp',
+ * 'acceptedValues' => '13.54.122.1, etc.',
+ * 'sqlSegment' => 'log_visit.location_ip',
+ * 'sqlFilter' => array('Piwik\IP', 'P2N'),
+ * 'permission' => $isAuthenticatedWithViewAccess,
+ * );
+ * }
+ *
+ * @param array &$dimensions The list of available segment dimensions. Append to this list to add
+ * new segments. Each element in this list must contain the
+ * following information:
+ *
+ * - **type**: Either `'metric'` or `'dimension'`. `'metric'` means
+ * the value is a numeric and `'dimension'` means it is
+ * a string. Also, `'metric'` values will be displayed
+ * under **Visit (metrics)** in the Segment Editor.
+ * - **category**: The segment category name. This can be an existing
+ * segment category visible in the segment editor.
+ * - **name**: The pretty name of the segment. Can be a translation token.
+ * - **segment**: The segment name, eg, `'visitIp'` or `'searches'`.
+ * - **acceptedValues**: A string describing one or two exacmple values, eg
+ * `'13.54.122.1, etc.'`.
+ * - **sqlSegment**: The table column this segment will segment by.
+ * For example, `'log_visit.location_ip'` for the
+ * **visitIp** segment.
+ * - **sqlFilter**: A PHP callback to apply to segment values before
+ * they are used in SQL.
+ * - **permission**: True if the current user has view access to this
+ * segment, false if otherwise.
+ * @param array $idSites The list of site IDs we're getting the available segments
+ * for. Some segments (such as Goal segments) depend on the
+ * site.
+ */
+ Piwik::postEvent('API.getSegmentDimensionMetadata', array(&$segments, $idSites));
+
+ $segments[] = array(
+ 'type' => 'dimension',
+ 'category' => Piwik::translate('General_Visit'),
+ 'name' => 'General_UserId',
+ 'segment' => 'userId',
+ 'acceptedValues' => 'any non empty unique string identifying the user (such as an email address or a username).',
+ 'sqlSegment' => 'log_visit.user_id',
+ 'permission' => $isAuthenticatedWithViewAccess,
+ );
+
+ $segments[] = array(
+ 'type' => 'dimension',
+ 'category' => Piwik::translate('General_Visit'),
+ 'name' => 'General_VisitorID',
+ 'segment' => 'visitorId',
+ 'acceptedValues' => '34c31e04394bdc63 - any 16 Hexadecimal chars ID, which can be fetched using the Tracking API function getVisitorId()',
+ 'sqlSegment' => 'log_visit.idvisitor',
+ 'sqlFilterValue' => array('Piwik\Common', 'convertVisitorIdToBin'),
+ 'permission' => $isAuthenticatedWithViewAccess,
+ );
+
+ $segments[] = array(
+ 'type' => 'dimension',
+ 'category' => Piwik::translate('General_Visit'),
+ 'name' => Piwik::translate('General_Visit') . " ID",
+ 'segment' => 'visitId',
+ 'acceptedValues' => 'Any integer. ',
+ 'sqlSegment' => 'log_visit.idvisit',
+ 'permission' => $isAuthenticatedWithViewAccess,
+ );
+
+ $segments[] = array(
+ 'type' => 'metric',
+ 'category' => Piwik::translate('General_Visit'),
+ 'name' => 'General_VisitorIP',
+ 'segment' => 'visitIp',
+ 'acceptedValues' => '13.54.122.1. </code>Select IP ranges with notation: <code>visitIp>13.54.122.0;visitIp<13.54.122.255',
+ 'sqlSegment' => 'log_visit.location_ip',
+ 'sqlFilterValue' => array('Piwik\Network\IPUtils', 'stringToBinaryIP'),
+ 'permission' => $isAuthenticatedWithViewAccess,
+ );
+
+ foreach ($segments as &$segment) {
+ $segment['name'] = Piwik::translate($segment['name']);
+ $segment['category'] = Piwik::translate($segment['category']);
+
+ if ($_hideImplementationData) {
+ unset($segment['sqlFilter']);
+ unset($segment['sqlFilterValue']);
+ unset($segment['sqlSegment']);
+ }
+
+ 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('type', 'category', 'name', 'segment');
+
+ foreach ($columns as $column) {
+ // Keep segments ordered alphabetically inside categories..
+ $type = -1;
+ if ($column == 'name') $type = 1;
+
+ $compare = $type * strcmp($row1[$column], $row2[$column]);
+
+ // hack so that custom variables "page" are grouped together in the doc
+ if ($row1['category'] == $customVarCategory
+ && $row1['category'] == $row2['category']
+ ) {
+ $compare = strcmp($row1['segment'], $row2['segment']);
+ return $compare;
+ }
+
+ if ($compare != 0) {
+ return $compare;
+ }
+ }
+
+ return $compare;
+ }
+
+} \ No newline at end of file
diff --git a/plugins/API/WidgetMetadata.php b/plugins/API/WidgetMetadata.php
new file mode 100644
index 0000000000..ef12800a39
--- /dev/null
+++ b/plugins/API/WidgetMetadata.php
@@ -0,0 +1,282 @@
+<?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\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) {
+
+ /** @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;
+ }
+
+ $flat[] = $this->buildWidgetMetadata($widget, $categoryList);
+ }
+ }
+
+ 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)
+ * @return array
+ */
+ public function buildWidgetMetadata(WidgetConfig $widget, $categoryList = null)
+ {
+ $item = array(
+ 'name' => Piwik::translate($widget->getName())
+ );
+
+ if (isset($categoryList)) {
+ $category = $categoryList->getCategory($widget->getCategoryId());
+ $subcategory = $category ? $category->getSubcategory($widget->getSubcategoryId()) : 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();
+
+ $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 0;
+ }
+
+ return $subOrderA > $subOrderB ? 1 : -1;
+
+ } elseif (!empty($orderA)) {
+
+ return 1;
+ }
+
+ return -1;
+ }
+
+ return $orderA > $orderB ? 1 : -1;
+ }
+
+ /**
+ * @param Category|null $category
+ * @return array
+ */
+ private function buildCategoryMetadata($category)
+ {
+ if (!isset($category)) {
+ return null;
+ }
+
+ return array(
+ 'id' => (string) $category->getId(),
+ 'name' => Piwik::translate($category->getId()),
+ 'order' => $category->getOrder(),
+ );
+ }
+
+ /**
+ * @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;
+ }
+
+} \ No newline at end of file
diff --git a/plugins/API/tests/Unit/WidgetMetadataTest.php b/plugins/API/tests/Unit/WidgetMetadataTest.php
new file mode 100644
index 0000000000..bb4379a010
--- /dev/null
+++ b/plugins/API/tests/Unit/WidgetMetadataTest.php
@@ -0,0 +1,278 @@
+<?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\tests\Unit;
+
+use Piwik\Category\Category;
+use Piwik\Category\CategoryList;
+use Piwik\Category\Subcategory;
+use Piwik\DataTable;
+use Piwik\Plugins\API\Renderer\Console;
+use Piwik\Plugins\API\WidgetMetadata;
+use Piwik\Plugins\CoreHome\CoreHome;
+use Piwik\Report\ReportWidgetConfig;
+use Piwik\Widget\WidgetConfig;
+use Piwik\Widget\WidgetContainerConfig;
+
+/**
+ * @group Widget
+ * @group Widgets
+ * @group WidgetMetadata
+ * @group WidgetMetadataTest
+ */
+class WidgetMetadataTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var WidgetMetadata
+ */
+ private $metadata;
+
+ public function setUp()
+ {
+ $this->metadata = new WidgetMetadata();
+ }
+
+ public function test_buildWidgetMetadata_ShouldGenerateMetadata()
+ {
+ $config = $this->createWidgetConfig('Test', 'CategoryId', 'SubcategoryId');
+ $list = $this->createCategoryList(array('CategoryId' => array('SubcategoryId')));
+ $metadata = $this->metadata->buildWidgetMetadata($config, $list);
+
+ $this->assertEquals(array(
+ 'name' => 'Test',
+ 'category' => array(
+ 'id' => 'CategoryId',
+ 'name' => 'CategoryId',
+ 'order' => 99,
+ ),
+ 'subcategory' => array(
+ 'id' => 'SubcategoryId',
+ 'name' => 'SubcategoryIdName',
+ 'order' => 99,
+ ),
+ 'module' => 'CoreHome',
+ 'action' => 'render',
+ 'order' => 99,
+ 'parameters' => array (
+ 'module' => 'CoreHome',
+ 'action' => 'render'
+ ),
+ 'uniqueId' => 'widgetCoreHomerender'
+ ), $metadata);
+ }
+
+ public function test_buildWidgetMetadata_ShouldSetCategoryAndSubcategoryToNull_IfBothGivenButNotExistInList()
+ {
+ $config = $this->createWidgetConfig('Test', 'CategoryId', 'SubcategoryId');
+ $list = $this->createCategoryList();
+ $metadata = $this->metadata->buildWidgetMetadata($config, $list);
+
+ $this->assertNull($metadata['category']);
+ $this->assertNull($metadata['subcategory']);
+ }
+
+ public function test_buildWidgetMetadata_ShouldSetSubcategoryToNull_IfCategoryGivenInListButSubcategoryNot()
+ {
+ $config = $this->createWidgetConfig('Test', 'CategoryId', 'SubcategoryId');
+ $list = $this->createCategoryList(array('CategoryId' => array()));
+ $metadata = $this->metadata->buildWidgetMetadata($config, $list);
+
+ $this->assertSame(array(
+ 'id' => 'CategoryId',
+ 'name' => 'CategoryId',
+ 'order' => 99,
+ ), $metadata['category']);
+ $this->assertNull($metadata['subcategory']);
+ }
+
+ public function test_buildWidgetMetadata_ShouldNotAddCategoryAndSubcategoryToNull_IfNoCategoryListGiven()
+ {
+ $config = $this->createWidgetConfig('Test', 'CategoryId', 'SubcategoryId');
+ $metadata = $this->metadata->buildWidgetMetadata($config);
+
+ $this->assertArrayNotHasKey('category', $metadata);
+ $this->assertArrayNotHasKey('subcategory', $metadata);
+ }
+
+ public function test_buildWidgetMetadata_ShouldAddOptionalMiddlewareParameters()
+ {
+ $config = $this->createWidgetConfig('Test', 'CategoryId', 'SubcategoryId');
+ $config->setMiddlewareParameters(array('module' => 'Goals', 'action' => 'hasAnyConversions'));
+ $metadata = $this->metadata->buildWidgetMetadata($config);
+
+ $this->assertSame(array('module' => 'Goals', 'action' => 'hasAnyConversions'), $metadata['middlewareParameters']);
+ }
+
+ public function test_buildWidgetMetadata_ShouldAddReportInformtion_IfReportWidgetConfigGiven()
+ {
+ $config = new ReportWidgetConfig();
+ $config->setDefaultViewDataTable('graph');
+ $metadata = $this->metadata->buildWidgetMetadata($config);
+
+ $this->assertSame('graph', $metadata['viewDataTable']);
+ $this->assertTrue($metadata['isReport']);
+ }
+
+ public function test_buildWidgetMetadata_ShouldAddContainerInformtion_IfWidgetContainerConfigGiven()
+ {
+ $config = new WidgetContainerConfig();
+ $config->setLayout('ByDimension');
+ $config->addWidgetConfig($this->createWidgetConfig('NestedName1', 'NestedCategory1', 'NestedSubcategory1'));
+ $config->addWidgetConfig($this->createWidgetConfig('NestedName2', 'NestedCategory2', 'NestedSubcategory2'));
+ $metadata = $this->metadata->buildWidgetMetadata($config);
+
+ $this->assertSame('ByDimension', $metadata['layout']);
+ $this->assertTrue($metadata['isContainer']);
+ $this->assertCount(2, $metadata['widgets']);
+
+ $widget1 = $metadata['widgets'][0];
+ $widget2 = $metadata['widgets'][1];
+ $this->assertSame(array(
+ 'name' => 'NestedName1',
+ 'category' => array (
+ 'id' => 'NestedCategory1',
+ 'name' => 'NestedCategory1',
+ 'order' => 99
+ ),
+ 'subcategory' => array (
+ 'id' => 'NestedSubcategory1',
+ 'name' => 'NestedSubcategory1',
+ 'order' => 99
+ ),
+ 'module' => 'CoreHome',
+ 'action' => 'render',
+ 'order' => 99,
+ 'parameters' => array (
+ 'module' => 'CoreHome',
+ 'action' => 'render',
+ ),
+ 'uniqueId' => 'widgetCoreHomerender'
+ ), $widget1);
+ $this->assertSame(array(
+ 'name' => 'NestedName2',
+ 'category' => array (
+ 'id' => 'NestedCategory2',
+ 'name' => 'NestedCategory2',
+ 'order' => 99
+ ),
+ 'subcategory' => array (
+ 'id' => 'NestedSubcategory2',
+ 'name' => 'NestedSubcategory2',
+ 'order' => 99
+ ),
+ 'module' => 'CoreHome',
+ 'action' => 'render',
+ 'order' => 99,
+ 'parameters' => array (
+ 'module' => 'CoreHome',
+ 'action' => 'render',
+ ),
+ 'uniqueId' => 'widgetCoreHomerender'
+ ), $widget2);
+ }
+
+ public function test_buildPageMetadata_ShouldAddContainerInformtion_IfWidgetContainerConfigGiven()
+ {
+ $config = new WidgetContainerConfig();
+ $config->setLayout('ByDimension');
+
+ $widgets = array(
+ $this->createWidgetConfig('NestedName1', 'NestedCategory1', 'NestedSubcategory1'),
+ $this->createWidgetConfig('NestedName2', 'NestedCategory2', 'NestedSubcategory1'),
+ );
+
+ $category = $this->createCategory('NestedCategory1');
+ $subcategory = $this->createSubcategory('NestedCategory1' ,'NestedSubcategory1');
+
+ $metadata = $this->metadata->buildPageMetadata($category, $subcategory, $widgets);
+
+ $this->assertSame(array(
+ 'uniqueId' => 'NestedCategory1.NestedSubcategory1',
+ 'category' => array (
+ 'id' => 'NestedCategory1',
+ 'name' => 'NestedCategory1',
+ 'order' => 99,
+ ),
+ 'subcategory' => array (
+ 'id' => 'NestedSubcategory1',
+ 'name' => 'NestedSubcategory1Name',
+ 'order' => 99,
+ ),
+ 'widgets' => array (
+ 0 => array ( // widgets should not have category / subcategory again, it's already present above
+ 'name' => 'NestedName1',
+ 'module' => 'CoreHome',
+ 'action' => 'render',
+ 'order' => 99,
+ 'parameters' => array (
+ 'module' => 'CoreHome',
+ 'action' => 'render',
+ ),
+ 'uniqueId' => 'widgetCoreHomerender',
+ ), array (
+ 'name' => 'NestedName2',
+ 'module' => 'CoreHome',
+ 'action' => 'render',
+ 'order' => 99,
+ 'parameters' => array (
+ 'module' => 'CoreHome',
+ 'action' => 'render',
+ ),
+ 'uniqueId' => 'widgetCoreHomerender'
+ )
+ )
+ ), $metadata);
+ }
+
+ private function createWidgetConfig($name, $categoryId, $subcategoryId = '')
+ {
+ $widgetConfig = new WidgetConfig();
+ $widgetConfig->setName($name);
+ $widgetConfig->setCategoryId($categoryId);
+ $widgetConfig->setSubcategoryId($subcategoryId);
+ $widgetConfig->setModule('CoreHome');
+ $widgetConfig->setAction('render');
+
+ return $widgetConfig;
+ }
+
+ private function createCategoryList($categories = array())
+ {
+ $list = new CategoryList();
+
+ foreach ($categories as $categoryId => $subcategoryIds) {
+ $category = $this->createCategory($categoryId);
+ $list->addCategory($category);
+
+ foreach ($subcategoryIds as $subcategoryId) {
+ $subcategory = $this->createSubcategory($categoryId, $subcategoryId);
+ $category->addSubcategory($subcategory);
+ }
+ }
+
+ return $list;
+ }
+
+ private function createSubcategory($categoryId, $subcategoryId)
+ {
+ $subcategory = new Subcategory();
+ $subcategory->setCategoryId($categoryId);
+ $subcategory->setId($subcategoryId);
+ $subcategory->setName($subcategoryId . 'Name');
+
+ return $subcategory;
+ }
+
+ private function createCategory($categoryId)
+ {
+ $category = new Category();
+ $category->setId($categoryId);
+ return $category;
+ }
+
+}