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,8 @@
<html>
<head>
<meta http-equiv="refresh" content="0;url=https://matomo.org/docs/installation/"/>
<meta name="robots" content="noindex,nofollow">
</head>
<body>You will be redirected to the Matomo Analytics Installation documentation on <a href='https://matomo.org/docs/installation/'>matomo.org/docs/installation</a>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,827 @@
# Matomo Platform Changelog
This is the Developer Changelog for Matomo platform developers. All changes in our HTTP API's, Plugins, Themes, SDKs, etc. are listed below.
The Product Changelog at **[matomo.org/changelog](https://matomo.org/changelog)** lets you see more details about any Matomo release, such as the list of new guides and FAQs, security fixes, and links to all closed issues.
## Matomo 3.9.0
### Breaking Changes
* `Referrers.getKeywordsForPageUrl` and `Referrers.getKeywordsForPageTitle` APIs have been deprecated and will be removed in Matomo 4.0.0
* By default, Matomo [application logs](https://matomo.org/faq/troubleshooting/faq_115/) will now be logged in `tmp/logs/matomo.log` instead of `tmp/logs/piwik.log`. This log file path can be edited in your config/config.ini.php in the INI setting `logger_file_path`.
### New Features
* It is now possible to locate plugins in a custom directory by setting an environment variable `MATOMO_PLUGIN_DIRS` or a `$GLOBALS['MATOMO_PLUGIN_DIRS']` variable in `$MATOMO_ROOT/bootstrap.php`.
* It is now possible to use monolog's FingersCrossedHandler which buffers all logs and logs all of them in case of warning or error.
### New APIs
* New API methods `Piwik\Plugin\Manager::getPluginsDirectories()` and `Piwik\Plugin\Manager::getPluginDirectory($pluginName)` have been added as it is now possible to locate Matomo plugins in different directories and it should be no longer assumed a plugin is located in the "/plugins" directory.
* A new tracker method `disableQueueRequest` has been added to disable queued requests which may be useful when logs are imported.
* The event `LanguageManager.getAvailableLanguages` has been deprecated. Use `LanguagesManager.getAvailableLanguages` instead.
## Matomo 3.8.0
### Breaking Changes
* When changing the email address or the password through the `UsersManager.updateUser` API, a new parameter `passwordConfirmation` needs to be sent along with the request containing the current password of the user issuing the API request.
* The output type "save on disk" in the API method `ScheduledReport.generateReport` has been replaced by the download output type.
* The method `Piwik\Piwik::doAsSuperUser` has been deprecated and will be removed in Matomo 4. Use `Piwik\Access::doAsSuperUser` instead.
### New APIs
* It is now possible to queue a request on the JavaScript tracker using the method `queueRequest(requestUrl)`. This can be useful to group multiple tracking requests into one bulk request to reduce the number of tracking requests that are sent to your server making the tracking more efficient.
* When specifying a callback in the JavaScript tracker in a tracker method, we now make sure to execute the callback even in error cases or when sentBeacon is used. The callback recevies an event parameter to determine which request was sent and whether the request was sent successfully.
* Added new event `Metrics.getEvolutionUnit` which lets you set the unit for a metric used in evolution charts and row evolution.
### New Features
* The log importer now supports the `--tracker-endpoint-path` parameter which allows you to use a different tracker endpoint than `/piwik.php`, if desired.
* It is now possible to define different log levels for different log writers via INI config. Set log_level_file, for example, to set the log level for the file writer, or log_level_screen for the screen writer.
### Internal change
* New Matomo installation will now use by default "matomo.js" and "matomo.php" as tracking endpoints. From Matomo 4.0 all installations will use "matomo.js" and "matomo.php" by default. We recommend you ensure those files can be accessed through the web and are not blocked.
### Deprecations
* The method `Piwik\SettingsPiwik::isPiwikInstalled()` has been deprecated and renamed to `isMatomoInstalled()`. It is still supported to use the method, but the method will be removed in Piwik 4.0.0
## Matomo 3.6.1
### New APIs
* Added new event `Access.modifyUserAccess` which lets plugins modify current user's access levels/permissions.
* Added new event `CustomMatomoJs.manipulateJsTracker` which lets plugins modify the JavaScript tracker.
### New Developer Features
* Logging to a file can now be easily enabled during tests. A new `[tests] enable_logging` INI option has been added, which you can set to `1` to enable logging for all tests. The `tests:run` and `tests:run-ui` commands now both have an `--enable-logging` option to enable logging for a specific run.
## Matomo 3.6.0
### New Features
* A new role has introduced called "write" which has less permissions than an admin but more than a view only user ([see FAQ](https://matomo.org/faq/general/faq_26910/)).
* Custom currencies can now be added using the `currencies[]` configuration key.
* A new segment `eventValue` lets you select all users who tracked a custom event with a given value or range of values.
### New config.ini.php settings
* `archiving_profile = 0`, if set to 1, core:archive profiling information will be recorded in a log file. the log file is determined by the `archive_profiling_log` option.
* `archive_profiling_log = `, if set to an absolute path, core:archive profiling information will be logged to specified file.
* `enable_internet_features=0` will now fully disable Internet access by preventing all outgoing connections. Note: changing this setting is not recommended for security, because you will lose the easy auto-update and email notifications.
* `login_whitelist_ip[]` now supports hostnames so you can [whitelist](https://matomo.org/faq/how-to/faq_25543/) your IP addresses and/or Hostnames and keep your Matomo secure.
### Updated commands
* New parameter `--concurrent-archivers` to define the number of maximum archivers to run in parallel on this server. Useful to prevent archiving processes piling up and ultimately failing.
### New APIs
* Added new event `API.addGlossaryItems` which lets you add items to the glossary.
* Added new event `Tracker.detectReferrerSocialNetwork` which lets you add custom social network detections
* Added new event `Report.unsubscribe` which is triggered whenever someone unsubscribe from a report
* Added new API method `UsersManager.getAvailableRoles` to fetch a list of all available roles that can be granted to a user.
* Added new API method `UsersManager.getAvailableCapabilities` to fetch a list of all available capabilities that can be granted to a user.
* Added new API method `UsersManager.addCapabilities` to grant one or multiple capabilities to a user.
* Added new API method `UsersManager.removeCapabilities` to remove one or multiple capabilities from a user.
* The API method `UsersManager.setUserAccess` now accepts an array to pass a role and multiple capabilities at once.
* Plugin classes can overwrite the method `requiresInternetConnection` to define if they should be automatically unloaded if no internet connection is available (enable_internet_features = 0)
* Added two new methods to the JS tracker: `removeEcommerceItem` and `clearEcommerceCart` to allow better control over what is in the ecommerce cart.
* Tracking API requests now include `&consent=1` in the Tracking API URL When [consent](https://developer.matomo.org/guides/tracking-javascript-guide#asking-for-consent) has been given by a user.
### Breaking Changes
* Changed some menu items to use translation keys instead (see [PR #12885](https://github.com/matomo-org/matomo/pull/12885)).
* The methods `assertResponseCode()` and `assertHttpResponseText()` in `Piwik\Tests\Framework\TestCase\SystemTestCase` have been deprecated and will be removed in Matomo 4.0. Please use `Piwik\Http` instead.
* The classes `PHPUnit\Framework\Constraint\HttpResponseText` and `PHPUnit\Framework\Constraint\ResponseCode` have been deprecated and will be removed in Matomo 4.0. Please use `Piwik\Http` instead.
* Creating links through the Proxy has been deprecated. Use rel="nofollow" instead.
* The console option `--piwik-domain` has been deprecated and will be removed in Matomo 4.0. Use `--matomo-domain` instead
* Social networks are now detected as new referrer type (ID=7), which allows improved reports and better segmentation
* New settings form field UI component "Field Array" that lets users enter multiple values for one setting as a flat array
## Matomo 3.5.1
### New APIs
* Added new method `Piwik\API\Request::isRootRequestApiRequest()` to detect if the root request is an API request.
## Matomo 3.5.0
### Breaking Changes
* Flattened action url reports now always include a leading `/` and will no longer include the `default_action_name`. e.g. `path/to/index` will now be `/path/to/`. This might affect configured custom alerts, as this plugin uses the flattened url reports for comparison.
### New APIs
* New JavaScript tracker functions to [ask for consent](https://developer.matomo.org/guides/tracking-javascript-guide#asking-for-consent): `requireConsent`, `rememberConsentGiven`, `setConsentGiven`, `forgetConsentGiven`.
### New Features
* New events `PrivacyManager.deleteLogsOlderThan`, `PrivacyManager.exportDataSubjects` and `PrivacyManager.deleteDataSubjects` to enable plugins to be GDPR compliant.
* New event `AssetManager.addStylesheets` to add additional less styles which are not located in a file.
* New event `Archiving.getIdSitesToMarkArchivesAsInvalidated` that lets plugins customize the behaviour of report invalidations.
* Reports and visualizations can now disable the 'all' rows limit selector: `$view->config->disable_all_rows_filter_limit`.
* New settings form field UI component "Multi Tuple" that lets users enter multiple values for one setting
## Matomo 3.4.0
### Breaking Changes
`piwik` font is deprecated and will be removed in Matomo 4.0. Please use new `matomo` font instead
Sending synchronous requests using ajaxHelper is now deprecated. All requests will be send async as of Matomo 4.0
### New APIs
* A new JavaScript tracker method `resetUserId` has been added to allow clearing user and visitor id.
* A new event `Actions.addActionTypes` has been added, to allow plugins to add their custom action types.
* Dashboard API has been extended by the methods `copyDashboardToUser`, `createNewDashboardForUser`, `removeDashboard` and `resetDashboardLayout`
* It is also now possible to delete the first dashboard for a user for automation purposes. Doing so and not adding a new first dashboard might result in buggy UX.
* `getDashboards` API method has been extended by additional parameters to fetch dashboards for specific user
* A new event `API.Request.intercept` has been added which allows plugins to intercept API requests to perform custom logic, overriding the original API method.
* A new event `Request.shouldDisablePostProcessing` has been added which allows plugins to disable DataTable post processing for individual API requests.
* A new event `SitesManager.shouldPerformEmptySiteCheck` has been added to allow plugins to disable the empty site check for individual sites.
* A new JavaScript tracker method `getCrossDomainLinkingUrlParameter` has been added so you can add cross domain tracking capability to dynamically created links. [Learn here how to append the result to said links' URLs, see the section "Advanced: Handling Dynamically Generated Links"](https://matomo.org/faq/how-to/faq_23654/)
## Matomo 3.3.0
Piwik is now Matomo. Read more about this change in the [official announcement](https://matomo.org/blog/2018/01/piwik-is-now-matomo).
### New APIs
* New HTTP API `API.getMatomoVersion` was introduced. The previous HTTP API `API.getPiwikVersion` will still work but will now be hidden from the API reference page.
## Piwik 3.2.2
### Breaking Changes
* The `historyService` along with `broadcast.init`, `broadcast.propagateAjax`, `broadcast.pageLoad` have been deprecated and will be removed in Piwik 4.
## Piwik 3.2.1
### New APIs
### New Features
* Themes can now customize the header text color using `@theme-color-header-text`
* New event `Widgetize.shouldEmbedIframeEmpty` added so plugins can optionally define the output of the widgetized HTML themselves
* New events added to add and filter visitor details: `Live.addProfileSummaries` and `Live.filterProfileSummaries`
* New JavaScript method `piwikHelper.registerShortcut` allows plugins to bind keyboard shortcuts. A summary for available shortcuts will be shown by pressing `?`
## Piwik 3.2.0
### New Segments
* New Segment added: `visitStartServerMinute` for Server time - minute (Start of visit)
* New Segment added: `visitEndServerMinute` for Server time - minute (End of visit)
* New events added to add and filter visitor details: `Live.addVisitorDetails` and `Live.filterVisitorDetails`
### New APIs
* Reports and visualizations can now hide the export icons with a new property `$view->config->show_export`.
* Reports and visualizations can now show a message above the report with a new property `$view->config->show_header_message`.
* The following events have been added:
* `Metric.addMetrics` Triggered to add new metrics that cannot be picked up automatically by the platform.
* `Metric.addComputedMetrics` Triggered to add computed metrics that are not generated automatically
* `Metric.filterMetrics` Triggered to filter metrics
* The following new API classes have been added:
* `Piwik\Columns\MetricsList` Holds a list of all available metrics
* `Piwik\Columns\ComputedMetricFactory` Can be used to create computed metrics
* `Piwik\Columns\DimensionMetricFactory` Can be used to create metrics directly within a dimension
### New Features
* New config.ini.php setting `show_update_notification_to_superusers_only` makes it possible to hide update notifications for all users except of superusers
## Piwik 3.1.0
### Breaking Changes
* The event `Live.getAllVisitorDetails` has been deprecated and will be removed in Piwik 4. Use a `VisitorDetails` class instead (see Live plugin).
### New Features
* New method `setSecureCookie` that sets the cookie's secure parameter
### New APIs
* The events `ScheduledTasks.shouldExecuteTask`, `ScheduledTasks.execute`, `ScheduledTasks.execute.end` have been added to customize the behaviour of scheduled tasks.
* A new event `CustomPiwikJs.shouldAddTrackerFile` has been added to let plugins customize which tracker files should be included in piwik.js JavaScript tracker
* A new event `Login.authenticate.successful` has been added, which is triggered when a user successful signs in
* A new API class `Piwik\Plugins\CustomPiwikJs\TrackerUpdater` has been added to update the piwik.js JavaScript tracker
### New commands
* The commands `plugin:activate` and `plugin:deactivate` can now activate and deactivate multiple plugins at once
## Piwik 3.0.4
### New APIs
* A new event `Db.getActionReferenceColumnsByTable` has been added in case a plugin defines a custom log table which references data to the log_action table
* The event `System.addSystemSummaryItems` and `System.filterSystemSummaryItems` have been added so plugins can add items and filter items of the system summary widget
* A new JavaScript tracker method `getPiwikUrl` has been added to retrieve the URL of where the Piwik instance is located
* A new JavaScript tracker method `getCurrentUrl` has been added to retrieve the current URL of the website.
* A new JavaScript tracker method `getNumTrackedPageViews` has been added to retrieve the number of tracked page views within the currently loaded page or web application.
* New JavaScript tracker methods `setSessionCookie`, `getCookie`, `hasCookies`, `getCookieDomain`, `getCookiePath`, and `getSessionCookieTimeout` have been added for better cookie support in plugins.
* `email` and `url` form fields can now be used in settings.
## Piwik 3.0.3
### Breaking Changes
* New config setting `enable_plugin_upload` lets you enable uploading and installing a Piwik plugin ZIP file by a Super User. This used to be enabled by default, but it is now disabled by default now for security reasons.
* New Report class property `Report::$supportsFlatten` lets you define if a report supports flattening (defaults to `true`). If set to `false` it will also set `ViewDataTable\Config::$show_flatten_table` to `false`
### New APIs
* A new event `Controller.triggerAdminNotifications` has been added to let plugins know when they are supposed to trigger notifications in the admin.
### Library updates
* pChart library has been removed in favor of [CpChart](https://github.com/szymach/c-pchart), a pChart fork with composer support and PSR standards.
## Piwik 3.0.2
### New Features
* A new SMS provider for sms reports has been added: [ASPSMS.com](https://www.aspsms.com/en/?REF=227830)
### New APIs
* The JavaScript Tracker now supports CrossDomain tracking. The following tracker methods were added for this: `enableCrossDomainLinking`, `disableCrossDomainLinking`, `isCrossDomainLinkingEnabled`
* Added JavaScript Tracker method `getLinkTrackingTimer` to get the value of the configured link tracking time
* Added JavaScript Tracker method `deleteCustomVariables` to delete all custom variables within a certain scope
* The method `enableLinkTracking` can now be called several times to make Piwik aware of newly added links when your DOM changes
* Added a new method `Piwik\Plugin\Report::getMetricNamesToProcessReportTotals()` that lets you define which metrics should show percentages in the table report visualization on hover. If defined, these percentages will be automatically calculated.
* The event `Tracker.newConversionInformation` now posts a new fourth parameter `$action`
* New HTTP API method `UserCountry.getCountryCodeMapping` to get a list of used country codes to country names
### Changes
* SMS provider now can define their credential fields by overwriting `getCredentialFields()`. This allows to have SMS providers that require more than only an API key.
* Therefore the MobileMessaging API method `setSMSAPICredential()` now takes the second parameter as an array filled with credentials (instead of a string containing an API key)
## Piwik 3.0.1
### New APIs
* Live API responses now return a new field generationTimeMilliseconds (the generation time for this page, in milliseconds) which is internally used to process the Average generation time in the [Visitor Profile](https://matomo.org/docs/user-profile/)
* Added new event `MultiSites.filterRowsForTotalsCalculation` to filter which sites will be included in the All Websites Dashboard totals calculation.
* The method `Piwik\Plugin\Archiver::shouldRunEvenWhenNoVisits()` has been added. By overwriting this method and returning true, a plugin archiver can force the archiving to run even when there was no visit for the website/date/period/segment combination (by default, archivers are skipped when there is no visit).
## Piwik 3.0.0
### New guide
Read more about migrating a plugin from Piwik 2.X to Piwik 3 in [our Migration guide](https://developer.matomo.org/guides/migrate-piwik-2-to-3).
### Breaking Changes
* When using the Piwik JavaScript Tracking via `_paq.push`, it is now required to configure the tracker (eg calling `setSiteId` and `setTrackerUrl`) before the `piwik.js` JavaScript tracker is loaded to ensure the tracker works correctly.
If the tracker is not initialised correctly, the browser console will display the error "_paq.push() was used but Piwik tracker was not initialized before the piwik.js file was loaded. [...]"
* The UserManager API methods do no longer return any `token_auth` properties when requesting a user
* The menu classes `Piwik\Menu\MenuReporting` and `Piwik\Menu\MenuMain` have been removed
* The class `Piwik\Plugin\Widgets` has been removed and replaced by `Piwik\Widget\Widget`. For each widget one class is needed from now on. You can generate a widget via `./console generate:widget`.
* The class `Piwik\WidgetList` class has been moved to `Piwik\Widget\WidgetsList`.
* The method `Piwik\Plugins\API\API::getLastDate()` has been removed.
* The method `Piwik\Archive::getDataTableFromArchive()` has been removed, use `Piwik\Archive::createDataTableFromArchive` instead.
* The method `Piwik\Plugin\Menu::configureReportingMenu` has been removed. To add something to the reporting menu you need to create widgets
* The method `Report::configureWidget()`, `Report::getWidgetTitle()` and `Report::configureReportingMenu()` have been removed, use the new method `Report::configureWidgets()` instead.
* The method `Report::getCategory()` has been moved to `Report::getCategoryId()` and does no longer return the translated category but the translation key of the category.
* The property `Report::$category` has been renamed to `Report::$categoryId`
* The methods `Report::factory()`, `Report::getAllReportClasses()`, `Report::getAllReports` have been moved to the `Piwik\Plugin\Reports` class.
* The properties `Report::$widgetTitle`, `Report::$widgetParams` and `Report::$menuTitle` were removed, use the method `Report::configureWidgets()` to create widgets instead
* In the HTTP API methods `Dashboard.getDefaultDashboard` and `Dashboard.getUserDashboards` we do no longer remove not existing widgets as it is up to the client which widgets actually exist
* The method `Piwik\Plugin\Controller::getEvolutionHtml` has been removed without a replacement as it should be no longer needed. The evolution is generated by ViewDataTables directly
* The `core:plugin` console command has been removed in favor of the new `plugin:list`, `plugin:activate` and `plugin:deactivate` commands as announced in Piwik 2.11
* The visibility of private properties and methods in `Piwik\Plugins\Login\Controller` were changed to `protected`
* Controller actions are now case sensitive. This means the URL and events have to use the same case as the name of the action defined in a controller.
* When calling the HTTP Reporting API, a default filter limit of 100 is now always applied. The default filter limit used to be not applied to API calls that do not return reports, such as when requesting sites, users or goals information.
* The "User Menu" was removed and should be replaced by "Admin Menu". Change `configureUserMenu(MenuUser $menu)` to `configureAdminMenu(MenuAdmin $menu)` in your `Menu.php`.
* The method `Piwik\Menu\MenuAbstract::add()` has been removed, use `Piwik\Menu\MenuAbstract::addItem()` instead
* The method `Piwik\Menu\MenuAdmin::addSettingsItem()` was removed, use `Piwik\Menu\MenuAdmin::addSystemItem()` instead.
* A new method `Piwik\Menu\MenuAdmin::addMeasurablesItem()` was added.
* The class `Piwik\Plugin\Settings` has been split to `Piwik\Settings\Plugin\SystemSettings` and `Piwik\Settings\Plugin\UserSettings`.
* The creation of settings has slightly changed to improve performance. It is now possible to create new settings via the method `$this->makeSetting()` see `Piwik\Plugins\ExampleSettingsPlugin\SystemSettings` for an example.
* It is no longer possible to define an introduction text for settings.
* If requesting multiple periods for one report, the keys that define the range are no longer translated. For example before 3.0 an API response may contain: `<result date="From 2010-02-01 to 2010-02-07">` which is now `<result date="2010-02-01,2010-02-07">`.
* The following deprecated events have been removed as mentioned.
* `Tracker.existingVisitInformation` Use [dimensions](https://developer.matomo.org/guides/dimensions) instead of using `Tracker` events.
* `Tracker.newVisitorInformation`
* `Tracker.recordAction`
* `Tracker.recordEcommerceGoal`
* `Tracker.recordStandardGoals`
* `API.getSegmentDimensionMetadata` Define segments in [Dimension](https://developer.matomo.org/guides/dimensions) instead
* `Menu.Admin.addItems` Create a [Menu](https://developer.matomo.org/guides/menus) instead of using `Menu` events
* `Menu.Reporting.addItems`
* `Menu.Top.addItems`
* `ViewDataTable.addViewDataTable` Create a [Visualization](https://developer.matomo.org/guides/visualizing-report-data) instead
* `ViewDataTable.getDefaultType` Specify the default type in a [Report](https://developer.matomo.org/guides/custom-reports) instead
* `Login.authenticate` Create a custom SessionInitializer instead of using `Login` events
* `Login.initSession.end`
* `Login.authenticate.successful`
* When posting one of the events `API.Request.dispatch`, `API.Request.dispatch.end`, `API.$plugin.$apiAction`, or `API.$plugin.$apiAction.end` the `$finalParameters` parameter is indexed in Piwik 2 (eg `array(1, 6)`), and named in Piwik 3 (eg `array('idSite' => 1, 'idGoal' => 6)`)
* Widgets using the already removed `UserSettings` plugin won't work any longer. Please update the module and action parameter in the widget url according to the following list
old module | old action | new module | new action
---------- | ---------- | ---------- | ----------
UserSettings | getPlugin | DevicePlugins | getPlugin
UserSettings | index | DevicesDetection | software
UserSettings | getBrowser | DevicesDetection | getBrowsers
UserSettings | getBrowserVersions | DevicesDetection | getBrowserVersions
UserSettings | getMobileVsDesktop | DevicesDetection | getType
UserSettings | getOS | DevicesDetection | getOsVersions
UserSettings | getOSFamily | DevicesDetection | getOsFamilies
UserSettings | getBrowserType | DevicesDetection | getBrowserEngines
UserSettings | getResolution | Resolution | getResolution
UserSettings | getConfiguration | Resolution | getConfiguration
UserSettings | getLanguage | UserLanguage | getLanguage
UserSettings | getLanguageCode | UserLanguage | getLanguageCode
Read more about migrating a plugin from Piwik 2.X to Piwik 3 on our [Migration guide](https://developer.matomo.org/guides/migrate-piwik-2-to-3).
### Deprecations
* The method `Piwik\Updates::getMigrationQueries()` has been deprecated and renamed to `getMigrations()`. It is still supported to use the method, but the method will be removed in Piwik 4.0.0
* The method `Piwik\Updater::executeMigrationQueries()` has been deprecated and renamed to `executeMigrations`. It is still supported to use the method, but the method will be removed in Piwik 4.0.0.
### New APIs
* Multiple widgets for one report can now be created via the `Report::configureWidgets()` method via the new classes `Piwik\Widget\ReportWidgetFactory` and `Piwik\Widget\ReportWidgetConfig`
* There is a new property `Report::$subCategory` that lets you add a report to the reporting UI. If a page having that name does not exist yet, it will be created automatically. The newly added method `Report::getSubCategory()` lets you get this value.
* The new classes `Piwik\Widget\Widget`, `Piwik\Widget\WidgetConfig` and `Piwik\Widget\WidgetContainerConfig` lets you create a new widget.
* The new class `Piwik\Category\Subcategory` let you change the name and order of menu items
* New HTTP API method `API.getWidgetMetadata` to get a list of available widgets
* New HTTP API method `API.getReportPagesMetadata` to get a list of all available pages that exist including the widgets they include
* New HTTP API method `SitesManager.getSiteSettings` to get a list of all available settings for a specific site
* The JavaScript AjaxHelper has a new method `ajaxHelper.withTokenInUrl()` to easily send a token along a XHR. Within the Controller the existence of this token can be checked via `$this->checkTokenInUrl();` to prevent CSRF attacks.
* The new class `Piwik\Updater\Migration\Factory` lets you easily create migrations that can be executed during an update. For example database or plugin related migrations. To generate a new update with migrations execute `./console generate:update`.
* The new method `Piwik\Updater::executeMigration` lets you execute a single migration.
* The new method `Piwik\Segment::willBeArchived` lets you detect whether a segment will be archived or not.
* The following events have been added:
* `ViewDataTable.filterViewDataTable` lets you filter available visualizations
* `Dimension.addDimension` lets you add custom dimensions
* `Dimension.filterDimension` lets you filter any dimensions
* `Report.addReports` lets you add dynamically created reports
* `Report.filterReports` lets you filter any report
* `Updater.componentUpdated` triggered after core or a plugin has been updated
* `PluginManager.pluginInstalled` triggered after a plugin was installed
* `PluginManager.pluginUninstalled` triggered after a plugin was uninstalled
* `Updater.componentInstalled` triggered after a component was installed
* `Updater.componentUninstalled` triggered after a component was uninstalled
* New HTTP Tracking API parameter `pv_id` which accepts a six character unique ID that identifies which actions were performed on a specific page view. Read more about it in the [HTTP Tracking API](https://developer.matomo.org/api-reference/tracking-api);
* New event `Segment.addSegments` that lets you add segments.
* New Piwik JavaScript Tracker method `disableHeartBeatTimer()` to disable the heartbeat timer if it was previously enabled.
* The `SitesManager.getJavascriptTag` has a new option `getJavascriptTag` to enable the tracking of users that have JavaScript disabled
### Changes
* New now accept tracking requests for up to 1 day in the past instead of only 4 hours
* If a tracking request has a custom timestamp that is older than one day and the tracking request is not authenticated, we ignore the whole tracking request instead of ignoring the custom timestamp and still tracking the request with the current timestamp
### New features
* Piwik JavaScript Tracking API: we now attempt to track Downloads and Outlinks when the user uses the mouse middle click or the mouse right right click. Previously only left clicks on Downloads and Outlinks were measured.
* New "Sparklines" visualization that lets you create a widget showing multiple sparklines.
* New config.ini.php setting: `tracking_requests_require_authentication_when_custom_timestamp_newer_than` to change how far back Piwik will track your requests without authentication. By default, value is set to 86400 (one day). The configured value is in seconds.
### Library updates
* Updated AngularJS from 1.2.28 to 1.4.3
* Updated several backend libraries to their latest version: doctrine/cache, php-di.
### Internal change
* Support for IE8 was dropped. This affects only the Piwik UI, not the Piwik.js Tracker.
* Required PHP version was increased from 5.3 to 5.5.9
* We have updated PhantomJS 1.9 to 2.1.1 for our UI screenshot tests.
## Piwik 2.16.3
### New APIs
* The Piwik JavaScript tracker has a new method `trackRequest` that allows you to send any tracking parameters to Piwik. For example `_paq.push(['trackRequest', 'te=foo&bar=baz'])`
### Internal Changes
* Expected screenshots for UI tests are now stored using Git LFS instead of a submodule. Running, creating or updating UI tests will require Git LFS to be installed.
The folder containing expected screenshots was renamed from `expected-ui-screenshots` to `expected-screenshots`. The UI-Test-Runner is now able to handle both names.
## Piwik 2.16.2
### New APIs
* Multiple JavaScript trackers can now be created easily via `_paq.push(['addTracker', piwikUrl, piwikSiteId])`. All tracking requests will be then sent to all added Piwik trackers. [Learn more.](http://developer.matomo.org/guides/tracking-javascript-guide#multiple-piwik-trackers)
* It is possible to get an asynchronously created tracker instance (`addTracker`) via the method `Piwik.getAsyncTracker(optionalPiwikUrl, optionalPiwikSiteId)`. This allows you to get the tracker instance and to send different tracking requests to this Piwik instance and to configure it differently than other tracker instances.
* Added a new API method `Goals.getGoal($idSite, $idGoal)` to fetch a single goal.
### Internal change
* Piwik is now compatible with PHP7.
* `piwik.js`, if you call the method `setDomains` note that that the behavior has slightly changed. The current page domain (hostname) will now be added automatically if none of the given host alias passed as a parameter to `setDomains` contain a path and if no host alias is already given for the current host alias.
Say you are on "example.org" and set `hostAlias = ['example.com', 'example.org/test']` then the current "example.org" domain will not be added as there is already a more restrictive hostAlias 'example.org/test' given.
We also do not add the current page domain (hostname) automatically if there was any other host specifying any path such as `['example.com', 'example2.com/test']`.
In this case we also do not add the current page domain "example.org" automatically as the "path" feature is used. As soon as someone uses the path feature, for Piwik JS Tracker to work correctly in all cases, one needs to specify all hosts manually. [Learn more.](http://developer.matomo.org/guides/tracking-javascript-guide#measuring-domains-andor-sub-domains)
* `piwik.js`: after an ecommerce order is tracked using `trackEcommerceOrder`, the items in the cart will now be removed from the JavaScript object. Calling `trackEcommerceCartUpdate` will not remove the items in the cart.
## Piwik 2.16.1
### New features
* New method `setIsWritableByCurrentUser` for `SystemSetting` to change the writable permission for certain system settings via DI.
* JS Tracker: `setDomains` function now supports page wildcards matching eg. `example.com/index*` which can be useful when [tracking a group of pages within a domain in a separate website in Piwik](http://developer.matomo.org/guides/tracking-javascript-guide#tracking-a-group-of-pages-in-a-separate-website)
* To customise the list of URL query parameters to be removed from your URLs, you can now define and overwrite `url_query_parameter_to_exclude_from_url` INI setting in your `config.ini.php` file. By default, the following query string parameters will be removed: `gclid, fb_xd_fragment, fb_comment_id, phpsessid, jsessionid, sessionid, aspsessionid, doing_wp_cron, sid`.
### Deprecations
* The following PHP functions have been deprecated and will be removed in Piwik 3.0:
* `SettingsServer::isApache()`
### New guides
* JavaScript Tracker: [Measuring domains and/or sub-domains](http://developer.matomo.org/guides/tracking-javascript-guide#measuring-domains-andor-sub-domains)
### Breaking Changes
* Reporting API: when a cell value in a CSV or TSV (excel) data export starts with a character `=`, `-` or `+`, Piwik will now prefix the value with `'` to ensure that it is displayed correctly in Excel or OpenOffice/LibreOffice.
### Internal change
* Tracking API: by default, when tracking a Page URL, Piwik will now remove the URL query string parameter `sid` if it is found.
* In the JavaScript tracker, the function `setDomains` will not anymore attempt to set a cookie path. Learn more about [configuring the tracker correctly](http://developer.matomo.org/guides/tracking-javascript-guide#tracking-one-domain) when tracking one or several domains and/or paths.
## Piwik 2.16.1
### Internal change
* The setting `[General]enable_marketplace=0/1` was removed, instead the new plugin Marketplace can be disabled/enabled. The updater should automatically migrate an existing setting.
## Piwik 2.16.0
### New features
* New segment `actionType` lets you segment all actions of a given type, eg. `actionType==events` or `actionType==downloads`. Action types values are: `pageviews`, `contents`, `sitesearches`, `events`, `outlinks`, `downloads`
* New segment `actionUrl` lets you segment any action that matches a given URL, whether they are Pageviews, Site searches, Contents, Downloads or Events.
* New segment `deviceBrand` lets you restrict your users to those using a particular device brand such as Apple, Samsung, LG, Google, Nokia, Sony, Lenovo, Alcatel, etc. View the [complete list of device brands.](http://developer.matomo.org/api-reference/segmentation)
* New segment operators `=^` "Starts with" and `=$` "Ends with" complement the existing segment operators: Contains, Does not contain, Equals, Not equals, Greater than or equal to, Less than or equal to.
* The JavaScript Tracker method `PiwikTracker.setDomains()` can now handle paths. This means when setting eg `_paq.push(['setDomains, '*.matomo.org/website1'])` all link that goes to the same domain `matomo.org` but to any other path than `website1/*` will be treated as outlink.
* In Administration > Websites, for each website, there is a checkbox "Only track visits and actions when the action URL starts with one of the above URLs". In Piwik 2.14.0, any action URL starting with one of the Alias URLs or starting with a subdomain of the Alias URL would be tracked. As of Piwik 2.15.0, when this checkbox is enabled, it may track less data: action URLs on an Alias URL subdomain will not be tracked anymore (you must specify each sub-domain as Alias URL).
* It is now possible to pass an option `php-cli-options` to the `core:archive` command. The given cli options will be forwarded to the actual PHP command. This allows to for example specify a different memory limit for the archiving process like this: `./console core:archive --php-cli-options="-d memory_limit=8G"`
* New less variable `@theme-color-menu-contrast-textSelected` that lets you specify the color of a selected menu item.
* in Administration > Diagnostics, there is a new page `Config file` which lets Super User view all config values from `global.ini.php` in the UI, and whether they were overridden in your `config/config.ini.php`
### New commands
* New command `config:set` lets you set INI config options from the command line. This command can be used for convenience or for automation.
### Internal changes
* `UsersManager.*` API calls: when an API request specifies a `token_auth` of a user with `admin` permission, the returned dataset will not include all usernames as previously, API will now only return usernames for users with `view` or `admin` permission to website(s) viewable by this `token_auth`.
* When generating a new plugin skeleton via `generate:plugin` command, plugin name must now contain only letters and numbers.
* JavaScript Tracker tests no longer require `SQLite`. The existing MySQL configuration for tests is used now. In order to run the tests make sure Piwik is installed and `[database_tests]` is configured in `config/config.ini.php`.
* The definitions for search engine and social network detection have been moved from bundled data files to a separate package (see [https://github.com/matomo-org/searchengine-and-social-list](https://github.com/matomo-org/searchengine-and-social-list)).
* In [UI screenshot tests](https://developer.matomo.org/guides/tests-ui), a test environment `configOverride` setting should be no longer overwritten. Instead new values should be added to the existing `configOverride` array in PHP or JavaScript. For example instead of `testEnvironment.configOverride = {group: {name: 1}}` use `testEnvironment.overrideConfig('group', 'name', '1')`.
### New APIs
* Add your own SMS/Text provider by creating a new class in the `SMSProvider` directory of your plugin. The class has to extend `Piwik\Plugins\MobileMessaging\SMSProvider` and implement the required methods.
* Segments can now be composed by a union of multiple segments. To do this set an array of segments that shall be used for that segment `$segment->setUnionOfSegments(array('outlinkUrl', 'downloadUrl'))` instead of defining a SQL column.
### Deprecations
* The method `DB::tableExists` was un-used and has been removed.
## Piwik 2.15.0
### New commands
* New command `diagnostics:analyze-archive-table` that analyzes archive tables
* New command `database:optimize-archive-tables` to optimize archive tables and possibly save disk space (even if on InnoDB)
* New Command `core:invalidate-report-data` to invalidate archive data (w/ period cascading) ([FAQ](https://matomo.org/faq/how-to/faq_155/))
### New APIs and features
* Piwik 2.15.0 is now mostly compatible with PHP7.
* The JavaScript Tracker `piwik.js` got a new method `logAllContentBlocksOnPage` to log all found content blocks within a page to the console. This is useful to debug / test content tracking. It can be triggered via `_paq.push(['logAllContentBlocksOnPage'])`
* The Class `Piwik\Plugins\Login\Controller` is now considered a public API.
* The new method `Piwik\Menu\MenuAbstract::registerMenuIcon()` can be used to define an icon for a menu category to replace the default arrow icon.
* New event `CronArchive.getIdSitesNotUsingTracker` that allows you to set a list of idSites that do not use the Tracker API to make sure we archive these sites if needed.
* New events `CronArchive.init.start` which is triggered when the CLI archiver starts and `CronArchive.end` when the archiver ended.
* Piwik tracker can now be configured with strict Content Security Policy ([CSP FAQ](https://matomo.org/faq/general/faq_20904/)).
* Super Users can choose whether to use the latest stable release or latest Long Term Support release.
### Breaking Changes
* The method `Dimension::getId()` has been set as `final`. It is not allowed to overwrite this method.
* We fixed a bug where the API method `Sites.getPatternMatchSites` only returned a very limited number of websites by default. We now return all websites by default unless a limit is specified specifically.
* Handling of localized date, time and range formats has been changed. Patterns no longer contain placeholders like %shortDay%, but work with CLDR pattern instead. You can use one of the predefined format constants in Date class for using getLocalized().
* As we are now using CLDR formats for all languages, some time formats were even changed in english. Attributes like prettyDate in API responses might so have been changed slightly.
* The config `enable_measure_piwik_usage_in_idsite` which is used to track the Piwik usage with Piwik was removed and replaced by a new plugin `AnonymousPiwikUsageMeasurement`
### Deprecations
* The following HTTP API methods have been deprecated and will be removed in Piwik 3.0:
* `SitesManager.getSitesIdWithVisits`
* `API.getLastDate`
* The following events have been deprecated and will be removed in Piwik 3.0. Use [dimensions](https://developer.matomo.org/guides/dimensions) instead.
* `Tracker.existingVisitInformation`
* `Tracker.getVisitFieldsToPersist`
* `Tracker.newConversionInformation`
* `Tracker.newVisitorInformation`
* `Tracker.recordAction`
* `Tracker.recordEcommerceGoal`
* `Tracker.recordStandardGoals`
* The Platform API method `\Piwik\Plugin::getListHooksRegistered()` has been deprecated and will be removed in Piwik 4.0. Use `\Piwik\Plugin::registerEvents()` instead.
### Internal changes
* When logging in, the username is now case insensitive
* URLs with emojis and any other unicode character will be tracked, with special characters replaced with `<60>`
* A permanent warning notification is now displayed when PHP is 5.4.* or older, since it has reached End Of Life
* In `piwik.js` we replaced [JSON2](https://github.com/douglascrockford/JSON-js) with [JSON3](https://bestiejs.github.io/json3/) to implement CSP (Content Security Policy) as JSON3 does not use `eval()`. JSON3 will be used if a browser does not provide a native JSON API. We are using `JSON3` in a way that it will not conflict if your website is using `JSON3` as well.
* The option `branch` of the console command `development:sync-system-test-processed` was removed as it is no longer needed.
* All numbers in reports will now appear formatted (eg. `1,000,000` instead of `1000000`)
* Database connections now use `UTF-8` charset explicitly to force UTF-8 data handling
## Piwik 2.14.0
### Breaking Changes
* The `UserSettings` API has been removed. The API was deprecated in earlier versions. Use `DevicesDetection`, `Resolution` and `DevicePlugins` API instead.
* Many translations have been moved to the new Intl plugin. Most of them will still work, but please update their usage. See https://github.com/matomo-org/piwik/pull/8101 for a full list
### New features
* The JavaScript Tracker does now track outlinks and downloads if a user opens the context menu if the `enabled` parameter of the `enableLinkTracking()` method is set to `true`. To use this new feature use `tracker.enableLinkTracking(true)` or `_paq.push(['enableLinkTracking', true]);`. This is not industry standard and is vulnerable to false positives since not every user will select "Open in a new tab" when the context menu is shown. Most users will do though and it will lead to more accurate results in most cases.
* The JavaScript Tracker now contains the 'heart beat' feature which can be used to obtain more accurate visit lengths by periodically sending 'ping' requests to Piwik. To use this feature use `tracker.enableHeartBeatTimer();` or `_paq.push(['enableHeartBeatTimer']);`. By default, a ping request will be sent every 15 seconds. You can specify a custom ping delay (in seconds) by passing an argument, eg, `tracker.enableHeartBeatTimer(10);` or `_paq.push(['enableHeartBeatTimer', 10]);`.
* New custom segment `languageCode` that lets you segment visitors that are using a particular language. Example values: `de`, `fr`, `en-gb`, `zh-cn`, etc.
* Segment `userId` now supports any segment operator (previously only operator Contains `=@` was supported for this segment).
### Commands updates
* The command `core:archive` now has two new parameter: `--force-idsegments` and `--skip-idsegments` that let you force (or skip) processing archives for one or several custom segments.
* The command `scheduled-tasks:run` now has an argument `task` that lets you force run a particular scheduled task.
### Library updates
* Updated pChart library from 2.1.3 to 2.1.4. The files were moved from the directory `libs/pChart2.1.3` to `libs/pChart`
### Internal change
* To execute UI tests "ImageMagick" is now required.
* The Q JavaScript promise library is now distributed with tests and can be used in the piwik.js tests.
## Piwik 2.13.0
### Breaking Changes
* The API method `Live.getLastVisitsDetails` does no longer support the API parameter `filter_sort_column` to prevent possible memory issues when `filter_offset` is large.
* The Event `Site.setSite` was removed as it causes performance problems.
* `piwik.php` does now return a HTTP 400 (Bad request) if requested without any tracking parameters (GET/POST). If you still want to use `piwik.php` for checks please use `piwik.php?rec=0`.
### Deprecations
* The method `Piwik\Archive::getBlob()` has been deprecated and will be removed from June 1st 2015. Use one of the methods `getDataTable*()` methods instead.
* The API parameter `countVisitorsToFetch` of the API method `Live.getLastVisitsDetails` has been deprecated as `filter_offset` and `filter_limit` work correctly now.
### New commands
* There is now a `diagnostic:run` command to run the system check from the command line.
* There is now an option `--xhprof` that can be used with any command to profile that command via XHProf.
### APIs Improvements
* Visitor details now additionally contain: `deviceTypeIcon`, `deviceBrand` and `deviceModel`
* In 2.6.0 we added the possibility to use `filter_limit` and `filter_offset` if an API returns an indexed array. This was not working in all cases and is fixed now.
* The API parameter `filter_pattern` and `filter_offset[]` can now be used if an API returns an indexed array.
### Internal changes
* The referrer spam filter has moved from the `referrer_urls_spam` INI option (in `global.ini.php`) to a separate package (see [https://github.com/matomo-org/referrer-spam-blacklist](https://github.com/matomo-org/referrer-spam-blacklist)).
## Piwik 2.12.0
### Breaking Changes
* The deprecated method `Period::factory()` has been removed. Use `Period\Factory` instead.
* The deprecated method `Config::getConfigSuperUserForBackwardCompatibility()` has been removed.
* The deprecated methods `MenuAdmin::addEntry()` and `MenuAdmin::removeEntry()` have been removed. Use `Piwik\Plugin\Menu` instead.
* The deprecated methods `MenuTop::addEntry()` and `MenuTop::removeEntry()` have been removed. Use `Piwik\Plugin\Menu` instead.
* The deprecated method `SettingsPiwik::rewriteTmpPathWithInstanceId()` has been removed.
* The following deprecated methods from the `Piwik\IP` class have been removed, use `Piwik\Network\IP` instead:
* `sanitizeIp()`
* `sanitizeIpRange()`
* `P2N()`
* `N2P()`
* `prettyPrint()`
* `isIPv4()`
* `long2ip()`
* `isIPv6()`
* `isMappedIPv4()`
* `getIPv4FromMappedIPv6()`
* `getIpsForRange()`
* `isIpInRange()`
* `getHostByAddr()`
### Deprecations
* `API` classes should no longer have a protected constructor. Classes with a protected constructor will generate a notice in the logs and should expose a public constructor instead.
* Update classes should not declare static `getSql()` and `update()` methods anymore. It is still supported to use those, but developers should instead override the `Updates::getMigrationQueries()` and `Updates::doUpdate()` instance methods.
### New features
* `API` classes can now use dependency injection in their constructor to inject other instances.
### New commands
* There is now a command `core:purge-old-archive-data` that can be used to manually purge temporary, error-ed and invalidated archives from one or more archive tables.
* There is now a command `usercountry:attribute` that can be used to re-attribute geolocated location data to existing visits and conversions. If you have visits that were tracked before setting up GeoIP, you can use this command to add location data to them.
## Piwik 2.11.0
### Breaking Changes
* The event `User.getLanguage` has been removed.
* The following deprecated event has been removed: `TaskScheduler.getScheduledTasks`
* Special handling for operating system `Windows` has been removed. Like other operating systems all versions will now only be reported as `Windows` with versions like `XP`, `7`, `8`, etc.
* Reporting for operating systems has been adjusted to report information according to browser information. Visitor details now contain: `operatingSystemName`, `operatingSystemIcon`, `operatingSystemCode` and `operatingSystemVersion`
### Deprecations
* The following methods have been deprecated in favor of the new `Piwik\Intl` component:
* `Piwik\Common::getContinentsList()`: use `RegionDataProvider::getContinentList()` instead
* `Piwik\Common::getCountriesList()`: use `RegionDataProvider::getCountryList()` instead
* `Piwik\Common::getLanguagesList()`: use `LanguageDataProvider::getLanguageList()` instead
* `Piwik\Common::getLanguageToCountryList()`: use `LanguageDataProvider::getLanguageToCountryList()` instead
* `Piwik\Metrics\Formatter::getCurrencyList()`: use `CurrencyDataProvider::getCurrencyList()` instead
* The `Piwik\Translate` class has been deprecated in favor of `Piwik\Translation\Translator`.
* The `core:plugin` console has been deprecated in favor of the new `plugin:list`, `plugin:activate` and `plugin:deactivate` commands
* The following classes have been deprecated:
* `Piwik\TaskScheduler`: use `Piwik\Scheduler\Scheduler` instead
* `Piwik\ScheduledTask`: use `Piwik\Scheduler\Task` instead
* The API method `UserSettings.getLanguage` is deprecated and will be removed from May 1st 2015. Use `UserLanguage.getLanguage` instead
* The API method `UserSettings.getLanguageCode` is deprecated and will be removed from May 1st 2015. Use `UserLanguage.getLanguageCode` instead
* The `Piwik\Registry` class has been deprecated in favor of using the container:
* `Registry::get('auth')` should be replaced with `StaticContainer::get('Piwik\Auth')`
* `Registry::set('auth', $auth)` should be replaced with `StaticContainer::getContainer()->set('Piwik\Auth', $auth)`
### New features
* You can now generate UI / screenshot tests using the command `generate:test`
* During UI tests we do now add a CSS class to the HTML element called `uiTest`. This allows you do hide content when screenshots are captured.
### New commands
* A new command (core:fix-duplicate-log-actions) has been added which can be used to remove duplicate actions and correct references to them in other tables. Duplicates were caused by this bug: [#6436](https://github.com/matomo-org/piwik/issues/6436)
### Library updates
* Updated AngularJS from 1.2.26 to 1.2.28
* Updated piwik/device-detector from 2.8 to 3.0
### Internal change
* UI specs were moved from `tests/PHPUnit/UI` to `tests/UI`. We also moved the UI specs directly into the Piwik repository meaning the [piwik-ui-tests](https://github.com/matomo-org/piwik-ui-tests) repository contains only the expected screenshots from now on.
* There is a new command `development:sync-system-test-processed` for core developers that allows you to copy processed test results from travis to your local dev environment.
## Piwik 2.10.0
### Breaking Changes
* API responses containing visitor information will no longer contain the fields `screenType` and `screenTypeIcon` as those reports have been completely removed
* os, browser and browser plugin icons are now located in the DevicesDetection and DevicePlugins plugin. If you are not using the Reporting or Metadata API to get the icon locations please update your paths.
* The deprecated method `Piwik\SettingsPiwik::rewriteTmpPathWithHostname()` has been removed.
* The following events have been removed:
* `Log.formatFileMessage`
* `Log.formatDatabaseMessage`
* `Log.formatScreenMessage`
* These events have been removed as Piwik now uses the Monolog logging library. [Learn more.](http://developer.matomo.org/guides/logging)
* The event `Log.getAvailableWriters` has been removed: to add custom log backends, you now need to configure Monolog handlers
* The INI options `log_only_when_cli` and `log_only_when_debug_parameter` have been removed
### Library updates
* We added the `symfony/var-dumper` library allowing you to better print any arbitrary PHP variable via `dump($var1, $var2, ...)`.
* Piwik now uses [Monolog](https://github.com/Seldaek/monolog) as a logger.
* The tracker proxy (previously in `misc/proxy-hide-piwik-url/`) has been moved to a separate repository: [https://github.com/matomo-org/tracker-proxy](https://github.com/matomo-org/tracker-proxy).
### Deprecations
* Some duplicate reports from UserSettings plugin have been removed. Widget URLs for those reports will still work till May 1st 2015. Please update those to the new reports of DevicesDetection plugin.
* The API method `UserSettings.getBrowserVersion` is deprecated and will be removed from May 1st 2015. Use `DevicesDetection.getBrowserVersions` instead
* The API method `UserSettings.getBrowser` is deprecated and will be removed from May 1st 2015. Use `DevicesDetection.getBrowsers` instead
* The API method `UserSettings.getOSFamily` is deprecated and will be removed from May 1st 2015. Use `DevicesDetection.getOsFamilies` instead
* The API method `UserSettings.getOS` is deprecated and will be removed from May 1st 2015. Use `DevicesDetection.getOsVersions` instead
* The API method `UserSettings.getMobileVsDesktop` is deprecated and will be removed from May 1st 2015. Use `DevicesDetection.getType` instead
* The API method `UserSettings.getBrowserType` is deprecated and will be removed from May 1st 2015. Use `DevicesDetection.getBrowserEngines` instead
* The API method `UserSettings.getResolution` is deprecated and will be removed from May 1st 2015. Use `Resolution.getResolution` instead
* The API method `UserSettings.getConfiguration` is deprecated and will be removed from May 1st 2015. Use `Resolution.getConfiguration` instead
* The API method `UserSettings.getPlugin` is deprecated and will be removed from May 1st 2015. Use `DevicePlugins.getPlugin` instead
* The API method `UserSettings.getWideScreen` has been removed. Use `UserSettings.getScreenType` instead.
* `Piwik\SettingsPiwik::rewriteTmpPathWithInstanceId()` has been deprecated. Instead of hardcoding the `tmp/` path everywhere in the codebase and then calling `rewriteTmpPathWithInstanceId()`, developers should get the `path.tmp` configuration value from the DI container (e.g. `StaticContainer::getContainer()->get('path.tmp')`).
* The method `Piwik\Log::setLogLevel()` has been deprecated
* The method `Piwik\Log::getLogLevel()` has been deprecated
## Piwik 2.9.1
### Breaking Changes
* The HTTP Tracker API does now respond with a HTTP 400 instead of a HTTP 500 in case an invalid `idsite` is used
### New APIs
* New URL parameter `send_image=0` in the [HTTP Tracking API](http://developer.matomo.org/api-reference/tracking-api) to receive a HTTP 204 response code instead of a GIF image. This improves performance and can fix errors if images are not allowed to be obtained directly (eg Chrome Apps).
### New commands
* `core:plugin list` lists all plugins currently activated in Piwik.
## Piwik 2.9.0
### Breaking Changes
* Development related [console commands](http://developer.matomo.org/guides/piwik-on-the-command-line) are only available if the development mode is enabled. To enable the development mode execute `./console development:enable`.
* The command `php console core:update` does no longer have a parameter `--dry-run`. A dry run is now executed by default followed by a question whether one actually wants to execute the updates. To skip this confirmation step one can use the `--yes` option.
### Deprecations
* Most methods of `Piwik\IP` have been deprecated in favor of the new [piwik/network](https://github.com/matomo-org/component-network) component.
* The file `tests/PHPUnit/phpunit.xml` is no longer needed in order to run tests and we suggest to delete it. The test configuration is now done automatically if possible. In case the tests do no longer work check out the `[tests]` section in `config/global.ini.php`
### Library updates
* Code for manipulating IP addresses has been moved to a separate standalone component: [piwik/network](https://github.com/matomo-org/component-network). Backward compatibility is kept in Piwik core.
## Piwik 2.8.2
### Library updates
* Updated AngularJS from 1.2.25 to 1.2.26
* Updated jQuery from 1.11.0 to 1.11.1
## Piwik 2.8.0
### Breaking Changes
* The Auth interface has been modified, existing Auth implementations will have to be modified. Changes include:
* The initSession method has been moved. Since this behavior must be executed for every Auth implementation, it has been put into a new class: SessionInitializer.
If your Auth implementation implements its own session logic you will have to extend and override SessionInitializer.
* The following methods have been added: setPassword, setPasswordHash, getTokenAuthSecret and getLogin.
* Clarifying semantics of each method and what they must support and can support.
* **Read the documentation for the [Auth interface](http://developer.matomo.org/2.x/api-reference/Piwik/Auth) to learn more.**
* The `Piwik\Unzip\*` classes have been extracted out of the Piwik repository into a separate component named [Decompress](https://github.com/matomo-org/component-decompress).
* `Piwik\Unzip` has not moved, it is kept for backward compatibility. If you have been using that class, you don't need to change anything.
* The `Piwik\Unzip\*` classes (Tar, PclZip, Gzip, ZipArchive) have moved to the `Piwik\Decompress\*` namespace (inside the new repository).
* `Piwik\Unzip\UncompressInterface` has been moved and renamed to `Piwik\Decompress\DecompressInterface` (inside the new repository).
### Deprecations
* The `Piwik::setUserHasSuperUserAccess` method is deprecated, instead use Access::doAsSuperUser. This method will ensure that super user access is properly rescinded after the callback finishes.
* The class `\IntegrationTestCase` is deprecated and will be removed from February 6th 2015. Use `\Piwik\Tests\Framework\TestCase\SystemTestCase` instead.
* The class `\DatabaseTestCase` is deprecated and will be removed from February 6th 2015. Use `\Piwik\Tests\Framework\TestCase\IntegrationTestCase` instead.
* The class `\BenchmarkTestCase` is deprecated and will be removed from February 6th 2015. Use `\Piwik\Tests\Framework\TestCase\BenchmarkTestCase` instead.
* The class `\ConsoleCommandTestCase` is deprecated and will be removed from February 6th 2015. Use `\Piwik\Tests\Framework\TestCase\ConsoleCommandTestCase` instead.
* The class `\FakeAccess` is deprecated and will be removed from February 6th 2015. Use `\Piwik\Tests\Framework\Mock\FakeAccess` instead.
* The class `\Piwik\Tests\Fixture` is deprecated and will be removed from February 6th 2015. Use `\Piwik\Tests\Framework\Fixture` instead.
* The class `\Piwik\Tests\OverrideLogin` is deprecated and will be removed from February 6ths 2015. Use `\Piwik\Framework\Framework\OverrideLogin` instead.
### New API Features
* The pivotBy and related query parameters can be used to pivot reports by another dimension. Read more about the new query parameters [here](http://developer.matomo.org/api-reference/reporting-api#optional-api-parameters).
### Library updates
* Updated AngularJS from 1.2.13 to 1.2.25
### New commands
* `generate:angular-directive` lets you easily generate a template for a new angular directive for any plugin.
### Internal change
* Piwik 2.8.0 now requires PHP >= 5.3.3.
* If you use an older PHP version, please upgrade now to the latest PHP so you can enjoy improvements and security fixes in Piwik.
## Piwik 2.7.0
### Reporting APIs
* Several APIs will now expose a new metric `nb_users` which measures the number of unique users when a [User ID](http://matomo.org/docs/user-id/) is set.
* New APIs have been added for [Content Tracking](https://matomo.org/docs/content-tracking/) feature: Contents.getContentNames, Contents.getContentPieces
### Deprecations
* The `Piwik\Menu\MenuAbstract::add()` method is deprecated in favor of `addItem()`. Read more about this here: [#6140](https://github.com/matomo-org/piwik/issues/6140). We do not plan to remove the deprecated method before Piwik 3.0.
### New APIs
* It is now easier to generate the URL for a menu item see [#6140](https://github.com/matomo-org/piwik/issues/6140), [urlForDefaultAction()](http://developer.matomo.org/2.x/api-reference/Piwik/Plugin/Menu#urlfordefaultaction), [urlForAction()](http://developer.matomo.org/2.x/api-reference/Piwik/Plugin/Menu#urlforaction), [urlForModuleAction()](http://developer.matomo.org/2.x/api-reference/Piwik/Plugin/Menu#urlformoduleaction)
### New commands
* `core:clear-caches` Lets you easily delete all caches. This command can be useful for instance after updating Piwik files manually.
## Piwik 2.6.0
### Deprecations
* The `'json'` API format is considered deprecated. We ask all new code to use the `'json2'` format. Eventually when Piwik 3.0 is released the `'json'` format will be replaced with `'json2'`. Differences in the json2 format include:
* A bug in JSON formatting was fixed so API methods that return simple associative arrays like `array('name' => 'value', 'name2' => 'value2')` will now appear correctly as `{"name":"value","name2":"value2"}` in JSON API output instead of `[{"name":"value","name2":"value2"}]`. API methods like **SitesManager.getSiteFromId** & **UsersManager.getUser** are affected.
#### Reporting API
* If an API returns an indexed array, it is now possible to use `filter_limit` and `filter_offset`. This was before only possible if an API returned a DataTable.
* The Live API now returns only visitor information of activated plugins. So if for instance the Referrers plugin is deactivated a visitor won't contain any referrers related properties. This is a bugfix as the API was crashing before if some core plugins were deactivated. Affected methods are for instance `getLastVisitDetails` or `getVisitorProfile`. If all core plugins are enabled as by default there will be no change at all except the order of the properties within one visitor.
### New commands
* `core:run-scheduled-tasks` lets you run all scheduled tasks due to run at this time. Useful for instance when testing tasks.
#### Internal change
* We removed our own autoloader that was used to load Piwik files in favor of the composer autoloader which we already have been using for some libraries. This means the file `core/Loader.php` will no longer exist. In case you are using Piwik from Git make sure to run `php composer.phar self-update && php composer.phar install` to make your Piwik work again. Also make sure to no longer include `core/Loader.php` in case it is used in any custom script.
* We do no longer store the list of plugins that are used during tracking in the config file. They are dynamically detect instead. The detection of a tracker plugin works the same as before. A plugin has to either listen to any `Tracker.*` or `Request.initAuthenticationObject` event or it has to define dimensions in order to be detected as a tracker plugin.
## Piwik 2.5.0
### Breaking Changes
* Javascript Tracking API: if you are using `getCustomVariable` function to access custom variables values that were set on previous page views, you now must also call `storeCustomVariablesInCookie` before the first call to `trackPageView`. Read more about [Javascript Tracking here](http://developer.matomo.org/api-reference/tracking-javascript).
* The [settings](http://developer.matomo.org/guides/piwik-configuration) API will receive the actual entered value and will no longer convert characters like `&` to `&amp;`. If you still want this behavior - for instance to prevent XSS - you can define a filter by setting the `transform` property like this:
`$setting->transform = function ($value) { return Common::sanitizeInputValue($value); }`
* Config setting `disable_merged_assets` moved from `Debug` section to `Development`. The updater will automatically change the section for you.
* `API.getRowEvolution` will throw an exception if a report is requested that does not have a dimension, for instance `VisitsSummary.get`. This is a fix as an invalid format was returned before see [#5951](https://github.com/matomo-org/piwik/issues/5951)
* `MultiSites.getAll` returns from now on always an array of websites. In the past it returned a single object and it didn't contain all properties in case only one website was found which was a bug see [#5987](https://github.com/matomo-org/piwik/issues/5987)
### Deprecations
The following events are considered as deprecated and the new structure should be used in the future. We have not scheduled when those events will be removed but probably in Piwik 3.0 which is not scheduled yet and won't be soon. New features will be added only to the new classes.
* `API.getReportMetadata`, `API.getSegmentDimensionMetadata`, `Goals.getReportsWithGoalMetrics`, `ViewDataTable.configure`, `ViewDataTable.getDefaultType`: use [Report](http://developer.matomo.org/2.x/api-reference/Piwik/Plugin/Report) class instead to define new reports. There is an updated guide as well [Part1](http://developer.matomo.org/guides/getting-started-part-1)
* `WidgetsList.addWidgets`: use [Widgets](http://developer.matomo.org/2.x/api-reference/Piwik/Plugin/Widgets) class instead to define new widgets
* `Menu.Admin.addItems`, `Menu.Reporting.addItems`, `Menu.Top.addItems`: use [Menu](http://developer.matomo.org/api-reference/Piwik/Plugin/Menu) class instead
* `TaskScheduler.getScheduledTasks`: use [Tasks](http://developer.matomo.org/2.x/api-reference/Piwik/Plugin/Tasks) class instead to define new tasks
* `Tracker.recordEcommerceGoal`, `Tracker.recordStandardGoals`, `Tracker.newConversionInformation`: use [Conversion Dimension](http://developer.matomo.org/api-reference/Piwik/Plugin/Dimension/ConversionDimension) class instead
* `Tracker.existingVisitInformation`, `Tracker.newVisitorInformation`, `Tracker.getVisitFieldsToPersist`: use [Visit Dimension](http://developer.matomo.org/api-reference/Piwik/Plugin/Dimension/VisitDimension) class instead
* `ViewDataTable.addViewDataTable`: This event is no longer needed. Visualizations are automatically discovered if they are placed within a `Visualizations` directory inside the plugin.
### New features
#### Translation search
As a plugin developer you might want to reuse existing translation keys. You can now find all available translations and translation keys by opening the page "Settings => Development:Translation search" in your Piwik installation. Read more about [internationalization](http://developer.matomo.org/guides/internationalization) here.
#### Reporting API
It is now possible to use the `filter_sort_column` parameter when requesting `Live.getLastVisitDetails`. For instance `&filter_sort_column=visitCount`.
#### @since annotation
We are using `@since` annotations in case we are introducing new API's to make it easy to see in which Piwik version a new method was added. This information is now displayed in the [Classes API-Reference](http://developer.matomo.org/api-reference/classes).
### New APIs
* [Report](http://developer.matomo.org/2.x/api-reference/Piwik/Plugin/Report) to add a new report
* [Action Dimension](http://developer.matomo.org/2.x/api-reference/Piwik/Plugin/Dimension/ActionDimension) to add a dimension that tracks action related information
* [Visit Dimension](http://developer.matomo.org/2.x/api-reference/Piwik/Plugin/Dimension/VisitDimension) to add a dimension that tracks visit related information
* [Conversion Dimension](http://developer.matomo.org/2.x/api-reference/Piwik/Plugin/Dimension/ConversionDimension) to add a dimension that tracks conversion related information
* [Dimension](http://developer.matomo.org/2.x/api-reference/Piwik/Columns/Dimension) to add a basic non tracking dimension that can be used in `Reports`
* [Widgets](http://developer.matomo.org/2.x/api-reference/Piwik/Plugin/Widgets) to add or modfiy widgets
* These Menu classes got new methods that make it easier to add new items to a specific section
* [MenuAdmin](http://developer.matomo.org/2.x/api-reference/Piwik/Menu/MenuAdmin) to add or modify admin menu items.
* [MenuReporting](http://developer.matomo.org/2.x/api-reference/Piwik/Menu/MenuReporting) to add or modify reporting menu items
* [MenuUser](http://developer.matomo.org/2.x/api-reference/Piwik/Menu/MenuUser) to add or modify user menu items
* [Tasks](http://developer.matomo.org/2.x/api-reference/Piwik/Plugin/Tasks) to add scheduled tasks
### New commands
* `generate:theme` lets you easily generate a new theme and customize colors, see the [Theming guide](http://developer.matomo.org/guides/theming)
* `generate:update` lets you generate an update file
* `generate:report` lets you generate a report
* `generate:dimension` lets you enhance the tracking by adding new dimensions
* `generate:menu` lets you generate a menu class to add or modify menu items
* `generate:widgets` lets you generate a widgets class to add or modify widgets
* `generate:tasks` lets you generate a tasks class to add or modify tasks
* `development:enable` lets you enable the development mode which will will disable some caching to make code changes directly visible and it will assist developers by performing additional checks to prevent for instance typos. Should not be used in production.
* `development:disable` lets you disable the development mode
<!--
## Template: Matomo version number
### Breaking Changes
### Deprecations
### New features
### New APIs
### New commands
### New guides
### Library updates
### Internal change
-->
Find the general Matomo Changelogs for each release at [matomo.org/changelog](https://matomo.org/changelog/)

View File

@ -0,0 +1,16 @@
# How to contribute
Great to have you here! Read the following guide on our developer zone to learn how you can help make this project better!
https://developer.matomo.org/guides/contributing-to-piwik-core
## How to submit a bug report or suggest a feature?
Please read the recommendations on writing a good [bug report](https://developer.matomo.org/guides/core-team-workflow#submitting-a-bug-report) or [feature request](https://developer.matomo.org/guides/core-team-workflow#submitting-a-feature-request).
## How to suggest improvements to translations?
You can help improve translations in Matomo, please read [contribute to translations](https://github.com/matomo-org/piwik/blob/master/lang/README.md).
## How to submit code improvements via pull requests?
You can help contribute to Matomo codebase via Pull Requests, see [Contributing to Matomo core](https://developer.matomo.org/guides/contributing-to-piwik-core)

View File

@ -0,0 +1,291 @@
COPYRIGHT
Matomo - free/libre analytics platform
The software package is:
Copyright (C) 2014 Matthieu Aubry
Individual contributions, components, and libraries are copyright
of their respective authors.
SOFTWARE LICENSE
The free software license of Matomo is GNU General Public License v3
or later. A copy of GNU GPL v3 should have been included in this
software package in misc/gpl-3.0.txt.
TRADEMARK
Matomo (TM) is an internationally registered trademark.
The software license does not grant any rights under trademark
law for use of the trademark. Refer to https://matomo.org/trademark/
for up-to-date trademark licensing information.
*
* The software license applies to both the aggregate software and
* application-specific portions of the software.
*
* You may not remove this legal notice or modify the software
* in such a way that misrepresents the origin of the software.
*
CREDITS
The software consists of contributions made by many individuals.
Major contributors are listed in https://matomo.org/the-piwik-team/.
For detailed contribution history, refer to the source, tickets,
patches, and Git revision history, available at
https://github.com/matomo-org/piwik/issues
https://github.com/matomo-org/piwik
SEPARATELY LICENSED COMPONENTS AND LIBRARIES
The following components/libraries are distributed in this package,
and subject to their respective licenses.
Name: javascriptCode.tpl - tracking tag to embed in your web pages
Link: https://github.com/matomo-org/piwik/blob/master/core/Tracker/javascriptTag.tpl
License: Public Domain
Name: jquery.truncate
Link: https://github.com/matomo-org/piwik/blob/master/libs/jquery/truncate/
License: New BSD
Name: matomo.js & piwik.js - JavaScript tracker
Link: https://github.com/matomo-org/matomo/blob/master/js/piwik.js
Link: https://github.com/matomo-org/matomo/blob/master/js/matomo.js
License: New BSD
Name: PiwikTracker - server-side tracker (PHP)
Link: https://github.com/matomo-org/piwik/blob/master/libs/PiwikTracker/
License: New BSD
Name: DeviceDetector
Link: https://github.com/matomo-org/device-detector
License: LGPL
Name: Piwik/Decompress
Link: https://github.com/matomo-org/component-decompress
License: LGPL v3.0
Name: Piwik/Network
Link: https://github.com/matomo-org/component-network
License: LGPL v3.0
THIRD-PARTY COMPONENTS AND LIBRARIES
The following components/libraries are redistributed in this package,
and subject to their respective licenses.
Name: jqPlot
Link: http://www.jqplot.com/
License: Dual-licensed: MIT (Expat) or GPL v2
Name: jQuery
Link: https://jquery.com/
License: Dual-licensed: MIT (Expat) or GPL
Notes:
- GPL version not explicitly stated in source but GPL v2 is in git
- includes Sizzle.js - multi-licensed: MIT (Expat), New BSD, or GPL [v2]
Name: jQuery UI
Link: https://jqueryui.com/
License: Dual-licensed: MIT (Expat) or GPL
Notes:
- GPL version not explicitly stated in source but GPL v2 is in git
Name: jquery.history
Link: https://tkyk.github.io/jquery-history-plugin/
License: MIT (Expat)
Name: jquery.scrollTo
Link: http://plugins.jquery.com/project/ScrollTo
License: Dual licensed: MIT (Expat) or GPL
Name: jquery Tooltip
Link: http://bassistance.de/jquery-plugins/jquery-plugin-tooltip/
License: Dual licensed: MIT (Expat) or GPL
Name: jquery placeholder
Link: http://mths.be/placeholder
License: Dual licensed: MIT (Expat) or GPL
Name: qrcode.js
Link: https://github.com/davidshimjs/qrcodejs
License: MIT
Name: json2.js
Link: http://json.org/
License: Public domain
Notes:
- reference implementation
Name: jshrink
Link: https://github.com/tedivm/jshrink
License: BSD-3-Clause
Name: Sparkline
Link: https://github.com/jamiebicknell/Sparkline
License: MIT
Name: sprintf
Link: http://www.diveintojavascript.com/projects/javascript-sprintf
License: New BSD
Name: upgrade.php
Link: http://upgradephp.berlios.de/
License: Public domain
Name: Archive Tar
Link: https://pear.php.net/package/Archive_Tar
License: New BSD
Name: Event Dispatcher (and Notification)
Link: https://pear.php.net/package/Event_Dispatcher/
License: New BSD
Name: HTML Common2
Link: https://pear.php.net/package/HTML_Common2/
License: New BSD
Name: HTML QuickForm2
Link: https://pear.php.net/package/HTML_QuickForm2/
License: New BSD
Name: HTML QuickForm2_Renderer_Smarty
Link: http://www.phcomp.co.uk/tmp/Smarty.phps
License: New BSD
Name: MaxMindGeoIP
Link: https://dev.maxmind.com/geoip/legacy/downloadable/#PHP-7
License: LGPL
Name: PclZip
Link: http://www.phpconcept.net/pclzip/
License: LGPL
Notes:
- GPL version not explicitly stated but tarball contains LGPL v2.1
Name: PEAR (base system)
Link: https://pear.php.net/package/PEAR
License: New BSD
Name: PhpSecInfo
Link: http://phpsec.org/projects/phpsecinfo/
License: New BSD
Name: RankChecker
Link: http://www.getrank.org/free-pagerank-script
License: GPL
Name: Twig
Link: https://twig.symfony.com/
License: BSD
Name: TCPDF
Link: https://sourceforge.net/projects/tcpdf/
License: LGPL v3 or later
Name: Zend Framework
Link: https://framework.zend.com/
License: New BSD
Name: pChart 2.1.4
Link: http://www.pchart.net
License: GPL v3
Name: Chroma.js
Link: https://github.com/gka/chroma.js
License: GPL v3
Name: qTip2 - Pretty powerful tooltips
Link: http://craigsworks.com/projects/qtip2/
License: GPL
Name: Kartograph.js
Link: http://kartograph.org/
License: LGPL v3 or later
Name: Raphaël - JavaScript Vector Library
Link: http://raphaeljs.com/
License: MIT (Expat)
Name: lessphp
Link: http://leafo.net/lessphp
License: GPL3, MIT (Expat)
Name: Symfony Console Component
Link: https://github.com/symfony/Console
License: MIT (Expat)
Name: AngularJS
Link: https://github.com/angular/angular.js
License: MIT (Expat)
Name: Mousetrap
Link: https://github.com/ccampbell/mousetrap
License: Apache 2.0
Name: PHP-DI
Link: http://php-di.org/
License: MIT (Expat)
THIRD-PARTY CONTENT
Name: FamFamFam icons - Mark James
Link: http://www.famfamfam.com/lab/icons/
License: CC BY 3.0
Name: Solar System icons - Dan Wiersema
Link: http://www.iconspedia.com/icon/neptune-4672.html
License: Free for non-commercial use
Notes:
- used in Matomo's ExampleUI plugin
Name: flag-icon-css - Lipis
Link: https://github.com/lipis/flag-icon-css
License: MIT (Expat)
Notes:
- used for flag PNGs
Name: Wine project - tahoma.ttf font
Link: http://source.winehq.org/git/wine.git/blob_plain/HEAD:/fonts/tahoma.ttf
License: LGPL v2.1
Notes:
- used in ImageGraph plugin
Name: plugins/Feedback/angularjs/ratefeature/thumbs-down.png
Link: https://www.iconfinder.com/icons/216428/down_thumbs_icon
License: Creative Commons (Attribution-Share Alike 3.0 Unported)
Name: plugins/Feedback/angularjs/ratefeature/thumbs-up.png
Link: https://www.iconfinder.com/icons/216429/thumbs_up_icon
License: Creative Commons (Attribution-Share Alike 3.0 Unported)
Name: plugins/Insights/images/idea.png
Link: https://www.iconfinder.com/icons/6074/brainstorm_bulb_idea_jabber_light_icon
License: GPL
By: Alessandro Rei - http://www.kde-look.org/usermanager/search.php?username=mentalrey
Name: Material icons ("icon-info2", "icon-outline", "icon-settings", "icon-form", "icon-play", "icon-pause", "icon-replay", "icon-skip-next", "icon-skip-forward", "icon-stop", "icon-fast-forward", "icon-fast-rewind", "icon-bug", "icon-upload", "icon-segmented-visits-log") in plugins/Morpheus/fonts
Link: https://design.google.com/icons/
License: Apache License Version 2.0
Name: IcoMoon - Free icons ("icon-funnel", "icon-lab", "icon-archive", "icon-rocket", "icon-embed") in plugins/Morpheus/fonts
Link: https://icomoon.io/#icons-icomoon
License: GPL
Notes:
- the "New BSD" license refers to either the "Modified BSD" and "Simplified BSD"
licenses (2- or 3-clause), which are GPL compatible.
- icons for browsers, operating systems, browser plugins, brands, search engines, social media websites
and flags of countries are nominative use of third-party trademarks when
referring to the corresponding product or entity

675
msd2/tracking/piwik/LICENSE Normal file
View File

@ -0,0 +1,675 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Matomo Copyright (C) 2007-2018 Matomo.org
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@ -0,0 +1,60 @@
# Privacy
This is a summary of all of the components within Matomo which may affect your privacy in some way. Please keep in mind
third party Themes, Plugins or Apps may introduce privacy concerns not listed here.
## Privacy for users being tracked by Matomo
In this section we document how to protect the privacy of visitors who are tracked by your Matomo analytics service.
### Anonymise visitor IP addresses
By default, Matomo stores the visitor IP address (IPv4 or IPv6 format) in the database for each new visitor.
If a visitor has a static IP address this means her browsing history can be easily identified across several days and
even across several websites tracked within the same Matomo server. You can anonymize IP addresses to ensure visitors cannot
be tracked this way: [How to anonymise IP addresses.](https://matomo.org/docs/privacy/#step-1-automatically-anonymize-visitor-ips)
### Delete old visitors logs
By default, Matomo stores tracked data forever. To better respect the privacy of your users, it is recommended to regularly
purge old data. You can configure Matomo to automatically delete log data older than a specified number of months:
[How to delete old visitors log data.](https://matomo.org/docs/privacy/#step-2-delete-old-visitors-logs)
### Include a tracking Opt-Out feature on your site
In your website, we recommended providing an easy way for your visitors to “opt-out” of being tracked by Matomo.
You can use the Opt-Out feature to display a link your website that sets a special browser cookie (`piwik_ignore`) when
clicked. Visitors that click that link will be ignored by Matomo in the future:
[How to include a tracking opt-out iframe.](https://matomo.org/docs/privacy/#step-3-include-a-web-analytics-opt-out-feature-on-your-site-using-an-iframe)
### Respect DoNotTrack preference
Do Not Track is a browser-level technology and policy proposal that lets visitors opt out of tracking by websites they
do not visit. Visitors can enable this preference in their browser, and then it's up to Matomo to respect it. By default,
Matomo is configured to ignore visitors that have enabled it:
[How to check if your Matomo respects DoNotTrack.] (https://matomo.org/docs/privacy/#step-4-respect-donottrack-preference)
### Disable tracking cookies
A cookie is a collection of information that a website stores on a visitors computer and accesses each time the visitor
returns. By default, Matomo uses cookies to aid in tracking visitor behavior. If someone gains access to a visitor's
computer, they could learn a few things about how the visitor visited your website. For many websites, this isn't a
problem, but for others where a strong level of privacy is required (like online banking), disabling tracking cookies may
be a good idea: [How to disable tracking cookies.](https://matomo.org/faq/general/faq_157/)
### Keep your visitors details private
Any user that has at least `view` access (the default access level) to Matomo can view detailed information for all users
tracked in Matomo (such as their IP addresses, visitor IDs, details of all past visits and actions, etc.) through features
provided by the `Live` plugin (such as the Visitor Log and Visitor Profile). As the Matomo administrator, you may decide
that not all of your users need access to this data. You can deactivate the `Live` plugin to prevent users from viewing
visitor details in the Administration > Plugins page.
## Privacy for Matomo admins and website owners
In this section we document how a Matomo administrator can better protect their own privacy.
### Keep your Matomo server URL private
By default, the Matomo Javascript code on all tracked websites contains the Matomo server URL. In some cases you might
want to hide this Matomo URL completely while still tracking all websites in your Matomo instance. To hide your Matomo
server's URL, you can modify the Javascript Tracking code and point it to a proxy piwik.php script instead of your actual
Matomo server: [How to keep Matomo server URL private.](https://matomo.org/faq/how-to/faq_132/)
### Automatic update check
From time to time, Matomo uses `api.matomo.org` to check if the current version of Matomo is the latest version of Matomo.
If an update is available, a notification is displayed allowing you to upgrade Matomo. To disable the update check,
and stop your instance from sending HTTP requests to `api.matomo.org`, deactivate the "Automatic update" feature by
setting `enable_auto_update = 0` in your configuration file `config/config.ini.php`.
Learn more about [Privacy in Matomo](https://matomo.org/privacy/).

View File

@ -0,0 +1,119 @@
# Matomo (formerly Piwik) - matomo.org
[![Latest Stable Version](https://poser.pugx.org/piwik/piwik/v/stable)](https://matomo.org/download/)
[![Latest Unstable Version](https://poser.pugx.org/piwik/piwik/v/unstable)](https://packagist.org/packages/piwik/piwik)
[![License](https://poser.pugx.org/piwik/piwik/license)](https://matomo.org/free-software/)
## Code Status
[![Build Status](https://travis-ci.org/matomo-org/matomo.svg?branch=master)](https://travis-ci.org/matomo-org/matomo/branches)
[![Percentage of issues still open](http://isitmaintained.com/badge/open/matomo-org/matomo.svg)](http://isitmaintained.com/project/matomo-org/matomo "Percentage of issues still open")
## Description
Matomo is the leading Free/Libre open analytics platform.
Matomo is a full featured PHP MySQL software program that you download and install on your own webserver.
At the end of the five minute installation process, you will be given a JavaScript code.
Simply copy and paste this tag on websites you wish to track and access your analytics reports in real time.
Matomo aims to be a Free software alternative to Google Analytics and is already used on more than 1,400,000 websites. Privacy is built-in!
## Mission Statement
> « To create, as a community, the leading international open source digital analytics platform, that gives every user full control of their data. »
Or in short:
> « Liberate Web Analytics »
## License
Matomo is released under the GPL v3 (or later) license, see [misc/gpl-3.0.txt](misc/gpl-3.0.txt)
## Requirements
* PHP 5.5.9 or greater
* MySQL version 5.5 or greater, or MariaDB
* PHP extension pdo and pdo_mysql, or the MySQLi extension.
* Matomo is OS / server independent
See https://matomo.org/docs/requirements/
## Install
* Upload matomo to your webserver
* Point your browser to the directory
* Follow the steps
* Add the given javascript code to your pages
* (You may also generate fake data to experiment, by enabling the plugin VisitorGenerator)
See https://matomo.org/docs/installation/
## Free trial
If you do not have a server or don't want to host yourself you can use our Matomo Cloud partner service (30 day free trial): https://www.innocraft.cloud/
## Online Demo
Check out the online demo for Matomo at [demo.matomo.org](https://demo.matomo.org/)
## Changelog
For the list of all tickets closed in the current and past releases, see https://matomo.org/changelog/. For the list of technical changes in the Matomo platform, see [https://developer.matomo.org/changelog](https://developer.matomo.org/changelog).
## Get involved!
We believe in liberating Web Analytics, providing a free platform for simple and advanced analytics. Matomo was built by dozens of people like you,
and we need your help to make Matomo better… Why not participate in a useful project today? [Learn how you can contribute to Matomo.](https://matomo.org/get-involved)
## Quality Assurance
The Matomo project uses an ever-expanding comprehensive set of thousands of unit tests and hundreds of automated integration tests, system tests, JavaScript tests, and screenshot UI tests, running on a continuous integration server as part of its software quality assurance. [Learn more](https://matomo.org/qa/)
We use [BrowserStack.com](https://www.browserstack.com/) testing tool to help check the Matomo user interface is compatible with many browsers.
## Security
Security is a top priority at Matomo. As potential issues are discovered, we validate, patch and release fixes as quickly as we can. We have a security bug bounty program in place that rewards researchers for finding security issues and disclosing them to us.
[Learn more](https://matomo.org/security/) or check out our [HackerOne program](https://hackerone.com/matomo).
## Support for Matomo
For **Free support**, post a message in our community forums: [forum.matomo.org](https://forum.matomo.org/)
For **Professional paid support**, send a message to our network of Matomo professionals: [matomo.org/support](https://matomo.org/contact/)
## Contact
Website: [matomo.org](https://matomo.org)
About us: [matomo.org/team/](https://matomo.org/team/)
Contact us: [matomo.org/contact/](https://matomo.org/contact/)
## More information
What makes Matomo unique from the competition:
* Real-time web analytics reports: in Matomo, reports are by default generated in real time.
For high traffic websites, you can choose the frequency for reports to be processed.
* You own your web analytics data: since Matomo is installed on your server, the data is stored in your own database and you can get all the statistics using the powerful Matomo Analytics API.
* Matomo is a Free Software which can easily be configured to respect your visitors' privacy.
* Modern, easy to use User Interface: you can fully customize your dashboard, drag and drop widgets and more.
* Matomo features are built inside plugins: you can add new features and remove the ones you dont need.
You can build your own web analytics plugins or hire a consultant to have your custom feature built-in Matomo
* A vibrant international Open community of more than 200,000 active users (tracking even more websites!)
* Advanced Web Analytics capabilities such as E-commerce Tracking, Goal tracking, Campaign tracking,
Custom Variables, Email Reports, Custom Segment Editor, Geo Location, Real-time maps, and more!
Documentation and more info on https://matomo.org

View File

@ -0,0 +1,21 @@
# Reporting Security Issues
## Security Bug Bounty Program
The Matomo Security Bug Bounty Program is designed to encourage security research in Matomo software and to reward those who help us create the safest web analytics platform. The bounty for valid critical security bugs is a **$555** (US) cash reward. The bounty for non-critical bugs is **$242** (US), paid via Paypal.
## Responsible disclosure by email
If you have found a security issue in Matomo please read [our security notes](https://matomo.org/security/) regarding responsible disclosures.
[Email your Report Vulnerability to the Matomo Security team](mailto:security@matomo.org?subject=Reporting%20Vulnerability%20in%20Matomo)
## Improve your Matomo Server Security
[Secure Matomo server](https://matomo.org/docs/security/): follow these steps to keep your Matomo data safe.
## Security announcements
Please subscribe to [the Changelog](https://matomo.org/changelog/) ([rss feed](https://matomo.org/changelog/feed/)) to be notified of new releases (including security releases).

View File

@ -0,0 +1,45 @@
{
"name": "Matomo",
"main": "piwik.js",
"homepage": "https://matomo.org",
"authors": [
"Matomo.org <hello@matomo.org>"
],
"description": "the leading free/libre analytics platform",
"private": true,
"keywords": [
"matomo",
"piwik",
"web",
"analytics"
],
"dependencies": {
"jquery-ui": "1.10.4",
"jquery": "~2.2.0",
"angular": "~1.6.0",
"angular-sanitize": "~1.6.0",
"angular-animate": "~1.6.0",
"angular-cookies": "~1.6.0",
"angular-mocks": "~1.6.0",
"ngDialog": "~1.3.0",
"html5shiv": "~3.7.0",
"mousetrap": "~1.4.0",
"sprintf": "~1.0.0",
"jScrollPane": "~2.0.0",
"jquery-mousewheel": "~3.1.12",
"jquery-placeholder": "~2.0.8",
"jQuery.dotdotdot": "~1.7.2",
"jquery.scrollTo": "~1.4.13",
"chroma-js": "~0.6.0",
"visibilityjs": "~1.2.1"
},
"license": "GPLv3 or later",
"ignore": [
"**/.*",
"node_modules",
"tests"
],
"resolutions": {
"angular": "~1.6.0"
}
}

View File

@ -0,0 +1,149 @@
{
"name": "piwik/piwik",
"type": "application",
"description": "the leading free/libre analytics platform",
"keywords": ["piwik","matomo","web","analytics"],
"homepage": "https://matomo.org",
"license": "GPL-3.0+",
"authors": [
{
"name": "The Matomo Team",
"email": "hello@matomo.org",
"homepage": "https://matomo.org/team/"
}
],
"support": {
"forum": "https://forum.matomo.org/",
"issues": "https://github.com/matomo-org/piwik/issues",
"wiki": "https://github.com/matomo-org/piwik/wiki",
"source": "https://github.com/matomo-org/piwik"
},
"config":{
"platform": {
"php": "5.5.9"
}
},
"require": {
"php": ">=5.5.9",
"twig/twig": "~1.0",
"leafo/lessphp": "~0.5.0",
"symfony/console": "~2.6.0",
"mustangostang/spyc": "~0.5.0",
"piwik/device-detector": "~3.0",
"piwik/decompress": "~1.0",
"piwik/network": "~0",
"piwik/cache": "~1.0.0",
"piwik/ini": "^1.0.6",
"php-di/php-di": "^5.0.0",
"psr/log": "~1.0",
"monolog/monolog": "~1.11",
"symfony/monolog-bridge": "~2.6.0",
"symfony/event-dispatcher": "~2.6.0",
"pear/pear_exception": "~1.0.0",
"matomo/referrer-spam-blacklist": "~1.0",
"matomo/searchengine-and-social-list": "~1.0",
"tecnickcom/tcpdf": "~6.0",
"piwik/piwik-php-tracker": "^1.0.0",
"composer/semver": "~1.3.0",
"szymach/c-pchart": "^2.0",
"geoip2/geoip2": "^2.8",
"davaxi/sparkline": "^1.1",
"matomo-org/jshrink": "1.3.1"
},
"require-dev": {
"aws/aws-sdk-php": "2.7.1",
"phpunit/phpunit": "~4.8",
"facebook/xhprof": "dev-master",
"phpseclib/phpseclib": "~0.3.8",
"symfony/var-dumper": "~2.6.0",
"symfony/yaml": "~2.6.0"
},
"repositories": [
{
"type": "package",
"package": {
"name": "facebook/xhprof",
"type": "library",
"description": "XHProf: A Hierarchical Profiler for PHP",
"keywords": ["profiling", "performance"],
"homepage": "http://pecl.php.net/package/xhprof",
"license": "Apache-2.0",
"version": "master",
"require": {
"php": ">=5.2.0"
},
"autoload": {
"files": [
"xhprof_lib/utils/xhprof_lib.php",
"xhprof_lib/utils/xhprof_runs.php"
]
},
"source": {
"type": "git",
"url": "https://github.com/phacility/xhprof",
"reference": "master"
}
}
},
{
"type": "package",
"package": {
"name": "matomo-org/jshrink",
"description": "Javascript Minifier built in PHP",
"keywords": ["minifier","javascript"],
"homepage": "http://github.com/tedious/JShrink",
"type": "library",
"license": "BSD-3-Clause",
"version": "1.3.1",
"authors": [
{
"name": "Robert Hafner",
"email": "tedivm@tedivm.com"
}
],
"require": {
"php": "*"
},
"autoload": {
"psr-0": {"JShrink": "src/"}
},
"source": {
"type": "git",
"url": "https://github.com/tedious/JShrink",
"reference": "v1.3.1"
}
}
}
],
"scripts": {
"pre-update-cmd": [
"Piwik\\Composer\\ScriptHandler::cleanXhprof"
],
"pre-install-cmd": [
"Piwik\\Composer\\ScriptHandler::cleanXhprof"
],
"post-update-cmd": [
"Piwik\\Composer\\ScriptHandler::buildXhprof"
],
"post-install-cmd": [
"Piwik\\Composer\\ScriptHandler::buildXhprof"
]
},
"autoload": {
"psr-4": {
"Piwik\\Plugins\\": "plugins/",
"Piwik\\": "core/"
},
"psr-0": {
"Zend_": "libs/",
"HTML_": "libs/",
"PEAR_": "libs/",
"Archive_": "libs/"
}
},
"autoload-dev": {
"psr-4": {
"Piwik\\Tests\\": "tests/PHPUnit/"
}
}
}

3289
msd2/tracking/piwik/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
<?php
return array(
'Piwik\Cache\Backend' => DI\object('Piwik\Cache\Backend\ArrayCache'),
'Piwik\Translation\Loader\LoaderInterface' => DI\object('Piwik\Translation\Loader\LoaderCache')
->constructor(DI\get('Piwik\Translation\Loader\DevelopmentLoader')),
'Piwik\Translation\Loader\DevelopmentLoader' => DI\object()
->constructor(DI\get('Piwik\Translation\Loader\JsonFileLoader')),
);

View File

@ -0,0 +1,137 @@
<?php
use Interop\Container\ContainerInterface;
use Piwik\Common;
use Piwik\Tests\Framework\Mock\FakeAccess;
use Piwik\Tests\Framework\Mock\TestConfig;
return array(
// Disable logging
'Psr\Log\LoggerInterface' => \DI\decorate(function ($previous, ContainerInterface $c) {
$enableLogging = $c->get('ini.tests.enable_logging') == 1 || !empty(getenv('MATOMO_TESTS_ENABLE_LOGGING'));
if ($enableLogging) {
return $previous;
} else {
return $c->get(\Psr\Log\NullLogger::class);
}
}),
'Tests.log.allowAllHandlers' => false,
'log.handlers' => \DI\decorate(function ($previous, ContainerInterface $c) {
if ($c->get('Tests.log.allowAllHandlers')) {
return $previous;
}
return [
$c->get('Piwik\Plugins\Monolog\Handler\FileHandler'),
];
}),
'Piwik\Cache\Backend' => function () {
return \Piwik\Cache::buildBackend('file');
},
'cache.eager.cache_id' => 'eagercache-test-',
// set in individual tests to override now value when needed
'Tests.now' => false,
// Disable loading core translations
'Piwik\Translation\Translator' => DI\decorate(function ($previous, ContainerInterface $c) {
$loadRealTranslations = $c->get('test.vars.loadRealTranslations');
if (!$loadRealTranslations) {
return new \Piwik\Translation\Translator($c->get('Piwik\Translation\Loader\LoaderInterface'), $directories = array());
} else {
return $previous;
}
}),
'Piwik\Config' => DI\decorate(function ($previous, ContainerInterface $c) {
$testingEnvironment = $c->get('Piwik\Tests\Framework\TestingEnvironmentVariables');
$dontUseTestConfig = $c->get('test.vars.dontUseTestConfig');
if (!$dontUseTestConfig) {
$settingsProvider = $c->get('Piwik\Application\Kernel\GlobalSettingsProvider');
return new TestConfig($settingsProvider, $testingEnvironment, $allowSave = false, $doSetTestEnvironment = true);
} else {
return $previous;
}
}),
'Piwik\Access' => DI\decorate(function ($previous, ContainerInterface $c) {
$testUseMockAuth = $c->get('test.vars.testUseMockAuth');
if ($testUseMockAuth) {
$idSitesAdmin = $c->get('test.vars.idSitesAdminAccess');
$idSitesView = $c->get('test.vars.idSitesViewAccess');
$idSitesWrite = $c->get('test.vars.idSitesWriteAccess');
$idSitesCapabilities = $c->get('test.vars.idSitesCapabilities');
$access = new FakeAccess();
if (!empty($idSitesView)) {
FakeAccess::$superUser = false;
FakeAccess::$idSitesView = $idSitesView;
FakeAccess::$idSitesWrite = !empty($idSitesWrite) ? $idSitesWrite : array();
FakeAccess::$idSitesAdmin = !empty($idSitesAdmin) ? $idSitesAdmin : array();
FakeAccess::$identity = 'viewUserLogin';
} elseif (!empty($idSitesWrite)) {
FakeAccess::$superUser = false;
FakeAccess::$idSitesWrite = !empty($idSitesWrite) ? $idSitesWrite : array();
FakeAccess::$idSitesAdmin = !empty($idSitesAdmin) ? $idSitesAdmin : array();
FakeAccess::$identity = 'writeUserLogin';
} elseif (!empty($idSitesAdmin)) {
FakeAccess::$superUser = false;
FakeAccess::$idSitesAdmin = $idSitesAdmin;
FakeAccess::$identity = 'adminUserLogin';
} else {
FakeAccess::$superUser = true;
FakeAccess::$superUserLogin = 'superUserLogin';
}
if (!empty($idSitesCapabilities)) {
FakeAccess::$idSitesCapabilities = (array) $idSitesCapabilities;
}
return $access;
} else {
return $previous;
}
}),
'observers.global' => DI\add(array(
array('AssetManager.getStylesheetFiles', function (&$stylesheets) {
$useOverrideCss = \Piwik\Container\StaticContainer::get('test.vars.useOverrideCss');
if ($useOverrideCss) {
$stylesheets[] = 'tests/resources/screenshot-override/override.css';
}
}),
array('AssetManager.getJavaScriptFiles', function (&$jsFiles) {
$useOverrideJs = \Piwik\Container\StaticContainer::get('test.vars.useOverrideJs');
if ($useOverrideJs) {
$jsFiles[] = 'tests/resources/screenshot-override/override.js';
}
}),
array('Updater.checkForUpdates', function () {
try {
@\Piwik\Filesystem::deleteAllCacheOnUpdate();
} catch (Exception $ex) {
// pass
}
}),
array('Test.Mail.send', function (\Zend_Mail $mail) {
$outputFile = PIWIK_INCLUDE_PATH . '/tmp/' . Common::getRequestVar('module', '') . '.' . Common::getRequestVar('action', '') . '.mail.json';
$outputContent = str_replace("=\n", "", $mail->getBodyText($textOnly = true));
$outputContent = str_replace("=0A", "\n", $outputContent);
$outputContent = str_replace("=3D", "=", $outputContent);
$outputContents = array(
'from' => $mail->getFrom(),
'to' => $mail->getRecipients(),
'subject' => $mail->getSubject(),
'contents' => $outputContent
);
file_put_contents($outputFile, json_encode($outputContents));
}),
)),
);

View File

@ -0,0 +1,62 @@
<?php
use Piwik\Container\StaticContainer;
return array(
// UI tests will remove the port from all URLs to the test server. if a test
// requires the ports in UI tests (eg, Overlay), add the api/controller methods
// to one of these blacklists
'tests.ui.url_normalizer_blacklist.api' => array(),
'tests.ui.url_normalizer_blacklist.controller' => array(),
'Piwik\Config' => \DI\decorate(function (\Piwik\Config $config) {
$config->General['cors_domains'][] = '*';
$config->General['trusted_hosts'][] = $config->tests['http_host'];
$config->General['trusted_hosts'][] = $config->tests['http_host'] . ':' . $config->tests['port'];
return $config;
}),
'observers.global' => \DI\add([
// removes port from all URLs to the test Piwik server so UI tests will pass no matter
// what port is used
array('Request.dispatch.end', function (&$result) {
$request = $_GET + $_POST;
$apiblacklist = StaticContainer::get('tests.ui.url_normalizer_blacklist.api');
if (!empty($request['method'])
&& in_array($request['method'], $apiblacklist)
) {
return;
}
$controllerActionblacklist = StaticContainer::get('tests.ui.url_normalizer_blacklist.controller');
if (!empty($request['module'])) {
$controllerAction = $request['module'] . '.' . (isset($request['action']) ? $request['action'] : 'index');
if (in_array($controllerAction, $controllerActionblacklist)) {
return;
}
}
$config = \Piwik\Config::getInstance();
$host = $config->tests['http_host'];
$port = $config->tests['port'];
if (!empty($port)) {
// remove the port from URLs if any so UI tests won't fail if the port isn't 80
$result = str_replace($host . ':' . $port, $host, $result);
}
// remove PIWIK_INCLUDE_PATH from result so tests don't change based on the machine used
$result = str_replace(realpath(PIWIK_INCLUDE_PATH), '', $result);
}),
array('Controller.RssWidget.rssPiwik.end', function (&$result, $parameters) {
$result = "";
}),
\Piwik\Tests\Framework\XssTesting::getJavaScriptAddEvent(),
]),
);

View File

@ -0,0 +1,965 @@
; <?php exit; ?> DO NOT REMOVE THIS LINE
; If you want to change some of these default values, the best practise is to override
; them in your configuration file in config/config.ini.php. If you directly edit this file,
; you will lose your changes when you upgrade Matomo.
; For example if you want to override action_title_category_delimiter,
; edit config/config.ini.php and add the following:
; [General]
; action_title_category_delimiter = "-"
;--------
; WARNING - YOU SHOULD NOT EDIT THIS FILE DIRECTLY - Edit config.ini.php instead.
;--------
[database]
host =
username =
password =
dbname =
tables_prefix =
port = 3306
adapter = PDO\MYSQL
type = InnoDB
schema = Mysql
; Database SSL Options START
; Turn on or off SSL connection to database, possible values for enable_ssl: 1 or 0
enable_ssl = 0
; Direct path to server CA file, CA bundle supported (required for ssl connection)
ssl_ca =
; Direct path to client cert file (optional)
ssl_cert =
; Direct path to client key file (optional)
ssl_key =
; Direct path to CA cert files directory (optional)
ssl_ca_path =
; List of one or more ciphers for SSL encryption, in OpenSSL format (optional)
ssl_cipher =
; Whether to skip verification of self signed certificates (optional, only supported
; w/ specific PHP versions, and is mostly for testing purposes)
ssl_no_verify =
; Database SSL Options END
; if charset is set to utf8, Matomo will ensure that it is storing its data using UTF8 charset.
; it will add a sql query SET at each page view.
; Matomo should work correctly without this setting but we recommend to have a charset set.
charset = utf8
[database_tests]
host = localhost
username = "@USERNAME@"
password =
dbname = matomo_tests
tables_prefix = matomotests_
port = 3306
adapter = PDO\MYSQL
type = InnoDB
schema = Mysql
charset = utf8
enable_ssl = 0
ssl_ca =
ssl_cert =
ssl_key =
ssl_ca_path =
ssl_cipher =
ssl_no_verify = 1
[tests]
; needed in order to run tests.
; if Matomo is available at http://localhost/dev/matomo/ replace @REQUEST_URI@ with /dev/matomo/
; note: the REQUEST_URI should not contain "plugins" or "tests" in the PATH
http_host = localhost
remote_addr = "127.0.0.1"
request_uri = "@REQUEST_URI@"
port =
enable_logging = 0
; access key and secret as listed in AWS -> IAM -> Users
aws_accesskey = ""
aws_secret = ""
; key pair name as listed in AWS -> EC2 -> Key Pairs. Key name should be different per user.
aws_keyname = ""
; PEM file can be downloaded after creating a new key pair in AWS -> EC2 -> Key Pairs
aws_pem_file = "<path to pem file>"
aws_securitygroups[] = "default"
aws_region = "us-east-1"
aws_ami = "ami-ac24bac4"
aws_instance_type = "c3.large"
[log]
; possible values for log: screen, database, file
log_writers[] = screen
; log level, everything logged w/ this level or one of greater severity
; will be logged. everything else will be ignored. possible values are:
; ERROR, WARN, INFO, DEBUG
; this setting will apply to every log writer, if there is no specific log level defined for a writer.
log_level = WARN
; you can also set specific log levels for different writers, by appending the writer name to log_level_, like so:
; this allows you to log more information to one backend vs another.
; log_level_screen =
; log_level_file =
; if configured to log in a file, log entries will be made to this file
logger_file_path = tmp/logs/matomo.log
[Cache]
; available backends are 'file', 'array', 'null', 'redis', 'chained'
; 'array' will cache data only during one request
; 'null' will not cache anything at all
; 'file' will cache on the filesystem
; 'redis' will cache on a Redis server, use this if you are running Matomo with multiple servers. Further configuration in [RedisCache] is needed
; 'chained' will chain multiple cache backends. Further configuration in [ChainedCache] is needed
backend = chained
[ChainedCache]
; The chained cache will always try to read from the fastest backend first (the first listed one) to avoid requesting
; the same cache entry from the slowest backend multiple times in one request.
backends[] = array
backends[] = file
[RedisCache]
; Redis server configuration.
host = "127.0.0.1"
port = 6379
; instead of host and port a unix socket path can be configured
unix_socket = ""
timeout = 0.0
password = ""
database = 14
; In case you are using queued tracking: Make sure to configure a different database! Otherwise queued requests might
; be flushed
[Debug]
; if set to 1, the archiving process will always be triggered, even if the archive has already been computed
; this is useful when making changes to the archiving code so we can force the archiving process
always_archive_data_period = 0;
always_archive_data_day = 0;
; Force archiving Custom date range (without re-archiving sub-periods used to process this date range)
always_archive_data_range = 0;
; if set to 1, all the SQL queries will be recorded by the profiler
; and a profiling summary will be printed at the end of the request
; NOTE: you must also set [log] log_writers[] = "screen" to enable the profiler to print on screen
enable_sql_profiler = 0
; If set to 1, all requests to matomo.php will be forced to be 'new visitors'
tracker_always_new_visitor = 0
; if set to 1, all SQL queries will be logged using the DEBUG log level
log_sql_queries = 0
; if set to 1, core:archive profiling information will be recorded in a log file. the log file is determined by the
; archive_profiling_log option.
archiving_profile = 0
; if set to an absolute path, core:archive profiling information will be logged to specified file
archive_profiling_log =
[DebugTests]
; When set to 1, standalone plugins (those with their own git repositories)
; will be loaded when executing tests.
enable_load_standalone_plugins_during_tests = 0
[Development]
; Enables the development mode where we avoid most caching to make sure code changes will be directly applied as
; some caches are only invalidated after an update otherwise. When enabled it'll also performs some validation checks.
; For instance if you register a method in a widget we will verify whether the method actually exists and is public.
; If not, we will show you a helpful warning to make it easy to find simple typos etc.
enabled = 0
; if set to 1, javascript files will be included individually and neither merged nor minified.
; this option must be set to 1 when adding, removing or modifying javascript files
; Note that for quick debugging, instead of using below setting, you can add `&disable_merged_assets=1` to the Matomo URL
disable_merged_assets = 0
[General]
; the following settings control whether Unique Visitors `nb_uniq_visitors` and Unique users `nb_users` will be processed for different period types.
; year and range periods are disabled by default, to ensure optimal performance for high traffic Matomo instances
; if you set it to 1 and want the Unique Visitors to be re-processed for reports in the past, drop all matomo_archive_* tables
; it is recommended to always enable Unique Visitors and Unique Users processing for 'day' periods
enable_processing_unique_visitors_day = 1
enable_processing_unique_visitors_week = 1
enable_processing_unique_visitors_month = 1
enable_processing_unique_visitors_year = 0
enable_processing_unique_visitors_range = 0
; controls whether Unique Visitors will be processed for groups of websites. these metrics describe the number
; of unique visitors across the entire set of websites, so if a visitor visited two websites in the group, she
; would still only be counted as one. only relevant when using plugins that group sites together
enable_processing_unique_visitors_multiple_sites = 0
; The list of periods that are available in the Matomo calendar
; Example use case: custom date range requests are processed in real time,
; so they may take a few minutes on very high traffic website: you may remove "range" below to disable this period
enabled_periods_UI = "day,week,month,year,range"
enabled_periods_API = "day,week,month,year,range"
; whether to enable subquery cache for Custom Segment archiving queries
enable_segments_subquery_cache = 0
; Any segment subquery that matches more than segments_subquery_cache_limit IDs will not be cached,
; and the original subquery executed instead.
segments_subquery_cache_limit = 100000
; TTL: Time to live for cache files, in seconds. Default to 60 minutes
segments_subquery_cache_ttl = 3600
; when set to 1, all requests to Matomo will return a maintenance message without connecting to the DB
; this is useful when upgrading using the shell command, to prevent other users from accessing the UI while Upgrade is in progress
maintenance_mode = 0
; Defines the release channel that shall be used. Currently available values are:
; "latest_stable", "latest_beta", "latest_2x_stable", "latest_2x_beta"
release_channel = "latest_stable"
; character used to automatically create categories in the Actions > Pages, Outlinks and Downloads reports
; for example a URL like "example.com/blog/development/first-post" will create
; the page first-post in the subcategory development which belongs to the blog category
action_url_category_delimiter = /
; similar to above, but this delimiter is only used for page titles in the Actions > Page titles report
action_title_category_delimiter = ""
; the maximum url category depth to track. if this is set to 2, then a url such as
; "example.com/blog/development/first-post" would be treated as "example.com/blog/development".
; this setting is used mainly to limit the amount of data that is stored by Matomo.
action_category_level_limit = 10
; minimum number of websites to run autocompleter
autocomplete_min_sites = 5
; maximum number of websites showed in search results in autocompleter
site_selector_max_sites = 15
; if set to 1, shows sparklines (evolution graph) in 'All Websites' report (MultiSites plugin)
show_multisites_sparklines = 1
; number of websites to display per page in the All Websites dashboard
all_websites_website_per_page = 50
; if set to 0, the anonymous user will not be able to use the 'segments' parameter in the API request
; this is useful to prevent full DB access to the anonymous user, or to limit performance usage
anonymous_user_enable_use_segments_API = 1
; if browser trigger archiving is disabled, API requests with a &segment= parameter will still trigger archiving.
; You can force the browser archiving to be disabled in most cases by setting this setting to 1
; The only time that the browser will still trigger archiving is when requesting a custom date range that is not pre-processed yet
browser_archiving_disabled_enforce = 0
; Add custom currencies to Sites Manager.
currencies[BTC] = Bitcoin
; By default, users can create Segments which are to be processed in Real-time.
; Setting this to 0 will force all newly created Custom Segments to be "Pre-processed (faster, requires archive.php cron)"
; This can be useful if you want to prevent users from adding much load on the server.
; Notes:
; * any existing Segment set to "processed in Real time", will still be set to Real-time.
; this will only affect custom segments added or modified after this setting is changed.
; * when set to 0 then any user with at least 'view' access will be able to create pre-processed segments.
enable_create_realtime_segments = 1
; Whether to enable the "Suggest values for segment" in the Segment Editor panel.
; Set this to 0 in case your Matomo database is very big, and suggested values may not appear in time
enable_segment_suggested_values = 1
; By default, any user with a "view" access for a website can create segment assigned to this website.
; Set this to "admin" or "superuser" to require that users should have at least this access to create new segments.
; Note: anonymous user (even if it has view access) is not allowed to create or edit segment.
; Possible values are "view", "admin", "superuser"
adding_segment_requires_access = "view"
; Whether it is allowed for users to add segments that affect all websites or not. If there are many websites
; this admin option can be used to prevent users from performing an action that will have a major impact
; on Matomo performance.
allow_adding_segments_for_all_websites = 1
; When archiving segments for the first time, this determines the oldest date that will be archived.
; This option can be used to avoid archiving (for isntance) the lastN years for every new segment.
; Valid option values include: "beginning_of_time" (start date of archiving will not be changed)
; "segment_last_edit_time" (start date of archiving will be the earliest last edit date found,
; if none is found, the created date is used)
; "segment_creation_time" (start date of archiving will be the creation date of the segment)
; lastN where N is an integer (eg "last10" to archive for 10 days before the segment creation date)
process_new_segments_from = "beginning_of_time"
; this action name is used when the URL ends with a slash /
; it is useful to have an actual string to write in the UI
action_default_name = index
; default language to use in Matomo
default_language = en
; default number of elements in the datatable
datatable_default_limit = 10
; Each datatable report has a Row Limit selector at the bottom right.
; By default you can select from 5 to 500 rows. You may customise the values below
; -1 will be displayed as 'all' and it will export all rows (filter_limit=-1)
datatable_row_limits = "5,10,25,50,100,250,500,-1"
; default number of rows returned in API responses
; this value is overwritten by the '# Rows to display' selector.
; if set to -1, a click on 'Export as' will export all rows independently of the current '# Rows to display'.
API_datatable_default_limit = 100
; When period=range, below the datatables, when user clicks on "export", the data will be aggregate of the range.
; Here you can specify the comma separated list of formats for which the data will be exported aggregated by day
; (ie. there will be a new "date" column). For example set to: "rss,tsv,csv"
datatable_export_range_as_day = "rss"
; This setting is overridden in the UI, under "User Settings".
; The date and period loaded by Matomo uses the defaults below. Possible values: yesterday, today.
default_day = yesterday
; Possible values: day, week, month, year.
default_period = day
; Time in seconds after which an archive will be computed again. This setting is used only for today's statistics.
; This setting is overriden in the UI, under "General Settings".
; This setting is only used if it hasn't been overriden via the UI yet, or if enable_general_settings_admin=0
time_before_today_archive_considered_outdated = 900
; Time in seconds after which an archive will be computed again. This setting is used only for week's statistics.
; If set to "-1" (default), it will fall back to the UI setting under "General settings" unless enable_general_settings_admin=0
; is set. In this case it will default to "time_before_today_archive_considered_outdated";
time_before_week_archive_considered_outdated = -1
; Same as config setting "time_before_week_archive_considered_outdated" but it is only applied to monthly archives
time_before_month_archive_considered_outdated = -1
; Same as config setting "time_before_week_archive_considered_outdated" but it is only applied to yearly archives
time_before_year_archive_considered_outdated = -1
; Same as config setting "time_before_week_archive_considered_outdated" but it is only applied to range archives
time_before_range_archive_considered_outdated = -1
; This setting is overriden in the UI, under "General Settings".
; The default value is to allow browsers to trigger the Matomo archiving process.
; This setting is only used if it hasn't been overridden via the UI yet, or if enable_general_settings_admin=0
enable_browser_archiving_triggering = 1
; By default, Matomo will force archiving of range periods from browser requests, even if enable_browser_archiving_triggering
; is set to 0. This can sometimes create too much of a demand on system resources. Setting this option to 0 and
; disabling browser trigger archiving will make sure ranges are not archived on browser request. Since the cron
; archiver does not archive any custom date ranges, you must either disable range (using enabled_periods_API and enabled_periods_UI)
; or make sure the date ranges users' want to see will be processed somehow.
archiving_range_force_on_browser_request = 1
; By default Matomo runs OPTIMIZE TABLE SQL queries to free spaces after deleting some data.
; If your Matomo tracks millions of pages, the OPTIMIZE TABLE queries might run for hours (seen in "SHOW FULL PROCESSLIST \g")
; so you can disable these special queries here:
enable_sql_optimize_queries = 1
; By default Matomo is purging complete date range archives to free spaces after deleting some data.
; If you are pre-processing custom ranges using CLI task to make them easily available in UI,
; you can prevent this action from happening by setting this parameter to value bigger than 1
purge_date_range_archives_after_X_days = 1
; MySQL minimum required version
; note: timezone support added in 4.1.3
minimum_mysql_version = 4.1
; PostgreSQL minimum required version
minimum_pgsql_version = 8.3
; Minimum advised memory limit in Mb in php.ini file (see memory_limit value)
; Set to "-1" to always use the configured memory_limit value in php.ini file.
minimum_memory_limit = 128
; Minimum memory limit in Mb enforced when archived via ./console core:archive
; Set to "-1" to always use the configured memory_limit value in php.ini file.
minimum_memory_limit_when_archiving = 768
; Matomo will check that usernames and password have a minimum length, and will check that characters are "allowed"
; This can be disabled, if for example you wish to import an existing User database in Matomo and your rules are less restrictive
disable_checks_usernames_attributes = 0
; Matomo will use the configured hash algorithm where possible.
; For legacy data, fallback or non-security scenarios, we use md5.
hash_algorithm = whirlpool
; Matomo uses PHP's dbtable for session.
; If you prefer configuring sessions through the php.ini directly, you may unset this value to an empty string
session_save_handler = dbtable
; If set to 1, Matomo will automatically redirect all http:// requests to https://
; If SSL / https is not correctly configured on the server, this will break Matomo
; If you set this to 1, and your SSL configuration breaks later on, you can always edit this back to 0
; it is recommended for security reasons to always use Matomo over https
force_ssl = 0
; (DEPRECATED) has no effect
login_cookie_name = piwik_auth
; By default, the auth cookie is set only for the duration of session.
; if "Remember me" is checked, the auth cookie will be valid for 14 days by default
login_cookie_expire = 1209600
; Sets the session cookie path
login_cookie_path =
; email address that appears as a Sender in the password recovery email
; if specified, {DOMAIN} will be replaced by the current Matomo domain
login_password_recovery_email_address = "password-recovery@{DOMAIN}"
; name that appears as a Sender in the password recovery email
login_password_recovery_email_name = Matomo
; email address that appears as a Reply-to in the password recovery email
; if specified, {DOMAIN} will be replaced by the current Matomo domain
login_password_recovery_replyto_email_address = "no-reply@{DOMAIN}"
; name that appears as a Reply-to in the password recovery email
login_password_recovery_replyto_email_name = "No-reply"
; When configured, only users from a configured IP can log into your Matomo. You can define one or multiple
; IPv4, IPv6, and IP ranges. You may also define hostnames. However, resolving hostnames in each request
; may slightly slow down your Matomo.
; This whitelist also affects API requests unless you disabled it via the setting
; "login_whitelist_apply_to_reporting_api_requests" below. Note that neither this setting, nor the
; "login_whitelist_apply_to_reporting_api_requests" restricts authenticated tracking requests (tracking requests
; with a "token_auth" URL parameter).
;
; Examples:
; login_whitelist_ip[] = 204.93.240.*
; login_whitelist_ip[] = 204.93.177.0/24
; login_whitelist_ip[] = 199.27.128.0/21
; login_whitelist_ip[] = 2001:db8::/48
; login_whitelist_ip[] = matomo.org
; By default, if a whitelisted IP address is specified via "login_whitelist_ip[]", the reporting user interface as
; well as HTTP Reporting API requests will only work for these whitelisted IPs.
; Set this setting to "0" to allow HTTP Reporting API requests from any IP address.
login_whitelist_apply_to_reporting_api_requests = 1
; By default when user logs out they are redirected to Matomo "homepage" usually the Login form.
; Uncomment the next line to set a URL to redirect the user to after they log out of Matomo.
; login_logout_url = http://...
; Set to 1 to disable the framebuster on standard Non-widgets pages (a click-jacking countermeasure).
; Default is 0 (i.e., bust frames on all non Widget pages such as Login, API, Widgets, Email reports, etc.).
enable_framed_pages = 0
; Set to 1 to disable the framebuster on Admin pages (a click-jacking countermeasure).
; Default is 0 (i.e., bust frames on the Settings forms).
enable_framed_settings = 0
; language cookie name for session
language_cookie_name = matomo_lang
; standard email address displayed when sending emails
noreply_email_address = "noreply@{DOMAIN}"
; standard email name displayed when sending emails. If not set, a default name will be used.
noreply_email_name = ""
; set to 0 to disable sending of all emails. useful for testing.
emails_enabled = 1
; feedback email address;
; when testing, use your own email address or "nobody"
feedback_email_address = "feedback@matomo.org"
; using to set reply_to in reports e-mail to login of report creator
scheduled_reports_replyto_is_user_email_and_alias = 0
; scheduled reports truncate limit
; the report will be rendered with the first 23 rows and will aggregate other rows in a summary row
; 23 rows table fits in one portrait page
scheduled_reports_truncate = 23
; during archiving, Matomo will limit the number of results recorded, for performance reasons
; maximum number of rows for any of the Referrers tables (keywords, search engines, campaigns, etc.)
datatable_archiving_maximum_rows_referrers = 1000
; maximum number of rows for any of the Referrers subtable (search engines by keyword, keyword by campaign, etc.)
datatable_archiving_maximum_rows_subtable_referrers = 50
; maximum number of rows for the Users report
datatable_archiving_maximum_rows_userid_users = 50000
; maximum number of rows for the Custom Variables names report
; Note: if the website is Ecommerce enabled, the two values below will be automatically set to 50000
datatable_archiving_maximum_rows_custom_variables = 1000
; maximum number of rows for the Custom Variables values reports
datatable_archiving_maximum_rows_subtable_custom_variables = 1000
; maximum number of rows for any of the Actions tables (pages, downloads, outlinks)
datatable_archiving_maximum_rows_actions = 500
; maximum number of rows for pages in categories (sub pages, when clicking on the + for a page category)
; note: should not exceed the display limit in Piwik\Actions\Controller::ACTIONS_REPORT_ROWS_DISPLAY
; because each subdirectory doesn't have paging at the bottom, so all data should be displayed if possible.
datatable_archiving_maximum_rows_subtable_actions = 100
; maximum number of rows for any of the Events tables (Categories, Actions, Names)
datatable_archiving_maximum_rows_events = 500
; maximum number of rows for sub-tables of the Events tables (eg. for the subtables Categories>Actions or Categories>Names).
datatable_archiving_maximum_rows_subtable_events = 500
; maximum number of rows for other tables (Providers, User settings configurations)
datatable_archiving_maximum_rows_standard = 500
; maximum number of rows to fetch from the database when archiving. if set to 0, no limit is used.
; this can be used to speed up the archiving process, but is only useful if you're site has a large
; amount of actions, referrers or custom variable name/value pairs.
archiving_ranking_query_row_limit = 50000
; maximum number of actions that is shown in the visitor log for each visitor
visitor_log_maximum_actions_per_visit = 500
; by default, the real time Live! widget will update every 5 seconds and refresh with new visits/actions/etc.
; you can change the timeout so the widget refreshes more often, or not as frequently
live_widget_refresh_after_seconds = 5
; by default, the Live! real time visitor count widget will check to see how many visitors your
; website received in the last 3 minutes. changing this value will change the number of minutes
; the widget looks in.
live_widget_visitor_count_last_minutes = 3
; by default visitor profile will show aggregated information for the last up to 100 visits of a visitor
; this limit can be adjusted by changing this value
live_visitor_profile_max_visits_to_aggregate = 100
; In "All Websites" dashboard, when looking at today's reports (or a date range including today),
; the page will automatically refresh every 5 minutes. Set to 0 to disable automatic refresh
multisites_refresh_after_seconds = 300
; by default, an update notification for a new version of Matomo is shown to every user. Set to 1 if only
; the superusers should see the notification.
show_update_notification_to_superusers_only = 0
; Set to 1 if you're using https on your Matomo server and Matomo can't detect it,
; e.g., a reverse proxy using https-to-http, or a web server that doesn't
; set the HTTPS environment variable.
assume_secure_protocol = 0
; Set to 1 if you're using more than one server for your Matomo installation. For example if you are using Matomo in a
; load balanced environment, if you have configured failover or if you're just using multiple servers in general.
; By enabling this flag we will for example not allow the installation of a plugin via the UI as a plugin would be only
; installed on one server or a config one change would be only made on one server instead of all servers.
multi_server_environment = 0
; List of proxy headers for client IP addresses
; Matomo will determine the user IP by extracting the first IP address found in this proxy header.
;
; CloudFlare (CF-Connecting-IP)
;proxy_client_headers[] = HTTP_CF_CONNECTING_IP
;
; ISP proxy (Client-IP)
;proxy_client_headers[] = HTTP_CLIENT_IP
;
; de facto standard (X-Forwarded-For)
;proxy_client_headers[] = HTTP_X_FORWARDED_FOR
; List of proxy headers for host IP addresses
;
; de facto standard (X-Forwarded-Host)
;proxy_host_headers[] = HTTP_X_FORWARDED_HOST
; List of proxy IP addresses (or IP address ranges) to skip (if present in the above headers).
; Generally, only required if there's more than one proxy between the visitor and the backend web server.
;
; Examples:
;proxy_ips[] = 204.93.240.*
;proxy_ips[] = 204.93.177.0/24
;proxy_ips[] = 199.27.128.0/21
;proxy_ips[] = 173.245.48.0/20
; Set to 1 if you're using a proxy which is rewriting the URI.
; By enabling this flag the header HTTP_X_FORWARDED_URI will be considered for the current script name.
proxy_uri_header = 0
; Whether to enable trusted host checking. This can be disabled if you're running Matomo
; on several URLs and do not wish to constantly edit the trusted host list.
enable_trusted_host_check = 1
; List of trusted hosts (eg domain or subdomain names) when generating absolute URLs.
;
; Examples:
;trusted_hosts[] = example.com
;trusted_hosts[] = stats.example.com
; List of Cross-origin resource sharing domains (eg domain or subdomain names) when generating absolute URLs.
; Described here: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
;
; Examples:
;cors_domains[] = http://example.com
;cors_domains[] = http://stats.example.com
;
; Or you may allow cross domain requests for all domains with:
;cors_domains[] = *
; If you use this Matomo instance over multiple hostnames, Matomo will need to know
; a unique instance_id for this instance, so that Matomo can serve the right custom logo and tmp/* assets,
; independently of the hostname Matomo is currently running under.
; instance_id = stats.example.com
; The API server is an essential part of the Matomo infrastructure/ecosystem to
; provide services to Matomo installations, e.g., getLatestVersion and
; subscribeNewsletter.
api_service_url = http://api.matomo.org
; When the ImageGraph plugin is activated, report metadata have an additional entry : 'imageGraphUrl'.
; This entry can be used to request a static graph for the requested report.
; When requesting report metadata with $period=range, Matomo needs to translate it to multiple periods for evolution graphs.
; eg. $period=range&date=previous10 becomes $period=day&date=previous10. Use this setting to override the $period value.
graphs_default_period_to_plot_when_period_range = day
; When the ImageGraph plugin is activated, enabling this option causes the image graphs to show the evolution
; within the selected period instead of the evolution across the last n periods.
graphs_show_evolution_within_selected_period = 0
; This option controls the default number of days in the past to show in evolution graphs generated by the ImageGraph plugin
graphs_default_evolution_graph_last_days_amount = 30
; The Overlay plugin shows the Top X following pages, Top X downloads and Top X outlinks which followed
; a view of the current page. The value X can be set here.
overlay_following_pages_limit = 300
; With this option, you can disable the framed mode of the Overlay plugin. Use it if your website contains a framebuster.
overlay_disable_framed_mode = 0
; By default we check whether the Custom logo is writable or not, before we display the Custom logo file uploader
enable_custom_logo_check = 1
; If php is running in a chroot environment, when trying to import CSV files with createTableFromCSVFile(),
; Mysql will try to load the chrooted path (which is incomplete). To prevent an error, here you can specify the
; absolute path to the chroot environment. eg. '/path/to/matomo/chrooted/'
absolute_chroot_path =
; The path (relative to the Matomo directory) in which Matomo temporary files are stored.
; Defaults to ./tmp (the tmp/ folder inside the Matomo directory)
tmp_path = "/tmp"
; In some rare cases it may be useful to explicitely tell Matomo not to use LOAD DATA INFILE
; This may for example be useful when doing Mysql AWS replication
enable_load_data_infile = 1
; By setting this option to 0:
; - links to Enable/Disable/Uninstall plugins will be hidden and disabled
; - links to Uninstall themes will be disabled (but user can still enable/disable themes)
enable_plugins_admin = 1
; By setting this option to 1, it will be possible for Super Users to upload Matomo plugin ZIP archives directly in Matomo Administration.
; Enabling this opens a remote code execution vulnerability where
; an attacker who gained Super User access could execute custom PHP code in a Matomo plugin.
enable_plugin_upload = 0
; By setting this option to 0 (e.g. in common.config.ini.php) the installer will be disabled.
enable_installer = 1
; By setting this option to 0, you can prevent Super User from editing the Geolocation settings.
enable_geolocation_admin = 1
; By setting this option to 0, the old raw data and old report data purging features will be hidden from the UI
; Note: log purging and old data purging still occurs, just the Super User cannot change the settings.
enable_delete_old_data_settings_admin = 1
; By setting this option to 0, the following settings will be hidden and disabled from being set in the UI:
; - Archiving settings
; - Update settings
; - Email server settings
; - Trusted Matomo Hostname
enable_general_settings_admin = 1
; Disabling this will disable features like automatic updates for Matomo,
; its plugins and components like the GeoIP database, referrer spam blacklist or search engines and social network definitions
enable_internet_features = 1
; By setting this option to 0, it will disable the "Auto update" feature
enable_auto_update = 1
; By setting this option to 0, no emails will be sent in case of an available core.
; If set to 0 it also disables the "sent plugin update emails" feature in general and the related setting in the UI.
enable_update_communication = 1
; Comma separated list of plugin names for which console commands should be loaded (applies when Matomo is not installed yet)
always_load_commands_from_plugin=
; This controls whether the pivotBy query parameter can be used with any dimension or just subtable
; dimensions. If set to 1, it will fetch a report with a segment for each row of the table being pivoted.
; At present, this is very inefficient, so it is disabled by default.
pivot_by_filter_enable_fetch_by_segment = 0
; This controls the default maximum number of columns to display in a pivot table. Since a pivot table displays
; a table's rows as columns, the number of columns can become very large, which will affect webpage layouts.
; Set to -1 to specify no limit. Note: The pivotByColumnLimit query parameter can be used to override this default
; on a per-request basis;
pivot_by_filter_default_column_limit = 10
; If set to 0 it will disable advertisements for providers of Professional Support for Matomo.
piwik_professional_support_ads_enabled = 1
; The number of days to wait before sending the JavaScript tracking code email reminder.
num_days_before_tracking_code_reminder = 5
[Tracker]
; Matomo uses "Privacy by default" model. When one of your users visit multiple of your websites tracked in this Matomo,
; Matomo will create for this user a fingerprint that will be different across the multiple websites.
; If you want to track unique users across websites you may set this setting to 1.
; Note: setting this to 0 increases your users' privacy.
enable_fingerprinting_across_websites = 0
; Matomo uses first party cookies by default. If set to 1,
; the visit ID cookie will be set on the Matomo server domain as well
; this is useful when you want to do cross websites analysis
use_third_party_id_cookie = 0
; If tracking does not work for you or you are stuck finding an issue, you might want to enable the tracker debug mode.
; Once enabled (set to 1) messages will be logged to all loggers defined in "[log] log_writers" config.
debug = 0
; This option is an alternative to the debug option above. When set to 1, you can debug tracker request by adding
; a debug=1 query parameter in the URL. All other HTTP requests will not have debug enabled. For security reasons this
; option should be only enabled if really needed and only for a short time frame. Otherwise anyone can set debug=1 and
; see the log output as well.
debug_on_demand = 0
; This setting is described in this FAQ: https://matomo.org/faq/how-to/faq_175/
; Note: generally this should only be set to 1 in an intranet setting, where most users have the same configuration (browsers, OS)
; and the same IP. If left to 0 in this setting, all visitors will be counted as one single visitor.
trust_visitors_cookies = 0
; name of the cookie used to store the visitor information
; This is used only if use_third_party_id_cookie = 1
cookie_name = _pk_uid
; by default, the Matomo tracking cookie expires in 13 months (365 + 28 days)
; This is used only if use_third_party_id_cookie = 1
cookie_expire = 33955200;
; The path on the server in which the cookie will be available on.
; Defaults to empty. See spec in https://curl.haxx.se/rfc/cookie_spec.html
; This is used for the Ignore cookie, and the third party cookie if use_third_party_id_cookie = 1
cookie_path =
; The domain on the server in which the cookie will be available on.
; Defaults to empty. See spec in https://curl.haxx.se/rfc/cookie_spec.html
; This is used for the third party cookie if use_third_party_id_cookie = 1
cookie_domain =
; set to 0 if you want to stop tracking the visitors. Useful if you need to stop all the connections on the DB.
record_statistics = 1
; length of a visit in seconds. If a visitor comes back on the website visit_standard_length seconds
; after their last page view, it will be recorded as a new visit. In case you are using the Matomo JavaScript tracker to
; calculate the visit count correctly, make sure to call the method "setSessionCookieTimeout" eg
; `_paq.push(['setSessionCookieTimeout', timeoutInSeconds=1800])`
visit_standard_length = 1800
; The amount of time in the past to match the current visitor to a known visitor via fingerprint. Defaults to visit_standard_length.
; If you are looking for higher accuracy of "returning visitors" metrics, you may set this value to 86400 or more.
; This is especially useful when you use the Tracking API where tracking Returning Visitors often depends on this setting.
; The value window_look_back_for_visitor is used only if it is set to greater than visit_standard_length.
; Note: visitors with visitor IDs will be matched by visitor ID from any point in time, this is only for recognizing visitors
; by device fingerprint.
window_look_back_for_visitor = 0
; visitors that stay on the website and view only one page will be considered as time on site of 0 second
default_time_one_page_visit = 0
; Comma separated list of URL query string variable names that will be removed from your tracked URLs
; By default, Matomo will remove the most common parameters which are known to change often (eg. session ID parameters)
url_query_parameter_to_exclude_from_url = "gclid,fb_xd_fragment,fb_comment_id,phpsessid,jsessionid,sessionid,aspsessionid,doing_wp_cron,sid,pk_vid"
; if set to 1, Matomo attempts a "best guess" at the visitor's country of
; origin when the preferred language tag omits region information.
; The mapping is defined in core/DataFiles/LanguageToCountry.php,
enable_language_to_country_guess = 1
; When the `./console core:archive` cron hasn't been setup, we still need to regularly run some maintenance tasks.
; Visits to the Tracker will try to trigger Scheduled Tasks (eg. scheduled PDF/HTML reports by email).
; Scheduled tasks will only run if 'Enable Matomo Archiving from Browser' is enabled in the General Settings.
; Tasks run once every hour maximum, they might not run every hour if traffic is low.
; Set to 0 to disable Scheduled tasks completely.
scheduled_tasks_min_interval = 3600
; name of the cookie to ignore visits
ignore_visits_cookie_name = piwik_ignore
; Comma separated list of variable names that will be read to define a Campaign name, for example CPC campaign
; Example: If a visitor first visits 'index.php?piwik_campaign=Adwords-CPC' then it will be counted as a campaign referrer named 'Adwords-CPC'
; Includes by default the GA style campaign parameters
campaign_var_name = "pk_cpn,pk_campaign,piwik_campaign,utm_campaign,utm_source,utm_medium"
; Comma separated list of variable names that will be read to track a Campaign Keyword
; Example: If a visitor first visits 'index.php?piwik_campaign=Adwords-CPC&piwik_kwd=My killer keyword' ;
; then it will be counted as a campaign referrer named 'Adwords-CPC' with the keyword 'My killer keyword'
; Includes by default the GA style campaign keyword parameter utm_term
campaign_keyword_var_name = "pk_kwd,pk_keyword,piwik_kwd,utm_term"
; if set to 1, actions that contain different campaign information from the visitor's ongoing visit will
; be treated as the start of a new visit. This will include situations when campaign information was absent before,
; but is present now.
create_new_visit_when_campaign_changes = 1
; if set to 1, actions that contain different website referrer information from the visitor's ongoing visit
; will be treated as the start of a new visit. This will include situations when website referrer information was
; absent before, but is present now.
create_new_visit_when_website_referrer_changes = 0
; ONLY CHANGE THIS VALUE WHEN YOU DO NOT USE MATOMO ARCHIVING, SINCE THIS COULD CAUSE PARTIALLY MISSING ARCHIVE DATA
; Whether to force a new visit at midnight for every visitor. Default 1.
create_new_visit_after_midnight = 1
; maximum length of a Page Title or a Page URL recorded in the log_action.name table
page_maximum_length = 1024;
; Tracker cache files are the simple caching layer for Tracking.
; TTL: Time to live for cache files, in seconds. Default to 5 minutes.
tracker_cache_file_ttl = 300
; Whether Bulk tracking requests to the Tracking API requires the token_auth to be set.
bulk_requests_require_authentication = 0
; Whether Bulk tracking requests will be wrapped within a DB Transaction.
; This greatly increases performance of Log Analytics and in general any Bulk Tracking API requests.
bulk_requests_use_transaction = 1
; DO NOT USE THIS SETTING ON PUBLICLY AVAILABLE MATOMO SERVER
; !!! Security risk: if set to 0, it would allow anyone to push data to Matomo with custom dates in the past/future and even with fake IPs!
; When using the Tracking API, to override either the datetime and/or the visitor IP,
; token_auth with an "admin" access is required. If you set this setting to 0, the token_auth will not be required anymore.
; DO NOT USE THIS SETTING ON PUBLIC MATOMO SERVERS
tracking_requests_require_authentication = 1
; By default, Matomo accepts only tracking requests for up to 1 day in the past. For tracking requests with a custom date
; date is older than 1 day, Matomo requires an authenticated tracking requests. By setting this config to another value
; You can change how far back Matomo will track your requests without authentication. The configured value is in seconds.
tracking_requests_require_authentication_when_custom_timestamp_newer_than = 86400;
[Segments]
; Reports with segmentation in API requests are processed in real time.
; On high traffic websites it is recommended to pre-process the data
; so that the analytics reports are always fast to load.
; You can define below the list of Segments strings
; for which all reports should be Archived during the cron execution
; All segment values MUST be URL encoded.
;Segments[]="visitorType==new"
;Segments[]="visitorType==returning,visitorType==returningCustomer"
; If you define Custom Variables for your visitor, for example set the visit type
;Segments[]="customVariableName1==VisitType;customVariableValue1==Customer"
[Deletelogs]
; delete_logs_enable - enable (1) or disable (0) delete log feature. Make sure that all archives for the given period have been processed (setup a cronjob!),
; otherwise you may lose tracking data.
; delete_logs_schedule_lowest_interval - lowest possible interval between two table deletes (in days, 1|7|30). Default: 7.
; delete_logs_older_than - delete data older than XX (days). Default: 180
delete_logs_enable = 0
delete_logs_schedule_lowest_interval = 7
delete_logs_older_than = 180
delete_logs_max_rows_per_query = 100000
enable_auto_database_size_estimate = 1
enable_database_size_estimate = 1
delete_logs_unused_actions_schedule_lowest_interval = 30
[Deletereports]
delete_reports_enable = 0
delete_reports_older_than = 12
delete_reports_keep_basic_metrics = 1
delete_reports_keep_day_reports = 0
delete_reports_keep_week_reports = 0
delete_reports_keep_month_reports = 1
delete_reports_keep_year_reports = 1
delete_reports_keep_range_reports = 0
delete_reports_keep_segment_reports = 0
[mail]
defaultHostnameIfEmpty = defaultHostnameIfEmpty.example.org ; default Email @hostname, if current host can't be read from system variables
transport = ; smtp (using the configuration below) or empty (using built-in mail() function)
port = ; optional; defaults to 25 when security is none or tls; 465 for ssl
host = ; SMTP server address
type = ; SMTP Auth type. By default: NONE. For example: LOGIN
username = ; SMTP username
password = ; SMTP password
encryption = ; SMTP transport-layer encryption, either 'ssl', 'tls', or empty (i.e., none).
[proxy]
type = BASIC ; proxy type for outbound/outgoing connections; currently, only BASIC is supported
host = ; Proxy host: the host name of your proxy server (mandatory)
port = ; Proxy port: the port that the proxy server listens to. There is no standard default, but 80, 1080, 3128, and 8080 are popular
username = ; Proxy username: optional; if specified, password is mandatory
password = ; Proxy password: optional; if specified, username is mandatory
[Plugins]
; list of plugins (in order they will be loaded) that are activated by default in the Matomo platform
Plugins[] = CorePluginsAdmin
Plugins[] = CoreAdminHome
Plugins[] = CoreHome
Plugins[] = WebsiteMeasurable
Plugins[] = IntranetMeasurable
Plugins[] = Diagnostics
Plugins[] = CoreVisualizations
Plugins[] = Proxy
Plugins[] = API
Plugins[] = ExamplePlugin
Plugins[] = Widgetize
Plugins[] = Transitions
Plugins[] = LanguagesManager
Plugins[] = Actions
Plugins[] = Dashboard
Plugins[] = MultiSites
Plugins[] = Referrers
Plugins[] = UserLanguage
Plugins[] = DevicesDetection
Plugins[] = Goals
Plugins[] = Ecommerce
Plugins[] = SEO
Plugins[] = Events
Plugins[] = UserCountry
Plugins[] = GeoIp2
Plugins[] = VisitsSummary
Plugins[] = VisitFrequency
Plugins[] = VisitTime
Plugins[] = VisitorInterest
Plugins[] = ExampleAPI
Plugins[] = RssWidget
Plugins[] = Feedback
Plugins[] = Monolog
Plugins[] = Login
Plugins[] = TwoFactorAuth
Plugins[] = UsersManager
Plugins[] = SitesManager
Plugins[] = Installation
Plugins[] = CoreUpdater
Plugins[] = CoreConsole
Plugins[] = ScheduledReports
Plugins[] = UserCountryMap
Plugins[] = Live
Plugins[] = CustomVariables
Plugins[] = PrivacyManager
Plugins[] = ImageGraph
Plugins[] = Annotations
Plugins[] = MobileMessaging
Plugins[] = Overlay
Plugins[] = SegmentEditor
Plugins[] = Insights
Plugins[] = Morpheus
Plugins[] = Contents
Plugins[] = BulkTracking
Plugins[] = Resolution
Plugins[] = DevicePlugins
Plugins[] = Heartbeat
Plugins[] = Intl
Plugins[] = Marketplace
Plugins[] = ProfessionalServices
Plugins[] = UserId
Plugins[] = CustomPiwikJs
[PluginsInstalled]
PluginsInstalled[] = Diagnostics
PluginsInstalled[] = Login
PluginsInstalled[] = CoreAdminHome
PluginsInstalled[] = UsersManager
PluginsInstalled[] = SitesManager
PluginsInstalled[] = Installation
PluginsInstalled[] = Monolog
PluginsInstalled[] = Intl
[APISettings]
; Any key/value pair can be added in this section, they will be available via the REST call
; index.php?module=API&method=API.getSettings
; This can be used to expose values from Matomo, to control for example a Mobile app tracking
SDK_batch_size = 10
SDK_interval_value = 30
; NOTE: do not directly edit this file! See notice at the top

View File

@ -0,0 +1,223 @@
<?php
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\NotFoundException;
use Piwik\Cache\Eager;
use Piwik\SettingsServer;
use Piwik\Config;
return array(
'path.root' => PIWIK_USER_PATH,
'path.tmp' => function (ContainerInterface $c) {
$root = $c->get('path.root');
// TODO remove that special case and instead have plugins override 'path.tmp' to add the instance id
if ($c->has('ini.General.instance_id')) {
$instanceId = $c->get('ini.General.instance_id');
$instanceId = $instanceId ? '/' . $instanceId : '';
} else {
$instanceId = '';
}
/** @var Piwik\Config\ $config */
$config = $c->get('Piwik\Config');
$general = $config->General;
$tmp = empty($general['tmp_path']) ? '/tmp' : $general['tmp_path'];
return $root . $tmp . $instanceId;
},
'path.cache' => DI\string('{path.tmp}/cache/tracker/'),
'Piwik\Cache\Eager' => function (ContainerInterface $c) {
$backend = $c->get('Piwik\Cache\Backend');
$cacheId = $c->get('cache.eager.cache_id');
if (SettingsServer::isTrackerApiRequest()) {
$eventToPersist = 'Tracker.end';
$cacheId .= 'tracker';
} else {
$eventToPersist = 'Request.dispatch.end';
$cacheId .= 'ui';
}
$cache = new Eager($backend, $cacheId);
\Piwik\Piwik::addAction($eventToPersist, function () use ($cache) {
$cache->persistCacheIfNeeded(43200);
});
return $cache;
},
'Piwik\Cache\Backend' => function (ContainerInterface $c) {
// If Piwik is not installed yet, it's possible the tmp/ folder is not writable
// we prevent failing with an unclear message eg. coming from doctrine-cache
// by forcing to use a cache backend which always works ie. array
if(!\Piwik\SettingsPiwik::isPiwikInstalled()) {
$backend = 'array';
} else {
try {
$backend = $c->get('ini.Cache.backend');
} catch (NotFoundException $ex) {
$backend = 'chained'; // happens if global.ini.php is not available
}
}
return \Piwik\Cache::buildBackend($backend);
},
'cache.eager.cache_id' => function () {
return 'eagercache-' . str_replace(array('.', '-'), '', \Piwik\Version::VERSION) . '-';
},
'entities.idNames' => DI\add(array('idGoal', 'idDimension')),
'Psr\Log\LoggerInterface' => DI\object('Psr\Log\NullLogger'),
'Piwik\Translation\Loader\LoaderInterface' => DI\object('Piwik\Translation\Loader\LoaderCache')
->constructor(DI\get('Piwik\Translation\Loader\JsonFileLoader')),
'observers.global' => array(),
/**
* By setting this option to false, the check that the DB schema version matches the version of the source code will be no longer performed.
* Thus it allows you to execute for example a newer version of Matomo with an older Matomo database version. Please note
* disabling this setting is not recommended because often an older DB version is not compatible with newer source code.
* If you disable this setting, make sure to execute the updates after updating the source code. The setting can be useful if
* you want to update Matomo without any outage when you know the current source code update will still run fine for a short time
* while in the background the database updates are running.
*/
'EnableDbVersionCheck' => true,
'fileintegrity.ignore' => DI\add(array(
'*.htaccess',
'*web.config',
'bootstrap.php',
'favicon.ico',
'robots.txt',
'.bowerrc',
'.lfsconfig',
'.phpstorm.meta.php',
'config/config.ini.php',
'config/config.php',
'config/common.ini.php',
'config/*.config.ini.php',
'config/manifest.inc.php',
'misc/*.dat',
'misc/*.dat.gz',
'misc/*.mmdb',
'misc/*.mmdb.gz',
'misc/*.bin',
'misc/user/*png',
'misc/user/*svg',
'misc/user/*js',
'misc/user/*/config.ini.php',
'misc/package',
'misc/package/WebAppGallery/*.xml',
'misc/package/WebAppGallery/install.sql',
'plugins/ImageGraph/fonts/unifont.ttf',
'vendor/autoload.php',
'vendor/composer/autoload_real.php',
'vendor/szymach/c-pchart/app/*',
'tmp/*',
// Search engine sites verification
'google*.html',
'BingSiteAuth.xml',
'yandex*.html',
// common files on shared hosters
'php.ini',
'.user.ini',
// Files below are not expected but they used to be present in older Piwik versions and may be still here
// As they are not going to cause any trouble we won't report them as 'File to delete'
'*.coveralls.yml',
'*.scrutinizer.yml',
'*.gitignore',
'*.gitkeep',
'*.gitmodules',
'*.gitattributes',
'*.bower.json',
'*.travis.yml',
)),
'Piwik\EventDispatcher' => DI\object()->constructorParameter('observers', DI\get('observers.global')),
'login.whitelist.ips' => function (ContainerInterface $c) {
/** @var Piwik\Config\ $config */
$config = $c->get('Piwik\Config');
$general = $config->General;
$ips = array();
if (!empty($general['login_whitelist_ip']) && is_array($general['login_whitelist_ip'])) {
$ips = $general['login_whitelist_ip'];
}
$ipsResolved = array();
foreach ($ips as $ip) {
if (filter_var($ip, FILTER_VALIDATE_IP)) {
$ipsResolved[] = $ip;
} else {
$ipFromHost = @gethostbyname($ip);
if (!empty($ipFromHost)) {
$ipsResolved[] = $ipFromHost;
}
}
}
return $ipsResolved;
},
'Zend_Mail_Transport_Abstract' => function () {
$mailConfig = Config::getInstance()->mail;
if (empty($mailConfig['host'])
|| $mailConfig['transport'] != 'smtp'
) {
return;
}
$smtpConfig = array();
if (!empty($mailConfig['type'])) {
$smtpConfig['auth'] = strtolower($mailConfig['type']);
}
if (!empty($mailConfig['username'])) {
$smtpConfig['username'] = $mailConfig['username'];
}
if (!empty($mailConfig['password'])) {
$smtpConfig['password'] = $mailConfig['password'];
}
if (!empty($mailConfig['encryption'])) {
$smtpConfig['ssl'] = $mailConfig['encryption'];
}
if (!empty($mailConfig['port'])) {
$smtpConfig['port'] = $mailConfig['port'];
}
$host = trim($mailConfig['host']);
$transport = new \Zend_Mail_Transport_Smtp($host, $smtpConfig);
return $transport;
},
'Zend_Validate_EmailAddress' => function () {
return new \Zend_Validate_EmailAddress(array(
'hostname' => new \Zend_Validate_Hostname(array(
'tld' => false,
))));
},
'Piwik\Tracker\VisitorRecognizer' => DI\object()
->constructorParameter('trustCookiesOnly', DI\get('ini.Tracker.trust_visitors_cookies'))
->constructorParameter('visitStandardLength', DI\get('ini.Tracker.visit_standard_length'))
->constructorParameter('lookbackNSecondsCustom', DI\get('ini.Tracker.window_look_back_for_visitor'))
->constructorParameter('trackerAlwaysNewVisitor', DI\get('ini.Debug.tracker_always_new_visitor')),
'Piwik\Tracker\Settings' => DI\object()
->constructorParameter('isSameFingerprintsAcrossWebsites', DI\get('ini.Tracker.enable_fingerprinting_across_websites')),
'archiving.performance.logger' => null,
\Piwik\CronArchive\Performance\Logger::class => DI\object()->constructorParameter('logger', DI\get('archiving.performance.logger')),
);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
#!/usr/bin/env php
<?php
if (!defined('PIWIK_DOCUMENT_ROOT')) {
define('PIWIK_DOCUMENT_ROOT', dirname(__FILE__) == '/' ? '' : dirname(__FILE__));
}
if (file_exists(PIWIK_DOCUMENT_ROOT . '/bootstrap.php')) {
require_once PIWIK_DOCUMENT_ROOT . '/bootstrap.php';
}
if (!defined('PIWIK_INCLUDE_PATH')) {
define('PIWIK_INCLUDE_PATH', PIWIK_DOCUMENT_ROOT);
}
require_once PIWIK_INCLUDE_PATH . '/core/bootstrap.php';
if (!Piwik\Common::isPhpCliMode()) {
exit;
}
if (!defined('PIWIK_ENABLE_ERROR_HANDLER') || PIWIK_ENABLE_ERROR_HANDLER) {
Piwik\ErrorHandler::registerErrorHandler();
Piwik\ExceptionHandler::setUp();
}
$console = new Piwik\Console();
$console->run();

View File

@ -0,0 +1,143 @@
<?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\API;
use Exception;
use Piwik\Common;
use Piwik\DataTable\Renderer;
use Piwik\DataTable;
use Piwik\Piwik;
use Piwik\Plugin;
/**
* API renderer
*/
abstract class ApiRenderer
{
protected $request;
final public function __construct($request)
{
$this->request = $request;
$this->init();
}
protected function init()
{
}
abstract public function sendHeader();
public function renderSuccess($message)
{
return 'Success:' . $message;
}
/**
* @param $message
* @param Exception|\Throwable $exception
* @return mixed
*/
public function renderException($message, $exception)
{
return $message;
}
public function renderScalar($scalar)
{
$dataTable = new DataTable\Simple();
$dataTable->addRowsFromArray(array($scalar));
return $this->renderDataTable($dataTable);
}
public function renderDataTable($dataTable)
{
$renderer = $this->buildDataTableRenderer($dataTable);
return $renderer->render();
}
public function renderArray($array)
{
$renderer = $this->buildDataTableRenderer($array);
return $renderer->render();
}
public function renderObject($object)
{
$exception = new Exception('The API cannot handle this data structure.');
return $this->renderException($exception->getMessage(), $exception);
}
public function renderResource($resource)
{
$exception = new Exception('The API cannot handle this data structure.');
return $this->renderException($exception->getMessage(), $exception);
}
/**
* @param $dataTable
* @return Renderer
*/
protected function buildDataTableRenderer($dataTable)
{
$format = self::getFormatFromClass(get_class($this));
if ($format == 'json2') {
$format = 'json';
}
$idSite = Common::getRequestVar('idSite', 0, 'int', $this->request);
if (empty($idSite)) {
$idSite = 'all';
}
$renderer = Renderer::factory($format);
$renderer->setTable($dataTable);
$renderer->setIdSite($idSite);
$renderer->setRenderSubTables(Common::getRequestVar('expanded', false, 'int', $this->request));
$renderer->setHideIdSubDatableFromResponse(Common::getRequestVar('hideIdSubDatable', false, 'int', $this->request));
return $renderer;
}
/**
* @param string $format
* @param array $request
* @return ApiRenderer
* @throws Exception
*/
public static function factory($format, $request)
{
$formatToCheck = '\\' . ucfirst(strtolower($format));
$rendererClassnames = Plugin\Manager::getInstance()->findMultipleComponents('Renderer', 'Piwik\\API\\ApiRenderer');
foreach ($rendererClassnames as $klassName) {
if (Common::stringEndsWith($klassName, $formatToCheck)) {
return new $klassName($request);
}
}
$availableRenderers = array();
foreach ($rendererClassnames as $rendererClassname) {
$availableRenderers[] = self::getFormatFromClass($rendererClassname);
}
$availableRenderers = implode(', ', $availableRenderers);
Common::sendHeader('Content-Type: text/plain; charset=utf-8');
throw new Exception(Piwik::translate('General_ExceptionInvalidRendererFormat', array($format, $availableRenderers)));
}
private static function getFormatFromClass($klassname)
{
$klass = explode('\\', $klassname);
return strtolower(end($klass));
}
}

View File

@ -0,0 +1,57 @@
<?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\API;
use Piwik\Common;
use Piwik\Url;
class CORSHandler
{
/**
* @var array
*/
protected $domains;
public function __construct()
{
$this->domains = Url::getCorsHostsFromConfig();
}
public function handle()
{
if (empty($this->domains)) {
return;
}
Common::sendHeader('Vary: Origin');
// allow Piwik to serve data to all domains
if (in_array("*", $this->domains)) {
Common::sendHeader('Access-Control-Allow-Credentials: true');
if (!empty($_SERVER['HTTP_ORIGIN'])) {
Common::sendHeader('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
return;
}
Common::sendHeader('Access-Control-Allow-Origin: *');
return;
}
// specifically allow if it is one of the whitelisted CORS domains
if (!empty($_SERVER['HTTP_ORIGIN'])) {
$origin = $_SERVER['HTTP_ORIGIN'];
if (in_array($origin, $this->domains, true)) {
Common::sendHeader('Access-Control-Allow-Credentials: true');
Common::sendHeader('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
}
}
}
}

View File

@ -0,0 +1,245 @@
<?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\API;
use Exception;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Plugin\ProcessedMetric;
use Piwik\Plugin\Report;
class DataTableGenericFilter
{
/**
* List of filter names not to run.
*
* @var string[]
*/
private $disabledFilters = array();
/**
* @var Report
*/
private $report;
/**
* @var array
*/
private $request;
/**
* Constructor
*
* @param $request
*/
public function __construct($request, $report)
{
$this->request = $request;
$this->report = $report;
}
/**
* Filters the given data table
*
* @param DataTable $table
*/
public function filter($table)
{
$this->applyGenericFilters($table);
}
/**
* Makes sure a set of filters are not run.
*
* @param string[] $filterNames The name of each filter to disable.
*/
public function disableFilters($filterNames)
{
$this->disabledFilters = array_unique(array_merge($this->disabledFilters, $filterNames));
}
/**
* Returns an array containing the information of the generic Filter
* to be applied automatically to the data resulting from the API calls.
*
* Order to apply the filters:
* 1 - Filter that remove filtered rows
* 2 - Filter that sort the remaining rows
* 3 - Filter that keep only a subset of the results
* 4 - Presentation filters
*
* @return array See the code for spec
*/
public static function getGenericFiltersInformation()
{
return array(
array('Pattern',
array(
'filter_column' => array('string', 'label'),
'filter_pattern' => array('string')
)),
array('PatternRecursive',
array(
'filter_column_recursive' => array('string', 'label'),
'filter_pattern_recursive' => array('string'),
)),
array('ExcludeLowPopulation',
array(
'filter_excludelowpop' => array('string'),
'filter_excludelowpop_value' => array('float', '0'),
)),
array('Sort',
array(
'filter_sort_column' => array('string'),
'filter_sort_order' => array('string', 'desc'),
$naturalSort = true,
$recursiveSort = true,
'filter_sort_column_secondary' => true
)),
array('Truncate',
array(
'filter_truncate' => array('integer'),
)),
array('Limit',
array(
'filter_offset' => array('integer', '0'),
'filter_limit' => array('integer'),
'keep_summary_row' => array('integer', '0'),
))
);
}
private function getGenericFiltersHavingDefaultValues()
{
$filters = self::getGenericFiltersInformation();
if ($this->report && $this->report->getDefaultSortColumn()) {
foreach ($filters as $index => $filter) {
if ($filter[0] === 'Sort') {
$filters[$index][1]['filter_sort_column'] = array('string', $this->report->getDefaultSortColumn());
$filters[$index][1]['filter_sort_order'] = array('string', $this->report->getDefaultSortOrder());
$callback = $this->report->getSecondarySortColumnCallback();
if (is_callable($callback)) {
$filters[$index][1]['filter_sort_column_secondary'] = $callback;
}
}
}
}
return $filters;
}
/**
* Apply generic filters to the DataTable object resulting from the API Call.
* Disable this feature by setting the parameter disable_generic_filters to 1 in the API call request.
*
* @param DataTable $datatable
* @return bool
*/
protected function applyGenericFilters($datatable)
{
if ($datatable instanceof DataTable\Map) {
$tables = $datatable->getDataTables();
foreach ($tables as $table) {
$this->applyGenericFilters($table);
}
return;
}
$tableDisabledFilters = $datatable->getMetadata(DataTable::GENERIC_FILTERS_TO_DISABLE_METADATA_NAME) ?: [];
$genericFilters = $this->getGenericFiltersHavingDefaultValues();
$filterApplied = false;
foreach ($genericFilters as $filterMeta) {
$filterName = $filterMeta[0];
$filterParams = $filterMeta[1];
$filterParameters = array();
$exceptionRaised = false;
if (in_array($filterName, $this->disabledFilters)
|| in_array($filterName, $tableDisabledFilters)
) {
continue;
}
foreach ($filterParams as $name => $info) {
if (!is_array($info)) {
// hard coded value that cannot be changed via API, see eg $naturalSort = true in 'Sort'
$filterParameters[] = $info;
} else {
// parameter type to cast to
$type = $info[0];
// default value if specified, when the parameter doesn't have a value
$defaultValue = null;
if (isset($info[1])) {
$defaultValue = $info[1];
}
try {
$value = Common::getRequestVar($name, $defaultValue, $type, $this->request);
settype($value, $type);
$filterParameters[] = $value;
} catch (Exception $e) {
$exceptionRaised = true;
break;
}
}
}
if (!$exceptionRaised) {
$datatable->filter($filterName, $filterParameters);
$filterApplied = true;
}
}
return $filterApplied;
}
public function areProcessedMetricsNeededFor($metrics)
{
$columnQueryParameters = array(
'filter_column',
'filter_column_recursive',
'filter_excludelowpop',
'filter_sort_column'
);
foreach ($columnQueryParameters as $queryParamName) {
$queryParamValue = Common::getRequestVar($queryParamName, false, $type = null, $this->request);
if (!empty($queryParamValue)
&& $this->containsProcessedMetric($metrics, $queryParamValue)
) {
return true;
}
}
return false;
}
/**
* @param ProcessedMetric[] $metrics
* @param string $name
* @return bool
*/
private function containsProcessedMetric($metrics, $name)
{
foreach ($metrics as $metric) {
if ($metric instanceof ProcessedMetric
&& $metric->getName() == $name
) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,207 @@
<?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\API;
use Exception;
use Piwik\Archive\DataTableFactory;
use Piwik\Container\StaticContainer;
use Piwik\DataTable\Row;
use Piwik\DataTable;
use Piwik\Period\Range;
use Piwik\Plugins\API\API;
/**
* Base class for manipulating data tables.
* It provides generic mechanisms like iteration and loading subtables.
*
* The manipulators are used in ResponseBuilder and are triggered by
* API parameters. They are not filters because they don't work on the pre-
* fetched nested data tables. Instead, they load subtables using this base
* class. This way, they can only load the tables they really need instead
* of using expanded=1. Another difference between manipulators and filters
* is that filters keep the overall structure of the table intact while
* manipulators can change the entire thing.
*/
abstract class DataTableManipulator
{
protected $apiModule;
protected $apiMethod;
protected $request;
protected $apiMethodForSubtable;
/**
* Constructor
*
* @param bool $apiModule
* @param bool $apiMethod
* @param array $request
*/
public function __construct($apiModule = false, $apiMethod = false, $request = array())
{
$this->apiModule = $apiModule;
$this->apiMethod = $apiMethod;
$this->request = $request;
}
/**
* This method can be used by subclasses to iterate over data tables that might be
* data table maps. It calls back the template method self::doManipulate for each table.
* This way, data table arrays can be handled in a transparent fashion.
*
* @param DataTable\Map|DataTable $dataTable
* @throws Exception
* @return DataTable\Map|DataTable
*/
protected function manipulate($dataTable)
{
if ($dataTable instanceof DataTable\Map) {
return $this->manipulateDataTableMap($dataTable);
} elseif ($dataTable instanceof DataTable) {
return $this->manipulateDataTable($dataTable);
} else {
return $dataTable;
}
}
/**
* Manipulates child DataTables of a DataTable\Map. See @manipulate for more info.
*
* @param DataTable\Map $dataTable
* @return DataTable\Map
*/
protected function manipulateDataTableMap($dataTable)
{
$result = $dataTable->getEmptyClone();
foreach ($dataTable->getDataTables() as $tableLabel => $childTable) {
$newTable = $this->manipulate($childTable);
$result->addTable($newTable, $tableLabel);
}
return $result;
}
/**
* Manipulates a single DataTable instance. Derived classes must define
* this function.
*/
abstract protected function manipulateDataTable($dataTable);
/**
* Load the subtable for a row.
* Returns null if none is found.
*
* @param DataTable $dataTable
* @param Row $row
*
* @return DataTable
*/
protected function loadSubtable($dataTable, $row)
{
if (!($this->apiModule && $this->apiMethod && count($this->request))) {
return null;
}
$request = $this->request;
$idSubTable = $row->getIdSubDataTable();
if ($idSubTable === null) {
return null;
}
$request['idSubtable'] = $idSubTable;
if ($dataTable) {
$period = $dataTable->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX);
if ($period instanceof Range) {
$request['date'] = $period->getDateStart() . ',' . $period->getDateEnd();
} else {
$request['date'] = $period->getDateStart()->toString();
}
}
$method = $this->getApiMethodForSubtable($request);
return $this->callApiAndReturnDataTable($this->apiModule, $method, $request);
}
/**
* In this method, subclasses can clean up the request array for loading subtables
* in order to make ResponseBuilder behave correctly (e.g. not trigger the
* manipulator again).
*
* @param $request
* @return
*/
abstract protected function manipulateSubtableRequest($request);
/**
* Extract the API method for loading subtables from the meta data
*
* @throws Exception
* @return string
*/
protected function getApiMethodForSubtable($request)
{
if (!$this->apiMethodForSubtable) {
if (!empty($request['idSite'])) {
$idSite = $request['idSite'];
} else {
$idSite = 'all';
}
$apiParameters = array();
$entityNames = StaticContainer::get('entities.idNames');
foreach ($entityNames as $idName) {
if (!empty($request[$idName])) {
$apiParameters[$idName] = $request[$idName];
}
}
$meta = API::getInstance()->getMetadata($idSite, $this->apiModule, $this->apiMethod, $apiParameters);
if (empty($meta) && array_key_exists('idGoal', $apiParameters)) {
unset($apiParameters['idGoal']);
$meta = API::getInstance()->getMetadata($idSite, $this->apiModule, $this->apiMethod, $apiParameters);
}
if (empty($meta)) {
throw new Exception(sprintf(
"The DataTable cannot be manipulated: Metadata for report %s.%s could not be found. You can define the metadata in a hook, see example at: https://developer.matomo.org/api-reference/events#apigetreportmetadata",
$this->apiModule, $this->apiMethod
));
}
if (isset($meta[0]['actionToLoadSubTables'])) {
$this->apiMethodForSubtable = $meta[0]['actionToLoadSubTables'];
} else {
$this->apiMethodForSubtable = $this->apiMethod;
}
}
return $this->apiMethodForSubtable;
}
protected function callApiAndReturnDataTable($apiModule, $method, $request)
{
$class = Request::getClassNameAPI($apiModule);
$request = $this->manipulateSubtableRequest($request);
$request['serialize'] = 0;
$request['expanded'] = 0;
$request['format'] = 'original';
$request['format_metrics'] = 0;
// don't want to run recursive filters on the subtables as they are loaded,
// otherwise the result will be empty in places (or everywhere). instead we
// run it on the flattened table.
unset($request['filter_pattern_recursive']);
$dataTable = Proxy::getInstance()->call($class, $method, $request);
$response = new ResponseBuilder($format = 'original', $request);
$response->disableSendHeader();
$dataTable = $response->getResponse($dataTable, $apiModule, $method);
return $dataTable;
}
}

View File

@ -0,0 +1,209 @@
<?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\API\DataTableManipulator;
use Piwik\API\DataTableManipulator;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\Plugin\ReportsProvider;
/**
* This class is responsible for flattening data tables.
*
* It loads subtables and combines them into a single table by concatenating the labels.
* This manipulator is triggered by using flat=1 in the API request.
*/
class Flattener extends DataTableManipulator
{
private $includeAggregateRows = false;
/**
* If the flattener is used after calling this method, aggregate rows will
* be included in the result. This can be useful when they contain data that
* the leafs don't have (e.g. conversion stats in some cases).
*/
public function includeAggregateRows()
{
$this->includeAggregateRows = true;
}
/**
* Separator for building recursive labels (or paths)
* @var string
*/
public $recursiveLabelSeparator = '';
/**
* @param DataTable $dataTable
* @param string $recursiveLabelSeparator
* @return DataTable|DataTable\Map
*/
public function flatten($dataTable, $recursiveLabelSeparator)
{
$this->recursiveLabelSeparator = $recursiveLabelSeparator;
return $this->manipulate($dataTable);
}
/**
* Template method called from self::manipulate.
* Flatten each data table.
*
* @param DataTable $dataTable
* @return DataTable
*/
protected function manipulateDataTable($dataTable)
{
$newDataTable = $dataTable->getEmptyClone($keepFilters = true);
if ($dataTable->getTotalsRow()) {
$newDataTable->setTotalsRow($dataTable->getTotalsRow());
}
// this recursive filter will be applied to subtables
$dataTable->filter('ReplaceSummaryRowLabel');
$dataTable->filter('ReplaceColumnNames');
$report = ReportsProvider::factory($this->apiModule, $this->apiMethod);
if (!empty($report)) {
$dimension = $report->getDimension();
}
$dimensionName = !empty($dimension) ? str_replace('.', '_', $dimension->getId()) : 'label1';
$this->flattenDataTableInto($dataTable, $newDataTable, $level = 1, $dimensionName);
return $newDataTable;
}
/**
* @param $dataTable DataTable
* @param $newDataTable
* @param $dimensionName
*/
protected function flattenDataTableInto($dataTable, $newDataTable, $level, $dimensionName, $prefix = '', $logo = false)
{
foreach ($dataTable->getRows() as $rowId => $row) {
$this->flattenRow($row, $rowId, $newDataTable, $level, $dimensionName, $prefix, $logo);
}
}
/**
* @param Row $row
* @param DataTable $dataTable
* @param string $labelPrefix
* @param string $dimensionName
* @param bool $parentLogo
*/
private function flattenRow
(Row $row, $rowId, DataTable $dataTable, $level, $dimensionName,
$labelPrefix = '', $parentLogo = false)
{
$origLabel = $label = $row->getColumn('label');
if ($label !== false) {
$label = trim($label);
if ($this->recursiveLabelSeparator == '/') {
if (substr($label, 0, 1) == '/' && substr($labelPrefix, -1) == '/') {
$label = substr($label, 1);
} elseif ($rowId === DataTable::ID_SUMMARY_ROW && $labelPrefix && $label != DataTable::LABEL_SUMMARY_ROW) {
$label = ' - ' . $label;
}
}
$origLabel = $label;
$label = $labelPrefix . $label;
$row->setColumn('label', $label);
if ($row->getMetadata($dimensionName)) {
$origLabel = $row->getMetadata($dimensionName) . $this->recursiveLabelSeparator . $origLabel;
}
$row->setMetadata($dimensionName, $origLabel);
}
$logo = $row->getMetadata('logo');
if ($logo === false && $parentLogo !== false) {
$logo = $parentLogo;
$row->setMetadata('logo', $logo);
}
/** @var DataTable $subTable */
$subTable = $row->getSubtable();
if ($subTable) {
$subTable->applyQueuedFilters();
$row->deleteMetadata('idsubdatatable_in_db');
} else {
$subTable = $this->loadSubtable($dataTable, $row);
}
$row->removeSubtable();
if ($subTable === null) {
if ($this->includeAggregateRows) {
$row->setMetadata('is_aggregate', 0);
}
$dataTable->addRow($row);
} else {
if ($this->includeAggregateRows) {
$row->setMetadata('is_aggregate', 1);
$dataTable->addRow($row);
}
$prefix = $label . $this->recursiveLabelSeparator;
$report = ReportsProvider::factory($this->apiModule, $this->apiMethod);
if (!empty($report)) {
$subDimension = $report->getSubtableDimension();
}
if ($level === 2) {
$subDimension = $report->getThirdLeveltableDimension();
}
if (empty($subDimension)) {
$report = ReportsProvider::factory($this->apiModule, $this->getApiMethodForSubtable($this->request));
$subDimension = $report->getDimension();
}
$subDimensionName = $subDimension ? str_replace('.', '_', $subDimension->getId()) : 'label' . (substr_count($prefix, $this->recursiveLabelSeparator) + 1);
if ($origLabel !== false) {
foreach ($subTable->getRows() as $subRow) {
foreach ($row->getMetadata() as $name => $value) {
if ($subRow->getMetadata($name) === false) {
$subRow->setMetadata($name, $value);
}
}
$subRow->setMetadata($dimensionName, $origLabel);
}
}
$this->flattenDataTableInto($subTable, $dataTable, $level + 1, $subDimensionName, $prefix, $logo);
}
}
/**
* Remove the flat parameter from the subtable request
*
* @param array $request
* @return array
*/
protected function manipulateSubtableRequest($request)
{
unset($request['flat']);
return $request;
}
}

View File

@ -0,0 +1,188 @@
<?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\API\DataTableManipulator;
use Piwik\API\DataTableManipulator;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\DataTable\Row;
/**
* This class is responsible for handling the label parameter that can be
* added to every API call. If the parameter is set, only the row with the matching
* label is returned.
*
* The labels passed to this class should be urlencoded.
* Some reports use recursive labels (e.g. action reports). Use > to join them.
*/
class LabelFilter extends DataTableManipulator
{
const SEPARATOR_RECURSIVE_LABEL = '>';
const TERMINAL_OPERATOR = '@';
private $labels;
private $addLabelIndex;
const FLAG_IS_ROW_EVOLUTION = 'label_index';
/**
* Filter a data table by label.
* The filtered table is returned, which might be a new instance.
*
* $apiModule, $apiMethod and $request are needed load sub-datatables
* for the recursive search. If the label is not recursive, these parameters
* are not needed.
*
* @param string $labels the labels to search for
* @param DataTable $dataTable the data table to be filtered
* @param bool $addLabelIndex Whether to add label_index metadata describing which
* label a row corresponds to.
* @return DataTable
*/
public function filter($labels, $dataTable, $addLabelIndex = false)
{
if (!is_array($labels)) {
$labels = array($labels);
}
$this->labels = $labels;
$this->addLabelIndex = (bool)$addLabelIndex;
return $this->manipulate($dataTable);
}
/**
* Method for the recursive descend
*
* @param array $labelParts
* @param DataTable $dataTable
* @return Row|bool
*/
private function doFilterRecursiveDescend($labelParts, $dataTable)
{
// we need to make sure to rebuild the index as some filters change the label column directly via
// $row->setColumn('label', '') which would not be noticed in the label index otherwise.
$dataTable->rebuildIndex();
// search for the first part of the tree search
$labelPart = array_shift($labelParts);
$row = false;
foreach ($this->getLabelVariations($labelPart) as $labelPart) {
$row = $dataTable->getRowFromLabel($labelPart);
if ($row !== false) {
break;
}
}
if ($row === false) {
// not found
return false;
}
// end of tree search reached
if (count($labelParts) == 0) {
return $row;
}
$subTable = $this->loadSubtable($dataTable, $row);
if ($subTable === null) {
// no more subtables but label parts left => no match found
return false;
}
return $this->doFilterRecursiveDescend($labelParts, $subTable);
}
/**
* Clean up request for ResponseBuilder to behave correctly
*
* @param $request
*/
protected function manipulateSubtableRequest($request)
{
unset($request['label']);
unset($request['flat']);
$request['totals'] = 0;
$request['filter_sort_column'] = ''; // do not sort, we only want to find a matching column
return $request;
}
/**
* Use variations of the label to make it easier to specify the desired label
*
* Note: The HTML Encoded version must be tried first, since in ResponseBuilder the $label is unsanitized
* via Common::unsanitizeLabelParameter.
*
* @param string $originalLabel
* @return array
*/
private function getLabelVariations($originalLabel)
{
static $pageTitleReports = array('getPageTitles', 'getEntryPageTitles', 'getExitPageTitles');
$originalLabel = trim($originalLabel);
$isTerminal = substr($originalLabel, 0, 1) == self::TERMINAL_OPERATOR;
if ($isTerminal) {
$originalLabel = substr($originalLabel, 1);
}
$variations = array();
$label = trim(urldecode($originalLabel));
$sanitizedLabel = Common::sanitizeInputValue($label);
$variations[] = $sanitizedLabel;
if ($this->apiModule == 'Actions'
&& in_array($this->apiMethod, $pageTitleReports)
) {
if ($isTerminal) {
array_unshift($variations, ' ' . $sanitizedLabel);
array_unshift($variations, ' ' . $label);
} else {
// special case: the Actions.getPageTitles report prefixes some labels with a blank.
// the blank might be passed by the user but is removed in Request::getRequestArrayFromString.
$variations[] = ' ' . $sanitizedLabel;
$variations[] = ' ' . $label;
}
}
$variations[] = $label;
$variations = array_unique($variations);
return $variations;
}
/**
* Filter a DataTable instance. See @filter for more info.
*
* @param DataTable\Simple|DataTable\Map $dataTable
* @return mixed
*/
protected function manipulateDataTable($dataTable)
{
$result = $dataTable->getEmptyClone();
foreach ($this->labels as $labelIndex => $label) {
$row = null;
foreach ($this->getLabelVariations($label) as $labelVariation) {
$labelVariation = explode(self::SEPARATOR_RECURSIVE_LABEL, $labelVariation);
$row = $this->doFilterRecursiveDescend($labelVariation, $dataTable);
if ($row) {
if ($this->addLabelIndex) {
$row->setMetadata(self::FLAG_IS_ROW_EVOLUTION, $labelIndex);
}
$result->addRow($row);
break;
}
}
}
return $result;
}
}

View File

@ -0,0 +1,249 @@
<?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\API\DataTableManipulator;
use Piwik\API\DataTableManipulator;
use Piwik\API\DataTablePostProcessor;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Metrics;
use Piwik\Period;
use Piwik\Piwik;
use Piwik\Plugin\Report;
use Piwik\Plugin\ReportsProvider;
/**
* This class is responsible for setting the metadata property 'totals' on each dataTable if the report
* has a dimension. 'Totals' means it tries to calculate the total report value for each metric. For each
* the total number of visits, actions, ... for a given report / dataTable.
*/
class ReportTotalsCalculator extends DataTableManipulator
{
/**
* @var Report
*/
private $report;
/**
* Constructor
*
* @param bool $apiModule
* @param bool $apiMethod
* @param array $request
* @param Report $report
*/
public function __construct($apiModule = false, $apiMethod = false, $request = array(), $report = null)
{
parent::__construct($apiModule, $apiMethod, $request);
$this->report = $report;
}
/**
* @param DataTable $table
* @return \Piwik\DataTable|\Piwik\DataTable\Map
*/
public function calculate($table)
{
// apiModule and/or apiMethod is empty for instance in case when flat=1 is called. Basically whenever a
// datamanipulator calls the API and wants the dataTable in return, see callApiAndReturnDataTable().
// it is also not set for some settings API request etc.
if (empty($this->apiModule) || empty($this->apiMethod)) {
return $table;
}
try {
return $this->manipulate($table);
} catch (\Exception $e) {
// eg. requests with idSubtable may trigger this exception
// (where idSubtable was removed in
// ?module=API&method=Events.getNameFromCategoryId&idSubtable=1&secondaryDimension=eventName&format=XML&idSite=1&period=day&date=yesterday&flat=0
return $table;
}
}
/**
* Adds ratio metrics if possible.
*
* @param DataTable $dataTable
* @return DataTable
*/
protected function manipulateDataTable($dataTable)
{
if (!empty($this->report) && !$this->report->getDimension() && !$this->isAllMetricsReport()) {
// we currently do not calculate the total value for reports having no dimension
return $dataTable;
}
if (1 != Common::getRequestVar('totals', 1, 'integer', $this->request)) {
return $dataTable;
}
$firstLevelTable = $this->makeSureToWorkOnFirstLevelDataTable($dataTable);
if (!$firstLevelTable->getRowsCount()
|| $dataTable->getTotalsRow()
|| $dataTable->getMetadata('totals')
) {
return $dataTable;
}
// keeping queued filters would not only add various metadata but also break the totals calculator for some reports
// eg when needed metadata is missing to get site information (multisites.getall) etc
$clone = $firstLevelTable->getEmptyClone($keepFilters = false);
foreach ($firstLevelTable->getQueuedFilters() as $queuedFilter) {
if (is_array($queuedFilter) && 'ReplaceColumnNames' === $queuedFilter['className']) {
$clone->queueFilter($queuedFilter['className'], $queuedFilter['parameters']);
}
}
$tableMeta = $firstLevelTable->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME);
/** @var DataTable\Row $totalRow */
$totalRow = null;
foreach ($firstLevelTable->getRows() as $row) {
if (!isset($totalRow)) {
$columns = $row->getColumns();
$columns['label'] = DataTable::LABEL_TOTALS_ROW;
$totalRow = new DataTable\Row(array(DataTable\Row::COLUMNS => $columns));
} else {
$totalRow->sumRow($row, $copyMetadata = false, $tableMeta);
}
}
$clone->addRow($totalRow);
if ($this->report
&& $this->report->getProcessedMetrics()
&& array_keys($this->report->getProcessedMetrics()) === array('nb_actions_per_visit', 'avg_time_on_site', 'bounce_rate', 'conversion_rate')) {
// hack for AllColumns table or default processed metrics
$clone->filter('AddColumnsProcessedMetrics', array($deleteRowsWithNoVisit = false));
}
$processor = new DataTablePostProcessor($this->apiModule, $this->apiMethod, $this->request);
$processor->applyComputeProcessedMetrics($clone);
$clone = $processor->applyQueuedFilters($clone);
$clone = $processor->applyMetricsFormatting($clone);
$totalRow = null;
foreach ($clone->getRows() as $row) {
/** * @var DataTable\Row $row */
if ($row->getColumn('label') === DataTable::LABEL_TOTALS_ROW) {
$totalRow = $row;
break;
}
}
if (!isset($totalRow) && $clone->getRowsCount() === 1) {
// if for some reason the processor renamed the totals row,
$totalRow = $clone->getFirstRow();
}
if (isset($totalRow)) {
$totals = $row->getColumns();
unset($totals['label']);
$dataTable->setMetadata('totals', $totals);
if (1 === Common::getRequestVar('keep_totals_row', 0, 'integer', $this->request)) {
$row->deleteMetadata(false);
$row->setColumn('label', Piwik::translate('General_Totals'));
$dataTable->setTotalsRow($row);
}
}
return $dataTable;
}
private function makeSureToWorkOnFirstLevelDataTable($table)
{
if (!array_key_exists('idSubtable', $this->request)) {
return $table;
}
$firstLevelReport = $this->findFirstLevelReport();
if (empty($firstLevelReport)) {
// it is not a subtable report
$module = $this->apiModule;
$action = $this->apiMethod;
} else {
$module = $firstLevelReport->getModule();
$action = $firstLevelReport->getAction();
}
$request = $this->request;
unset($request['idSubtable']); // to make sure we work on first level table
/** @var \Piwik\Period $period */
$period = $table->getMetadata('period');
if (!empty($period)) {
// we want a dataTable, not a dataTable\map
if (Period::isMultiplePeriod($request['date'], $request['period']) || 'range' == $period->getLabel()) {
$request['date'] = $period->getRangeString();
$request['period'] = 'range';
} else {
$request['date'] = $period->getDateStart()->toString();
$request['period'] = $period->getLabel();
}
}
$table = $this->callApiAndReturnDataTable($module, $action, $request);
if ($table instanceof DataTable\Map) {
$table = $table->mergeChildren();
}
return $table;
}
/**
* Make sure to get all rows of the first level table.
*
* @param array $request
* @return array
*/
protected function manipulateSubtableRequest($request)
{
$request['totals'] = 0;
$request['expanded'] = 0;
$request['filter_limit'] = -1;
$request['filter_offset'] = 0;
$request['filter_sort_column'] = '';
$parametersToRemove = array('flat');
if (!array_key_exists('idSubtable', $this->request)) {
$parametersToRemove[] = 'idSubtable';
}
foreach ($parametersToRemove as $param) {
if (array_key_exists($param, $request)) {
unset($request[$param]);
}
}
return $request;
}
private function findFirstLevelReport()
{
$reports = new ReportsProvider();
foreach ($reports->getAllReports() as $report) {
$actionToLoadSubtables = $report->getActionToLoadSubTables();
if ($actionToLoadSubtables == $this->apiMethod
&& $this->apiModule == $report->getModule()
) {
return $report;
}
}
return null;
}
private function isAllMetricsReport()
{
return $this->report->getModule() == 'API' && $this->report->getAction() == 'get';
}
}

View File

@ -0,0 +1,459 @@
<?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\API;
use Exception;
use Piwik\API\DataTableManipulator\Flattener;
use Piwik\API\DataTableManipulator\LabelFilter;
use Piwik\API\DataTableManipulator\ReportTotalsCalculator;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\DataTable\DataTableInterface;
use Piwik\DataTable\Filter\PivotByDimension;
use Piwik\Metrics\Formatter;
use Piwik\Plugin\ProcessedMetric;
use Piwik\Plugin\Report;
use Piwik\Plugin\ReportsProvider;
/**
* Processes DataTables that should be served through Piwik's APIs. This processing handles
* special query parameters and computes processed metrics. It does not included rendering to
* output formats (eg, 'xml').
*/
class DataTablePostProcessor
{
const PROCESSED_METRICS_COMPUTED_FLAG = 'processed_metrics_computed';
/**
* @var null|Report
*/
private $report;
/**
* @var string[]
*/
private $request;
/**
* @var string
*/
private $apiModule;
/**
* @var string
*/
private $apiMethod;
/**
* @var Inconsistencies
*/
private $apiInconsistencies;
/**
* @var Formatter
*/
private $formatter;
private $callbackBeforeGenericFilters;
private $callbackAfterGenericFilters;
/**
* Constructor.
*/
public function __construct($apiModule, $apiMethod, $request)
{
$this->apiModule = $apiModule;
$this->apiMethod = $apiMethod;
$this->setRequest($request);
$this->report = ReportsProvider::factory($apiModule, $apiMethod);
$this->apiInconsistencies = new Inconsistencies();
$this->setFormatter(new Formatter());
}
public function setFormatter(Formatter $formatter)
{
$this->formatter = $formatter;
}
public function setRequest($request)
{
$this->request = $request;
}
public function setCallbackBeforeGenericFilters($callbackBeforeGenericFilters)
{
$this->callbackBeforeGenericFilters = $callbackBeforeGenericFilters;
}
public function setCallbackAfterGenericFilters($callbackAfterGenericFilters)
{
$this->callbackAfterGenericFilters = $callbackAfterGenericFilters;
}
/**
* Apply post-processing logic to a DataTable of a report for an API request.
*
* @param DataTableInterface $dataTable The data table to process.
* @return DataTableInterface A new data table.
*/
public function process(DataTableInterface $dataTable)
{
// TODO: when calculating metrics before hand, only calculate for needed metrics, not all. NOTE:
// this is non-trivial since it will require, eg, to make sure processed metrics aren't added
// after pivotBy is handled.
$dataTable = $this->applyPivotByFilter($dataTable);
$dataTable = $this->applyTotalsCalculator($dataTable);
$dataTable = $this->applyFlattener($dataTable);
if ($this->callbackBeforeGenericFilters) {
call_user_func($this->callbackBeforeGenericFilters, $dataTable);
}
$dataTable = $this->applyGenericFilters($dataTable);
$this->applyComputeProcessedMetrics($dataTable);
if ($this->callbackAfterGenericFilters) {
call_user_func($this->callbackAfterGenericFilters, $dataTable);
}
// we automatically safe decode all datatable labels (against xss)
$dataTable->queueFilter('SafeDecodeLabel');
$dataTable = $this->convertSegmentValueToSegment($dataTable);
$dataTable = $this->applyQueuedFilters($dataTable);
$dataTable = $this->applyRequestedColumnDeletion($dataTable);
$dataTable = $this->applyLabelFilter($dataTable);
$dataTable = $this->applyMetricsFormatting($dataTable);
return $dataTable;
}
private function convertSegmentValueToSegment(DataTableInterface $dataTable)
{
$dataTable->filter('AddSegmentBySegmentValue', array($this->report));
$dataTable->filter('ColumnCallbackDeleteMetadata', array('segmentValue'));
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
* @return DataTableInterface
*/
public function applyPivotByFilter(DataTableInterface $dataTable)
{
$pivotBy = Common::getRequestVar('pivotBy', false, 'string', $this->request);
if (!empty($pivotBy)) {
$this->applyComputeProcessedMetrics($dataTable);
$dataTable = $this->convertSegmentValueToSegment($dataTable);
$pivotByColumn = Common::getRequestVar('pivotByColumn', false, 'string', $this->request);
$pivotByColumnLimit = Common::getRequestVar('pivotByColumnLimit', false, 'int', $this->request);
$dataTable->filter('PivotByDimension', array($this->report, $pivotBy, $pivotByColumn, $pivotByColumnLimit,
PivotByDimension::isSegmentFetchingEnabledInConfig()));
$dataTable->filter('ColumnCallbackDeleteMetadata', array('segment'));
}
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
* @return DataTable|DataTableInterface|DataTable\Map
*/
public function applyFlattener($dataTable)
{
if (Common::getRequestVar('flat', '0', 'string', $this->request) == '1') {
// skip flattening if not supported by report and remove subtables only
if ($this->report && !$this->report->supportsFlatten()) {
$dataTable->filter('RemoveSubtables');
return $dataTable;
}
$flattener = new Flattener($this->apiModule, $this->apiMethod, $this->request);
if (Common::getRequestVar('include_aggregate_rows', '0', 'string', $this->request) == '1') {
$flattener->includeAggregateRows();
}
$recursiveLabelSeparator = ' - ';
if ($this->report) {
$recursiveLabelSeparator = $this->report->getRecursiveLabelSeparator();
}
$dataTable = $flattener->flatten($dataTable, $recursiveLabelSeparator);
}
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
* @return DataTableInterface
*/
public function applyTotalsCalculator($dataTable)
{
if (1 == Common::getRequestVar('totals', '1', 'integer', $this->request)) {
$calculator = new ReportTotalsCalculator($this->apiModule, $this->apiMethod, $this->request, $this->report);
$dataTable = $calculator->calculate($dataTable);
}
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
* @return DataTableInterface
*/
public function applyGenericFilters($dataTable)
{
// if the flag disable_generic_filters is defined we skip the generic filters
if (0 == Common::getRequestVar('disable_generic_filters', '0', 'string', $this->request)) {
$this->applyProcessedMetricsGenericFilters($dataTable);
$genericFilter = new DataTableGenericFilter($this->request, $this->report);
$self = $this;
$report = $this->report;
$dataTable->filter(function (DataTable $table) use ($genericFilter, $report, $self) {
$processedMetrics = Report::getProcessedMetricsForTable($table, $report);
if ($genericFilter->areProcessedMetricsNeededFor($processedMetrics)) {
$self->computeProcessedMetrics($table);
}
});
$label = self::getLabelFromRequest($this->request);
if (!empty($label)) {
$genericFilter->disableFilters(array('Limit', 'Truncate'));
}
$genericFilter->filter($dataTable);
}
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
* @return DataTableInterface
*/
public function applyProcessedMetricsGenericFilters($dataTable)
{
$addNormalProcessedMetrics = null;
try {
$addNormalProcessedMetrics = Common::getRequestVar(
'filter_add_columns_when_show_all_columns', null, 'integer', $this->request);
} catch (Exception $ex) {
// ignore
}
if ($addNormalProcessedMetrics !== null) {
$dataTable->filter('AddColumnsProcessedMetrics', array($addNormalProcessedMetrics));
}
$addGoalProcessedMetrics = null;
try {
$addGoalProcessedMetrics = Common::getRequestVar(
'filter_update_columns_when_show_all_goals', null, 'integer', $this->request);
} catch (Exception $ex) {
// ignore
}
if ($addGoalProcessedMetrics !== null) {
$idGoal = Common::getRequestVar(
'idGoal', DataTable\Filter\AddColumnsProcessedMetricsGoal::GOALS_OVERVIEW, 'string', $this->request);
$dataTable->filter('AddColumnsProcessedMetricsGoal', array($ignore = true, $idGoal));
}
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
* @return DataTableInterface
*/
public function applyQueuedFilters($dataTable)
{
// if the flag disable_queued_filters is defined we skip the filters that were queued
if (Common::getRequestVar('disable_queued_filters', 0, 'int', $this->request) == 0) {
$dataTable->applyQueuedFilters();
}
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
* @return DataTableInterface
*/
public function applyRequestedColumnDeletion($dataTable)
{
// use the ColumnDelete filter if hideColumns/showColumns is provided (must be done
// after queued filters are run so processed metrics can be removed, too)
$hideColumns = Common::getRequestVar('hideColumns', '', 'string', $this->request);
$showColumns = Common::getRequestVar('showColumns', '', 'string', $this->request);
$showRawMetrics = Common::getRequestVar('showRawMetrics', 0, 'int', $this->request);
if (!empty($hideColumns)
|| !empty($showColumns)
) {
$dataTable->filter('ColumnDelete', array($hideColumns, $showColumns));
} else if ($showRawMetrics !== 1) {
$this->removeTemporaryMetrics($dataTable);
}
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
*/
public function removeTemporaryMetrics(DataTableInterface $dataTable)
{
$allColumns = !empty($this->report) ? $this->report->getAllMetrics() : array();
$report = $this->report;
$dataTable->filter(function (DataTable $table) use ($report, $allColumns) {
$processedMetrics = Report::getProcessedMetricsForTable($table, $report);
$allTemporaryMetrics = array();
foreach ($processedMetrics as $metric) {
$allTemporaryMetrics = array_merge($allTemporaryMetrics, $metric->getTemporaryMetrics());
}
if (!empty($allTemporaryMetrics)) {
$table->filter('ColumnDelete', array($allTemporaryMetrics));
}
});
}
/**
* @param DataTableInterface $dataTable
* @return DataTableInterface
*/
public function applyLabelFilter($dataTable)
{
$label = self::getLabelFromRequest($this->request);
// apply label filter: only return rows matching the label parameter (more than one if more than one label)
if (!empty($label)) {
$addLabelIndex = Common::getRequestVar('labelFilterAddLabelIndex', 0, 'int', $this->request) == 1;
$filter = new LabelFilter($this->apiModule, $this->apiMethod, $this->request);
$dataTable = $filter->filter($label, $dataTable, $addLabelIndex);
}
return $dataTable;
}
/**
* @param DataTableInterface $dataTable
* @return DataTableInterface
*/
public function applyMetricsFormatting($dataTable)
{
$formatMetrics = Common::getRequestVar('format_metrics', 0, 'string', $this->request);
if ($formatMetrics == '0') {
return $dataTable;
}
// in Piwik 2.X & below, metrics are not formatted in API responses except for percents.
// this code implements this inconsistency
$onlyFormatPercents = $formatMetrics === 'bc';
$metricsToFormat = null;
if ($onlyFormatPercents) {
$metricsToFormat = $this->apiInconsistencies->getPercentMetricsToFormat();
}
// 'all' is a special value that indicates we should format non-processed metrics that are identified
// by string, like 'revenue'. this should be removed when all metrics are using the `Metric` class.
$formatAll = $formatMetrics === 'all';
$dataTable->filter(array($this->formatter, 'formatMetrics'), array($this->report, $metricsToFormat, $formatAll));
return $dataTable;
}
/**
* Returns the value for the label query parameter which can be either a string
* (ie, label=...) or array (ie, label[]=...).
*
* @param array $request
* @return array
*/
public static function getLabelFromRequest($request)
{
$label = Common::getRequestVar('label', array(), 'array', $request);
if (empty($label)) {
$label = Common::getRequestVar('label', '', 'string', $request);
if (!empty($label)) {
$label = array($label);
}
}
$label = self::unsanitizeLabelParameter($label);
return $label;
}
public static function unsanitizeLabelParameter($label)
{
// this is needed because Proxy uses Common::getRequestVar which in turn
// uses Common::sanitizeInputValue. This causes the > that separates recursive labels
// to become &gt; and we need to undo that here.
$label = str_replace( htmlentities('>', ENT_COMPAT | ENT_HTML401, 'UTF-8'), '>', $label);
return $label;
}
public function computeProcessedMetrics(DataTable $dataTable)
{
if ($dataTable->getMetadata(self::PROCESSED_METRICS_COMPUTED_FLAG)) {
return;
}
/** @var ProcessedMetric[] $processedMetrics */
$processedMetrics = Report::getProcessedMetricsForTable($dataTable, $this->report);
if (empty($processedMetrics)) {
return;
}
$dataTable->setMetadata(self::PROCESSED_METRICS_COMPUTED_FLAG, true);
foreach ($processedMetrics as $name => $processedMetric) {
if (!$processedMetric->beforeCompute($this->report, $dataTable)) {
continue;
}
foreach ($dataTable->getRows() as $row) {
if ($row->getColumn($name) !== false) { // only compute the metric if it has not been computed already
continue;
}
$computedValue = $processedMetric->compute($row);
if ($computedValue !== false) {
$row->addColumn($name, $computedValue);
}
}
}
foreach ($dataTable->getRows() as $row) {
$subtable = $row->getSubtable();
if (!empty($subtable)) {
foreach ($processedMetrics as $name => $processedMetric) {
$processedMetric->beforeComputeSubtable($row);
}
$this->computeProcessedMetrics($subtable);
foreach ($processedMetrics as $name => $processedMetric) {
$processedMetric->afterComputeSubtable($row);
}
}
}
}
public function applyComputeProcessedMetrics(DataTableInterface $dataTable)
{
$dataTable->filter(array($this, 'computeProcessedMetrics'));
}
}

View File

@ -0,0 +1,390 @@
<?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\API;
use Exception;
use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\Piwik;
use Piwik\Url;
use ReflectionClass;
/**
* Possible tags to use in APIs
*
* @hide -> Won't be shown in list of all APIs but is also not possible to be called via HTTP API
* @hideForAll Same as @hide
* @hideExceptForSuperUser Same as @hide but still shown and possible to be called by a user with super user access
* @internal -> Won't be shown in list of all APIs but is possible to be called via HTTP API
*/
class DocumentationGenerator
{
protected $countPluginsLoaded = 0;
/**
* trigger loading all plugins with an API.php file in the Proxy
*/
public function __construct()
{
$plugins = \Piwik\Plugin\Manager::getInstance()->getLoadedPluginsName();
foreach ($plugins as $plugin) {
try {
$className = Request::getClassNameAPI($plugin);
Proxy::getInstance()->registerClass($className);
} catch (Exception $e) {
}
}
}
/**
* Returns a HTML page containing help for all the successfully loaded APIs.
*
* @param bool $outputExampleUrls
* @return string
*/
public function getApiDocumentationAsString($outputExampleUrls = true)
{
list($toc, $str) = $this->generateDocumentation($outputExampleUrls, $prefixUrls = '', $displayTitlesAsAngularDirective = true);
return "<div piwik-content-block content-title='Quick access to APIs' id='topApiRef' name='topApiRef'>
$toc</div>
$str";
}
/**
* Used on developer.piwik.org
*
* @param bool|true $outputExampleUrls
* @param string $prefixUrls
* @return string
*/
public function getApiDocumentationAsStringForDeveloperReference($outputExampleUrls = true, $prefixUrls = '')
{
list($toc, $str) = $this->generateDocumentation($outputExampleUrls, $prefixUrls, $displayTitlesAsAngularDirective = false);
return "<h2 id='topApiRef' name='topApiRef'>Quick access to APIs</h2>
$toc
$str";
}
protected function prepareModuleToDisplay($moduleName)
{
return "<a href='#$moduleName'>$moduleName</a><br/>";
}
protected function prepareMethodToDisplay($moduleName, $info, $methods, $class, $outputExampleUrls, $prefixUrls, $displayTitlesAsAngularDirective)
{
$str = '';
$str .= "\n<a name='$moduleName' id='$moduleName'></a>";
if($displayTitlesAsAngularDirective) {
$str .= "<div piwik-content-block content-title='Module " . $moduleName . "'>";
} else {
$str .= "<h2>Module " . $moduleName . "</h2>";
}
$info['__documentation'] = $this->checkDocumentation($info['__documentation']);
$str .= "<div class='apiDescription'> " . $info['__documentation'] . " </div>";
foreach ($methods as $methodName) {
if (Proxy::getInstance()->isDeprecatedMethod($class, $methodName)) {
continue;
}
$params = $this->getParametersString($class, $methodName);
$str .= "\n <div class='apiMethod'>- <b>$moduleName.$methodName </b>" . $params . "";
$str .= '<small>';
if ($outputExampleUrls) {
$str .= $this->addExamples($class, $methodName, $prefixUrls);
}
$str .= '</small>';
$str .= "</div>\n";
}
if($displayTitlesAsAngularDirective) {
$str .= "</div>";
}
return $str;
}
protected function prepareModulesAndMethods($info, $moduleName)
{
$toDisplay = array();
foreach ($info as $methodName => $infoMethod) {
if ($methodName == '__documentation') {
continue;
}
$toDisplay[$moduleName][] = $methodName;
}
return $toDisplay;
}
protected function addExamples($class, $methodName, $prefixUrls)
{
$token_auth = "&token_auth=" . Piwik::getCurrentUserTokenAuth();
$parametersToSet = array(
'idSite' => Common::getRequestVar('idSite', 1, 'int'),
'period' => Common::getRequestVar('period', 'day', 'string'),
'date' => Common::getRequestVar('date', 'today', 'string')
);
$str = '';
$str .= "<span class=\"example\">";
$exampleUrl = $this->getExampleUrl($class, $methodName, $parametersToSet);
if ($exampleUrl !== false) {
$lastNUrls = '';
if (preg_match('/(&period)|(&date)/', $exampleUrl)) {
$exampleUrlRss = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last10', 'period' => 'day') + $parametersToSet);
$lastNUrls = ", RSS of the last <a target='_blank' href='$exampleUrlRss&format=rss$token_auth&translateColumnNames=1'>10 days</a>";
}
$exampleUrl = $prefixUrls . $exampleUrl;
$str .= " [ Example in
<a target='_blank' href='$exampleUrl&format=xml$token_auth'>XML</a>,
<a target='_blank' href='$exampleUrl&format=JSON$token_auth'>Json</a>,
<a target='_blank' href='$exampleUrl&format=Tsv$token_auth&translateColumnNames=1'>Tsv (Excel)</a>
$lastNUrls
]";
} else {
$str .= " [ No example available ]";
}
$str .= "</span>";
return $str;
}
/**
* Check if Class contains @hide
*
* @param ReflectionClass $rClass instance of ReflectionMethod
* @return bool
*/
public function checkIfClassCommentContainsHideAnnotation(ReflectionClass $rClass)
{
return false !== strstr($rClass->getDocComment(), '@hide');
}
/**
* Check if Class contains @internal
*
* @param ReflectionClass|\ReflectionMethod $rClass instance of ReflectionMethod
* @return bool
*/
private function checkIfCommentContainsInternalAnnotation($rClass)
{
return false !== strstr($rClass->getDocComment(), '@internal');
}
/**
* Check if documentation contains @hide annotation and deletes it
*
* @param $moduleToCheck
* @return mixed
*/
public function checkDocumentation($moduleToCheck)
{
if (strpos($moduleToCheck, '@hide') == true) {
$moduleToCheck = str_replace(strtok(strstr($moduleToCheck, '@hide'), "\n"), "", $moduleToCheck);
}
return $moduleToCheck;
}
/**
* Returns a string containing links to examples on how to call a given method on a given API
* It will export links to XML, CSV, HTML, JSON, PHP, etc.
* It will not export links for methods such as deleteSite or deleteUser
*
* @param string $class the class
* @param string $methodName the method
* @param array $parametersToSet parameters to set
* @return string|bool when not possible
*/
public function getExampleUrl($class, $methodName, $parametersToSet = array())
{
$knowExampleDefaultParametersValues = array(
'access' => 'view',
'userLogin' => 'test',
'passwordMd5ied' => 'passwordExample',
'email' => 'test@example.org',
'languageCode' => 'fr',
'url' => 'http://forum.piwik.org/',
'pageUrl' => 'http://forum.piwik.org/',
'apiModule' => 'UserCountry',
'apiAction' => 'getCountry',
'lastMinutes' => '30',
'abandonedCarts' => '0',
'segmentName' => 'pageTitle',
'ip' => '194.57.91.215',
'idSites' => '1,2',
'idAlert' => '1',
'seconds' => '3600',
// 'segmentName' => 'browserCode',
);
foreach ($parametersToSet as $name => $value) {
$knowExampleDefaultParametersValues[$name] = $value;
}
// no links for these method names
$doNotPrintExampleForTheseMethods = array(
//Sites
'deleteSite',
'addSite',
'updateSite',
'addSiteAliasUrls',
//Users
'deleteUser',
'addUser',
'updateUser',
'setUserAccess',
//Goals
'addGoal',
'updateGoal',
'deleteGoal',
//Marketplace
'deleteLicenseKey'
);
if (in_array($methodName, $doNotPrintExampleForTheseMethods)) {
return false;
}
// we try to give an URL example to call the API
$aParameters = Proxy::getInstance()->getParametersList($class, $methodName);
$aParameters['format'] = false;
$aParameters['hideIdSubDatable'] = false;
$aParameters['serialize'] = false;
$aParameters['language'] = false;
$aParameters['translateColumnNames'] = false;
$aParameters['label'] = false;
$aParameters['flat'] = false;
$aParameters['include_aggregate_rows'] = false;
$aParameters['filter_offset'] = false;
$aParameters['filter_limit'] = false;
$aParameters['filter_sort_column'] = false;
$aParameters['filter_sort_order'] = false;
$aParameters['filter_excludelowpop'] = false;
$aParameters['filter_excludelowpop_value'] = false;
$aParameters['filter_column_recursive'] = false;
$aParameters['filter_pattern'] = false;
$aParameters['filter_pattern_recursive'] = false;
$aParameters['filter_truncate'] = false;
$aParameters['hideColumns'] = false;
$aParameters['showColumns'] = false;
$aParameters['filter_pattern_recursive'] = false;
$aParameters['pivotBy'] = false;
$aParameters['pivotByColumn'] = false;
$aParameters['pivotByColumnLimit'] = false;
$aParameters['disable_queued_filters'] = false;
$aParameters['disable_generic_filters'] = false;
$aParameters['expanded'] = false;
$aParameters['idDimenson'] = false;
$aParameters['format_metrics'] = false;
$entityNames = StaticContainer::get('entities.idNames');
foreach ($entityNames as $entityName) {
if (isset($aParameters[$entityName])) {
continue;
}
$aParameters[$entityName] = false;
}
$moduleName = Proxy::getInstance()->getModuleNameFromClassName($class);
$aParameters = array_merge(array('module' => 'API', 'method' => $moduleName . '.' . $methodName), $aParameters);
foreach ($aParameters as $nameVariable => &$defaultValue) {
if (isset($knowExampleDefaultParametersValues[$nameVariable])) {
$defaultValue = $knowExampleDefaultParametersValues[$nameVariable];
} // if there isn't a default value for a given parameter,
// we need a 'know default value' or we can't generate the link
elseif ($defaultValue instanceof NoDefaultValue) {
return false;
}
}
return '?' . Url::getQueryStringFromParameters($aParameters);
}
/**
* Returns the methods $class.$name parameters (and default value if provided) as a string.
*
* @param string $class The class name
* @param string $name The method name
* @return string For example "(idSite, period, date = 'today')"
*/
protected function getParametersString($class, $name)
{
$aParameters = Proxy::getInstance()->getParametersList($class, $name);
$asParameters = array();
foreach ($aParameters as $nameVariable => $defaultValue) {
// Do not show API parameters starting with _
// They are supposed to be used only in internal API calls
if (strpos($nameVariable, '_') === 0) {
continue;
}
$str = $nameVariable;
if (!($defaultValue instanceof NoDefaultValue)) {
if (is_array($defaultValue)) {
$str .= " = 'Array'";
} else {
$str .= " = '$defaultValue'";
}
}
$asParameters[] = $str;
}
$sParameters = implode(", ", $asParameters);
return "($sParameters)";
}
/**
* @param $outputExampleUrls
* @param $prefixUrls
* @param $displayTitlesAsAngularDirective
* @return array
*/
protected function generateDocumentation($outputExampleUrls, $prefixUrls, $displayTitlesAsAngularDirective)
{
$str = $toc = '';
foreach (Proxy::getInstance()->getMetadata() as $class => $info) {
$moduleName = Proxy::getInstance()->getModuleNameFromClassName($class);
$rClass = new ReflectionClass($class);
if (!Piwik::hasUserSuperUserAccess() && $this->checkIfClassCommentContainsHideAnnotation($rClass)) {
continue;
}
if ($this->checkIfCommentContainsInternalAnnotation($rClass)) {
continue;
}
$toDisplay = $this->prepareModulesAndMethods($info, $moduleName);
foreach ($toDisplay as $moduleName => $methods) {
foreach ($methods as $index => $method) {
if (!method_exists($class, $method)) { // method is handled through API.Request.intercept event
continue;
}
$reflectionMethod = new \ReflectionMethod($class, $method);
if ($this->checkIfCommentContainsInternalAnnotation($reflectionMethod)) {
unset($toDisplay[$moduleName][$index]);
}
}
if (empty($toDisplay[$moduleName])) {
unset($toDisplay[$moduleName]);
}
}
foreach ($toDisplay as $moduleName => $methods) {
$toc .= $this->prepareModuleToDisplay($moduleName);
$str .= $this->prepareMethodToDisplay($moduleName, $info, $methods, $class, $outputExampleUrls, $prefixUrls, $displayTitlesAsAngularDirective);
}
}
return array($toc, $str);
}
}

View File

@ -0,0 +1,45 @@
<?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\API;
/**
* Contains logic to replicate inconsistencies in Piwik's API. This class exists
* to provide a way to clean up existing Piwik code and behavior without breaking
* backwards compatibility immediately.
*
* Code that handles the case when the 'format_metrics' query parameter value is
* 'bc' should be removed as well. This code is in API\Request and DataTablePostProcessor.
*
* Should be removed before releasing Piwik 3.0.
*/
class Inconsistencies
{
/**
* In Piwik 2.X and below, the "raw" API would format percent values but no others.
* This method returns the list of percent metrics that were returned from the API
* formatted so we can maintain BC.
*
* Used by DataTablePostProcessor.
*/
public function getPercentMetricsToFormat()
{
return array(
'bounce_rate',
'conversion_rate',
'abandoned_rate',
'interaction_rate',
'exit_rate',
'bounce_rate_returning',
'nb_visits_percentage',
'/.*_evolution/',
'/goal_.*_conversion_rate/',
'/form_.*_rate/',
'/field_.*_rate/',
);
}
}

View File

@ -0,0 +1,582 @@
<?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\API;
use Exception;
use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\Context;
use Piwik\Piwik;
use Piwik\Plugin\Manager;
use Piwik\Singleton;
use ReflectionClass;
use ReflectionMethod;
/**
* Proxy is a singleton that has the knowledge of every method available, their parameters
* and default values.
* Proxy receives all the API calls requests via call() and forwards them to the right
* object, with the parameters in the right order.
*
* It will also log the performance of API calls (time spent, parameter values, etc.) if logger available
*/
class Proxy
{
// array of already registered plugins names
protected $alreadyRegistered = array();
protected $metadataArray = array();
private $hideIgnoredFunctions = true;
// when a parameter doesn't have a default value we use this
private $noDefaultValue;
public function __construct()
{
$this->noDefaultValue = new NoDefaultValue();
}
public static function getInstance()
{
return StaticContainer::get(self::class);
}
/**
* Returns array containing reflection meta data for all the loaded classes
* eg. number of parameters, method names, etc.
*
* @return array
*/
public function getMetadata()
{
ksort($this->metadataArray);
return $this->metadataArray;
}
/**
* Registers the API information of a given module.
*
* The module to be registered must be
* - a singleton (providing a getInstance() method)
* - the API file must be located in plugins/ModuleName/API.php
* for example plugins/Referrers/API.php
*
* The method will introspect the methods, their parameters, etc.
*
* @param string $className ModuleName eg. "API"
*/
public function registerClass($className)
{
if (isset($this->alreadyRegistered[$className])) {
return;
}
$this->includeApiFile($className);
$this->checkClassIsSingleton($className);
$rClass = new ReflectionClass($className);
if (!$this->shouldHideAPIMethod($rClass->getDocComment())) {
foreach ($rClass->getMethods() as $method) {
$this->loadMethodMetadata($className, $method);
}
$this->setDocumentation($rClass, $className);
$this->alreadyRegistered[$className] = true;
}
}
/**
* Will be displayed in the API page
*
* @param ReflectionClass $rClass Instance of ReflectionClass
* @param string $className Name of the class
*/
private function setDocumentation($rClass, $className)
{
// Doc comment
$doc = $rClass->getDocComment();
$doc = str_replace(" * " . PHP_EOL, "<br>", $doc);
// boldify the first line only if there is more than one line, otherwise too much bold
if (substr_count($doc, '<br>') > 1) {
$firstLineBreak = strpos($doc, "<br>");
$doc = "<div class='apiFirstLine'>" . substr($doc, 0, $firstLineBreak) . "</div>" . substr($doc, $firstLineBreak + strlen("<br>"));
}
$doc = preg_replace("/(@package)[a-z _A-Z]*/", "", $doc);
$doc = preg_replace("/(@method).*/", "", $doc);
$doc = str_replace(array("\t", "\n", "/**", "*/", " * ", " *", " ", "\t*", " * @package"), " ", $doc);
// replace 'foo' and `bar` and "foobar" with code blocks... much magic
$doc = preg_replace('/`(.*?)`/', '<code>$1</code>', $doc);
$this->metadataArray[$className]['__documentation'] = $doc;
}
/**
* Returns number of classes already loaded
* @return int
*/
public function getCountRegisteredClasses()
{
return count($this->alreadyRegistered);
}
/**
* Will execute $className->$methodName($parametersValues)
* If any error is detected (wrong number of parameters, method not found, class not found, etc.)
* it will throw an exception
*
* It also logs the API calls, with the parameters values, the returned value, the performance, etc.
* You can enable logging in config/global.ini.php (log_api_call)
*
* @param string $className The class name (eg. API)
* @param string $methodName The method name
* @param array $parametersRequest The parameters pairs (name=>value)
*
* @return mixed|null
* @throws Exception|\Piwik\NoAccessException
*/
public function call($className, $methodName, $parametersRequest)
{
// Temporarily sets the Request array to this API call context
return Context::executeWithQueryParameters($parametersRequest, function () use ($className, $methodName, $parametersRequest) {
$returnedValue = null;
$this->registerClass($className);
// instanciate the object
$object = $className::getInstance();
// check method exists
$this->checkMethodExists($className, $methodName);
// get the list of parameters required by the method
$parameterNamesDefaultValues = $this->getParametersList($className, $methodName);
// load parameters in the right order, etc.
$finalParameters = $this->getRequestParametersArray($parameterNamesDefaultValues, $parametersRequest);
// allow plugins to manipulate the value
$pluginName = $this->getModuleNameFromClassName($className);
$returnedValue = null;
/**
* Triggered before an API request is dispatched.
*
* This event can be used to modify the arguments passed to one or more API methods.
*
* **Example**
*
* Piwik::addAction('API.Request.dispatch', function (&$parameters, $pluginName, $methodName) {
* if ($pluginName == 'Actions') {
* if ($methodName == 'getPageUrls') {
* // ... do something ...
* } else {
* // ... do something else ...
* }
* }
* });
*
* @param array &$finalParameters List of parameters that will be passed to the API method.
* @param string $pluginName The name of the plugin the API method belongs to.
* @param string $methodName The name of the API method that will be called.
*/
Piwik::postEvent('API.Request.dispatch', array(&$finalParameters, $pluginName, $methodName));
/**
* Triggered before an API request is dispatched.
*
* This event exists for convenience and is triggered directly after the {@hook API.Request.dispatch}
* event is triggered. It can be used to modify the arguments passed to a **single** API method.
*
* _Note: This is can be accomplished with the {@hook API.Request.dispatch} event as well, however
* event handlers for that event will have to do more work._
*
* **Example**
*
* Piwik::addAction('API.Actions.getPageUrls', function (&$parameters) {
* // force use of a single website. for some reason.
* $parameters['idSite'] = 1;
* });
*
* @param array &$finalParameters List of parameters that will be passed to the API method.
*/
Piwik::postEvent(sprintf('API.%s.%s', $pluginName, $methodName), array(&$finalParameters));
/**
* Triggered before an API request is dispatched.
*
* Use this event to intercept an API request and execute your own code instead. If you set
* `$returnedValue` in a handler for this event, the original API method will not be executed,
* and the result will be what you set in the event handler.
*
* @param mixed &$returnedValue Set this to set the result and preempt normal API invocation.
* @param array &$finalParameters List of parameters that will be passed to the API method.
* @param string $pluginName The name of the plugin the API method belongs to.
* @param string $methodName The name of the API method that will be called.
* @param array $parametersRequest The query parameters for this request.
*/
Piwik::postEvent('API.Request.intercept', [&$returnedValue, $finalParameters, $pluginName, $methodName, $parametersRequest]);
$apiParametersInCorrectOrder = array();
foreach ($parameterNamesDefaultValues as $name => $defaultValue) {
if (isset($finalParameters[$name]) || array_key_exists($name, $finalParameters)) {
$apiParametersInCorrectOrder[] = $finalParameters[$name];
}
}
// call the method if a hook hasn't already set an output variable
if ($returnedValue === null) {
$returnedValue = call_user_func_array(array($object, $methodName), $apiParametersInCorrectOrder);
}
$endHookParams = array(
&$returnedValue,
array('className' => $className,
'module' => $pluginName,
'action' => $methodName,
'parameters' => $finalParameters)
);
/**
* Triggered directly after an API request is dispatched.
*
* This event exists for convenience and is triggered immediately before the
* {@hook API.Request.dispatch.end} event. It can be used to modify the output of a **single**
* API method.
*
* _Note: This can be accomplished with the {@hook API.Request.dispatch.end} event as well,
* however event handlers for that event will have to do more work._
*
* **Example**
*
* // append (0 hits) to the end of row labels whose row has 0 hits
* Piwik::addAction('API.Actions.getPageUrls', function (&$returnValue, $info)) {
* $returnValue->filter('ColumnCallbackReplace', 'label', function ($label, $hits) {
* if ($hits === 0) {
* return $label . " (0 hits)";
* } else {
* return $label;
* }
* }, null, array('nb_hits'));
* }
*
* @param mixed &$returnedValue The API method's return value. Can be an object, such as a
* {@link Piwik\DataTable DataTable} instance.
* could be a {@link Piwik\DataTable DataTable}.
* @param array $extraInfo An array holding information regarding the API request. Will
* contain the following data:
*
* - **className**: The namespace-d class name of the API instance
* that's being called.
* - **module**: The name of the plugin the API request was
* dispatched to.
* - **action**: The name of the API method that was executed.
* - **parameters**: The array of parameters passed to the API
* method.
*/
Piwik::postEvent(sprintf('API.%s.%s.end', $pluginName, $methodName), $endHookParams);
/**
* Triggered directly after an API request is dispatched.
*
* This event can be used to modify the output of any API method.
*
* **Example**
*
* // append (0 hits) to the end of row labels whose row has 0 hits for any report that has the 'nb_hits' metric
* Piwik::addAction('API.Actions.getPageUrls.end', function (&$returnValue, $info)) {
* // don't process non-DataTable reports and reports that don't have the nb_hits column
* if (!($returnValue instanceof DataTableInterface)
* || in_array('nb_hits', $returnValue->getColumns())
* ) {
* return;
* }
*
* $returnValue->filter('ColumnCallbackReplace', 'label', function ($label, $hits) {
* if ($hits === 0) {
* return $label . " (0 hits)";
* } else {
* return $label;
* }
* }, null, array('nb_hits'));
* }
*
* @param mixed &$returnedValue The API method's return value. Can be an object, such as a
* {@link Piwik\DataTable DataTable} instance.
* @param array $extraInfo An array holding information regarding the API request. Will
* contain the following data:
*
* - **className**: The namespace-d class name of the API instance
* that's being called.
* - **module**: The name of the plugin the API request was
* dispatched to.
* - **action**: The name of the API method that was executed.
* - **parameters**: The array of parameters passed to the API
* method.
*/
Piwik::postEvent('API.Request.dispatch.end', $endHookParams);
return $returnedValue;
});
}
/**
* Returns the parameters names and default values for the method $name
* of the class $class
*
* @param string $class The class name
* @param string $name The method name
* @return array Format array(
* 'testParameter' => null, // no default value
* 'life' => 42, // default value = 42
* 'date' => 'yesterday',
* );
*/
public function getParametersList($class, $name)
{
return $this->metadataArray[$class][$name]['parameters'];
}
/**
* Check if given method name is deprecated or not.
*/
public function isDeprecatedMethod($class, $methodName)
{
return $this->metadataArray[$class][$methodName]['isDeprecated'];
}
/**
* Returns the 'moduleName' part of '\\Piwik\\Plugins\\moduleName\\API'
*
* @param string $className "API"
* @return string "Referrers"
*/
public function getModuleNameFromClassName($className)
{
return str_replace(array('\\Piwik\\Plugins\\', '\\API'), '', $className);
}
public function isExistingApiAction($pluginName, $apiAction)
{
$namespacedApiClassName = "\\Piwik\\Plugins\\$pluginName\\API";
$api = $namespacedApiClassName::getInstance();
return method_exists($api, $apiAction);
}
public function buildApiActionName($pluginName, $apiAction)
{
return sprintf("%s.%s", $pluginName, $apiAction);
}
/**
* Sets whether to hide '@ignore'd functions from method metadata or not.
*
* @param bool $hideIgnoredFunctions
*/
public function setHideIgnoredFunctions($hideIgnoredFunctions)
{
$this->hideIgnoredFunctions = $hideIgnoredFunctions;
// make sure metadata gets reloaded
$this->alreadyRegistered = array();
$this->metadataArray = array();
}
/**
* Returns an array containing the values of the parameters to pass to the method to call
*
* @param array $requiredParameters array of (parameter name, default value)
* @param array $parametersRequest
* @throws Exception
* @return array values to pass to the function call
*/
private function getRequestParametersArray($requiredParameters, $parametersRequest)
{
$finalParameters = array();
foreach ($requiredParameters as $name => $defaultValue) {
try {
if ($defaultValue instanceof NoDefaultValue) {
$requestValue = Common::getRequestVar($name, null, null, $parametersRequest);
} else {
try {
if ($name == 'segment' && !empty($parametersRequest['segment'])) {
// segment parameter is an exception: we do not want to sanitize user input or it would break the segment encoding
$requestValue = ($parametersRequest['segment']);
} else {
$requestValue = Common::getRequestVar($name, $defaultValue, null, $parametersRequest);
}
} catch (Exception $e) {
// Special case: empty parameter in the URL, should return the empty string
if (isset($parametersRequest[$name])
&& $parametersRequest[$name] === ''
) {
$requestValue = '';
} else {
$requestValue = $defaultValue;
}
}
}
} catch (Exception $e) {
throw new Exception(Piwik::translate('General_PleaseSpecifyValue', array($name)));
}
$finalParameters[$name] = $requestValue;
}
return $finalParameters;
}
/**
* Includes the class API by looking up plugins/xxx/API.php
*
* @param string $fileName api class name eg. "API"
* @throws Exception
*/
private function includeApiFile($fileName)
{
$module = self::getModuleNameFromClassName($fileName);
$path = Manager::getPluginDirectory($module) . '/API.php';
if (is_readable($path)) {
require_once $path; // prefixed by PIWIK_INCLUDE_PATH
} else {
throw new Exception("API module $module not found.");
}
}
/**
* @param string $class name of a class
* @param ReflectionMethod $method instance of ReflectionMethod
*/
private function loadMethodMetadata($class, $method)
{
if (!$this->checkIfMethodIsAvailable($method)) {
return;
}
$name = $method->getName();
$parameters = $method->getParameters();
$docComment = $method->getDocComment();
$aParameters = array();
foreach ($parameters as $parameter) {
$nameVariable = $parameter->getName();
$defaultValue = $this->noDefaultValue;
if ($parameter->isDefaultValueAvailable()) {
$defaultValue = $parameter->getDefaultValue();
}
$aParameters[$nameVariable] = $defaultValue;
}
$this->metadataArray[$class][$name]['parameters'] = $aParameters;
$this->metadataArray[$class][$name]['numberOfRequiredParameters'] = $method->getNumberOfRequiredParameters();
$this->metadataArray[$class][$name]['isDeprecated'] = false !== strstr($docComment, '@deprecated');
}
/**
* Checks that the method exists in the class
*
* @param string $className The class name
* @param string $methodName The method name
* @throws Exception If the method is not found
*/
private function checkMethodExists($className, $methodName)
{
if (!$this->isMethodAvailable($className, $methodName)) {
throw new Exception(Piwik::translate('General_ExceptionMethodNotFound', array($methodName, $className)));
}
}
/**
* @param $docComment
* @return bool
*/
public function shouldHideAPIMethod($docComment)
{
$hideLine = strstr($docComment, '@hide');
if ($hideLine === false) {
return false;
}
$hideLine = trim($hideLine);
$hideLine .= ' ';
$token = trim(strtok($hideLine, " "), "\n");
$hide = false;
if (!empty($token)) {
/**
* This event exists for checking whether a Plugin API class or a Plugin API method tagged
* with a `@hideXYZ` should be hidden in the API listing.
*
* @param bool &$hide whether to hide APIs tagged with $token should be displayed.
*/
Piwik::postEvent(sprintf('API.DocumentationGenerator.%s', $token), array(&$hide));
}
return $hide;
}
/**
* @param ReflectionMethod $method
* @return bool
*/
protected function checkIfMethodIsAvailable(ReflectionMethod $method)
{
if (!$method->isPublic() || $method->isConstructor() || $method->getName() === 'getInstance') {
return false;
}
if ($this->hideIgnoredFunctions && false !== strstr($method->getDocComment(), '@ignore')) {
return false;
}
if ($this->shouldHideAPIMethod($method->getDocComment())) {
return false;
}
return true;
}
/**
* Returns true if the method is found in the API of the given class name.
*
* @param string $className The class name
* @param string $methodName The method name
* @return bool
*/
private function isMethodAvailable($className, $methodName)
{
return isset($this->metadataArray[$className][$methodName]);
}
/**
* Checks that the class is a Singleton (presence of the getInstance() method)
*
* @param string $className The class name
* @throws Exception If the class is not a Singleton
*/
private function checkClassIsSingleton($className)
{
if (!method_exists($className, "getInstance")) {
throw new Exception("$className that provide an API must be Singleton and have a 'public static function getInstance()' method.");
}
}
}
/**
* To differentiate between "no value" and default value of null
*
*/
class NoDefaultValue
{
}

View File

@ -0,0 +1,643 @@
<?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\API;
use Exception;
use Piwik\Access;
use Piwik\Cache;
use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\Context;
use Piwik\DataTable;
use Piwik\Exception\PluginDeactivatedException;
use Piwik\IP;
use Piwik\Log;
use Piwik\Piwik;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Plugins\CoreHome\LoginWhitelist;
use Piwik\SettingsServer;
use Piwik\Url;
use Piwik\UrlHelper;
use Psr\Log\LoggerInterface;
/**
* Dispatches API requests to the appropriate API method.
*
* The Request class is used throughout Piwik to call API methods. The difference
* between using Request and calling API methods directly is that Request
* will do more after calling the API including: applying generic filters, applying queued filters,
* and handling the **flat** and **label** query parameters.
*
* Additionally, the Request class will **forward current query parameters** to the request
* which is more convenient than calling {@link Piwik\Common::getRequestVar()} many times over.
*
* In most cases, using a Request object to query the API is the correct approach.
*
* ### Post-processing
*
* The return value of API methods undergo some extra processing before being returned by Request.
*
* ### Output Formats
*
* The value returned by Request will be serialized to a certain format before being returned.
*
* ### Examples
*
* **Basic Usage**
*
* $request = new Request('method=UserLanguage.getLanguage&idSite=1&date=yesterday&period=week'
* . '&format=xml&filter_limit=5&filter_offset=0')
* $result = $request->process();
* echo $result;
*
* **Getting a unrendered DataTable**
*
* // use the convenience method 'processRequest'
* $dataTable = Request::processRequest('UserLanguage.getLanguage', array(
* 'idSite' => 1,
* 'date' => 'yesterday',
* 'period' => 'week',
* 'filter_limit' => 5,
* 'filter_offset' => 0
*
* 'format' => 'original', // this is the important bit
* ));
* echo "This DataTable has " . $dataTable->getRowsCount() . " rows.";
*
* @see http://piwik.org/docs/analytics-api
* @api
*/
class Request
{
/**
* The count of nested API request invocations. Used to determine if the currently executing request is the root or not.
*
* @var int
*/
private static $nestedApiInvocationCount = 0;
private $request = null;
/**
* Converts the supplied request string into an array of query paramater name/value
* mappings. The current query parameters (everything in `$_GET` and `$_POST`) are
* forwarded to request array before it is returned.
*
* @param string|array|null $request The base request string or array, eg,
* `'module=UserLanguage&action=getLanguage'`.
* @param array $defaultRequest Default query parameters. If a query parameter is absent in `$request`, it will be loaded
* from this. Defaults to `$_GET + $_POST`.
* @return array
*/
public static function getRequestArrayFromString($request, $defaultRequest = null)
{
if ($defaultRequest === null) {
$defaultRequest = self::getDefaultRequest();
$requestRaw = self::getRequestParametersGET();
if (!empty($requestRaw['segment'])) {
$defaultRequest['segment'] = $requestRaw['segment'];
}
if (!isset($defaultRequest['format_metrics'])) {
$defaultRequest['format_metrics'] = 'bc';
}
}
$requestArray = $defaultRequest;
if (!is_null($request)) {
if (is_array($request)) {
$requestParsed = $request;
} else {
$request = trim($request);
$request = str_replace(array("\n", "\t"), '', $request);
$requestParsed = UrlHelper::getArrayFromQueryString($request);
}
$requestArray = $requestParsed + $defaultRequest;
}
foreach ($requestArray as &$element) {
if (!is_array($element)) {
$element = trim($element);
}
}
return $requestArray;
}
/**
* Constructor.
*
* @param string|array $request Query string that defines the API call (must at least contain a **method** parameter),
* eg, `'method=UserLanguage.getLanguage&idSite=1&date=yesterday&period=week&format=xml'`
* If a request is not provided, then we use the values in the `$_GET` and `$_POST`
* superglobals.
* @param array $defaultRequest Default query parameters. If a query parameter is absent in `$request`, it will be loaded
* from this. Defaults to `$_GET + $_POST`.
*/
public function __construct($request = null, $defaultRequest = null)
{
$this->request = self::getRequestArrayFromString($request, $defaultRequest);
$this->sanitizeRequest();
$this->renameModuleAndActionInRequest();
}
/**
* For backward compatibility: Piwik API still works if module=Referers,
* we rewrite to correct renamed plugin: Referrers
*
* @param $module
* @param $action
* @return array( $module, $action )
* @ignore
*/
public static function getRenamedModuleAndAction($module, $action)
{
/**
* This event is posted in the Request dispatcher and can be used
* to overwrite the Module and Action to dispatch.
* This is useful when some Controller methods or API methods have been renamed or moved to another plugin.
*
* @param $module string
* @param $action string
*/
Piwik::postEvent('Request.getRenamedModuleAndAction', array(&$module, &$action));
return array($module, $action);
}
/**
* Make sure that the request contains no logical errors
*/
private function sanitizeRequest()
{
// The label filter does not work with expanded=1 because the data table IDs have a different meaning
// depending on whether the table has been loaded yet. expanded=1 causes all tables to be loaded, which
// is why the label filter can't descend when a recursive label has been requested.
// To fix this, we remove the expanded parameter if a label parameter is set.
if (isset($this->request['label']) && !empty($this->request['label'])
&& isset($this->request['expanded']) && $this->request['expanded']
) {
unset($this->request['expanded']);
}
}
/**
* Dispatches the API request to the appropriate API method and returns the result
* after post-processing.
*
* Post-processing includes:
*
* - flattening if **flat** is 0
* - running generic filters unless **disable_generic_filters** is set to 1
* - URL decoding label column values
* - running queued filters unless **disable_queued_filters** is set to 1
* - removing columns based on the values of the **hideColumns** and **showColumns** query parameters
* - filtering rows if the **label** query parameter is set
* - converting the result to the appropriate format (ie, XML, JSON, etc.)
*
* If `'original'` is supplied for the output format, the result is returned as a PHP
* object.
*
* @throws PluginDeactivatedException if the module plugin is not activated.
* @throws Exception if the requested API method cannot be called, if required parameters for the
* API method are missing or if the API method throws an exception and the **format**
* query parameter is **original**.
* @return DataTable|Map|string The data resulting from the API call.
*/
public function process()
{
// read the format requested for the output data
$outputFormat = strtolower(Common::getRequestVar('format', 'xml', 'string', $this->request));
$disablePostProcessing = $this->shouldDisablePostProcessing();
// create the response
$response = new ResponseBuilder($outputFormat, $this->request);
if ($disablePostProcessing) {
$response->disableDataTablePostProcessor();
}
$corsHandler = new CORSHandler();
$corsHandler->handle();
$tokenAuth = Common::getRequestVar('token_auth', '', 'string', $this->request);
$shouldReloadAuth = false;
try {
++self::$nestedApiInvocationCount;
// IP check is needed here as we cannot listen to API.Request.authenticate as it would then not return proper API format response.
// We can also not do it by listening to API.Request.dispatch as by then the user is already authenticated and we want to make sure
// to not expose any information in case the IP is not whitelisted.
$whitelist = new LoginWhitelist();
if ($whitelist->shouldCheckWhitelist() && $whitelist->shouldWhitelistApplyToAPI()) {
$ip = IP::getIpFromHeader();
$whitelist->checkIsWhitelisted($ip);
}
// read parameters
$moduleMethod = Common::getRequestVar('method', null, 'string', $this->request);
list($module, $method) = $this->extractModuleAndMethod($moduleMethod);
list($module, $method) = self::getRenamedModuleAndAction($module, $method);
PluginManager::getInstance()->checkIsPluginActivated($module);
$apiClassName = self::getClassNameAPI($module);
if ($shouldReloadAuth = self::shouldReloadAuthUsingTokenAuth($this->request)) {
$access = Access::getInstance();
$tokenAuthToRestore = $access->getTokenAuth();
$hadSuperUserAccess = $access->hasSuperUserAccess();
self::forceReloadAuthUsingTokenAuth($tokenAuth);
}
// call the method
$returnedValue = Proxy::getInstance()->call($apiClassName, $method, $this->request);
// get the response with the request query parameters loaded, since DataTablePost processor will use the Report
// class instance, which may inspect the query parameters. (eg, it may look for the idCustomReport parameters
// which may only exist in $this->request, if the request was called programatically)
$toReturn = Context::executeWithQueryParameters($this->request, function () use ($response, $returnedValue, $module, $method) {
return $response->getResponse($returnedValue, $module, $method);
});
} catch (Exception $e) {
StaticContainer::get(LoggerInterface::class)->error('Uncaught exception in API: {exception}', [
'exception' => $e,
'ignoreInScreenWriter' => true,
]);
$toReturn = $response->getResponseException($e);
} finally {
--self::$nestedApiInvocationCount;
}
if ($shouldReloadAuth) {
$this->restoreAuthUsingTokenAuth($tokenAuthToRestore, $hadSuperUserAccess);
}
return $toReturn;
}
private function restoreAuthUsingTokenAuth($tokenToRestore, $hadSuperUserAccess)
{
// if we would not make sure to unset super user access, the tokenAuth would be not authenticated and any
// token would just keep super user access (eg if the token that was reloaded before had super user access)
Access::getInstance()->setSuperUserAccess(false);
// we need to restore by reloading the tokenAuth as some permissions could have been removed in the API
// request etc. Otherwise we could just store a clone of Access::getInstance() and restore here
self::forceReloadAuthUsingTokenAuth($tokenToRestore);
if ($hadSuperUserAccess && !Access::getInstance()->hasSuperUserAccess()) {
// we are in context of `doAsSuperUser()` and need to restore this behaviour
Access::getInstance()->setSuperUserAccess(true);
}
}
/**
* Returns the name of a plugin's API class by plugin name.
*
* @param string $plugin The plugin name, eg, `'Referrers'`.
* @return string The fully qualified API class name, eg, `'\Piwik\Plugins\Referrers\API'`.
*/
public static function getClassNameAPI($plugin)
{
return sprintf('\Piwik\Plugins\%s\API', $plugin);
}
/**
* @ignore
* @internal
* @param string $currentApiMethod
*/
public static function setIsRootRequestApiRequest($currentApiMethod)
{
Cache::getTransientCache()->save('API.setIsRootRequestApiRequest', $currentApiMethod);
}
/**
* @ignore
* @internal
* @return string current Api Method if it is an api request
*/
public static function getRootApiRequestMethod()
{
return Cache::getTransientCache()->fetch('API.setIsRootRequestApiRequest');
}
/**
* Detect if the root request (the actual request) is an API request or not. To detect whether an API is currently
* request within any request, have a look at {@link isApiRequest()}.
*
* @return bool
* @throws Exception
*/
public static function isRootRequestApiRequest()
{
$apiMethod = Cache::getTransientCache()->fetch('API.setIsRootRequestApiRequest');
return !empty($apiMethod);
}
/**
* Checks if the currently executing API request is the root API request or not.
*
* Note: the "root" API request is the first request made. Within that request, further API methods
* can be called programmatically. These requests are considered "child" API requests.
*
* @return bool
* @throws Exception
*/
public static function isCurrentApiRequestTheRootApiRequest()
{
return self::$nestedApiInvocationCount == 1;
}
/**
* Detect if request is an API request. Meaning the module is 'API' and an API method having a valid format was
* specified. Note that this method will return true even if the actual request is for example a regular UI
* reporting page request but within this request we are currently processing an API request (eg a
* controller calls Request::processRequest('API.getMatomoVersion')). To find out if the root request is an API
* request or not, call {@link isRootRequestApiRequest()}
*
* @param array $request eg array('module' => 'API', 'method' => 'Test.getMethod')
* @return bool
* @throws Exception
*/
public static function isApiRequest($request)
{
$method = self::getMethodIfApiRequest($request);
return !empty($method);
}
/**
* Returns the current API method being executed, if the current request is an API request.
*
* @param array $request eg array('module' => 'API', 'method' => 'Test.getMethod')
* @return string|null
* @throws Exception
*/
public static function getMethodIfApiRequest($request)
{
$module = Common::getRequestVar('module', '', 'string', $request);
$method = Common::getRequestVar('method', '', 'string', $request);
$isApi = $module === 'API' && !empty($method) && (count(explode('.', $method)) === 2);
return $isApi ? $method : null;
}
/**
* If the token_auth is found in the $request parameter,
* the current session will be authenticated using this token_auth.
* It will overwrite the previous Auth object.
*
* @param array $request If null, uses the default request ($_GET)
* @return void
* @ignore
*/
public static function reloadAuthUsingTokenAuth($request = null)
{
// if a token_auth is specified in the API request, we load the right permissions
$token_auth = Common::getRequestVar('token_auth', '', 'string', $request);
if (self::shouldReloadAuthUsingTokenAuth($request)) {
self::forceReloadAuthUsingTokenAuth($token_auth);
}
}
/**
* The current session will be authenticated using this token_auth.
* It will overwrite the previous Auth object.
*
* @param string $tokenAuth
* @return void
*/
private static function forceReloadAuthUsingTokenAuth($tokenAuth)
{
/**
* Triggered when authenticating an API request, but only if the **token_auth**
* query parameter is found in the request.
*
* Plugins that provide authentication capabilities should subscribe to this event
* and make sure the global authentication object (the object returned by `StaticContainer::get('Piwik\Auth')`)
* is setup to use `$token_auth` when its `authenticate()` method is executed.
*
* @param string $token_auth The value of the **token_auth** query parameter.
*/
Piwik::postEvent('API.Request.authenticate', array($tokenAuth));
if (!Access::getInstance()->reloadAccess() && $tokenAuth && $tokenAuth !== 'anonymous') {
/**
* @ignore
* @internal
*/
Piwik::postEvent('API.Request.authenticate.failed');
}
SettingsServer::raiseMemoryLimitIfNecessary();
}
private static function shouldReloadAuthUsingTokenAuth($request)
{
if (is_null($request)) {
$request = self::getDefaultRequest();
}
if (!isset($request['token_auth'])) {
// no token is given so we just keep the current loaded user
return false;
}
// a token is specified, we need to reload auth in case it is different than the current one, even if it is empty
$tokenAuth = Common::getRequestVar('token_auth', '', 'string', $request);
// not using !== is on purpose as getTokenAuth() might return null whereas $tokenAuth is '' . In this case
// we do not need to reload.
return $tokenAuth != Access::getInstance()->getTokenAuth();
}
/**
* Returns array($class, $method) from the given string $class.$method
*
* @param string $parameter
* @throws Exception
* @return array
*/
private function extractModuleAndMethod($parameter)
{
$a = explode('.', $parameter);
if (count($a) != 2) {
throw new Exception("The method name is invalid. Expected 'module.methodName'");
}
return $a;
}
/**
* Helper method that processes an API request in one line using the variables in `$_GET`
* and `$_POST`.
*
* @param string $method The API method to call, ie, `'Actions.getPageTitles'`.
* @param array $paramOverride The parameter name-value pairs to use instead of what's
* in `$_GET` & `$_POST`.
* @param array $defaultRequest Default query parameters. If a query parameter is absent in `$request`, it will be loaded
* from this. Defaults to `$_GET + $_POST`.
*
* To avoid using any parameters from $_GET or $_POST, set this to an empty `array()`.
* @return mixed The result of the API request. See {@link process()}.
*/
public static function processRequest($method, $paramOverride = array(), $defaultRequest = null)
{
$params = array();
$params['format'] = 'original';
$params['serialize'] = '0';
$params['module'] = 'API';
$params['method'] = $method;
$params = $paramOverride + $params;
// process request
$request = new Request($params, $defaultRequest);
return $request->process();
}
/**
* Returns the original request parameters in the current query string as an array mapping
* query parameter names with values. The result of this function will not be affected
* by any modifications to `$_GET` and will not include parameters in `$_POST`.
*
* @return array
*/
public static function getRequestParametersGET()
{
if (empty($_SERVER['QUERY_STRING'])) {
return array();
}
$GET = UrlHelper::getArrayFromQueryString($_SERVER['QUERY_STRING']);
return $GET;
}
/**
* Returns the URL for the current requested report w/o any filter parameters.
*
* @param string $module The API module.
* @param string $action The API action.
* @param array $queryParams Query parameter overrides.
* @return string
*/
public static function getBaseReportUrl($module, $action, $queryParams = array())
{
$params = array_merge($queryParams, array('module' => $module, 'action' => $action));
return Request::getCurrentUrlWithoutGenericFilters($params);
}
/**
* Returns the current URL without generic filter query parameters.
*
* @param array $params Query parameter values to override in the new URL.
* @return string
*/
public static function getCurrentUrlWithoutGenericFilters($params)
{
// unset all filter query params so the related report will show up in its default state,
// unless the filter param was in $queryParams
$genericFiltersInfo = DataTableGenericFilter::getGenericFiltersInformation();
foreach ($genericFiltersInfo as $filter) {
foreach ($filter[1] as $queryParamName => $queryParamInfo) {
if (!isset($params[$queryParamName])) {
$params[$queryParamName] = null;
}
}
}
return Url::getCurrentQueryStringWithParametersModified($params);
}
/**
* Returns whether the DataTable result will have to be expanded for the
* current request before rendering.
*
* @return bool
* @ignore
*/
public static function shouldLoadExpanded()
{
// if filter_column_recursive & filter_pattern_recursive are supplied, and flat isn't supplied
// we have to load all the child subtables.
return Common::getRequestVar('filter_column_recursive', false) !== false
&& Common::getRequestVar('filter_pattern_recursive', false) !== false
&& !self::shouldLoadFlatten();
}
/**
* @return bool
*/
public static function shouldLoadFlatten()
{
return Common::getRequestVar('flat', false) == 1;
}
/**
* Returns the segment query parameter from the original request, without modifications.
*
* @return array|bool
*/
public static function getRawSegmentFromRequest()
{
// we need the URL encoded segment parameter, we fetch it from _SERVER['QUERY_STRING'] instead of default URL decoded _GET
$segmentRaw = false;
$segment = Common::getRequestVar('segment', '', 'string');
if (!empty($segment)) {
$request = Request::getRequestParametersGET();
if (!empty($request['segment'])) {
$segmentRaw = $request['segment'];
}
}
return $segmentRaw;
}
private function renameModuleAndActionInRequest()
{
if (empty($this->request['apiModule'])) {
return;
}
if (empty($this->request['apiAction'])) {
$this->request['apiAction'] = null;
}
list($this->request['apiModule'], $this->request['apiAction']) = $this->getRenamedModuleAndAction($this->request['apiModule'], $this->request['apiAction']);
}
/**
* @return array
*/
private static function getDefaultRequest()
{
return $_GET + $_POST;
}
private function shouldDisablePostProcessing()
{
$shouldDisable = false;
/**
* After an API method returns a value, the value is post processed (eg, rows are sorted
* based on the `filter_sort_column` query parameter, rows are truncated based on the
* `filter_limit`/`filter_offset` parameters, amongst other things).
*
* If you're creating a plugin that needs to disable post processing entirely for
* certain requests, use this event.
*
* @param bool &$shouldDisable Set this to true to disable datatable post processing for a request.
* @param array $request The request parameters.
*/
Piwik::postEvent('Request.shouldDisablePostProcessing', [&$shouldDisable, $this->request]);
return $shouldDisable;
}
}

View File

@ -0,0 +1,233 @@
<?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\API;
use Exception;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\DataTable\Renderer;
use Piwik\DataTable\DataTableInterface;
use Piwik\DataTable\Filter\ColumnDelete;
use Piwik\DataTable\Filter\Pattern;
use Piwik\Plugins\Monolog\Processor\ExceptionToTextProcessor;
/**
*/
class ResponseBuilder
{
private $outputFormat = null;
private $apiRenderer = null;
private $request = null;
private $sendHeader = true;
private $postProcessDataTable = true;
private $apiModule = false;
private $apiMethod = false;
private $shouldPrintBacktrace = false;
/**
* @param string $outputFormat
* @param array $request
*/
public function __construct($outputFormat, $request = array(), $shouldPrintBacktrace = null)
{
$this->outputFormat = $outputFormat;
$this->request = $request;
$this->apiRenderer = ApiRenderer::factory($outputFormat, $request);
$this->shouldPrintBacktrace = $shouldPrintBacktrace === null ? \Piwik_ShouldPrintBackTraceWithMessage() : $shouldPrintBacktrace;
}
public function disableSendHeader()
{
$this->sendHeader = false;
}
public function disableDataTablePostProcessor()
{
$this->postProcessDataTable = false;
}
/**
* This method processes the data resulting from the API call.
*
* - If the data resulted from the API call is a DataTable then
* - we apply the standard filters if the parameters have been found
* in the URL. For example to offset,limit the Table you can add the following parameters to any API
* call that returns a DataTable: filter_limit=10&filter_offset=20
* - we apply the filters that have been previously queued on the DataTable
* @see DataTable::queueFilter()
* - we apply the renderer that generate the DataTable in a given format (XML, PHP, HTML, JSON, etc.)
* the format can be changed using the 'format' parameter in the request.
* Example: format=xml
*
* - If there is nothing returned (void) we display a standard success message
*
* - If there is a PHP array returned, we try to convert it to a dataTable
* It is then possible to convert this datatable to any requested format (xml/etc)
*
* - If a bool is returned we convert to a string (true is displayed as 'true' false as 'false')
*
* - If an integer / float is returned, we simply return it
*
* @param mixed $value The initial returned value, before post process. If set to null, success response is returned.
* @param bool|string $apiModule The API module that was called
* @param bool|string $apiMethod The API method that was called
* @return mixed Usually a string, but can still be a PHP data structure if the format requested is 'original'
*/
public function getResponse($value = null, $apiModule = false, $apiMethod = false)
{
$this->apiModule = $apiModule;
$this->apiMethod = $apiMethod;
$this->sendHeaderIfEnabled();
// when null or void is returned from the api call, we handle it as a successful operation
if (!isset($value)) {
if (ob_get_contents()) {
return null;
}
return $this->apiRenderer->renderSuccess('ok');
}
// If the returned value is an object DataTable we
// apply the set of generic filters if asked in the URL
// and we render the DataTable according to the format specified in the URL
if ($value instanceof DataTableInterface) {
return $this->handleDataTable($value);
}
// Case an array is returned from the API call, we convert it to the requested format
// - if calling from inside the application (format = original)
// => the data stays unchanged (ie. a standard php array or whatever data structure)
// - if any other format is requested, we have to convert this data structure (which we assume
// to be an array) to a DataTable in order to apply the requested DataTable_Renderer (for example XML)
if (is_array($value)) {
return $this->handleArray($value);
}
if (is_object($value)) {
return $this->apiRenderer->renderObject($value);
}
if (is_resource($value)) {
return $this->apiRenderer->renderResource($value);
}
return $this->apiRenderer->renderScalar($value);
}
/**
* Returns an error $message in the requested $format
*
* @param Exception|\Throwable $e
* @throws Exception
* @return string
*/
public function getResponseException($e)
{
$e = $this->decorateExceptionWithDebugTrace($e);
$message = $this->formatExceptionMessage($e);
$this->sendHeaderIfEnabled();
return $this->apiRenderer->renderException($message, $e);
}
/**
* @param Exception|\Throwable $e
* @return Exception
*/
private function decorateExceptionWithDebugTrace($e)
{
// If we are in tests, show full backtrace
if (defined('PIWIK_PATH_TEST_TO_ROOT')) {
if ($this->shouldPrintBacktrace) {
$message = $e->getMessage() . " in \n " . $e->getFile() . ":" . $e->getLine() . " \n " . $e->getTraceAsString();
} else {
$message = $e->getMessage() . "\n \n --> To temporarily debug this error further, set const PIWIK_PRINT_ERROR_BACKTRACE=true; in index.php";
}
return new Exception($message);
}
return $e;
}
/**
* @param Exception|\Throwable $exception
* @return string
*/
private function formatExceptionMessage($exception)
{
$message = ExceptionToTextProcessor::getWholeBacktrace($exception, $this->shouldPrintBacktrace);
return Renderer::formatValueXml($message);
}
private function handleDataTable(DataTableInterface $datatable)
{
if ($this->postProcessDataTable) {
$postProcessor = new DataTablePostProcessor($this->apiModule, $this->apiMethod, $this->request);
$datatable = $postProcessor->process($datatable);
}
return $this->apiRenderer->renderDataTable($datatable);
}
private function handleArray($array)
{
$firstArray = null;
$firstKey = null;
if (!empty($array)) {
$firstArray = reset($array);
$firstKey = key($array);
}
$isAssoc = !empty($firstArray) && is_numeric($firstKey) && is_array($firstArray) && count(array_filter(array_keys($firstArray), 'is_string'));
if (is_numeric($firstKey)) {
$columns = Common::getRequestVar('filter_column', false, 'array', $this->request);
$pattern = Common::getRequestVar('filter_pattern', '', 'string', $this->request);
if ($columns != array(false) && $pattern !== '') {
$pattern = new Pattern(new DataTable(), $columns, $pattern);
$array = $pattern->filterArray($array);
}
$limit = Common::getRequestVar('filter_limit', -1, 'integer', $this->request);
$offset = Common::getRequestVar('filter_offset', '0', 'integer', $this->request);
if ($limit >= 0 || $offset > 0) {
if ($limit < 0) {
$limit = null; // make sure to return all results from offset
}
$array = array_slice($array, $offset, $limit, $preserveKeys = false);
}
}
if ($isAssoc) {
$hideColumns = Common::getRequestVar('hideColumns', '', 'string', $this->request);
$showColumns = Common::getRequestVar('showColumns', '', 'string', $this->request);
if ($hideColumns !== '' || $showColumns !== '') {
$columnDelete = new ColumnDelete(new DataTable(), $hideColumns, $showColumns);
$array = $columnDelete->filter($array);
}
}
return $this->apiRenderer->renderArray($array);
}
private function sendHeaderIfEnabled()
{
if ($this->sendHeader) {
$this->apiRenderer->sendHeader();
}
}
}

View File

@ -0,0 +1,700 @@
<?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;
use Exception;
use Piwik\Access\CapabilitiesProvider;
use Piwik\Access\RolesProvider;
use Piwik\Container\StaticContainer;
use Piwik\Plugins\SitesManager\API as SitesManagerApi;
/**
* Singleton that manages user access to Piwik resources.
*
* To check whether a user has access to a resource, use one of the {@link Piwik Piwik::checkUser...}
* methods.
*
* In Piwik there are four different access levels:
*
* - **no access**: Users with this access level cannot view the resource.
* - **view access**: Users with this access level can view the resource, but cannot modify it.
* - **admin access**: Users with this access level can view and modify the resource.
* - **Super User access**: Only the Super User has this access level. It means the user can do
* whatever they want.
*
* Super user access is required to set some configuration options.
* All other options are specific to the user or to a website.
*
* Access is granted per website. Uses with access for a website can view all
* data associated with that website.
*
*/
class Access
{
/**
* Array of idsites available to the current user, indexed by permission level
* @see getSitesIdWith*()
*
* @var array
*/
protected $idsitesByAccess = null;
/**
* Login of the current user
*
* @var string
*/
protected $login = null;
/**
* token_auth of the current user
*
* @var string
*/
protected $token_auth = null;
/**
* Defines if the current user is the Super User
* @see hasSuperUserAccess()
*
* @var bool
*/
protected $hasSuperUserAccess = false;
/**
* Authentification object (see Auth)
*
* @var Auth
*/
private $auth = null;
/**
* Gets the singleton instance. Creates it if necessary.
*
* @return self
*/
public static function getInstance()
{
return StaticContainer::get('Piwik\Access');
}
/**
* @var CapabilitiesProvider
*/
protected $capabilityProvider;
/**
* @var RolesProvider
*/
private $roleProvider;
/**
* Constructor
*/
public function __construct(RolesProvider $roleProvider = null, CapabilitiesProvider $capabilityProvider = null)
{
if (!isset($roleProvider)) {
$roleProvider = StaticContainer::get('Piwik\Access\RolesProvider');
}
if (!isset($capabilityProvider)) {
$capabilityProvider = StaticContainer::get('Piwik\Access\CapabilitiesProvider');
}
$this->roleProvider = $roleProvider;
$this->capabilityProvider = $capabilityProvider;
$this->resetSites();
}
private function resetSites()
{
$this->idsitesByAccess = array(
'view' => array(),
'write' => array(),
'admin' => array(),
'superuser' => array()
);
}
/**
* Loads the access levels for the current user.
*
* Calls the authentication method to try to log the user in the system.
* If the user credentials are not correct we don't load anything.
* If the login/password is correct the user is either the SuperUser or a normal user.
* We load the access levels for this user for all the websites.
*
* @param null|Auth $auth Auth adapter
* @return bool true on success, false if reloading access failed (when auth object wasn't specified and user is not enforced to be Super User)
*/
public function reloadAccess(Auth $auth = null)
{
$this->resetSites();
if (isset($auth)) {
$this->auth = $auth;
}
if ($this->hasSuperUserAccess()) {
$this->makeSureLoginNameIsSet();
return true;
}
$this->token_auth = null;
$this->login = null;
// if the Auth wasn't set, we may be in the special case of setSuperUser(), otherwise we fail TODO: docs + review
if (!isset($this->auth)) {
return false;
}
// access = array ( idsite => accessIdSite, idsite2 => accessIdSite2)
$result = $this->auth->authenticate();
if (!$result->wasAuthenticationSuccessful()) {
return false;
}
$this->login = $result->getIdentity();
$this->token_auth = $result->getTokenAuth();
// case the superUser is logged in
if ($result->hasSuperUserAccess()) {
$this->setSuperUserAccess(true);
}
return true;
}
public function getRawSitesWithSomeViewAccess($login)
{
$sql = self::getSqlAccessSite("access, t2.idsite");
return Db::fetchAll($sql, $login);
}
/**
* Returns the SQL query joining sites and access table for a given login
*
* @param string $select Columns or expression to SELECT FROM table, eg. "MIN(ts_created)"
* @return string SQL query
*/
public static function getSqlAccessSite($select)
{
$access = Common::prefixTable('access');
$siteTable = Common::prefixTable('site');
return "SELECT " . $select . " FROM " . $access . " as t1
JOIN " . $siteTable . " as t2 USING (idsite) WHERE login = ?";
}
/**
* Make sure a login name is set
*
* @return true
*/
protected function makeSureLoginNameIsSet()
{
if (empty($this->login)) {
// flag to force non empty login so Super User is not mistaken for anonymous
$this->login = 'super user was set';
}
}
protected function loadSitesIfNeeded()
{
if ($this->hasSuperUserAccess) {
if (empty($this->idsitesByAccess['superuser'])) {
try {
$api = SitesManagerApi::getInstance();
$allSitesId = $api->getAllSitesId();
} catch (\Exception $e) {
$allSitesId = array();
}
$this->idsitesByAccess['superuser'] = $allSitesId;
}
} elseif (isset($this->login)) {
if (empty($this->idsitesByAccess['view'])
&& empty($this->idsitesByAccess['write'])
&& empty($this->idsitesByAccess['admin'])
) {
// we join with site in case there are rows in access for an idsite that doesn't exist anymore
// (backward compatibility ; before we deleted the site without deleting rows in _access table)
$accessRaw = $this->getRawSitesWithSomeViewAccess($this->login);
foreach ($accessRaw as $access) {
$accessType = $access['access'];
$this->idsitesByAccess[$accessType][] = $access['idsite'];
if ($this->roleProvider->isValidRole($accessType)) {
foreach ($this->capabilityProvider->getAllCapabilities() as $capability) {
if ($capability->hasRoleCapability($accessType)) {
// we automatically add this capability
if (!isset($this->idsitesByAccess[$capability->getId()])) {
$this->idsitesByAccess[$capability->getId()] = array();
}
$this->idsitesByAccess[$capability->getId()][] = $access['idsite'];
}
}
}
}
/**
* Triggered after the initial access levels and permissions for the current user are loaded. Use this
* event to modify the current user's permissions (for example, making sure every user has view access
* to a specific site).
*
* **Example**
*
* function (&$idsitesByAccess, $login) {
* if ($login == 'somespecialuser') {
* return;
* }
*
* $idsitesByAccess['view'][] = $mySpecialIdSite;
* }
*
* @param array[] &$idsitesByAccess The current user's access levels for individual sites. Maps role and
* capability IDs to list of site IDs, eg:
*
* ```
* [
* 'view' => [1, 2, 3],
* 'write' => [4, 5],
* 'admin' => [],
* ]
* ```
* @param string $login The current user's login.
*/
Piwik::postEvent('Access.modifyUserAccess', [&$this->idsitesByAccess, $this->login]);
}
}
}
/**
* We bypass the normal auth method and give the current user Super User rights.
* This should be very carefully used.
*
* @param bool $bool
*/
public function setSuperUserAccess($bool = true)
{
$this->hasSuperUserAccess = (bool) $bool;
if ($bool) {
$this->makeSureLoginNameIsSet();
} else {
$this->resetSites();
}
}
/**
* Returns true if the current user is logged in as the Super User
*
* @return bool
*/
public function hasSuperUserAccess()
{
return $this->hasSuperUserAccess;
}
/**
* Returns the current user login
*
* @return string|null
*/
public function getLogin()
{
return $this->login;
}
/**
* Returns the token_auth used to authenticate this user in the API
*
* @return string|null
*/
public function getTokenAuth()
{
return $this->token_auth;
}
/**
* Returns an array of ID sites for which the user has at least a VIEW access.
* Which means VIEW OR WRITE or ADMIN or SUPERUSER.
*
* @return array Example if the user is ADMIN for 4
* and has VIEW access for 1 and 7, it returns array(1, 4, 7);
*/
public function getSitesIdWithAtLeastViewAccess()
{
$this->loadSitesIfNeeded();
return array_unique(array_merge(
$this->idsitesByAccess['view'],
$this->idsitesByAccess['write'],
$this->idsitesByAccess['admin'],
$this->idsitesByAccess['superuser'])
);
}
/**
* Returns an array of ID sites for which the user has at least a WRITE access.
* Which means WRITE or ADMIN or SUPERUSER.
*
* @return array Example if the user is WRITE for 4 and 8
* and has VIEW access for 1 and 7, it returns array(4, 8);
*/
public function getSitesIdWithAtLeastWriteAccess()
{
$this->loadSitesIfNeeded();
return array_unique(array_merge(
$this->idsitesByAccess['write'],
$this->idsitesByAccess['admin'],
$this->idsitesByAccess['superuser'])
);
}
/**
* Returns an array of ID sites for which the user has an ADMIN access.
*
* @return array Example if the user is ADMIN for 4 and 8
* and has VIEW access for 1 and 7, it returns array(4, 8);
*/
public function getSitesIdWithAdminAccess()
{
$this->loadSitesIfNeeded();
return array_unique(array_merge(
$this->idsitesByAccess['admin'],
$this->idsitesByAccess['superuser'])
);
}
/**
* Returns an array of ID sites for which the user has a VIEW access only.
*
* @return array Example if the user is ADMIN for 4
* and has VIEW access for 1 and 7, it returns array(1, 7);
* @see getSitesIdWithAtLeastViewAccess()
*/
public function getSitesIdWithViewAccess()
{
$this->loadSitesIfNeeded();
return $this->idsitesByAccess['view'];
}
/**
* Returns an array of ID sites for which the user has a WRITE access only.
*
* @return array Example if the user is ADMIN for 4
* and has WRITE access for 1 and 7, it returns array(1, 7);
* @see getSitesIdWithAtLeastWriteAccess()
*/
public function getSitesIdWithWriteAccess()
{
$this->loadSitesIfNeeded();
return $this->idsitesByAccess['write'];
}
/**
* Throws an exception if the user is not the SuperUser
*
* @throws \Piwik\NoAccessException
*/
public function checkUserHasSuperUserAccess()
{
if (!$this->hasSuperUserAccess()) {
throw new NoAccessException(Piwik::translate('General_ExceptionPrivilege', array("'superuser'")));
}
}
/**
* Returns `true` if the current user has admin access to at least one site.
*
* @return bool
*/
public function isUserHasSomeWriteAccess()
{
if ($this->hasSuperUserAccess()) {
return true;
}
$idSitesAccessible = $this->getSitesIdWithAtLeastWriteAccess();
return count($idSitesAccessible) > 0;
}
/**
* Returns `true` if the current user has admin access to at least one site.
*
* @return bool
*/
public function isUserHasSomeAdminAccess()
{
if ($this->hasSuperUserAccess()) {
return true;
}
$idSitesAccessible = $this->getSitesIdWithAdminAccess();
return count($idSitesAccessible) > 0;
}
/**
* If the user doesn't have an WRITE access for at least one website, throws an exception
*
* @throws \Piwik\NoAccessException
*/
public function checkUserHasSomeWriteAccess()
{
if (!$this->isUserHasSomeWriteAccess()) {
throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('write')));
}
}
/**
* If the user doesn't have an ADMIN access for at least one website, throws an exception
*
* @throws \Piwik\NoAccessException
*/
public function checkUserHasSomeAdminAccess()
{
if (!$this->isUserHasSomeAdminAccess()) {
throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('admin')));
}
}
/**
* If the user doesn't have any view permission, throw exception
*
* @throws \Piwik\NoAccessException
*/
public function checkUserHasSomeViewAccess()
{
if ($this->hasSuperUserAccess()) {
return;
}
$idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess();
if (count($idSitesAccessible) == 0) {
throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('view')));
}
}
/**
* This method checks that the user has ADMIN access for the given list of websites.
* If the user doesn't have ADMIN access for at least one website of the list, we throw an exception.
*
* @param int|array $idSites List of ID sites to check
* @throws \Piwik\NoAccessException If for any of the websites the user doesn't have an ADMIN access
*/
public function checkUserHasAdminAccess($idSites)
{
if ($this->hasSuperUserAccess()) {
return;
}
$idSites = $this->getIdSites($idSites);
$idSitesAccessible = $this->getSitesIdWithAdminAccess();
foreach ($idSites as $idsite) {
if (!in_array($idsite, $idSitesAccessible)) {
throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'admin'", $idsite)));
}
}
}
/**
* This method checks that the user has VIEW or ADMIN access for the given list of websites.
* If the user doesn't have VIEW or ADMIN access for at least one website of the list, we throw an exception.
*
* @param int|array|string $idSites List of ID sites to check (integer, array of integers, string comma separated list of integers)
* @throws \Piwik\NoAccessException If for any of the websites the user doesn't have an VIEW or ADMIN access
*/
public function checkUserHasViewAccess($idSites)
{
if ($this->hasSuperUserAccess()) {
return;
}
$idSites = $this->getIdSites($idSites);
$idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess();
foreach ($idSites as $idsite) {
if (!in_array($idsite, $idSitesAccessible)) {
throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'view'", $idsite)));
}
}
}
/**
* This method checks that the user has VIEW or ADMIN access for the given list of websites.
* If the user doesn't have VIEW or ADMIN access for at least one website of the list, we throw an exception.
*
* @param int|array|string $idSites List of ID sites to check (integer, array of integers, string comma separated list of integers)
* @throws \Piwik\NoAccessException If for any of the websites the user doesn't have an VIEW or ADMIN access
*/
public function checkUserHasWriteAccess($idSites)
{
if ($this->hasSuperUserAccess()) {
return;
}
$idSites = $this->getIdSites($idSites);
$idSitesAccessible = $this->getSitesIdWithAtLeastWriteAccess();
foreach ($idSites as $idsite) {
if (!in_array($idsite, $idSitesAccessible)) {
throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'write'", $idsite)));
}
}
}
private function getSitesIdWithCapability($capability)
{
if (!empty($this->idsitesByAccess[$capability])) {
return $this->idsitesByAccess[$capability];
}
return array();
}
public function checkUserHasCapability($idSites, $capability)
{
if ($this->hasSuperUserAccess()) {
return;
}
$idSites = $this->getIdSites($idSites);
$idSitesAccessible = $this->getSitesIdWithCapability($capability);
foreach ($idSites as $idsite) {
if (!in_array($idsite, $idSitesAccessible)) {
throw new NoAccessException(Piwik::translate('ExceptionCapabilityAccessWebsite', array("'" . $capability ."'", $idsite)));
}
}
// a capability applies only when the user also has at least view access
$this->checkUserHasViewAccess($idSites);
}
/**
* @param int|array|string $idSites
* @return array
* @throws \Piwik\NoAccessException
*/
protected function getIdSites($idSites)
{
if ($idSites === 'all') {
$idSites = $this->getSitesIdWithAtLeastViewAccess();
}
$idSites = Site::getIdSitesFromIdSitesString($idSites);
if (empty($idSites)) {
throw new NoAccessException("The parameter 'idSite=' is missing from the request.");
}
return $idSites;
}
/**
* Executes a callback with superuser privileges, making sure those privileges are rescinded
* before this method exits. Privileges will be rescinded even if an exception is thrown.
*
* @param callback $function The callback to execute. Should accept no arguments.
* @return mixed The result of `$function`.
* @throws Exception rethrows any exceptions thrown by `$function`.
* @api
*/
public static function doAsSuperUser($function)
{
$isSuperUser = self::getInstance()->hasSuperUserAccess();
$access = self::getInstance();
$login = $access->getLogin();
$shouldResetLogin = empty($login); // make sure to reset login if a login was set by "makeSureLoginNameIsSet()"
$access->setSuperUserAccess(true);
try {
$result = $function();
} catch (Exception $ex) {
$access->setSuperUserAccess($isSuperUser);
if ($shouldResetLogin) {
$access->login = null;
}
throw $ex;
}
if ($shouldResetLogin) {
$access->login = null;
}
$access->setSuperUserAccess($isSuperUser);
return $result;
}
/**
* Returns the level of access the current user has to the given site.
*
* @param int $idSite The site to check.
* @return string The access level, eg, 'view', 'admin', 'noaccess'.
*/
public function getRoleForSite($idSite)
{
if ($this->hasSuperUserAccess
|| in_array($idSite, $this->getSitesIdWithAdminAccess())
) {
return 'admin';
}
if (in_array($idSite, $this->getSitesIdWithWriteAccess())) {
return 'write';
}
if (in_array($idSite, $this->getSitesIdWithViewAccess())) {
return 'view';
}
return 'noaccess';
}
/**
* Returns the capabilities the current user has for a given site.
*
* @param int $idSite The site to check.
* @return string[] The capabilities the user has.
*/
public function getCapabilitiesForSite($idSite)
{
$result = [];
foreach ($this->capabilityProvider->getAllCapabilityIds() as $capabilityId) {
if (empty($this->idsitesByAccess[$capabilityId])) {
continue;
}
if (in_array($idSite, $this->idsitesByAccess[$capabilityId])) {
$result[] = $capabilityId;
}
}
return $result;
}
}
/**
* Exception thrown when a user doesn't have sufficient access to a resource.
*
* @api
*/
class NoAccessException extends \Exception
{
}

View File

@ -0,0 +1,123 @@
<?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\Access;
use Exception;
use Piwik\CacheId;
use Piwik\Piwik;
use Piwik\Cache as PiwikCache;
class CapabilitiesProvider
{
/**
* @return Capability[]
*/
public function getAllCapabilities()
{
$cacheId = CacheId::siteAware(CacheId::languageAware('Capabilities'));
$cache = PiwikCache::getTransientCache();
if (!$cache->contains($cacheId)) {
$capabilities = array();
/**
* Triggered to add new capabilities.
*
* **Example**
*
* public function addCapabilities(&$capabilities)
* {
* $capabilities[] = new MyNewCapabilitiy();
* }
*
* @param Capability[] $reports An array of reports
* @internal
*/
Piwik::postEvent('Access.Capability.addCapabilities', array(&$capabilities));
/**
* Triggered to filter / restrict capabilities.
*
* **Example**
*
* public function filterCapabilities(&$capabilities)
* {
* foreach ($capabilities as $index => $capability) {
* if ($capability->getId() === 'tagmanager_write') {}
* unset($capabilities[$index]); // remove the given capability
* }
* }
* }
*
* @param Capability[] $reports An array of reports
* @internal
*/
Piwik::postEvent('Access.Capability.filterCapabilities', array(&$capabilities));
$capabilities = array_values($capabilities);
$this->checkCapabilityIds($capabilities);
$cache->save($cacheId, $capabilities);
return $capabilities;
}
return $cache->fetch($cacheId);
}
/**
* @param $capabilityId
* @return Capability|null
*/
public function getCapability($capabilityId)
{
foreach ($this->getAllCapabilities() as $capability) {
if ($capabilityId === $capability->getId()) {
return $capability;
}
}
}
public function getAllCapabilityIds()
{
$ids = array();
foreach ($this->getAllCapabilities() as $capability) {
$ids[] = $capability->getId();
}
return $ids;
}
public function isValidCapability($capabilityId)
{
$capabilities = $this->getAllCapabilityIds();
return in_array($capabilityId, $capabilities, true);
}
public function checkValidCapability($capabilityId)
{
if (!$this->isValidCapability($capabilityId)) {
$capabilities = $this->getAllCapabilityIds();
throw new Exception(Piwik::translate("UsersManager_ExceptionAccessValues", implode(", ", $capabilities)));
}
}
/**
* @param Capability[] $capabilities
*/
private function checkCapabilityIds($capabilities)
{
foreach ($capabilities as $capability) {
$id = $capability->getId();
if (preg_match('/[^a-zA-Z0-9_-]/', $id)) {
throw new \Exception("Capability with invalid ID found: '$id'. Valid characters are 'a-zA-Z0-9_-'.");
}
}
}
}

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\Access;
abstract class Capability
{
abstract public function getId();
abstract public function getName();
abstract public function getCategory();
abstract public function getDescription();
abstract public function getIncludedInRoles();
public function getHelpUrl()
{
return '';
}
public function hasRoleCapability($idRole)
{
return in_array($idRole, $this->getIncludedInRoles(), true);
}
}

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\Access;
abstract class Role
{
abstract public function getName();
abstract public function getId();
abstract public function getDescription();
public function getHelpUrl()
{
return '';
}
}

View File

@ -0,0 +1,40 @@
<?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\Access\Role;
use Piwik\Access\Role;
use Piwik\Piwik;
class Admin extends Role
{
const ID = 'admin';
public function getName()
{
return Piwik::translate('UsersManager_PrivAdmin');
}
public function getId()
{
return self::ID;
}
public function getDescription()
{
return Piwik::translate('UsersManager_PrivAdminDescription', array(
Piwik::translate('UsersManager_PrivWrite')
));
}
public function getHelpUrl()
{
return 'https://matomo.org/faq/general/faq_69/';
}
}

View File

@ -0,0 +1,39 @@
<?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\Access\Role;
use Piwik\Access\Role;
use Piwik\Piwik;
class View extends Role
{
const ID = 'view';
public function getName()
{
return Piwik::translate('UsersManager_PrivView');
}
public function getId()
{
return self::ID;
}
public function getDescription()
{
return Piwik::translate('UsersManager_PrivViewDescription');
}
public function getHelpUrl()
{
return 'https://matomo.org/faq/general/faq_70/';
}
}

View File

@ -0,0 +1,38 @@
<?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\Access\Role;
use Piwik\Access\Role;
use Piwik\Piwik;
class Write extends Role
{
const ID = 'write';
public function getName()
{
return Piwik::translate('UsersManager_PrivWrite');
}
public function getId()
{
return self::ID;
}
public function getDescription()
{
return Piwik::translate('UsersManager_PrivWriteDescription');
}
public function getHelpUrl()
{
return '';
}
}

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\Access;
use Piwik\Access\Role\Admin;
use Piwik\Access\Role\View;
use Piwik\Access\Role\Write;
use Piwik\Piwik;
use Exception;
class RolesProvider
{
/**
* @return Role[]
*/
public function getAllRoles()
{
return array(
new View(),
new Write(),
new Admin()
);
}
/**
* Returns the list of the existing Access level.
* Useful when a given API method requests a given acccess Level.
* We first check that the required access level exists.
*
* @return array
*/
public function getAllRoleIds()
{
$ids = array();
foreach ($this->getAllRoles() as $role) {
$ids[] = $role->getId();
}
return $ids;
}
public function isValidRole($roleId)
{
$roles = $this->getAllRoleIds();
return in_array($roleId, $roles, true);
}
public function checkValidRole($roleId)
{
if (!$this->isValidRole($roleId)) {
$roles = $this->getAllRoleIds();
throw new Exception(Piwik::translate("UsersManager_ExceptionAccessValues", implode(", ", $roles)));
}
}
}

View File

@ -0,0 +1,246 @@
<?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\Application;
use DI\Container;
use Piwik\Application\Kernel\EnvironmentValidator;
use Piwik\Application\Kernel\GlobalSettingsProvider;
use Piwik\Application\Kernel\PluginList;
use Piwik\Container\ContainerFactory;
use Piwik\Container\StaticContainer;
use Piwik\Piwik;
/**
* Encapsulates Piwik environment setup and access.
*
* The Piwik environment consists of two main parts: the kernel and the DI container.
*
* The 'kernel' is the core part of Piwik that cannot be modified / extended through the DI container.
* It includes components that are required to create the DI container.
*
* Currently the only objects in the 'kernel' are a GlobalSettingsProvider object and a
* PluginList object. The GlobalSettingsProvider object is required for the current PluginList
* implementation and for checking whether Development mode is enabled. The PluginList is
* needed in order to determine what plugins are activated, since plugins can provide their
* own DI configuration.
*
* The DI container contains every other Piwik object, including the Plugin\Manager,
* plugin API instances, dependent services, etc. Plugins and users can override/extend
* the objects in this container.
*
* NOTE: DI support in Piwik is currently a work in process; not everything is currently
* stored in the DI container, but we are working towards this.
*/
class Environment
{
/**
* @internal
* @var EnvironmentManipulator
*/
private static $globalEnvironmentManipulator = null;
/**
* @var string
*/
private $environment;
/**
* @var array
*/
private $definitions;
/**
* @var Container
*/
private $container;
/**
* @var GlobalSettingsProvider
*/
private $globalSettingsProvider;
/**
* @var PluginList
*/
private $pluginList;
/**
* @param string $environment
* @param array $definitions
*/
public function __construct($environment, array $definitions = array())
{
$this->environment = $environment;
$this->definitions = $definitions;
}
/**
* Initializes the kernel globals and DI container.
*/
public function init()
{
$this->invokeBeforeContainerCreatedHook();
$this->container = $this->createContainer();
StaticContainer::push($this->container);
$this->validateEnvironment();
$this->invokeEnvironmentBootstrappedHook();
Piwik::postEvent('Environment.bootstrapped'); // this event should be removed eventually
}
/**
* Destroys an environment. MUST be called when embedding environments.
*/
public function destroy()
{
StaticContainer::pop();
}
/**
* Returns the DI container. All Piwik objects for a specific Piwik instance should be stored
* in this container.
*
* @return Container
*/
public function getContainer()
{
return $this->container;
}
/**
* @link http://php-di.org/doc/container-configuration.html
*/
private function createContainer()
{
$pluginList = $this->getPluginListCached();
$settings = $this->getGlobalSettingsCached();
$extraDefinitions = $this->getExtraDefinitionsFromManipulators();
$definitions = array_merge(StaticContainer::getDefinitions(), $extraDefinitions, array($this->definitions));
$environments = array($this->environment);
$environments = array_merge($environments, $this->getExtraEnvironmentsFromManipulators());
$containerFactory = new ContainerFactory($pluginList, $settings, $environments, $definitions);
return $containerFactory->create();
}
protected function getGlobalSettingsCached()
{
if ($this->globalSettingsProvider === null) {
$original = $this->getGlobalSettings();
$globalSettingsProvider = $this->getGlobalSettingsProviderOverride($original);
$this->globalSettingsProvider = $globalSettingsProvider ?: $original;
}
return $this->globalSettingsProvider;
}
protected function getPluginListCached()
{
if ($this->pluginList === null) {
$pluginList = $this->getPluginListOverride();
$this->pluginList = $pluginList ?: $this->getPluginList();
}
return $this->pluginList;
}
/**
* Returns the kernel global GlobalSettingsProvider object. Derived classes can override this method
* to provide a different implementation.
*
* @return null|GlobalSettingsProvider
*/
protected function getGlobalSettings()
{
return new GlobalSettingsProvider();
}
/**
* Returns the kernel global PluginList object. Derived classes can override this method to
* provide a different implementation.
*
* @return PluginList
*/
protected function getPluginList()
{
// TODO: in tracker should only load tracker plugins. can't do properly until tracker entrypoint is encapsulated.
return new PluginList($this->getGlobalSettingsCached());
}
private function validateEnvironment()
{
/** @var EnvironmentValidator $validator */
$validator = $this->container->get('Piwik\Application\Kernel\EnvironmentValidator');
$validator->validate();
}
/**
* @param EnvironmentManipulator $manipulator
* @internal
*/
public static function setGlobalEnvironmentManipulator(EnvironmentManipulator $manipulator)
{
self::$globalEnvironmentManipulator = $manipulator;
}
private function getGlobalSettingsProviderOverride(GlobalSettingsProvider $original)
{
if (self::$globalEnvironmentManipulator) {
return self::$globalEnvironmentManipulator->makeGlobalSettingsProvider($original);
} else {
return null;
}
}
private function invokeBeforeContainerCreatedHook()
{
if (self::$globalEnvironmentManipulator) {
return self::$globalEnvironmentManipulator->beforeContainerCreated();
}
}
private function getExtraDefinitionsFromManipulators()
{
if (self::$globalEnvironmentManipulator) {
return self::$globalEnvironmentManipulator->getExtraDefinitions();
} else {
return array();
}
}
private function invokeEnvironmentBootstrappedHook()
{
if (self::$globalEnvironmentManipulator) {
self::$globalEnvironmentManipulator->onEnvironmentBootstrapped();
}
}
private function getExtraEnvironmentsFromManipulators()
{
if (self::$globalEnvironmentManipulator) {
return self::$globalEnvironmentManipulator->getExtraEnvironments();
} else {
return array();
}
}
private function getPluginListOverride()
{
if (self::$globalEnvironmentManipulator) {
return self::$globalEnvironmentManipulator->makePluginList($this->getGlobalSettingsCached());
} else {
return null;
}
}
}

View File

@ -0,0 +1,59 @@
<?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\Application;
use Piwik\Application\Kernel\GlobalSettingsProvider;
use Piwik\Application\Kernel\PluginList;
/**
* Used to manipulate Environment instances before the container is created.
* Only used by the testing environment setup code, shouldn't be used anywhere
* else.
*/
interface EnvironmentManipulator
{
/**
* Create a custom GlobalSettingsProvider kernel object, overriding the default behavior.
*
* @return GlobalSettingsProvider
*/
public function makeGlobalSettingsProvider(GlobalSettingsProvider $original);
/**
* Create a custom PluginList kernel object, overriding the default behavior.@deprecated
*
* @param GlobalSettingsProvider $globalSettingsProvider
* @return PluginList
*/
public function makePluginList(GlobalSettingsProvider $globalSettingsProvider);
/**
* Invoked before the container is created.
*/
public function beforeContainerCreated();
/**
* Return an array of definition arrays that override DI config specified in PHP config files.
*
* @return array[]
*/
public function getExtraDefinitions();
/**
* Invoked after the container is created and the environment is considered bootstrapped.
*/
public function onEnvironmentBootstrapped();
/**
* Return an array of environment names to apply after the normal environment.
*
* @return string[]
*/
public function getExtraEnvironments();
}

View File

@ -0,0 +1,143 @@
<?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\Application\Kernel;
use Piwik\Common;
use Piwik\Exception\NotYetInstalledException;
use Piwik\Filechecks;
use Piwik\Piwik;
use Piwik\SettingsPiwik;
use Piwik\SettingsServer;
use Piwik\Translation\Translator;
/**
* Validates the Piwik environment. This includes making sure the required config files
* are present, and triggering the correct behaviour if otherwise.
*/
class EnvironmentValidator
{
/**
* @var GlobalSettingsProvider
*/
protected $settingsProvider;
/**
* @var Translator
*/
protected $translator;
public function __construct(GlobalSettingsProvider $settingsProvider, Translator $translator)
{
$this->settingsProvider = $settingsProvider;
$this->translator = $translator;
}
public function validate()
{
$this->checkConfigFileExists($this->settingsProvider->getPathGlobal());
if(SettingsPiwik::isPiwikInstalled()) {
$this->checkConfigFileExists($this->settingsProvider->getPathLocal(), $startInstaller = false);
return;
}
$startInstaller = true;
if(SettingsServer::isTrackerApiRequest()) {
// if Piwik is not installed yet, the piwik.php should do nothing and not return an error
throw new NotYetInstalledException("As Matomo is not installed yet, the Tracking API cannot proceed and will exit without error.");
}
if(Common::isPhpCliMode()) {
// in CLI, do not start/redirect to installer, simply output the exception at the top
$startInstaller = false;
}
// Start the installation when config file not found
$this->checkConfigFileExists($this->settingsProvider->getPathLocal(), $startInstaller);
}
/**
* @param $path
* @param bool $startInstaller
* @throws \Exception
*/
private function checkConfigFileExists($path, $startInstaller = false)
{
if (is_readable($path)) {
return;
}
$message = $this->getSpecificMessageWhetherFileExistsOrNot($path);
$exception = new NotYetInstalledException($message);
if ($startInstaller) {
$this->startInstallation($exception);
} else {
throw $exception;
}
}
/**
* @param $exception
*/
private function startInstallation($exception)
{
/**
* Triggered when the configuration file cannot be found or read, which usually
* means Piwik is not installed yet.
*
* This event can be used to start the installation process or to display a custom error message.
*
* @param \Exception $exception The exception that was thrown by `Config::getInstance()`.
*/
Piwik::postEvent('Config.NoConfigurationFile', array($exception), $pending = true);
}
/**
* @param $path
* @return string
*/
private function getMessageWhenFileExistsButNotReadable($path)
{
$format = " \n<b>» %s </b>";
if(Common::isPhpCliMode()) {
$format = "\n » %s \n";
}
return sprintf($format,
$this->translator->translate('General_ExceptionConfigurationFilePleaseCheckReadableByUser',
array($path, Filechecks::getUser())));
}
/**
* @param $path
* @return string
*/
private function getSpecificMessageWhetherFileExistsOrNot($path)
{
if (!file_exists($path)) {
$message = $this->translator->translate('General_ExceptionConfigurationFileNotFound', array($path));
if (Common::isPhpCliMode()) {
$message .= $this->getMessageWhenFileExistsButNotReadable($path);
}
} else {
$message = $this->translator->translate('General_ExceptionConfigurationFileExistsButNotReadable',
array($path));
$message .= $this->getMessageWhenFileExistsButNotReadable($path);
}
if (Common::isPhpCliMode()) {
$message = "\n" . $message;
}
return $message;
}
}

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\Application\Kernel;
use Piwik\Config;
use Piwik\Config\IniFileChain;
/**
* Provides global settings. Global settings are organized in sections where
* each section contains a list of name => value pairs. Setting values can
* be primitive values or arrays of primitive values.
*
* Uses the config.ini.php, common.ini.php and global.ini.php files to provide global settings.
*
* At the moment a singleton instance of this class is used in order to get tests to pass.
*/
class GlobalSettingsProvider
{
/**
* @var IniFileChain
*/
protected $iniFileChain;
/**
* @var string
*/
protected $pathGlobal = null;
/**
* @var string
*/
protected $pathCommon = null;
/**
* @var string
*/
protected $pathLocal = null;
/**
* @param string|null $pathGlobal Path to the global.ini.php file. Or null to use the default.
* @param string|null $pathLocal Path to the config.ini.php file. Or null to use the default.
* @param string|null $pathCommon Path to the common.ini.php file. Or null to use the default.
*/
public function __construct($pathGlobal = null, $pathLocal = null, $pathCommon = null)
{
$this->pathGlobal = $pathGlobal ?: Config::getGlobalConfigPath();
$this->pathCommon = $pathCommon ?: Config::getCommonConfigPath();
$this->pathLocal = $pathLocal ?: Config::getLocalConfigPath();
$this->iniFileChain = new IniFileChain();
$this->reload();
}
public function reload($pathGlobal = null, $pathLocal = null, $pathCommon = null)
{
$this->pathGlobal = $pathGlobal ?: $this->pathGlobal;
$this->pathCommon = $pathCommon ?: $this->pathCommon;
$this->pathLocal = $pathLocal ?: $this->pathLocal;
$this->iniFileChain->reload(array($this->pathGlobal, $this->pathCommon), $this->pathLocal);
}
/**
* Returns a settings section.
*
* @param string $name
* @return array
*/
public function &getSection($name)
{
$section =& $this->iniFileChain->get($name);
return $section;
}
/**
* Sets a settings section.
*
* @param string $name
* @param array $value
*/
public function setSection($name, $value)
{
$this->iniFileChain->set($name, $value);
}
public function getIniFileChain()
{
return $this->iniFileChain;
}
public function getPathGlobal()
{
return $this->pathGlobal;
}
public function getPathLocal()
{
return $this->pathLocal;
}
public function getPathCommon()
{
return $this->pathCommon;
}
}

View File

@ -0,0 +1,193 @@
<?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\Application\Kernel;
use Piwik\Plugin\MetadataLoader;
/**
* Lists the currently activated plugins. Used when setting up Piwik's environment before
* initializing the DI container.
*
* Uses the [Plugins] section in Piwik's INI config to get the activated plugins.
*
* Depends on GlobalSettingsProvider being used.
*
* TODO: parts of Plugin\Manager edit the plugin list; maybe PluginList implementations should be mutable?
*/
class PluginList
{
/**
* @var GlobalSettingsProvider
*/
private $settings;
/**
* Plugins bundled with core package, disabled by default
* @var array
*/
private $corePluginsDisabledByDefault = array(
'DBStats',
'ExampleCommand',
'ExampleSettingsPlugin',
'ExampleUI',
'ExampleVisualization',
'ExamplePluginTemplate',
'ExampleTracker',
'ExampleLogTables',
'ExampleReport',
'MobileAppMeasurable',
'Provider',
'TagManager'
);
// Themes bundled with core package, disabled by default
private $coreThemesDisabledByDefault = array(
'ExampleTheme'
);
public function __construct(GlobalSettingsProvider $settings)
{
$this->settings = $settings;
}
/**
* Returns the list of plugins that should be loaded. Used by the container factory to
* load plugin specific DI overrides.
*
* @return string[]
*/
public function getActivatedPlugins()
{
$section = $this->settings->getSection('Plugins');
return @$section['Plugins'] ?: array();
}
/**
* Returns the list of plugins that are bundled with Piwik.
*
* @return string[]
*/
public function getPluginsBundledWithPiwik()
{
$pathGlobal = $this->settings->getPathGlobal();
$section = $this->settings->getIniFileChain()->getFrom($pathGlobal, 'Plugins');
return $section['Plugins'];
}
/**
* Returns the plugins bundled with core package that are disabled by default.
*
* @return string[]
*/
public function getCorePluginsDisabledByDefault()
{
return array_merge($this->corePluginsDisabledByDefault, $this->coreThemesDisabledByDefault);
}
/**
* Sorts an array of plugins in the order they should be loaded. We cannot use DI here as DI is not initialized
* at this stage.
*
* @params string[] $plugins
* @return \string[]
*/
public function sortPlugins(array $plugins)
{
$global = $this->getPluginsBundledWithPiwik();
if (empty($global)) {
return $plugins;
}
// we need to make sure a possibly disabled plugin will be still loaded before any 3rd party plugin
$global = array_merge($global, $this->corePluginsDisabledByDefault);
$global = array_values($global);
$plugins = array_values($plugins);
$defaultPluginsLoadedFirst = array_intersect($global, $plugins);
$otherPluginsToLoadAfterDefaultPlugins = array_diff($plugins, $defaultPluginsLoadedFirst);
// sort by name to have a predictable order for those extra plugins
natcasesort($otherPluginsToLoadAfterDefaultPlugins);
$sorted = array_merge($defaultPluginsLoadedFirst, $otherPluginsToLoadAfterDefaultPlugins);
return $sorted;
}
/**
* Sorts an array of plugins in the order they should be saved in config.ini.php. This basically influences
* the order of the plugin config.php and which config will be loaded first. We want to make sure to require the
* config or a required plugin first before loading the plugin that requires it.
*
* We do not sort using this logic on each request since it is much slower than `sortPlugins()`. The order
* of plugins in config.ini.php is only important for the ContainerFactory. During a regular request it is otherwise
* fine to load the plugins in the order of `sortPlugins()` since we will make sure that required plugins will be
* loaded first in plugin manager.
*
* @param string[] $plugins
* @param array[] $pluginJsonCache For internal testing only
* @return \string[]
*/
public function sortPluginsAndRespectDependencies(array $plugins, $pluginJsonCache = array())
{
$global = $this->getPluginsBundledWithPiwik();
if (empty($global)) {
return $plugins;
}
// we need to make sure a possibly disabled plugin will be still loaded before any 3rd party plugin
$global = array_merge($global, $this->corePluginsDisabledByDefault);
$global = array_values($global);
$plugins = array_values($plugins);
$defaultPluginsLoadedFirst = array_intersect($global, $plugins);
$otherPluginsToLoadAfterDefaultPlugins = array_diff($plugins, $defaultPluginsLoadedFirst);
// we still want to sort alphabetically by default
natcasesort($otherPluginsToLoadAfterDefaultPlugins);
$sorted = array();
foreach ($otherPluginsToLoadAfterDefaultPlugins as $pluginName) {
$sorted = $this->sortRequiredPlugin($pluginName, $pluginJsonCache, $otherPluginsToLoadAfterDefaultPlugins, $sorted);
}
$sorted = array_merge($defaultPluginsLoadedFirst, $sorted);
return $sorted;
}
private function sortRequiredPlugin($pluginName, &$pluginJsonCache, $toBeSorted, $sorted)
{
if (!isset($pluginJsonCache[$pluginName])) {
$loader = new MetadataLoader($pluginName);
$pluginJsonCache[$pluginName] = $loader->loadPluginInfoJson();
}
if (!empty($pluginJsonCache[$pluginName]['require'])) {
$dependencies = $pluginJsonCache[$pluginName]['require'];
foreach ($dependencies as $possiblePluginName => $key) {
if (in_array($possiblePluginName, $toBeSorted, true) && !in_array($possiblePluginName, $sorted, true)) {
$sorted = $this->sortRequiredPlugin($possiblePluginName, $pluginJsonCache, $toBeSorted, $sorted);
}
}
}
if (!in_array($pluginName, $sorted, true)) {
$sorted[] = $pluginName;
}
return $sorted;
}
}

View File

@ -0,0 +1,893 @@
<?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;
use Piwik\Archive\ArchiveQuery;
use Piwik\Archive\ArchiveQueryFactory;
use Piwik\Archive\Parameters;
use Piwik\ArchiveProcessor\Rules;
use Piwik\Archive\ArchiveInvalidator;
use Piwik\Container\StaticContainer;
use Piwik\DataAccess\ArchiveSelector;
/**
* The **Archive** class is used to query cached analytics statistics
* (termed "archive data").
*
* You can use **Archive** instances to get data that was archived for one or more sites,
* for one or more periods and one optional segment.
*
* If archive data is not found, this class will initiate the archiving process. [1](#footnote-1)
*
* **Archive** instances must be created using the {@link build()} factory method;
* they cannot be constructed.
*
* You can search for metrics (such as `nb_visits`) using the {@link getNumeric()} and
* {@link getDataTableFromNumeric()} methods. You can search for
* reports using the {@link getBlob()}, {@link getDataTable()} and {@link getDataTableExpanded()} methods.
*
* If you're creating an API that returns report data, you may want to use the
* {@link createDataTableFromArchive()} helper function.
*
* ### Learn more
*
* Learn more about _archiving_ [here](/guides/all-about-analytics-data).
*
* ### Limitations
*
* - You cannot get data for multiple range periods in a single query.
* - You cannot get data for periods of different types in a single query.
*
* ### Examples
*
* **_Querying metrics for an API method_**
*
* // one site and one period
* $archive = Archive::build($idSite = 1, $period = 'week', $date = '2013-03-08');
* return $archive->getDataTableFromNumeric(array('nb_visits', 'nb_actions'));
*
* // all sites and multiple dates
* $archive = Archive::build($idSite = 'all', $period = 'month', $date = '2013-01-02,2013-03-08');
* return $archive->getDataTableFromNumeric(array('nb_visits', 'nb_actions'));
*
* **_Querying and using metrics immediately_**
*
* // one site and one period
* $archive = Archive::build($idSite = 1, $period = 'week', $date = '2013-03-08');
* $data = $archive->getNumeric(array('nb_visits', 'nb_actions'));
*
* $visits = $data['nb_visits'];
* $actions = $data['nb_actions'];
*
* // ... do something w/ metric data ...
*
* // multiple sites and multiple dates
* $archive = Archive::build($idSite = '1,2,3', $period = 'month', $date = '2013-01-02,2013-03-08');
* $data = $archive->getNumeric('nb_visits');
*
* $janSite1Visits = $data['1']['2013-01-01,2013-01-31']['nb_visits'];
* $febSite1Visits = $data['1']['2013-02-01,2013-02-28']['nb_visits'];
* // ... etc.
*
* **_Querying for reports_**
*
* $archive = Archive::build($idSite = 1, $period = 'week', $date = '2013-03-08');
* $dataTable = $archive->getDataTable('MyPlugin_MyReport');
* // ... manipulate $dataTable ...
* return $dataTable;
*
* **_Querying a report for an API method_**
*
* public function getMyReport($idSite, $period, $date, $segment = false, $expanded = false)
* {
* $dataTable = Archive::createDataTableFromArchive('MyPlugin_MyReport', $idSite, $period, $date, $segment, $expanded);
* return $dataTable;
* }
*
* **_Querying data for multiple range periods_**
*
* // get data for first range
* $archive = Archive::build($idSite = 1, $period = 'range', $date = '2013-03-08,2013-03-12');
* $dataTable = $archive->getDataTableFromNumeric(array('nb_visits', 'nb_actions'));
*
* // get data for second range
* $archive = Archive::build($idSite = 1, $period = 'range', $date = '2013-03-15,2013-03-20');
* $dataTable = $archive->getDataTableFromNumeric(array('nb_visits', 'nb_actions'));
*
* <a name="footnote-1"></a>
* [1]: The archiving process will not be launched if browser archiving is disabled
* and the current request came from a browser.
*
*
* @api
*/
class Archive implements ArchiveQuery
{
const REQUEST_ALL_WEBSITES_FLAG = 'all';
const ARCHIVE_ALL_PLUGINS_FLAG = 'all';
const ID_SUBTABLE_LOAD_ALL_SUBTABLES = 'all';
/**
* List of archive IDs for the site, periods and segment we are querying with.
* Archive IDs are indexed by done flag and period, ie:
*
* array(
* 'done.Referrers' => array(
* '2010-01-01' => 1,
* '2010-01-02' => 2,
* ),
* 'done.VisitsSummary' => array(
* '2010-01-01' => 3,
* '2010-01-02' => 4,
* ),
* )
*
* or,
*
* array(
* 'done.all' => array(
* '2010-01-01' => 1,
* '2010-01-02' => 2
* )
* )
*
* @var array
*/
private $idarchives = array();
/**
* If set to true, the result of all get functions (ie, getNumeric, getBlob, etc.)
* will be indexed by the site ID, even if we're only querying data for one site.
*
* @var bool
*/
private $forceIndexedBySite;
/**
* If set to true, the result of all get functions (ie, getNumeric, getBlob, etc.)
* will be indexed by the period, even if we're only querying data for one period.
*
* @var bool
*/
private $forceIndexedByDate;
/**
* @var Parameters
*/
private $params;
/**
* @var \Piwik\Cache\Cache
*/
private static $cache;
/**
* @var ArchiveInvalidator
*/
private $invalidator;
/**
* @param Parameters $params
* @param bool $forceIndexedBySite Whether to force index the result of a query by site ID.
* @param bool $forceIndexedByDate Whether to force index the result of a query by period.
*/
public function __construct(Parameters $params, $forceIndexedBySite = false,
$forceIndexedByDate = false)
{
$this->params = $params;
$this->forceIndexedBySite = $forceIndexedBySite;
$this->forceIndexedByDate = $forceIndexedByDate;
$this->invalidator = StaticContainer::get('Piwik\Archive\ArchiveInvalidator');
}
/**
* Returns a new Archive instance that will query archive data for the given set of
* sites and periods, using an optional Segment.
*
* This method uses data that is found in query parameters, so the parameters to this
* function can be string values.
*
* If you want to create an Archive instance with an array of Period instances, use
* {@link Archive::factory()}.
*
* @param string|int|array $idSites A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
* or `'all'`.
* @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
* @param Date|string $strDate 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
* or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
* @param bool|false|string $segment Segment definition or false if no segment should be used. {@link Piwik\Segment}
* @param bool|false|string $_restrictSitesToLogin Used only when running as a scheduled task.
* @return ArchiveQuery
*/
public static function build($idSites, $period, $strDate, $segment = false, $_restrictSitesToLogin = false)
{
return StaticContainer::get(ArchiveQueryFactory::class)->build($idSites, $period, $strDate, $segment,
$_restrictSitesToLogin);
}
/**
* Returns a new Archive instance that will query archive data for the given set of
* sites and periods, using an optional segment.
*
* This method uses an array of Period instances and a Segment instance, instead of strings
* like {@link build()}.
*
* If you want to create an Archive instance using data found in query parameters,
* use {@link build()}.
*
* @param Segment $segment The segment to use. For no segment, use `new Segment('', $idSites)`.
* @param array $periods An array of Period instances.
* @param array $idSites An array of site IDs (eg, `array(1, 2, 3)`).
* @param bool $idSiteIsAll Whether `'all'` sites are being queried or not. If true, then
* the result of querying functions will be indexed by site, regardless
* of whether `count($idSites) == 1`.
* @param bool $isMultipleDate Whether multiple dates are being queried or not. If true, then
* the result of querying functions will be indexed by period,
* regardless of whether `count($periods) == 1`.
*
* @return ArchiveQuery
*/
public static function factory(Segment $segment, array $periods, array $idSites, $idSiteIsAll = false,
$isMultipleDate = false)
{
return StaticContainer::get(ArchiveQueryFactory::class)->factory($segment, $periods, $idSites, $idSiteIsAll,
$isMultipleDate);
}
/**
* Queries and returns metric data in an array.
*
* If multiple sites were requested in {@link build()} or {@link factory()} the result will
* be indexed by site ID.
*
* If multiple periods were requested in {@link build()} or {@link factory()} the result will
* be indexed by period.
*
* The site ID index is always first, so if multiple sites & periods were requested, the result
* will be indexed by site ID first, then period.
*
* @param string|array $names One or more archive names, eg, `'nb_visits'`, `'Referrers_distinctKeywords'`,
* etc.
* @return false|integer|array `false` if there is no data to return, a single numeric value if we're not querying
* for multiple sites/periods, or an array if multiple sites, periods or names are
* queried for.
*/
public function getNumeric($names)
{
$data = $this->get($names, 'numeric');
$resultIndices = $this->getResultIndices();
$result = $data->getIndexedArray($resultIndices);
// if only one metric is returned, just return it as a numeric value
if (empty($resultIndices)
&& count($result) <= 1
&& (!is_array($names) || count($names) == 1)
) {
$result = (float)reset($result); // convert to float in case $result is empty
}
return $result;
}
/**
* Queries and returns metric data in a DataTable instance.
*
* If multiple sites were requested in {@link build()} or {@link factory()} the result will
* be a DataTable\Map that is indexed by site ID.
*
* If multiple periods were requested in {@link build()} or {@link factory()} the result will
* be a {@link DataTable\Map} that is indexed by period.
*
* The site ID index is always first, so if multiple sites & periods were requested, the result
* will be a {@link DataTable\Map} indexed by site ID which contains {@link DataTable\Map} instances that are
* indexed by period.
*
* _Note: Every DataTable instance returned will have at most one row in it. The contents of each
* row will be the requested metrics for the appropriate site and period._
*
* @param string|array $names One or more archive names, eg, 'nb_visits', 'Referrers_distinctKeywords',
* etc.
* @return DataTable|DataTable\Map A DataTable if multiple sites and periods were not requested.
* An appropriately indexed DataTable\Map if otherwise.
*/
public function getDataTableFromNumeric($names)
{
$data = $this->get($names, 'numeric');
return $data->getDataTable($this->getResultIndices());
}
/**
* Similar to {@link getDataTableFromNumeric()} but merges all children on the created DataTable.
*
* This is the same as doing `$this->getDataTableFromNumeric()->mergeChildren()` but this way it is much faster.
*
* @return DataTable|DataTable\Map
*
* @internal Currently only used by MultiSites.getAll plugin. Feel free to remove internal tag if needed somewhere
* else. If no longer needed by MultiSites.getAll please remove this method. If you need this to work in
* a bit different way feel free to refactor as always.
*/
public function getDataTableFromNumericAndMergeChildren($names)
{
$data = $this->get($names, 'numeric');
$resultIndexes = $this->getResultIndices();
return $data->getMergedDataTable($resultIndexes);
}
/**
* Queries and returns one or more reports as DataTable instances.
*
* This method will query blob data that is a serialized array of of {@link DataTable\Row}'s and
* unserialize it.
*
* If multiple sites were requested in {@link build()} or {@link factory()} the result will
* be a {@link DataTable\Map} that is indexed by site ID.
*
* If multiple periods were requested in {@link build()} or {@link factory()} the result will
* be a DataTable\Map that is indexed by period.
*
* The site ID index is always first, so if multiple sites & periods were requested, the result
* will be a {@link DataTable\Map} indexed by site ID which contains {@link DataTable\Map} instances that are
* indexed by period.
*
* @param string $name The name of the record to get. This method can only query one record at a time.
* @param int|string|null $idSubtable The ID of the subtable to get (if any).
* @return DataTable|DataTable\Map A DataTable if multiple sites and periods were not requested.
* An appropriately indexed {@link DataTable\Map} if otherwise.
*/
public function getDataTable($name, $idSubtable = null)
{
$data = $this->get($name, 'blob', $idSubtable);
return $data->getDataTable($this->getResultIndices());
}
/**
* Queries and returns one report with all of its subtables loaded.
*
* If multiple sites were requested in {@link build()} or {@link factory()} the result will
* be a DataTable\Map that is indexed by site ID.
*
* If multiple periods were requested in {@link build()} or {@link factory()} the result will
* be a DataTable\Map that is indexed by period.
*
* The site ID index is always first, so if multiple sites & periods were requested, the result
* will be a {@link DataTable\Map indexed} by site ID which contains {@link DataTable\Map} instances that are
* indexed by period.
*
* @param string $name The name of the record to get.
* @param int|string|null $idSubtable The ID of the subtable to get (if any). The subtable will be expanded.
* @param int|null $depth The maximum number of subtable levels to load. If null, all levels are loaded.
* For example, if `1` is supplied, then the DataTable returned will have its subtables
* loaded. Those subtables, however, will NOT have their subtables loaded.
* @param bool $addMetadataSubtableId Whether to add the database subtable ID as metadata to each datatable,
* or not.
* @return DataTable|DataTable\Map
*/
public function getDataTableExpanded($name, $idSubtable = null, $depth = null, $addMetadataSubtableId = true)
{
$data = $this->get($name, 'blob', self::ID_SUBTABLE_LOAD_ALL_SUBTABLES);
return $data->getExpandedDataTable($this->getResultIndices(), $idSubtable, $depth, $addMetadataSubtableId);
}
/**
* Returns the list of plugins that archive the given reports.
*
* @param array $archiveNames
* @return array
*/
private function getRequestedPlugins($archiveNames)
{
$result = array();
foreach ($archiveNames as $name) {
$result[] = self::getPluginForReport($name);
}
return array_unique($result);
}
/**
* Returns an object describing the set of sites, the set of periods and the segment
* this Archive will query data for.
*
* @return Parameters
*/
public function getParams()
{
return $this->params;
}
/**
* Helper function that creates an Archive instance and queries for report data using
* query parameter data. API methods can use this method to reduce code redundancy.
*
* @param string $recordName The name of the report to return.
* @param int|string|array $idSite @see {@link build()}
* @param string $period @see {@link build()}
* @param string $date @see {@link build()}
* @param string $segment @see {@link build()}
* @param bool $expanded If true, loads all subtables. See {@link getDataTableExpanded()}
* @param bool $flat If true, loads all subtables and disabled all recursive filters.
* @param int|null $idSubtable See {@link getDataTableExpanded()}
* @param int|null $depth See {@link getDataTableExpanded()}
* @return DataTable|DataTable\Map
*/
public static function createDataTableFromArchive($recordName, $idSite, $period, $date, $segment, $expanded = false, $flat = false, $idSubtable = null, $depth = null)
{
Piwik::checkUserHasViewAccess($idSite);
if ($flat && !$idSubtable) {
$expanded = true;
}
$archive = Archive::build($idSite, $period, $date, $segment, $_restrictSitesToLogin = false);
if ($idSubtable === false) {
$idSubtable = null;
}
if ($expanded) {
$dataTable = $archive->getDataTableExpanded($recordName, $idSubtable, $depth);
} else {
$dataTable = $archive->getDataTable($recordName, $idSubtable);
}
$dataTable->queueFilter('ReplaceSummaryRowLabel');
$dataTable->queueFilter('ReplaceColumnNames');
if ($expanded) {
$dataTable->queueFilterSubtables('ReplaceColumnNames');
}
if ($flat) {
$dataTable->disableRecursiveFilters();
}
return $dataTable;
}
private function getSiteIdsThatAreRequestedInThisArchiveButWereNotInvalidatedYet()
{
if (is_null(self::$cache)) {
self::$cache = Cache::getTransientCache();
}
$id = 'Archive.SiteIdsOfRememberedReportsInvalidated';
if (!self::$cache->contains($id)) {
self::$cache->save($id, array());
}
$siteIdsAlreadyHandled = self::$cache->fetch($id);
$siteIdsRequested = $this->params->getIdSites();
foreach ($siteIdsRequested as $index => $siteIdRequested) {
$siteIdRequested = (int) $siteIdRequested;
if (in_array($siteIdRequested, $siteIdsAlreadyHandled)) {
unset($siteIdsRequested[$index]); // was already handled previously, do not do it again
} else {
$siteIdsAlreadyHandled[] = $siteIdRequested; // we will handle this id this time
}
}
self::$cache->save($id, $siteIdsAlreadyHandled);
return $siteIdsRequested;
}
private function invalidatedReportsIfNeeded()
{
$siteIdsRequested = $this->getSiteIdsThatAreRequestedInThisArchiveButWereNotInvalidatedYet();
if (empty($siteIdsRequested)) {
return; // all requested site ids were already handled
}
$sitesPerDays = $this->invalidator->getRememberedArchivedReportsThatShouldBeInvalidated();
foreach ($sitesPerDays as $date => $siteIds) {
if (empty($siteIds)) {
continue;
}
$siteIdsToActuallyInvalidate = array_intersect($siteIds, $siteIdsRequested);
if (empty($siteIdsToActuallyInvalidate)) {
continue; // all site ids that should be handled are already handled
}
try {
$this->invalidator->markArchivesAsInvalidated($siteIdsToActuallyInvalidate, array(Date::factory($date)), false);
} catch (\Exception $e) {
Site::clearCache();
throw $e;
}
}
Site::clearCache();
}
/**
* Queries archive tables for data and returns the result.
* @param array|string $archiveNames
* @param $archiveDataType
* @param null|int $idSubtable
* @return Archive\DataCollection
*/
protected function get($archiveNames, $archiveDataType, $idSubtable = null)
{
if (!is_array($archiveNames)) {
$archiveNames = array($archiveNames);
}
// apply idSubtable
if ($idSubtable !== null
&& $idSubtable != self::ID_SUBTABLE_LOAD_ALL_SUBTABLES
) {
// this is also done in ArchiveSelector. It should be actually only done in ArchiveSelector but DataCollection
// does require to have the subtableId appended. Needs to be changed in refactoring to have it only in one
// place.
$dataNames = array();
foreach ($archiveNames as $name) {
$dataNames[] = ArchiveSelector::appendIdsubtable($name, $idSubtable);
}
} else {
$dataNames = $archiveNames;
}
$result = new Archive\DataCollection(
$dataNames, $archiveDataType, $this->params->getIdSites(), $this->params->getPeriods(), $defaultRow = null);
$archiveIds = $this->getArchiveIds($archiveNames);
if (empty($archiveIds)) {
/**
* Triggered when no archive data is found in an API request.
* @ignore
*/
Piwik::postEvent('Archive.noArchivedData');
return $result;
}
$archiveData = ArchiveSelector::getArchiveData($archiveIds, $archiveNames, $archiveDataType, $idSubtable);
$isNumeric = $archiveDataType == 'numeric';
foreach ($archiveData as $row) {
// values are grouped by idsite (site ID), date1-date2 (date range), then name (field name)
$periodStr = $row['date1'] . ',' . $row['date2'];
if ($isNumeric) {
$row['value'] = $this->formatNumericValue($row['value']);
} else {
$result->addMetadata($row['idsite'], $periodStr, DataTable::ARCHIVED_DATE_METADATA_NAME, $row['ts_archived']);
}
$result->set($row['idsite'], $periodStr, $row['name'], $row['value']);
}
return $result;
}
/**
* Returns archive IDs for the sites, periods and archive names that are being
* queried. This function will use the idarchive cache if it has the right data,
* query archive tables for IDs w/o launching archiving, or launch archiving and
* get the idarchive from ArchiveProcessor instances.
*
* @param string $archiveNames
* @return array
*/
private function getArchiveIds($archiveNames)
{
$plugins = $this->getRequestedPlugins($archiveNames);
// figure out which archives haven't been processed (if an archive has been processed,
// then we have the archive IDs in $this->idarchives)
$doneFlags = array();
$archiveGroups = array();
foreach ($plugins as $plugin) {
$doneFlag = $this->getDoneStringForPlugin($plugin, $this->params->getIdSites());
$doneFlags[$doneFlag] = true;
if (!isset($this->idarchives[$doneFlag])) {
$archiveGroup = $this->getArchiveGroupOfPlugin($plugin);
if ($archiveGroup == self::ARCHIVE_ALL_PLUGINS_FLAG) {
$archiveGroup = reset($plugins);
}
$archiveGroups[] = $archiveGroup;
}
$globalDoneFlag = Rules::getDoneFlagArchiveContainsAllPlugins($this->params->getSegment());
if ($globalDoneFlag !== $doneFlag) {
$doneFlags[$globalDoneFlag] = true;
}
}
$archiveGroups = array_unique($archiveGroups);
// cache id archives for plugins we haven't processed yet
if (!empty($archiveGroups)) {
if (!Rules::isArchivingDisabledFor($this->params->getIdSites(), $this->params->getSegment(), $this->getPeriodLabel())) {
$this->cacheArchiveIdsAfterLaunching($archiveGroups, $plugins);
} else {
$this->cacheArchiveIdsWithoutLaunching($plugins);
}
}
$idArchivesByMonth = $this->getIdArchivesByMonth($doneFlags);
return $idArchivesByMonth;
}
/**
* Gets the IDs of the archives we're querying for and stores them in $this->archives.
* This function will launch the archiving process for each period/site/plugin if
* metrics/reports have not been calculated/archived already.
*
* @param array $archiveGroups @see getArchiveGroupOfReport
* @param array $plugins List of plugin names to archive.
*/
private function cacheArchiveIdsAfterLaunching($archiveGroups, $plugins)
{
$this->invalidatedReportsIfNeeded();
$today = Date::today();
foreach ($this->params->getPeriods() as $period) {
$twoDaysBeforePeriod = $period->getDateStart()->subDay(2);
$twoDaysAfterPeriod = $period->getDateEnd()->addDay(2);
foreach ($this->params->getIdSites() as $idSite) {
$site = new Site($idSite);
// if the END of the period is BEFORE the website creation date
// we already know there are no stats for this period
// we add one day to make sure we don't miss the day of the website creation
if ($twoDaysAfterPeriod->isEarlier($site->getCreationDate())) {
Log::debug("Archive site %s, %s (%s) skipped, archive is before the website was created.",
$idSite, $period->getLabel(), $period->getPrettyString());
continue;
}
// if the starting date is in the future we know there is no visiidsite = ?t
if ($twoDaysBeforePeriod->isLater($today)) {
Log::debug("Archive site %s, %s (%s) skipped, archive is after today.",
$idSite, $period->getLabel(), $period->getPrettyString());
continue;
}
$this->prepareArchive($archiveGroups, $site, $period);
}
}
}
/**
* Gets the IDs of the archives we're querying for and stores them in $this->archives.
* This function will not launch the archiving process (and is thus much, much faster
* than cacheArchiveIdsAfterLaunching).
*
* @param array $plugins List of plugin names from which data is being requested.
*/
private function cacheArchiveIdsWithoutLaunching($plugins)
{
$idarchivesByReport = ArchiveSelector::getArchiveIds(
$this->params->getIdSites(), $this->params->getPeriods(), $this->params->getSegment(), $plugins);
// initialize archive ID cache for each report
foreach ($plugins as $plugin) {
$doneFlag = $this->getDoneStringForPlugin($plugin, $this->params->getIdSites());
$this->initializeArchiveIdCache($doneFlag);
$globalDoneFlag = Rules::getDoneFlagArchiveContainsAllPlugins($this->params->getSegment());
$this->initializeArchiveIdCache($globalDoneFlag);
}
foreach ($idarchivesByReport as $doneFlag => $idarchivesByDate) {
foreach ($idarchivesByDate as $dateRange => $idarchives) {
foreach ($idarchives as $idarchive) {
$this->idarchives[$doneFlag][$dateRange][] = $idarchive;
}
}
}
}
/**
* Returns the done string flag for a plugin using this instance's segment & periods.
* @param string $plugin
* @return string
*/
private function getDoneStringForPlugin($plugin, $idSites)
{
return Rules::getDoneStringFlagFor(
$idSites,
$this->params->getSegment(),
$this->getPeriodLabel(),
$plugin
);
}
private function getPeriodLabel()
{
$periods = $this->params->getPeriods();
return reset($periods)->getLabel();
}
/**
* Returns an array describing what metadata to use when indexing a query result.
* For use with DataCollection.
*
* @return array
*/
private function getResultIndices()
{
$indices = array();
if (count($this->params->getIdSites()) > 1
|| $this->forceIndexedBySite
) {
$indices['site'] = 'idSite';
}
if (count($this->params->getPeriods()) > 1
|| $this->forceIndexedByDate
) {
$indices['period'] = 'date';
}
return $indices;
}
private function formatNumericValue($value)
{
// If there is no dot, we return as is
// Note: this could be an integer bigger than 32 bits
if (strpos($value, '.') === false) {
if ($value === false) {
return 0;
}
return (float)$value;
}
// Round up the value with 2 decimals
// we cast the result as float because returns false when no visitors
return round((float)$value, 2);
}
/**
* Initializes the archive ID cache ($this->idarchives) for a particular 'done' flag.
*
* It is necessary that each archive ID caching function call this method for each
* unique 'done' flag it encounters, since the getArchiveIds function determines
* whether archiving should be launched based on whether $this->idarchives has a
* an entry for a specific 'done' flag.
*
* If this function is not called, then periods with no visits will not add
* entries to the cache. If the archive is used again, SQL will be executed to
* try and find the archive IDs even though we know there are none.
*
* @param string $doneFlag
*/
private function initializeArchiveIdCache($doneFlag)
{
if (!isset($this->idarchives[$doneFlag])) {
$this->idarchives[$doneFlag] = array();
}
}
/**
* Returns the archiving group identifier given a plugin.
*
* More than one plugin can be called at once when archiving. In such a case
* we don't want to launch archiving three times for three plugins if doing
* it once is enough, so getArchiveIds makes sure to get the archive group of
* all reports.
*
* If the period isn't a range, then all plugins' archiving code is executed.
* If the period is a range, then archiving code is executed individually for
* each plugin.
*/
private function getArchiveGroupOfPlugin($plugin)
{
$periods = $this->params->getPeriods();
$periodLabel = reset($periods)->getLabel();
if (Rules::shouldProcessReportsAllPlugins($this->params->getIdSites(), $this->params->getSegment(), $periodLabel)) {
return self::ARCHIVE_ALL_PLUGINS_FLAG;
}
return $plugin;
}
/**
* Returns the name of the plugin that archives a given report.
*
* @param string $report Archive data name, eg, `'nb_visits'`, `'DevicesDetection_...'`, etc.
* @return string Plugin name.
* @throws \Exception If a plugin cannot be found or if the plugin for the report isn't
* activated.
*/
public static function getPluginForReport($report)
{
// Core metrics are always processed in Core, for the requested date/period/segment
if (in_array($report, Metrics::getVisitsMetricNames())) {
$report = 'VisitsSummary_CoreMetrics';
} // Goal_* metrics are processed by the Goals plugin (HACK)
elseif (strpos($report, 'Goal_') === 0) {
$report = 'Goals_Metrics';
} elseif (strrpos($report, '_returning') === strlen($report) - strlen('_returning')) { // HACK
$report = 'VisitFrequency_Metrics';
}
$plugin = substr($report, 0, strpos($report, '_'));
if (empty($plugin)
|| !\Piwik\Plugin\Manager::getInstance()->isPluginActivated($plugin)
) {
throw new \Exception("Error: The report '$report' was requested but it is not available at this stage."
. " (Plugin '$plugin' is not activated.)");
}
return $plugin;
}
/**
* @param $archiveGroups
* @param $site
* @param $period
*/
private function prepareArchive(array $archiveGroups, Site $site, Period $period)
{
$parameters = new ArchiveProcessor\Parameters($site, $period, $this->params->getSegment());
$archiveLoader = new ArchiveProcessor\Loader($parameters);
$periodString = $period->getRangeString();
$idSites = array($site->getId());
// process for each plugin as well
foreach ($archiveGroups as $plugin) {
$doneFlag = $this->getDoneStringForPlugin($plugin, $idSites);
$this->initializeArchiveIdCache($doneFlag);
$idArchive = $archiveLoader->prepareArchive($plugin);
if ($idArchive) {
$this->idarchives[$doneFlag][$periodString][] = $idArchive;
}
}
}
private function getIdArchivesByMonth($doneFlags)
{
// order idarchives by the table month they belong to
$idArchivesByMonth = array();
foreach (array_keys($doneFlags) as $doneFlag) {
if (empty($this->idarchives[$doneFlag])) {
continue;
}
foreach ($this->idarchives[$doneFlag] as $dateRange => $idarchives) {
foreach ($idarchives as $id) {
$idArchivesByMonth[$dateRange][] = $id;
}
}
}
return $idArchivesByMonth;
}
/**
* @internal
*/
public static function clearStaticCache()
{
self::$cache = null;
}
}

View File

@ -0,0 +1,362 @@
<?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\Archive;
use Piwik\Archive\ArchiveInvalidator\InvalidationResult;
use Piwik\CronArchive\SitesToReprocessDistributedList;
use Piwik\DataAccess\ArchiveTableCreator;
use Piwik\DataAccess\Model;
use Piwik\Date;
use Piwik\Option;
use Piwik\Piwik;
use Piwik\Plugins\CoreAdminHome\Tasks\ArchivesToPurgeDistributedList;
use Piwik\Plugins\PrivacyManager\PrivacyManager;
use Piwik\Period;
use Piwik\Segment;
/**
* Service that can be used to invalidate archives or add archive references to a list so they will
* be invalidated later.
*
* Archives are put in an "invalidated" state by setting the done flag to `ArchiveWriter::DONE_INVALIDATED`.
* This class also adds the archive's associated site to the a distributed list and adding the archive's year month to another
* distributed list.
*
* CronArchive will reprocess the archive data for all sites in the first list, and a scheduled task
* will purge the old, invalidated data in archive tables identified by the second list.
*
* Until CronArchive, or browser triggered archiving, re-processes data for an invalidated archive, the invalidated
* archive data will still be displayed in the UI and API.
*
* ### Deferred Invalidation
*
* Invalidating archives means running queries on one or more archive tables. In some situations, like during
* tracking, this is not desired. In such cases, archive references can be added to a list via the
* rememberToInvalidateArchivedReportsLater method, which will add the reference to a distributed list
*
* Later, during Piwik's normal execution, the list will be read and every archive it references will
* be invalidated.
*/
class ArchiveInvalidator
{
private $rememberArchivedReportIdStart = 'report_to_invalidate_';
/**
* @var Model
*/
private $model;
public function __construct(Model $model)
{
$this->model = $model;
}
public function rememberToInvalidateArchivedReportsLater($idSite, Date $date)
{
// To support multiple transactions at once, look for any other process to have set (and committed)
// this report to be invalidated.
$key = $this->buildRememberArchivedReportIdForSiteAndDate($idSite, $date->toString());
// we do not really have to get the value first. we could simply always try to call set() and it would update or
// insert the record if needed but we do not want to lock the table (especially since there are still some
// MyISAM installations)
$value = Option::getLike($key . '%');
// In order to support multiple concurrent transactions, add our pid to the end of the key so that it will just insert
// rather than waiting on some other process to commit before proceeding.The issue is that with out this, more than
// one process is trying to add the exact same value to the table, which causes contention. With the pid suffixed to
// the value, each process can successfully enter its own row in the table. The net result will be the same. We could
// always just set this, but it would result in a lot of rows in the options table.. more than needed. With this
// change you'll have at most N rows per date/site, where N is the number of parallel requests on this same idsite/date
// that happen to run in overlapping transactions.
$mykey = $this->buildRememberArchivedReportIdProcessSafe($idSite, $date->toString());
// getLike() returns an empty array rather than 'false'
if (empty($value)) {
Option::set($mykey, '1');
}
}
public function getRememberedArchivedReportsThatShouldBeInvalidated()
{
$reports = Option::getLike($this->rememberArchivedReportIdStart . '%_%');
$sitesPerDay = array();
foreach ($reports as $report => $value) {
$report = str_replace($this->rememberArchivedReportIdStart, '', $report);
$report = explode('_', $report);
$siteId = (int) $report[0];
$date = $report[1];
if (empty($sitesPerDay[$date])) {
$sitesPerDay[$date] = array();
}
$sitesPerDay[$date][] = $siteId;
}
return $sitesPerDay;
}
private function buildRememberArchivedReportIdForSite($idSite)
{
return $this->rememberArchivedReportIdStart . (int) $idSite;
}
private function buildRememberArchivedReportIdForSiteAndDate($idSite, $date)
{
$id = $this->buildRememberArchivedReportIdForSite($idSite);
$id .= '_' . trim($date);
return $id;
}
// This version is multi process safe on the insert of a new date to invalidate.
private function buildRememberArchivedReportIdProcessSafe($idSite, $date)
{
$id = $this->buildRememberArchivedReportIdForSiteAndDate($idSite, $date);
$id .= '_' . getmypid();
return $id;
}
public function forgetRememberedArchivedReportsToInvalidateForSite($idSite)
{
$id = $this->buildRememberArchivedReportIdForSite($idSite) . '_%';
Option::deleteLike($id);
}
/**
* @internal
*/
public function forgetRememberedArchivedReportsToInvalidate($idSite, Date $date)
{
$id = $this->buildRememberArchivedReportIdForSiteAndDate($idSite, $date->toString());
// The process pid is added to the end of the entry in order to support multiple concurrent transactions.
// So this must be a deleteLike call to get all the entries, where there used to only be one.
Option::deleteLike($id . '%');
}
/**
* @param $idSites int[]
* @param $dates Date[]
* @param $period string
* @param $segment Segment
* @param bool $cascadeDown
* @return InvalidationResult
* @throws \Exception
*/
public function markArchivesAsInvalidated(array $idSites, array $dates, $period, Segment $segment = null, $cascadeDown = false)
{
$invalidationInfo = new InvalidationResult();
/**
* Triggered when a Matomo user requested the invalidation of some reporting archives. Using this event, plugin
* developers can automatically invalidate another site, when a site is being invalidated. A plugin may even
* remove an idSite from the list of sites that should be invalidated to prevent it from ever being
* invalidated.
*
* **Example**
*
* public function getIdSitesToMarkArchivesAsInvalidates(&$idSites)
* {
* if (in_array(1, $idSites)) {
* $idSites[] = 5; // when idSite 1 is being invalidated, also invalidate idSite 5
* }
* }
*
* @param array &$idSites An array containing a list of site IDs which are requested to be invalidated.
*/
Piwik::postEvent('Archiving.getIdSitesToMarkArchivesAsInvalidated', array(&$idSites));
// we trigger above event on purpose here and it is good that the segment was created like
// `new Segment($segmentString, $idSites)` because when a user adds a site via this event, the added idSite
// might not have this segment meaning we avoid a possible error. For the workflow to work, any added or removed
// idSite does not need to be added to $segment.
$datesToInvalidate = $this->removeDatesThatHaveBeenPurged($dates, $invalidationInfo);
if (empty($period)) {
// if the period is empty, we don't need to cascade in any way, since we'll remove all periods
$periodDates = $this->getDatesByYearMonthAndPeriodType($dates);
} else {
$periods = $this->getPeriodsToInvalidate($datesToInvalidate, $period, $cascadeDown);
$periodDates = $this->getPeriodDatesByYearMonthAndPeriodType($periods);
}
$periodDates = $this->getUniqueDates($periodDates);
$this->markArchivesInvalidated($idSites, $periodDates, $segment);
$yearMonths = array_keys($periodDates);
$this->markInvalidatedArchivesForReprocessAndPurge($idSites, $yearMonths);
foreach ($idSites as $idSite) {
foreach ($dates as $date) {
$this->forgetRememberedArchivedReportsToInvalidate($idSite, $date);
}
}
return $invalidationInfo;
}
/**
* @param string[][][] $periodDates
* @return string[][][]
*/
private function getUniqueDates($periodDates)
{
$result = array();
foreach ($periodDates as $yearMonth => $periodsByYearMonth) {
foreach ($periodsByYearMonth as $periodType => $periods) {
$result[$yearMonth][$periodType] = array_unique($periods);
}
}
return $result;
}
/**
* @param Date[] $dates
* @param string $periodType
* @param bool $cascadeDown
* @return Period[]
*/
private function getPeriodsToInvalidate($dates, $periodType, $cascadeDown)
{
$periodsToInvalidate = array();
foreach ($dates as $date) {
if ($periodType == 'range') {
$date = $date . ',' . $date;
}
$period = Period\Factory::build($periodType, $date);
$periodsToInvalidate[] = $period;
if ($cascadeDown) {
$periodsToInvalidate = array_merge($periodsToInvalidate, $period->getAllOverlappingChildPeriods());
}
if ($periodType != 'year'
&& $periodType != 'range'
) {
$periodsToInvalidate[] = Period\Factory::build('year', $date);
}
}
return $periodsToInvalidate;
}
/**
* @param Period[] $periods
* @return string[][][]
*/
private function getPeriodDatesByYearMonthAndPeriodType($periods)
{
$result = array();
foreach ($periods as $period) {
$date = $period->getDateStart();
$periodType = $period->getId();
$yearMonth = ArchiveTableCreator::getTableMonthFromDate($date);
$result[$yearMonth][$periodType][] = $date->toString();
}
return $result;
}
/**
* Called when deleting all periods.
*
* @param Date[] $dates
* @return string[][][]
*/
private function getDatesByYearMonthAndPeriodType($dates)
{
$result = array();
foreach ($dates as $date) {
$yearMonth = ArchiveTableCreator::getTableMonthFromDate($date);
$result[$yearMonth][null][] = $date->toString();
// since we're removing all periods, we must make sure to remove year periods as well.
// this means we have to make sure the january table is processed.
$janYearMonth = $date->toString('Y') . '_01';
$result[$janYearMonth][null][] = $date->toString();
}
return $result;
}
/**
* @param int[] $idSites
* @param string[][][] $dates
* @throws \Exception
*/
private function markArchivesInvalidated($idSites, $dates, Segment $segment = null)
{
$archiveNumericTables = ArchiveTableCreator::getTablesArchivesInstalled($type = ArchiveTableCreator::NUMERIC_TABLE);
foreach ($archiveNumericTables as $table) {
$tableDate = ArchiveTableCreator::getDateFromTableName($table);
if (empty($dates[$tableDate])) {
continue;
}
$this->model->updateArchiveAsInvalidated($table, $idSites, $dates[$tableDate], $segment);
}
}
/**
* @param Date[] $dates
* @param InvalidationResult $invalidationInfo
* @return \Piwik\Date[]
*/
private function removeDatesThatHaveBeenPurged($dates, InvalidationResult $invalidationInfo)
{
$this->findOlderDateWithLogs($invalidationInfo);
$result = array();
foreach ($dates as $date) {
// we should only delete reports for dates that are more recent than N days
if ($invalidationInfo->minimumDateWithLogs
&& $date->isEarlier($invalidationInfo->minimumDateWithLogs)
) {
$invalidationInfo->warningDates[] = $date->toString();
continue;
}
$result[] = $date;
$invalidationInfo->processedDates[] = $date->toString();
}
return $result;
}
private function findOlderDateWithLogs(InvalidationResult $info)
{
// If using the feature "Delete logs older than N days"...
$purgeDataSettings = PrivacyManager::getPurgeDataSettings();
$logsDeletedWhenOlderThanDays = (int)$purgeDataSettings['delete_logs_older_than'];
$logsDeleteEnabled = $purgeDataSettings['delete_logs_enable'];
if ($logsDeleteEnabled
&& $logsDeletedWhenOlderThanDays
) {
$info->minimumDateWithLogs = Date::factory('today')->subDay($logsDeletedWhenOlderThanDays);
}
}
/**
* @param array $idSites
* @param array $yearMonths
*/
private function markInvalidatedArchivesForReprocessAndPurge(array $idSites, $yearMonths)
{
$store = new SitesToReprocessDistributedList();
$store->add($idSites);
$archivesToPurge = new ArchivesToPurgeDistributedList();
$archivesToPurge->add($yearMonths);
}
}

View File

@ -0,0 +1,56 @@
<?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\Archive\ArchiveInvalidator;
use Piwik\Date;
/**
* Information about the result of an archive invalidation operation.
*/
class InvalidationResult
{
/**
* Dates that couldn't be invalidated because they are earlier than the configured log
* deletion limit.
*
* @var array
*/
public $warningDates = array();
/**
* Dates that were successfully invalidated.
*
* @var array
*/
public $processedDates = array();
/**
* The day of the oldest log entry.
*
* @var Date|bool
*/
public $minimumDateWithLogs = false;
/**
* @return string[]
*/
public function makeOutputLogs()
{
$output = array();
if ($this->warningDates) {
$output[] = 'Warning: the following Dates have not been invalidated, because they are earlier than your Log Deletion limit: ' .
implode(", ", $this->warningDates) .
"\n The last day with logs is " . $this->minimumDateWithLogs . ". " .
"\n Please disable 'Delete old Logs' or set it to a higher deletion threshold (eg. 180 days or 365 years).'.";
}
$output[] = "Success. The following dates were invalidated successfully: " . implode(", ", $this->processedDates);
return $output;
}
}

View File

@ -0,0 +1,272 @@
<?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\Archive;
use Piwik\ArchiveProcessor\Rules;
use Piwik\Config;
use Piwik\Container\StaticContainer;
use Piwik\DataAccess\ArchiveTableCreator;
use Piwik\DataAccess\Model;
use Piwik\Date;
use Piwik\Piwik;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
/**
* Service that purges temporary, error-ed, invalid and custom range archives from archive tables.
*
* Temporary archives are purged if they were archived before a specific time. The time is dependent
* on whether browser triggered archiving is enabled or not.
*
* Error-ed archives are purged w/o constraint.
*
* Invalid archives are purged if a new, valid, archive exists w/ the same site, date, period combination.
* Archives are marked as invalid via Piwik\Archive\ArchiveInvalidator.
*/
class ArchivePurger
{
/**
* @var Model
*/
private $model;
/**
* Date threshold for purging custom range archives. Archives that are older than this date
* are purged unconditionally from the requested archive table.
*
* @var Date
*/
private $purgeCustomRangesOlderThan;
/**
* Date to use for 'yesterday'. Exists so tests can override this value.
*
* @var Date
*/
private $yesterday;
/**
* Date to use for 'today'. Exists so tests can override this value.
*
* @var $today
*/
private $today;
/**
* Date to use for 'now'. Exists so tests can override this value.
*
* @var int
*/
private $now;
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(Model $model = null, Date $purgeCustomRangesOlderThan = null, LoggerInterface $logger = null)
{
$this->model = $model ?: new Model();
$this->purgeCustomRangesOlderThan = $purgeCustomRangesOlderThan ?: self::getDefaultCustomRangeToPurgeAgeThreshold();
$this->yesterday = Date::factory('yesterday');
$this->today = Date::factory('today');
$this->now = time();
$this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
}
/**
* Purge all invalidate archives for whom there are newer, valid archives from the archive
* table that stores data for `$date`.
*
* @param Date $date The date identifying the archive table.
* @return int The total number of archive rows deleted (from both the blog & numeric tables).
*/
public function purgeInvalidatedArchivesFrom(Date $date)
{
$numericTable = ArchiveTableCreator::getNumericTable($date);
// we don't want to do an INNER JOIN on every row in a archive table that can potentially have tens to hundreds of thousands of rows,
// so we first look for sites w/ invalidated archives, and use this as a constraint in getInvalidatedArchiveIdsSafeToDelete() below.
// the constraint will hit an INDEX and speed up the inner join that happens in getInvalidatedArchiveIdsSafeToDelete().
$idSites = $this->model->getSitesWithInvalidatedArchive($numericTable);
if (empty($idSites)) {
$this->logger->debug("No sites with invalidated archives found in {table}.", array('table' => $numericTable));
return 0;
}
$archiveIds = $this->model->getInvalidatedArchiveIdsSafeToDelete($numericTable, $idSites);
if (empty($archiveIds)) {
$this->logger->debug("No invalidated archives found in {table} with newer, valid archives.", array('table' => $numericTable));
return 0;
}
$this->logger->info("Found {countArchiveIds} invalidated archives safe to delete in {table}.", array(
'table' => $numericTable, 'countArchiveIds' => count($archiveIds)
));
$deletedRowCount = $this->deleteArchiveIds($date, $archiveIds);
$this->logger->debug("Deleted {count} rows in {table} and its associated blob table.", array(
'table' => $numericTable, 'count' => $deletedRowCount
));
return $deletedRowCount;
}
/**
* Removes the outdated archives for the given month.
* (meaning they are marked with a done flag of ArchiveWriter::DONE_OK_TEMPORARY or ArchiveWriter::DONE_ERROR)
*
* @param Date $dateStart Only the month will be used
* @return int Returns the total number of rows deleted.
*/
public function purgeOutdatedArchives(Date $dateStart)
{
$purgeArchivesOlderThan = $this->getOldestTemporaryArchiveToKeepThreshold();
$deletedRowCount = 0;
$idArchivesToDelete = $this->getOutdatedArchiveIds($dateStart, $purgeArchivesOlderThan);
if (!empty($idArchivesToDelete)) {
$deletedRowCount = $this->deleteArchiveIds($dateStart, $idArchivesToDelete);
$this->logger->info("Deleted {count} rows in archive tables (numeric + blob) for {date}.", array(
'count' => $deletedRowCount,
'date' => $dateStart
));
} else {
$this->logger->debug("No outdated archives found in archive numeric table for {date}.", array('date' => $dateStart));
}
$this->logger->debug("Purging temporary archives: done [ purged archives older than {date} in {yearMonth} ] [Deleted IDs: {deletedIds}]", array(
'date' => $purgeArchivesOlderThan,
'yearMonth' => $dateStart->toString('Y-m'),
'deletedIds' => implode(',', $idArchivesToDelete)
));
return $deletedRowCount;
}
protected function getOutdatedArchiveIds(Date $date, $purgeArchivesOlderThan)
{
$archiveTable = ArchiveTableCreator::getNumericTable($date);
$result = $this->model->getTemporaryArchivesOlderThan($archiveTable, $purgeArchivesOlderThan);
$idArchivesToDelete = array();
if (!empty($result)) {
foreach ($result as $row) {
$idArchivesToDelete[] = $row['idarchive'];
}
}
return $idArchivesToDelete;
}
/**
* Deleting "Custom Date Range" reports after 1 day, since they can be re-processed and would take up un-necessary space.
*
* @param $date Date
* @return int The total number of rows deleted from both the numeric & blob table.
*/
public function purgeArchivesWithPeriodRange(Date $date)
{
$numericTable = ArchiveTableCreator::getNumericTable($date);
$blobTable = ArchiveTableCreator::getBlobTable($date);
$deletedCount = $this->model->deleteArchivesWithPeriod(
$numericTable, $blobTable, Piwik::$idPeriods['range'], $this->purgeCustomRangesOlderThan);
$level = $deletedCount == 0 ? LogLevel::DEBUG : LogLevel::INFO;
$this->logger->log($level, "Purged {count} range archive rows from {numericTable} & {blobTable}.", array(
'count' => $deletedCount,
'numericTable' => $numericTable,
'blobTable' => $blobTable
));
$this->logger->debug(" [ purged archives older than {threshold} ]", array('threshold' => $this->purgeCustomRangesOlderThan));
return $deletedCount;
}
/**
* Deletes by batches Archive IDs in the specified month,
*
* @param Date $date
* @param $idArchivesToDelete
* @return int Number of rows deleted from both numeric + blob table.
*/
protected function deleteArchiveIds(Date $date, $idArchivesToDelete)
{
$batches = array_chunk($idArchivesToDelete, 1000);
$numericTable = ArchiveTableCreator::getNumericTable($date);
$blobTable = ArchiveTableCreator::getBlobTable($date);
$deletedCount = 0;
foreach ($batches as $idsToDelete) {
$deletedCount += $this->model->deleteArchiveIds($numericTable, $blobTable, $idsToDelete);
}
return $deletedCount;
}
/**
* Returns a timestamp indicating outdated archives older than this timestamp (processed before) can be purged.
*
* @return int|bool Outdated archives older than this timestamp should be purged
*/
protected function getOldestTemporaryArchiveToKeepThreshold()
{
$temporaryArchivingTimeout = Rules::getTodayArchiveTimeToLive();
if (Rules::isBrowserTriggerEnabled()) {
// If Browser Archiving is enabled, it is likely there are many more temporary archives
// We delete more often which is safe, since reports are re-processed on demand
return Date::factory($this->now - 2 * $temporaryArchivingTimeout)->getDateTime();
}
// If cron core:archive command is building the reports, we should keep all temporary reports from today
return $this->yesterday->getDateTime();
}
private static function getDefaultCustomRangeToPurgeAgeThreshold()
{
$daysRangesValid = Config::getInstance()->General['purge_date_range_archives_after_X_days'];
return Date::factory('today')->subDay($daysRangesValid)->getDateTime();
}
/**
* For tests.
*
* @param Date $yesterday
*/
public function setYesterdayDate(Date $yesterday)
{
$this->yesterday = $yesterday;
}
/**
* For tests.
*
* @param Date $today
*/
public function setTodayDate(Date $today)
{
$this->today = $today;
}
/**
* For tests.
*
* @param int $now
*/
public function setNow($now)
{
$this->now = $now;
}
}

View File

@ -0,0 +1,49 @@
<?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\Archive;
use Piwik\DataTable;
interface ArchiveQuery
{
/**
* @param string|string[] $names
* @return false|number|array
*/
public function getNumeric($names);
/**
* @param string|string[] $names
* @return DataTable|DataTable\Map
*/
public function getDataTableFromNumeric($names);
/**
* @param $names
* @return mixed
*/
public function getDataTableFromNumericAndMergeChildren($names);
/**
* @param string $name
* @param int|string|null $idSubtable
* @return DataTable|DataTable\Map
*/
public function getDataTable($name, $idSubtable = null);
/**
* @param string $name
* @param int|string|null $idSubtable
* @param int|null $depth
* @param bool $addMetadataSubtableId
* @return DataTable|DataTable\Map
*/
public function getDataTableExpanded($name, $idSubtable = null, $depth = null, $addMetadataSubtableId = true);
}

View File

@ -0,0 +1,127 @@
<?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\Archive;
use Piwik\Archive;
use Piwik\Period;
use Piwik\Segment;
use Piwik\Site;
use Piwik\Period\Factory as PeriodFactory;
class ArchiveQueryFactory
{
public function __construct()
{
// empty
}
/**
* @see \Piwik\Archive::build()
*/
public function build($idSites, $strPeriod, $strDate, $strSegment = false, $_restrictSitesToLogin = false)
{
list($websiteIds, $timezone, $idSiteIsAll) = $this->getSiteInfoFromQueryParam($idSites, $_restrictSitesToLogin);
list($allPeriods, $isMultipleDate) = $this->getPeriodInfoFromQueryParam($strDate, $strPeriod, $timezone);
$segment = $this->getSegmentFromQueryParam($strSegment, $websiteIds);
return $this->factory($segment, $allPeriods, $websiteIds, $idSiteIsAll, $isMultipleDate);
}
/**
* @see \Piwik\Archive::factory()
*/
public function factory(Segment $segment, array $periods, array $idSites, $idSiteIsAll = false, $isMultipleDate = false)
{
$forceIndexedBySite = false;
$forceIndexedByDate = false;
if ($idSiteIsAll || count($idSites) > 1) {
$forceIndexedBySite = true;
}
if (count($periods) > 1 || $isMultipleDate) {
$forceIndexedByDate = true;
}
$params = new Parameters($idSites, $periods, $segment);
return $this->newInstance($params, $forceIndexedBySite, $forceIndexedByDate);
}
public function newInstance(Parameters $params, $forceIndexedBySite, $forceIndexedByDate)
{
return new Archive($params, $forceIndexedBySite, $forceIndexedByDate);
}
/**
* Parses the site ID string provided in the 'idSite' query parameter to a list of
* website IDs.
*
* @param string $idSites the value of the 'idSite' query parameter
* @param bool $_restrictSitesToLogin
* @return array an array containing three elements:
* - an array of website IDs
* - string timezone to use (or false to use no timezone) when creating periods.
* - true if the request was for all websites (this forces the archive result to
* be indexed by site, even if there is only one site in Piwik)
*/
protected function getSiteInfoFromQueryParam($idSites, $_restrictSitesToLogin)
{
$websiteIds = Site::getIdSitesFromIdSitesString($idSites, $_restrictSitesToLogin);
$timezone = false;
if (count($websiteIds) == 1) {
$timezone = Site::getTimezoneFor($websiteIds[0]);
}
$idSiteIsAll = $idSites == Archive::REQUEST_ALL_WEBSITES_FLAG;
return [$websiteIds, $timezone, $idSiteIsAll];
}
/**
* Parses the date & period query parameters into a list of periods.
*
* @param string $strDate the value of the 'date' query parameter
* @param string $strPeriod the value of the 'period' query parameter
* @param string $timezone the timezone to use when constructing periods.
* @return array an array containing two elements:
* - the list of period objects to query archive data for
* - true if the request was for multiple periods (ie, two months, two weeks, etc.), false if otherwise.
* (this forces the archive result to be indexed by period, even if the list of periods
* has only one period).
*/
protected function getPeriodInfoFromQueryParam($strDate, $strPeriod, $timezone)
{
if (Period::isMultiplePeriod($strDate, $strPeriod)) {
$oPeriod = PeriodFactory::build($strPeriod, $strDate, $timezone);
$allPeriods = $oPeriod->getSubperiods();
} else {
$oPeriod = PeriodFactory::makePeriodFromQueryParams($timezone, $strPeriod, $strDate);
$allPeriods = array($oPeriod);
}
$isMultipleDate = Period::isMultiplePeriod($strDate, $strPeriod);
return [$allPeriods, $isMultipleDate];
}
/**
* Parses the segment query parameter into a Segment object.
*
* @param string $strSegment the value of the 'segment' query parameter.
* @param int[] $websiteIds the list of sites being queried.
* @return Segment
*/
protected function getSegmentFromQueryParam($strSegment, $websiteIds)
{
return new Segment($strSegment, $websiteIds);
}
}

View File

@ -0,0 +1,144 @@
<?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\Archive;
use Piwik\DataTable;
/**
* This class is used to split blobs of DataTables into chunks. Each blob used to be stored under one blob in the
* archive table. For better efficiency we do now combine multiple DataTable into one blob entry.
*
* Chunks are identified by having the recordName $recordName_chunk_0_99, $recordName_chunk_100_199 (this chunk stores
* the subtable 100-199).
*/
class Chunk
{
const ARCHIVE_APPENDIX_SUBTABLES = 'chunk';
const NUM_TABLES_IN_CHUNK = 100;
/**
* Get's the record name to use for a given tableId/subtableId.
*
* @param string $recordName eg 'Actions_ActionsUrl'
* @param int $tableId eg '5' for tableId '5'
* @return string eg 'Actions_ActionsUrl_chunk_0_99' as the table should be stored under this blob id.
*/
public function getRecordNameForTableId($recordName, $tableId)
{
$chunk = (floor($tableId / self::NUM_TABLES_IN_CHUNK));
$start = $chunk * self::NUM_TABLES_IN_CHUNK;
$end = $start + self::NUM_TABLES_IN_CHUNK - 1;
return $recordName . $this->getAppendix() . $start . '_' . $end;
}
/**
* Moves the given blobs into chunks and assigns a proper record name containing the chunk number.
*
* @param string $recordName The original archive record name, eg 'Actions_ActionsUrl'
* @param array $blobs An array containg a mapping of tableIds to blobs. Eg array(0 => 'blob', 1 => 'subtableBlob', ...)
* @return array An array where each blob is moved into a chunk, indexed by recordNames.
* eg array('Actions_ActionsUrl_chunk_0_99' => array(0 => 'blob', 1 => 'subtableBlob', ...),
* 'Actions_ActionsUrl_chunk_100_199' => array(...))
*/
public function moveArchiveBlobsIntoChunks($recordName, $blobs)
{
$chunks = array();
foreach ($blobs as $tableId => $blob) {
$name = $this->getRecordNameForTableId($recordName, $tableId);
if (!array_key_exists($name, $chunks)) {
$chunks[$name] = array();
}
$chunks[$name][$tableId] = $blob;
}
return $chunks;
}
/**
* Detects whether a recordName like 'Actions_ActionUrls_chunk_0_99' or 'Actions_ActionUrls' belongs to a
* chunk or not.
*
* To be a valid recordName that belongs to a chunk it must end with '_chunk_NUMERIC_NUMERIC'.
*
* @param string $recordName
* @return bool
*/
public function isRecordNameAChunk($recordName)
{
$posAppendix = $this->getEndPosOfChunkAppendix($recordName);
if (false === $posAppendix) {
return false;
}
// will contain "0_99" of "chunk_0_99"
$blobId = substr($recordName, $posAppendix);
return $this->isChunkRange($blobId);
}
private function isChunkRange($blobId)
{
$blobId = explode('_', $blobId);
return 2 === count($blobId) && is_numeric($blobId[0]) && is_numeric($blobId[1]);
}
/**
* When having a record like 'Actions_ActionUrls_chunk_0_99" it will return the raw recordName 'Actions_ActionUrls'.
*
* @param string $recordName
* @return string
*/
public function getRecordNameWithoutChunkAppendix($recordName)
{
if (!$this->isRecordNameAChunk($recordName)) {
return $recordName;
}
$posAppendix = $this->getStartPosOfChunkAppendix($recordName);
if (false === $posAppendix) {
return $recordName;
}
return substr($recordName, 0, $posAppendix);
}
/**
* Returns the string that is appended to the original record name. This appendix identifes a record name is a
* chunk.
* @return string
*/
public function getAppendix()
{
return '_' . self::ARCHIVE_APPENDIX_SUBTABLES . '_';
}
private function getStartPosOfChunkAppendix($recordName)
{
return strpos($recordName, $this->getAppendix());
}
private function getEndPosOfChunkAppendix($recordName)
{
$pos = strpos($recordName, $this->getAppendix());
if ($pos === false) {
return false;
}
return $pos + strlen($this->getAppendix());
}
}

View File

@ -0,0 +1,375 @@
<?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\Archive;
use Exception;
use Piwik\DataTable;
/**
* This class is used to hold and transform archive data for the Archive class.
*
* Archive data is loaded into an instance of this type, can be indexed by archive
* metadata (such as the site ID, period string, etc.), and can be transformed into
* DataTable and Map instances.
*/
class DataCollection
{
const METADATA_CONTAINER_ROW_KEY = '_metadata';
/**
* The archive data, indexed first by site ID and then by period date range. Eg,
*
* array(
* '0' => array(
* array(
* '2012-01-01,2012-01-01' => array(...),
* '2012-01-02,2012-01-02' => array(...),
* )
* ),
* '1' => array(
* array(
* '2012-01-01,2012-01-01' => array(...),
* )
* )
* )
*
* Archive data can be either a numeric value or a serialized string blob. Every
* piece of archive data is associated by it's archive name. For example,
* the array(...) above could look like:
*
* array(
* 'nb_visits' => 1,
* 'nb_actions' => 2
* )
*
* There is a special element '_metadata' in data rows that holds values treated
* as DataTable metadata.
*/
private $data = array();
/**
* The whole list of metric/record names that were used in the archive query.
*
* @var array
*/
private $dataNames;
/**
* The type of data that was queried for (ie, "blob" or "numeric").
*
* @var string
*/
private $dataType;
/**
* The default values to use for each metric/record name that's being queried
* for.
*
* @var array
*/
private $defaultRow;
/**
* The list of all site IDs that were queried for.
*
* @var array
*/
private $sitesId;
/**
* The list of all periods that were queried for. Each period is associated with
* the period's range string. Eg,
*
* array(
* '2012-01-01,2012-01-31' => new Period(...),
* '2012-02-01,2012-02-28' => new Period(...),
* )
*
* @var \Piwik\Period[]
*/
private $periods;
/**
* Constructor.
*
* @param array $dataNames @see $this->dataNames
* @param string $dataType @see $this->dataType
* @param array $sitesId @see $this->sitesId
* @param \Piwik\Period[] $periods @see $this->periods
* @param array $defaultRow @see $this->defaultRow
*/
public function __construct($dataNames, $dataType, $sitesId, $periods, $defaultRow = null)
{
$this->dataNames = $dataNames;
$this->dataType = $dataType;
if ($defaultRow === null) {
$defaultRow = array_fill_keys($dataNames, 0);
}
$this->sitesId = $sitesId;
foreach ($periods as $period) {
$this->periods[$period->getRangeString()] = $period;
}
$this->defaultRow = $defaultRow;
}
/**
* Returns a reference to the data for a specific site & period. If there is
* no data for the given site ID & period, it is set to the default row.
*
* @param int $idSite
* @param string $period eg, '2012-01-01,2012-01-31'
*/
public function &get($idSite, $period)
{
if (!isset($this->data[$idSite][$period])) {
$this->data[$idSite][$period] = $this->defaultRow;
}
return $this->data[$idSite][$period];
}
/**
* Set data for a specific site & period. If there is no data for the given site ID & period,
* it is set to the default row.
*
* @param int $idSite
* @param string $period eg, '2012-01-01,2012-01-31'
* @param string $name eg 'nb_visits'
* @param string $value eg 5
*/
public function set($idSite, $period, $name, $value)
{
$row = & $this->get($idSite, $period);
$row[$name] = $value;
}
/**
* Adds a new metadata to the data for specific site & period. If there is no
* data for the given site ID & period, it is set to the default row.
*
* Note: Site ID and period range string are two special types of metadata. Since
* the data stored in this class is indexed by site & period, this metadata is not
* stored in individual data rows.
*
* @param int $idSite
* @param string $period eg, '2012-01-01,2012-01-31'
* @param string $name The metadata name.
* @param mixed $value The metadata name.
*/
public function addMetadata($idSite, $period, $name, $value)
{
$row = & $this->get($idSite, $period);
$row[self::METADATA_CONTAINER_ROW_KEY][$name] = $value;
}
/**
* Returns archive data as an array indexed by metadata.
*
* @param array $resultIndices An array mapping metadata names to pretty labels
* for them. Each archive data row will be indexed
* by the metadata specified here.
*
* Eg, array('site' => 'idSite', 'period' => 'Date')
* @return array
*/
public function getIndexedArray($resultIndices)
{
$indexKeys = array_keys($resultIndices);
$result = $this->createOrderedIndex($indexKeys);
foreach ($this->data as $idSite => $rowsByPeriod) {
foreach ($rowsByPeriod as $period => $row) {
// FIXME: This hack works around a strange bug that occurs when getting
// archive IDs through ArchiveProcessing instances. When a table
// does not already exist, for some reason the archive ID for
// today (or from two days ago) will be added to the Archive
// instances list. The Archive instance will then select data
// for periods outside of the requested set.
// working around the bug here, but ideally, we need to figure
// out why incorrect idarchives are being selected.
if (empty($this->periods[$period])) {
continue;
}
$this->putRowInIndex($result, $indexKeys, $row, $idSite, $period);
}
}
return $result;
}
/**
* Returns archive data as a DataTable indexed by metadata. Indexed data will
* be represented by Map instances.
*
* @param array $resultIndices An array mapping metadata names to pretty labels
* for them. Each archive data row will be indexed
* by the metadata specified here.
*
* Eg, array('site' => 'idSite', 'period' => 'Date')
* @return DataTable|DataTable\Map
*/
public function getDataTable($resultIndices)
{
$dataTableFactory = new DataTableFactory(
$this->dataNames, $this->dataType, $this->sitesId, $this->periods, $this->defaultRow);
$index = $this->getIndexedArray($resultIndices);
return $dataTableFactory->make($index, $resultIndices);
}
/**
* See {@link DataTableFactory::makeMerged()}
*
* @param array $resultIndices
* @return DataTable|DataTable\Map
* @throws Exception
*/
public function getMergedDataTable($resultIndices)
{
$dataTableFactory = new DataTableFactory(
$this->dataNames, $this->dataType, $this->sitesId, $this->periods, $this->defaultRow);
$index = $this->getIndexedArray($resultIndices);
return $dataTableFactory->makeMerged($index, $resultIndices);
}
/**
* Returns archive data as a DataTable indexed by metadata. Indexed data will
* be represented by Map instances. Each DataTable will have
* its subtable IDs set.
*
* This function will only work if blob data was loaded and only one record
* was loaded (not including subtables of the record).
*
* @param array $resultIndices An array mapping metadata names to pretty labels
* for them. Each archive data row will be indexed
* by the metadata specified here.
*
* Eg, array('site' => 'idSite', 'period' => 'Date')
* @param int|null $idSubTable The subtable to return.
* @param int|null $depth max depth for subtables.
* @param bool $addMetadataSubTableId Whether to add the DB subtable ID as metadata
* to each datatable, or not.
* @throws Exception
* @return DataTable|DataTable\Map
*/
public function getExpandedDataTable($resultIndices, $idSubTable = null, $depth = null, $addMetadataSubTableId = false)
{
if ($this->dataType != 'blob') {
throw new Exception("DataCollection: cannot call getExpandedDataTable with "
. "{$this->dataType} data types. Only works with blob data.");
}
if (count($this->dataNames) !== 1) {
throw new Exception("DataCollection: cannot call getExpandedDataTable with "
. "more than one record.");
}
$dataTableFactory = new DataTableFactory(
$this->dataNames, 'blob', $this->sitesId, $this->periods, $this->defaultRow);
$dataTableFactory->expandDataTable($depth, $addMetadataSubTableId);
$dataTableFactory->useSubtable($idSubTable);
$index = $this->getIndexedArray($resultIndices);
return $dataTableFactory->make($index, $resultIndices);
}
/**
* Returns metadata for a data row.
*
* @param array $data The data row.
* @return array
*/
public static function getDataRowMetadata($data)
{
if (isset($data[self::METADATA_CONTAINER_ROW_KEY])) {
return $data[self::METADATA_CONTAINER_ROW_KEY];
} else {
return array();
}
}
/**
* Removes all table metadata from a data row.
*
* @param array $data The data row.
*/
public static function removeMetadataFromDataRow(&$data)
{
unset($data[self::METADATA_CONTAINER_ROW_KEY]);
}
/**
* Creates an empty index using a list of metadata names. If the 'site' and/or
* 'period' metadata names are supplied, empty rows are added for every site/period
* that was queried for.
*
* Using this function ensures consistent ordering in the indexed result.
*
* @param array $metadataNamesToIndexBy List of metadata names to index archive data by.
* @return array
*/
private function createOrderedIndex($metadataNamesToIndexBy)
{
$result = array();
if (!empty($metadataNamesToIndexBy)) {
$metadataName = array_shift($metadataNamesToIndexBy);
if ($metadataName == DataTableFactory::TABLE_METADATA_SITE_INDEX) {
$indexKeyValues = array_values($this->sitesId);
} elseif ($metadataName == DataTableFactory::TABLE_METADATA_PERIOD_INDEX) {
$indexKeyValues = array_keys($this->periods);
}
if (empty($metadataNamesToIndexBy)) {
$result = array_fill_keys($indexKeyValues, array());
} else {
foreach ($indexKeyValues as $key) {
$result[$key] = $this->createOrderedIndex($metadataNamesToIndexBy);
}
}
}
return $result;
}
/**
* Puts an archive data row in an index.
*/
private function putRowInIndex(&$index, $metadataNamesToIndexBy, $row, $idSite, $period)
{
$currentLevel = & $index;
foreach ($metadataNamesToIndexBy as $metadataName) {
if ($metadataName == DataTableFactory::TABLE_METADATA_SITE_INDEX) {
$key = $idSite;
} elseif ($metadataName == DataTableFactory::TABLE_METADATA_PERIOD_INDEX) {
$key = $period;
} else {
$key = $row[self::METADATA_CONTAINER_ROW_KEY][$metadataName];
}
if (!isset($currentLevel[$key])) {
$currentLevel[$key] = array();
}
$currentLevel = & $currentLevel[$key];
}
$currentLevel = $row;
}
}

View File

@ -0,0 +1,552 @@
<?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\Archive;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\Period\Week;
use Piwik\Site;
/**
* Creates a DataTable or Set instance based on an array
* index created by DataCollection.
*
* This class is only used by DataCollection.
*/
class DataTableFactory
{
/**
* @see DataCollection::$dataNames.
*/
private $dataNames;
/**
* @see DataCollection::$dataType.
*/
private $dataType;
/**
* Whether to expand the DataTables that're created or not. Expanding a DataTable
* means creating DataTables using subtable blobs and correctly setting the subtable
* IDs of all DataTables.
*
* @var bool
*/
private $expandDataTable = false;
/**
* Whether to add the subtable ID used in the database to the in-memory DataTables
* as metadata or not.
*
* @var bool
*/
private $addMetadataSubtableId = false;
/**
* The maximum number of subtable levels to create when creating an expanded
* DataTable.
*
* @var int
*/
private $maxSubtableDepth = null;
/**
* @see DataCollection::$sitesId.
*/
private $sitesId;
/**
* @see DataCollection::$periods.
*/
private $periods;
/**
* The ID of the subtable to create a DataTable for. Only relevant for blob data.
*
* @var int|null
*/
private $idSubtable = null;
/**
* @see DataCollection::$defaultRow.
*/
private $defaultRow;
const TABLE_METADATA_SITE_INDEX = 'site';
const TABLE_METADATA_PERIOD_INDEX = 'period';
/**
* Constructor.
*/
public function __construct($dataNames, $dataType, $sitesId, $periods, $defaultRow)
{
$this->dataNames = $dataNames;
$this->dataType = $dataType;
$this->sitesId = $sitesId;
//here index period by string only
$this->periods = $periods;
$this->defaultRow = $defaultRow;
}
/**
* Returns the ID of the site a table is related to based on the 'site' metadata entry,
* or null if there is none.
*
* @param DataTable $table
* @return int|null
*/
public static function getSiteIdFromMetadata(DataTable $table)
{
$site = $table->getMetadata('site');
if (empty($site)) {
return null;
} else {
return $site->getId();
}
}
/**
* Tells the factory instance to expand the DataTables that are created by
* creating subtables and setting the subtable IDs of rows w/ subtables correctly.
*
* @param null|int $maxSubtableDepth max depth for subtables.
* @param bool $addMetadataSubtableId Whether to add the subtable ID used in the
* database to the in-memory DataTables as
* metadata or not.
*/
public function expandDataTable($maxSubtableDepth = null, $addMetadataSubtableId = false)
{
$this->expandDataTable = true;
$this->maxSubtableDepth = $maxSubtableDepth;
$this->addMetadataSubtableId = $addMetadataSubtableId;
}
/**
* Tells the factory instance to create a DataTable using a blob with the
* supplied subtable ID.
*
* @param int $idSubtable An in-database subtable ID.
* @throws \Exception
*/
public function useSubtable($idSubtable)
{
if (count($this->dataNames) !== 1) {
throw new \Exception("DataTableFactory: Getting subtables for multiple records in one"
. " archive query is not currently supported.");
}
$this->idSubtable = $idSubtable;
}
private function isNumericDataType()
{
return $this->dataType == 'numeric';
}
/**
* Creates a DataTable|Set instance using an index of
* archive data.
*
* @param array $index @see DataCollection
* @param array $resultIndices an array mapping metadata names with pretty metadata
* labels.
* @return DataTable|DataTable\Map
*/
public function make($index, $resultIndices)
{
$keyMetadata = $this->getDefaultMetadata();
if (empty($resultIndices)) {
// for numeric data, if there's no index (and thus only 1 site & period in the query),
// we want to display every queried metric name
if (empty($index)
&& $this->isNumericDataType()
) {
$index = $this->defaultRow;
}
$dataTable = $this->createDataTable($index, $keyMetadata);
} else {
$dataTable = $this->createDataTableMapFromIndex($index, $resultIndices, $keyMetadata);
}
return $dataTable;
}
/**
* Creates a merged DataTable|Map instance using an index of archive data similar to {@link make()}.
*
* Whereas {@link make()} creates a Map for each result index (period and|or site), this will only create a Map
* for a period result index and move all site related indices into one dataTable. This is the same as doing
* `$dataTableFactory->make()->mergeChildren()` just much faster. It is mainly useful for reports across many sites
* eg `MultiSites.getAll`. Was done as part of https://github.com/piwik/piwik/issues/6809
*
* @param array $index @see DataCollection
* @param array $resultIndices an array mapping metadata names with pretty metadata labels.
*
* @return DataTable|DataTable\Map
* @throws \Exception
*/
public function makeMerged($index, $resultIndices)
{
if (!$this->isNumericDataType()) {
throw new \Exception('This method is supposed to work with non-numeric data types but it is not tested. To use it, remove this exception and write tests to be sure it works.');
}
$hasSiteIndex = isset($resultIndices[self::TABLE_METADATA_SITE_INDEX]);
$hasPeriodIndex = isset($resultIndices[self::TABLE_METADATA_PERIOD_INDEX]);
$isNumeric = $this->isNumericDataType();
// to be backwards compatible use a Simple table if needed as it will be formatted differently
$useSimpleDataTable = !$hasSiteIndex && $isNumeric;
if (!$hasSiteIndex) {
$firstIdSite = reset($this->sitesId);
$index = array($firstIdSite => $index);
}
if ($hasPeriodIndex) {
$dataTable = $this->makeMergedTableWithPeriodAndSiteIndex($index, $resultIndices, $useSimpleDataTable, $isNumeric);
} else {
$dataTable = $this->makeMergedWithSiteIndex($index, $useSimpleDataTable, $isNumeric);
}
return $dataTable;
}
/**
* Creates a DataTable|Set instance using an array
* of blobs.
*
* If only one record is being queried, a single DataTable will
* be returned. Otherwise, a DataTable\Map is returned that indexes
* DataTables by record name.
*
* If expandDataTable was called, and only one record is being queried,
* the created DataTable's subtables will be expanded.
*
* @param array $blobRow
* @return DataTable|DataTable\Map
*/
private function makeFromBlobRow($blobRow, $keyMetadata)
{
if ($blobRow === false) {
return new DataTable();
}
if (count($this->dataNames) === 1) {
return $this->makeDataTableFromSingleBlob($blobRow, $keyMetadata);
} else {
return $this->makeIndexedByRecordNameDataTable($blobRow, $keyMetadata);
}
}
/**
* Creates a DataTable for one record from an archive data row.
*
* @see makeFromBlobRow
*
* @param array $blobRow
* @return DataTable
*/
private function makeDataTableFromSingleBlob($blobRow, $keyMetadata)
{
$recordName = reset($this->dataNames);
if ($this->idSubtable !== null) {
$recordName .= '_' . $this->idSubtable;
}
if (!empty($blobRow[$recordName])) {
$table = DataTable::fromSerializedArray($blobRow[$recordName]);
} else {
$table = new DataTable();
}
// set table metadata
$table->setAllTableMetadata(array_merge(DataCollection::getDataRowMetadata($blobRow), $keyMetadata));
if ($this->expandDataTable) {
$table->enableRecursiveFilters();
$this->setSubtables($table, $blobRow);
}
return $table;
}
/**
* Creates a DataTable for every record in an archive data row and puts them
* in a DataTable\Map instance.
*
* @param array $blobRow
* @return DataTable\Map
*/
private function makeIndexedByRecordNameDataTable($blobRow, $keyMetadata)
{
$table = new DataTable\Map();
$table->setKeyName('recordName');
$tableMetadata = array_merge(DataCollection::getDataRowMetadata($blobRow), $keyMetadata);
foreach ($blobRow as $name => $blob) {
$newTable = DataTable::fromSerializedArray($blob);
$newTable->setAllTableMetadata($tableMetadata);
$table->addTable($newTable, $name);
}
return $table;
}
/**
* Creates a Set from an array index.
*
* @param array $index @see DataCollection
* @param array $resultIndices @see make
* @param array $keyMetadata The metadata to add to the table when it's created.
* @return DataTable\Map
*/
private function createDataTableMapFromIndex($index, $resultIndices, $keyMetadata)
{
$result = new DataTable\Map();
$result->setKeyName(reset($resultIndices));
$resultIndex = key($resultIndices);
array_shift($resultIndices);
$hasIndices = !empty($resultIndices);
foreach ($index as $label => $value) {
$keyMetadata[$resultIndex] = $this->createTableIndexMetadata($resultIndex, $label);
if ($hasIndices) {
$newTable = $this->createDataTableMapFromIndex($value, $resultIndices, $keyMetadata);
} else {
$newTable = $this->createDataTable($value, $keyMetadata);
}
$result->addTable($newTable, $this->prettifyIndexLabel($resultIndex, $label));
}
return $result;
}
private function createTableIndexMetadata($resultIndex, $label)
{
if ($resultIndex === DataTableFactory::TABLE_METADATA_SITE_INDEX) {
return new Site($label);
} elseif ($resultIndex === DataTableFactory::TABLE_METADATA_PERIOD_INDEX) {
return $this->periods[$label];
}
}
/**
* Creates a DataTable instance from an index row.
*
* @param array $data An archive data row.
* @param array $keyMetadata The metadata to add to the table(s) when created.
* @return DataTable|DataTable\Map
*/
private function createDataTable($data, $keyMetadata)
{
if ($this->dataType == 'blob') {
$result = $this->makeFromBlobRow($data, $keyMetadata);
} else {
$result = $this->makeFromMetricsArray($data, $keyMetadata);
}
return $result;
}
/**
* Creates DataTables from $dataTable's subtable blobs (stored in $blobRow) and sets
* the subtable IDs of each DataTable row.
*
* @param DataTable $dataTable
* @param array $blobRow An array associating record names (w/ subtable if applicable)
* with blob values. This should hold every subtable blob for
* the loaded DataTable.
* @param int $treeLevel
*/
private function setSubtables($dataTable, $blobRow, $treeLevel = 0)
{
if ($this->maxSubtableDepth
&& $treeLevel >= $this->maxSubtableDepth
) {
// unset the subtables so DataTableManager doesn't throw
foreach ($dataTable->getRowsWithoutSummaryRow() as $row) {
$row->removeSubtable();
}
return;
}
$dataName = reset($this->dataNames);
foreach ($dataTable->getRowsWithoutSummaryRow() as $row) {
$sid = $row->getIdSubDataTable();
if ($sid === null) {
continue;
}
$blobName = $dataName . "_" . $sid;
if (isset($blobRow[$blobName])) {
$subtable = DataTable::fromSerializedArray($blobRow[$blobName]);
$this->setSubtables($subtable, $blobRow, $treeLevel + 1);
// we edit the subtable ID so that it matches the newly table created in memory
// NB: we don't overwrite the datatableid in the case we are displaying the table expanded.
if ($this->addMetadataSubtableId) {
// this will be written back to the column 'idsubdatatable' just before rendering,
// see Renderer/Php.php
$row->addMetadata('idsubdatatable_in_db', $row->getIdSubDataTable());
}
$row->setSubtable($subtable);
}
}
}
private function getDefaultMetadata()
{
return array(
DataTableFactory::TABLE_METADATA_SITE_INDEX => new Site(reset($this->sitesId)),
DataTableFactory::TABLE_METADATA_PERIOD_INDEX => reset($this->periods),
);
}
/**
* Returns the pretty version of an index label.
*
* @param string $labelType eg, 'site', 'period', etc.
* @param string $label eg, '0', '1', '2012-01-01,2012-01-31', etc.
* @return string
*/
private function prettifyIndexLabel($labelType, $label)
{
if ($labelType == self::TABLE_METADATA_PERIOD_INDEX) { // prettify period labels
$period = $this->periods[$label];
$label = $period->getLabel();
if ($label === 'week' || $label === 'range') {
return $period->getRangeString();
}
return $period->getPrettyString();
}
return $label;
}
/**
* @param $data
* @return DataTable\Simple
*/
private function makeFromMetricsArray($data, $keyMetadata)
{
$table = new DataTable\Simple();
if (!empty($data)) {
$table->setAllTableMetadata(array_merge(DataCollection::getDataRowMetadata($data), $keyMetadata));
DataCollection::removeMetadataFromDataRow($data);
$table->addRow(new Row(array(Row::COLUMNS => $data)));
} else {
// if we're querying numeric data, we couldn't find any, and we're only
// looking for one metric, add a row w/ one column w/ value 0. this is to
// ensure that the PHP renderer outputs 0 when only one column is queried.
// w/o this code, an empty array would be created, and other parts of Piwik
// would break.
if (count($this->dataNames) == 1
&& $this->isNumericDataType()
) {
$name = reset($this->dataNames);
$table->addRow(new Row(array(Row::COLUMNS => array($name => 0))));
}
$table->setAllTableMetadata($keyMetadata);
}
$result = $table;
return $result;
}
private function makeMergedTableWithPeriodAndSiteIndex($index, $resultIndices, $useSimpleDataTable, $isNumeric)
{
$map = new DataTable\Map();
$map->setKeyName($resultIndices[self::TABLE_METADATA_PERIOD_INDEX]);
// we save all tables of the map in this array to be able to add rows fast
$tables = array();
foreach ($this->periods as $range => $period) {
// as the resulting table is "merged", we do only set Period metedata and no metadata for site. Instead each
// row will have an idsite metadata entry.
$metadata = array(self::TABLE_METADATA_PERIOD_INDEX => $period);
if ($useSimpleDataTable) {
$table = new DataTable\Simple();
} else {
$table = new DataTable();
}
$table->setAllTableMetadata($metadata);
$map->addTable($table, $this->prettifyIndexLabel(self::TABLE_METADATA_PERIOD_INDEX, $range));
$tables[$range] = $table;
}
foreach ($index as $idsite => $table) {
$rowMeta = array('idsite' => $idsite);
foreach ($table as $range => $row) {
if (!empty($row)) {
$tables[$range]->addRow(new Row(array(
Row::COLUMNS => $row,
Row::METADATA => $rowMeta)
));
} elseif ($isNumeric) {
$tables[$range]->addRow(new Row(array(
Row::COLUMNS => $this->defaultRow,
Row::METADATA => $rowMeta)
));
}
}
}
return $map;
}
private function makeMergedWithSiteIndex($index, $useSimpleDataTable, $isNumeric)
{
if ($useSimpleDataTable) {
$table = new DataTable\Simple();
} else {
$table = new DataTable();
}
$table->setAllTableMetadata(array(DataTableFactory::TABLE_METADATA_PERIOD_INDEX => reset($this->periods)));
foreach ($index as $idsite => $row) {
if (!empty($row)) {
$table->addRow(new Row(array(
Row::COLUMNS => $row,
Row::METADATA => array('idsite' => $idsite))
));
} elseif ($isNumeric) {
$table->addRow(new Row(array(
Row::COLUMNS => $this->defaultRow,
Row::METADATA => array('idsite' => $idsite))
));
}
}
return $table;
}
}

View File

@ -0,0 +1,59 @@
<?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\Archive;
use Piwik\Period;
use Piwik\Segment;
class Parameters
{
/**
* The list of site IDs to query archive data for.
*
* @var array
*/
private $idSites = array();
/**
* The list of Period's to query archive data for.
*
* @var Period[]
*/
private $periods = array();
/**
* Segment applied to the visits set.
*
* @var Segment
*/
private $segment;
public function getSegment()
{
return $this->segment;
}
public function __construct($idSites, $periods, Segment $segment)
{
$this->idSites = $idSites;
$this->periods = $periods;
$this->segment = $segment;
}
public function getPeriods()
{
return $this->periods;
}
public function getIdSites()
{
return $this->idSites;
}
}

View File

@ -0,0 +1,640 @@
<?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;
use Exception;
use Piwik\Archive\DataTableFactory;
use Piwik\ArchiveProcessor\Parameters;
use Piwik\ArchiveProcessor\Rules;
use Piwik\DataAccess\ArchiveWriter;
use Piwik\DataAccess\LogAggregator;
use Piwik\DataTable\Manager;
use Piwik\DataTable\Map;
use Piwik\DataTable\Row;
use Piwik\Segment\SegmentExpression;
/**
* Used by {@link Piwik\Plugin\Archiver} instances to insert and aggregate archive data.
*
* ### See also
*
* - **{@link Piwik\Plugin\Archiver}** - to learn how plugins should implement their own analytics
* aggregation logic.
* - **{@link Piwik\DataAccess\LogAggregator}** - to learn how plugins can perform data aggregation
* across Piwik's log tables.
*
* ### Examples
*
* **Inserting numeric data**
*
* // function in an Archiver descendant
* public function aggregateDayReport()
* {
* $archiveProcessor = $this->getProcessor();
*
* $myFancyMetric = // ... calculate the metric value ...
* $archiveProcessor->insertNumericRecord('MyPlugin_myFancyMetric', $myFancyMetric);
* }
*
* **Inserting serialized DataTables**
*
* // function in an Archiver descendant
* public function aggregateDayReport()
* {
* $archiveProcessor = $this->getProcessor();
*
* $maxRowsInTable = Config::getInstance()->General['datatable_archiving_maximum_rows_standard'];j
*
* $dataTable = // ... build by aggregating visits ...
* $serializedData = $dataTable->getSerialized($maxRowsInTable, $maxRowsInSubtable = $maxRowsInTable,
* $columnToSortBy = Metrics::INDEX_NB_VISITS);
*
* $archiveProcessor->insertBlobRecords('MyPlugin_myFancyReport', $serializedData);
* }
*
* **Aggregating archive data**
*
* // function in Archiver descendant
* public function aggregateMultipleReports()
* {
* $archiveProcessor = $this->getProcessor();
*
* // aggregate a metric
* $archiveProcessor->aggregateNumericMetrics('MyPlugin_myFancyMetric');
* $archiveProcessor->aggregateNumericMetrics('MyPlugin_mySuperFancyMetric', 'max');
*
* // aggregate a report
* $archiveProcessor->aggregateDataTableRecords('MyPlugin_myFancyReport');
* }
*
*/
class ArchiveProcessor
{
/**
* @var \Piwik\DataAccess\ArchiveWriter
*/
private $archiveWriter;
/**
* @var \Piwik\DataAccess\LogAggregator
*/
private $logAggregator;
/**
* @var Archive
*/
public $archive = null;
/**
* @var Parameters
*/
private $params;
/**
* @var int
*/
private $numberOfVisits = false;
private $numberOfVisitsConverted = false;
/**
* If true, unique visitors are not calculated when we are aggregating data for multiple sites.
* The `[General] enable_processing_unique_visitors_multiple_sites` INI config option controls
* the value of this variable.
*
* @var bool
*/
private $skipUniqueVisitorsCalculationForMultipleSites = true;
public function __construct(Parameters $params, ArchiveWriter $archiveWriter, LogAggregator $logAggregator)
{
$this->params = $params;
$this->logAggregator = $logAggregator;
$this->archiveWriter = $archiveWriter;
$this->skipUniqueVisitorsCalculationForMultipleSites = Rules::shouldSkipUniqueVisitorsCalculationForMultipleSites();
}
protected function getArchive()
{
if (empty($this->archive)) {
$subPeriods = $this->params->getSubPeriods();
$idSites = $this->params->getIdSites();
$this->archive = Archive::factory($this->params->getSegment(), $subPeriods, $idSites);
}
return $this->archive;
}
public function setNumberOfVisits($visits, $visitsConverted)
{
$this->numberOfVisits = $visits;
$this->numberOfVisitsConverted = $visitsConverted;
}
/**
* Returns the {@link Parameters} object containing the site, period and segment we're archiving
* data for.
*
* @return Parameters
* @api
*/
public function getParams()
{
return $this->params;
}
/**
* Returns a `{@link Piwik\DataAccess\LogAggregator}` instance for the site, period and segment this
* ArchiveProcessor will insert archive data for.
*
* @return LogAggregator
* @api
*/
public function getLogAggregator()
{
return $this->logAggregator;
}
/**
* Array of (column name before => column name renamed) of the columns for which sum operation is invalid.
* These columns will be renamed as per this mapping.
* @var array
*/
protected static $columnsToRenameAfterAggregation = array(
Metrics::INDEX_NB_UNIQ_VISITORS => Metrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS,
Metrics::INDEX_NB_USERS => Metrics::INDEX_SUM_DAILY_NB_USERS,
);
/**
* Sums records for every subperiod of the current period and inserts the result as the record
* for this period.
*
* DataTables are summed recursively so subtables will be summed as well.
*
* @param string|array $recordNames Name(s) of the report we are aggregating, eg, `'Referrers_type'`.
* @param int $maximumRowsInDataTableLevelZero Maximum number of rows allowed in the top level DataTable.
* @param int $maximumRowsInSubDataTable Maximum number of rows allowed in each subtable.
* @param string $columnToSortByBeforeTruncation The name of the column to sort by before truncating a DataTable.
* @param array $columnsAggregationOperation Operations for aggregating columns, see {@link Row::sumRow()}.
* @param array $columnsToRenameAfterAggregation Columns mapped to new names for columns that must change names
* when summed because they cannot be summed, eg,
* `array('nb_uniq_visitors' => 'sum_daily_nb_uniq_visitors')`.
* @param bool|array $countRowsRecursive if set to true, will calculate the recursive rows count for all record names
* which makes it slower. If you only need it for some records pass an array of
* recordNames that defines for which ones you need a recursive row count.
* @return array Returns the row counts of each aggregated report before truncation, eg,
*
* array(
* 'report1' => array('level0' => $report1->getRowsCount,
* 'recursive' => $report1->getRowsCountRecursive()),
* 'report2' => array('level0' => $report2->getRowsCount,
* 'recursive' => $report2->getRowsCountRecursive()),
* ...
* )
* @api
*/
public function aggregateDataTableRecords($recordNames,
$maximumRowsInDataTableLevelZero = null,
$maximumRowsInSubDataTable = null,
$columnToSortByBeforeTruncation = null,
&$columnsAggregationOperation = null,
$columnsToRenameAfterAggregation = null,
$countRowsRecursive = true)
{
if (!is_array($recordNames)) {
$recordNames = array($recordNames);
}
$nameToCount = array();
foreach ($recordNames as $recordName) {
$latestUsedTableId = Manager::getInstance()->getMostRecentTableId();
$table = $this->aggregateDataTableRecord($recordName, $columnsAggregationOperation, $columnsToRenameAfterAggregation);
$nameToCount[$recordName]['level0'] = $table->getRowsCount();
if ($countRowsRecursive === true || (is_array($countRowsRecursive) && in_array($recordName, $countRowsRecursive))) {
$nameToCount[$recordName]['recursive'] = $table->getRowsCountRecursive();
}
$blob = $table->getSerialized($maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation);
Common::destroy($table);
$this->insertBlobRecord($recordName, $blob);
unset($blob);
DataTable\Manager::getInstance()->deleteAll($latestUsedTableId);
}
return $nameToCount;
}
/**
* Aggregates one or more metrics for every subperiod of the current period and inserts the results
* as metrics for the current period.
*
* @param array|string $columns Array of metric names to aggregate.
* @param bool|string $operationToApply The operation to apply to the metric. Either `'sum'`, `'max'` or `'min'`.
* @return array|int Returns the array of aggregate values. If only one metric was aggregated,
* the aggregate value will be returned as is, not in an array.
* For example, if `array('nb_visits', 'nb_hits')` is supplied for `$columns`,
*
* array(
* 'nb_visits' => 3040,
* 'nb_hits' => 405
* )
*
* could be returned. If `array('nb_visits')` or `'nb_visits'` is used for `$columns`,
* then `3040` would be returned.
* @api
*/
public function aggregateNumericMetrics($columns, $operationToApply = false)
{
$metrics = $this->getAggregatedNumericMetrics($columns, $operationToApply);
foreach ($metrics as $column => $value) {
$value = Common::forceDotAsSeparatorForDecimalPoint($value);
$this->archiveWriter->insertRecord($column, $value);
}
// if asked for only one field to sum
if (count($metrics) == 1) {
return reset($metrics);
}
// returns the array of records once summed
return $metrics;
}
public function getNumberOfVisits()
{
if ($this->numberOfVisits === false) {
throw new Exception("visits should have been set here");
}
return $this->numberOfVisits;
}
public function getNumberOfVisitsConverted()
{
return $this->numberOfVisitsConverted;
}
/**
* Caches multiple numeric records in the archive for this processor's site, period
* and segment.
*
* @param array $numericRecords A name-value mapping of numeric values that should be
* archived, eg,
*
* array('Referrers_distinctKeywords' => 23, 'Referrers_distinctCampaigns' => 234)
* @api
*/
public function insertNumericRecords($numericRecords)
{
foreach ($numericRecords as $name => $value) {
$this->insertNumericRecord($name, $value);
}
}
/**
* Caches a single numeric record in the archive for this processor's site, period and
* segment.
*
* Numeric values are not inserted if they equal `0`.
*
* @param string $name The name of the numeric value, eg, `'Referrers_distinctKeywords'`.
* @param float $value The numeric value.
* @api
*/
public function insertNumericRecord($name, $value)
{
$value = round($value, 2);
$value = Common::forceDotAsSeparatorForDecimalPoint($value);
$this->archiveWriter->insertRecord($name, $value);
}
/**
* Caches one or more blob records in the archive for this processor's site, period
* and segment.
*
* @param string $name The name of the record, eg, 'Referrers_type'.
* @param string|array $values A blob string or an array of blob strings. If an array
* is used, the first element in the array will be inserted
* with the `$name` name. The others will be inserted with
* `$name . '_' . $index` as the record name (where $index is
* the index of the blob record in `$values`).
* @api
*/
public function insertBlobRecord($name, $values)
{
$this->archiveWriter->insertBlobRecord($name, $values);
}
/**
* This method selects all DataTables that have the name $name over the period.
* All these DataTables are then added together, and the resulting DataTable is returned.
*
* @param string $name
* @param array $columnsAggregationOperation Operations for aggregating columns, @see Row::sumRow()
* @param array $columnsToRenameAfterAggregation columns in the array (old name, new name) to be renamed as the sum operation is not valid on them (eg. nb_uniq_visitors->sum_daily_nb_uniq_visitors)
* @return DataTable
*/
protected function aggregateDataTableRecord($name, $columnsAggregationOperation = null, $columnsToRenameAfterAggregation = null)
{
try {
ErrorHandler::pushFatalErrorBreadcrumb(__CLASS__, ['name' => $name]);
// By default we shall aggregate all sub-tables.
$dataTable = $this->getArchive()->getDataTableExpanded($name, $idSubTable = null, $depth = null, $addMetadataSubtableId = false);
$columnsRenamed = false;
if ($dataTable instanceof Map) {
$columnsRenamed = true;
// see https://github.com/piwik/piwik/issues/4377
$self = $this;
$dataTable->filter(function ($table) use ($self, $columnsToRenameAfterAggregation) {
if ($self->areColumnsNotAlreadyRenamed($table)) {
/**
* This makes archiving and range dates a lot faster. Imagine we archive a week, then we will
* rename all columns of each 7 day archives. Afterwards we know the columns will be replaced in a
* week archive. When generating month archives, which uses mostly week archives, we do not have
* to replace those columns for the week archives again since we can be sure they were already
* replaced. Same when aggregating year and range archives. This can save up 10% or more when
* aggregating Month, Year and Range archives.
*/
$self->renameColumnsAfterAggregation($table, $columnsToRenameAfterAggregation);
}
});
}
$dataTable = $this->getAggregatedDataTableMap($dataTable, $columnsAggregationOperation);
if (!$columnsRenamed) {
$this->renameColumnsAfterAggregation($dataTable, $columnsToRenameAfterAggregation);
}
} finally {
ErrorHandler::popFatalErrorBreadcrumb();
}
return $dataTable;
}
/**
* Note: public only for use in closure in PHP 5.3.
*
* @param $table
* @return \Piwik\Period
*/
public function areColumnsNotAlreadyRenamed($table)
{
$period = $table->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX);
return !$period || $period->getLabel() === 'day';
}
protected function getOperationForColumns($columns, $defaultOperation)
{
$operationForColumn = array();
foreach ($columns as $name) {
$operation = $defaultOperation;
if (empty($operation)) {
$operation = $this->guessOperationForColumn($name);
}
$operationForColumn[$name] = $operation;
}
return $operationForColumn;
}
protected function enrichWithUniqueVisitorsMetric(Row $row)
{
// skip unique visitors metrics calculation if calculating for multiple sites is disabled
if (!$this->getParams()->isSingleSite()
&& $this->skipUniqueVisitorsCalculationForMultipleSites
) {
return;
}
if ($row->getColumn('nb_uniq_visitors') === false
&& $row->getColumn('nb_users') === false
) {
return;
}
if (!SettingsPiwik::isUniqueVisitorsEnabled($this->getParams()->getPeriod()->getLabel())) {
$row->deleteColumn('nb_uniq_visitors');
$row->deleteColumn('nb_users');
return;
}
$metrics = array(
Metrics::INDEX_NB_USERS
);
if ($this->getParams()->isSingleSite()) {
$uniqueVisitorsMetric = Metrics::INDEX_NB_UNIQ_VISITORS;
} else {
if (!SettingsPiwik::isSameFingerprintAcrossWebsites()) {
throw new Exception("Processing unique visitors across websites is enabled for this instance,
but to process this metric you must first set enable_fingerprinting_across_websites=1
in the config file, under the [Tracker] section.");
}
$uniqueVisitorsMetric = Metrics::INDEX_NB_UNIQ_FINGERPRINTS;
}
$metrics[] = $uniqueVisitorsMetric;
$uniques = $this->computeNbUniques($metrics);
// see edge case as described in https://github.com/piwik/piwik/issues/9357 where uniq_visitors might be higher
// than visits because we archive / process it after nb_visits. Between archiving nb_visits and nb_uniq_visitors
// there could have been a new visit leading to a higher nb_unique_visitors than nb_visits which is not possible
// by definition. In this case we simply use the visits metric instead of unique visitors metric.
$visits = $row->getColumn('nb_visits');
if ($visits !== false && $uniques[$uniqueVisitorsMetric] !== false) {
$uniques[$uniqueVisitorsMetric] = min($uniques[$uniqueVisitorsMetric], $visits);
}
$row->setColumn('nb_uniq_visitors', $uniques[$uniqueVisitorsMetric]);
$row->setColumn('nb_users', $uniques[Metrics::INDEX_NB_USERS]);
}
protected function guessOperationForColumn($column)
{
if (strpos($column, 'max_') === 0) {
return 'max';
}
if (strpos($column, 'min_') === 0) {
return 'min';
}
return 'sum';
}
/**
* Processes number of unique visitors for the given period
*
* This is the only Period metric (ie. week/month/year/range) that we process from the logs directly,
* since unique visitors cannot be summed like other metrics.
*
* @param array Metrics Ids for which to aggregates count of values
* @return array of metrics, where the key is metricid and the value is the metric value
*/
protected function computeNbUniques($metrics)
{
$logAggregator = $this->getLogAggregator();
$query = $logAggregator->queryVisitsByDimension(array(), false, array(), $metrics);
$data = $query->fetch();
return $data;
}
/**
* If the DataTable is a Map, sums all DataTable in the map and return the DataTable.
*
*
* @param $data DataTable|DataTable\Map
* @param $columnsToRenameAfterAggregation array
* @return DataTable
*/
protected function getAggregatedDataTableMap($data, $columnsAggregationOperation)
{
$table = new DataTable();
if (!empty($columnsAggregationOperation)) {
$table->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnsAggregationOperation);
}
if ($data instanceof DataTable\Map) {
// as $date => $tableToSum
$this->aggregatedDataTableMapsAsOne($data, $table);
} else {
$table->addDataTable($data);
}
return $table;
}
/**
* Aggregates the DataTable\Map into the destination $aggregated
* @param $map
* @param $aggregated
*/
protected function aggregatedDataTableMapsAsOne(Map $map, DataTable $aggregated)
{
foreach ($map->getDataTables() as $tableToAggregate) {
if ($tableToAggregate instanceof Map) {
$this->aggregatedDataTableMapsAsOne($tableToAggregate, $aggregated);
} else {
$aggregated->addDataTable($tableToAggregate);
}
}
}
/**
* Note: public only for use in closure in PHP 5.3.
*/
public function renameColumnsAfterAggregation(DataTable $table, $columnsToRenameAfterAggregation = null)
{
// Rename columns after aggregation
if (is_null($columnsToRenameAfterAggregation)) {
$columnsToRenameAfterAggregation = self::$columnsToRenameAfterAggregation;
}
if (empty($columnsToRenameAfterAggregation)) {
return;
}
foreach ($table->getRows() as $row) {
foreach ($columnsToRenameAfterAggregation as $oldName => $newName) {
$row->renameColumn($oldName, $newName);
}
$subTable = $row->getSubtable();
if ($subTable) {
$this->renameColumnsAfterAggregation($subTable, $columnsToRenameAfterAggregation);
}
}
}
protected function getAggregatedNumericMetrics($columns, $operationToApply)
{
if (!is_array($columns)) {
$columns = array($columns);
}
$operationForColumn = $this->getOperationForColumns($columns, $operationToApply);
$dataTable = $this->getArchive()->getDataTableFromNumeric($columns);
$results = $this->getAggregatedDataTableMap($dataTable, $operationForColumn);
if ($results->getRowsCount() > 1) {
throw new Exception("A DataTable is an unexpected state:" . var_export($results, true));
}
$rowMetrics = $results->getFirstRow();
if ($rowMetrics === false) {
$rowMetrics = new Row;
}
$this->enrichWithUniqueVisitorsMetric($rowMetrics);
$this->renameColumnsAfterAggregation($results, self::$columnsToRenameAfterAggregation);
$metrics = $rowMetrics->getColumns();
foreach ($columns as $name) {
if (!isset($metrics[$name])) {
$metrics[$name] = 0;
}
}
return $metrics;
}
/**
* Initiate archiving for a plugin during an ongoing archiving. The plugin can be another
* plugin or the same plugin.
*
* This method should be called during archiving when one plugin uses the report of another
* plugin with a segment. It will ensure reports for that segment & plugin will be archived
* without initiating archiving for every plugin with that segment (which would be a performance
* killer).
*
* @param string $plugin
* @param string $segment
*/
public function processDependentArchive($plugin, $segment)
{
$params = $this->getParams();
if (!$params->isRootArchiveRequest()) { // prevent all recursion
return;
}
$idSites = [$params->getSite()->getId()];
$newSegment = Segment::combine($params->getSegment()->getString(), SegmentExpression::AND_DELIMITER, $segment);
if ($newSegment === $segment && $params->getRequestedPlugin() === $plugin) { // being processed now
return;
}
$newSegment = new Segment($newSegment, $idSites);
if (ArchiveProcessor\Rules::isSegmentPreProcessed($idSites, $newSegment)) {
// will be processed anyway
return;
}
$parameters = new ArchiveProcessor\Parameters($params->getSite(), $params->getPeriod(), $newSegment);
$parameters->onlyArchiveRequestedPlugin();
$parameters->setIsRootArchiveRequest(false);
$archiveLoader = new ArchiveProcessor\Loader($parameters);
$archiveLoader->prepareArchive($plugin);
}
public function getArchiveWriter()
{
return $this->archiveWriter;
}
}

View File

@ -0,0 +1,260 @@
<?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\ArchiveProcessor;
use Piwik\Archive;
use Piwik\Cache;
use Piwik\CacheId;
use Piwik\Common;
use Piwik\Config;
use Piwik\Context;
use Piwik\DataAccess\ArchiveSelector;
use Piwik\Date;
use Piwik\Period;
use Piwik\Piwik;
/**
* This class uses PluginsArchiver class to trigger data aggregation and create archives.
*/
class Loader
{
/**
* Is the current archive temporary. ie.
* - today
* - current week / month / year
*/
protected $temporaryArchive;
/**
* Idarchive in the DB for the requested archive
*
* @var int
*/
protected $idArchive;
/**
* @var Parameters
*/
protected $params;
public function __construct(Parameters $params)
{
$this->params = $params;
}
/**
* @return bool
*/
protected function isThereSomeVisits($visits)
{
return $visits > 0;
}
/**
* @return bool
*/
protected function mustProcessVisitCount($visits)
{
return $visits === false;
}
public function prepareArchive($pluginName)
{
return Context::changeIdSite($this->params->getSite()->getId(), function () use ($pluginName) {
return $this->prepareArchiveImpl($pluginName);
});
}
private function prepareArchiveImpl($pluginName)
{
$this->params->setRequestedPlugin($pluginName);
list($idArchive, $visits, $visitsConverted) = $this->loadExistingArchiveIdFromDb();
if (!empty($idArchive)) {
return $idArchive;
}
list($visits, $visitsConverted) = $this->prepareCoreMetricsArchive($visits, $visitsConverted);
list($idArchive, $visits) = $this->prepareAllPluginsArchive($visits, $visitsConverted);
if ($this->isThereSomeVisits($visits) || PluginsArchiver::doesAnyPluginArchiveWithoutVisits()) {
return $idArchive;
}
return false;
}
/**
* Prepares the core metrics if needed.
*
* @param $visits
* @return array
*/
protected function prepareCoreMetricsArchive($visits, $visitsConverted)
{
$createSeparateArchiveForCoreMetrics = $this->mustProcessVisitCount($visits)
&& !$this->doesRequestedPluginIncludeVisitsSummary();
if ($createSeparateArchiveForCoreMetrics) {
$requestedPlugin = $this->params->getRequestedPlugin();
$this->params->setRequestedPlugin('VisitsSummary');
$pluginsArchiver = new PluginsArchiver($this->params, $this->isArchiveTemporary());
$metrics = $pluginsArchiver->callAggregateCoreMetrics();
$pluginsArchiver->finalizeArchive();
$this->params->setRequestedPlugin($requestedPlugin);
$visits = $metrics['nb_visits'];
$visitsConverted = $metrics['nb_visits_converted'];
}
return array($visits, $visitsConverted);
}
protected function prepareAllPluginsArchive($visits, $visitsConverted)
{
$pluginsArchiver = new PluginsArchiver($this->params, $this->isArchiveTemporary());
if ($this->mustProcessVisitCount($visits)
|| $this->doesRequestedPluginIncludeVisitsSummary()
) {
$metrics = $pluginsArchiver->callAggregateCoreMetrics();
$visits = $metrics['nb_visits'];
$visitsConverted = $metrics['nb_visits_converted'];
}
$forceArchivingWithoutVisits = !$this->isThereSomeVisits($visits) && $this->shouldArchiveForSiteEvenWhenNoVisits();
$pluginsArchiver->callAggregateAllPlugins($visits, $visitsConverted, $forceArchivingWithoutVisits);
$idArchive = $pluginsArchiver->finalizeArchive();
return array($idArchive, $visits);
}
protected function doesRequestedPluginIncludeVisitsSummary()
{
$processAllReportsIncludingVisitsSummary =
Rules::shouldProcessReportsAllPlugins($this->params->getIdSites(), $this->params->getSegment(), $this->params->getPeriod()->getLabel());
$doesRequestedPluginIncludeVisitsSummary = $processAllReportsIncludingVisitsSummary
|| $this->params->getRequestedPlugin() == 'VisitsSummary';
return $doesRequestedPluginIncludeVisitsSummary;
}
protected function isArchivingForcedToTrigger()
{
$period = $this->params->getPeriod()->getLabel();
$debugSetting = 'always_archive_data_period'; // default
if ($period == 'day') {
$debugSetting = 'always_archive_data_day';
} elseif ($period == 'range') {
$debugSetting = 'always_archive_data_range';
}
return (bool) Config::getInstance()->Debug[$debugSetting];
}
/**
* Returns the idArchive if the archive is available in the database for the requested plugin.
* Returns false if the archive needs to be processed.
*
* @return array
*/
protected function loadExistingArchiveIdFromDb()
{
$noArchiveFound = array(false, false, false);
// see isArchiveTemporary()
$minDatetimeArchiveProcessedUTC = $this->getMinTimeArchiveProcessed();
if ($this->isArchivingForcedToTrigger()) {
return $noArchiveFound;
}
$idAndVisits = ArchiveSelector::getArchiveIdAndVisits($this->params, $minDatetimeArchiveProcessedUTC);
if (!$idAndVisits) {
return $noArchiveFound;
}
return $idAndVisits;
}
/**
* Returns the minimum archive processed datetime to look at. Only public for tests.
*
* @return int|bool Datetime timestamp, or false if must look at any archive available
*/
protected function getMinTimeArchiveProcessed()
{
$endDateTimestamp = self::determineIfArchivePermanent($this->params->getDateEnd());
$isArchiveTemporary = ($endDateTimestamp === false);
$this->temporaryArchive = $isArchiveTemporary;
if ($endDateTimestamp) {
// Permanent archive
return $endDateTimestamp;
}
$dateStart = $this->params->getDateStart();
$period = $this->params->getPeriod();
$segment = $this->params->getSegment();
$site = $this->params->getSite();
// Temporary archive
return Rules::getMinTimeProcessedForTemporaryArchive($dateStart, $period, $segment, $site);
}
protected static function determineIfArchivePermanent(Date $dateEnd)
{
$now = time();
$endTimestampUTC = strtotime($dateEnd->getDateEndUTC());
if ($endTimestampUTC <= $now) {
// - if the period we are looking for is finished, we look for a ts_archived that
// is greater than the last day of the archive
return $endTimestampUTC;
}
return false;
}
protected function isArchiveTemporary()
{
if (is_null($this->temporaryArchive)) {
throw new \Exception("getMinTimeArchiveProcessed() should be called prior to isArchiveTemporary()");
}
return $this->temporaryArchive;
}
private function shouldArchiveForSiteEvenWhenNoVisits()
{
$idSitesToArchive = $this->getIdSitesToArchiveWhenNoVisits();
return in_array($this->params->getSite()->getId(), $idSitesToArchive);
}
private function getIdSitesToArchiveWhenNoVisits()
{
$cache = Cache::getTransientCache();
$cacheKey = 'Archiving.getIdSitesToArchiveWhenNoVisits';
if (!$cache->contains($cacheKey)) {
$idSites = array();
// leaving undocumented unless decided otherwise
Piwik::postEvent('Archiving.getIdSitesToArchiveWhenNoVisits', array(&$idSites));
$cache->save($cacheKey, $idSites);
}
return $cache->fetch($cacheKey);
}
}

View File

@ -0,0 +1,264 @@
<?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\ArchiveProcessor;
use Piwik\Date;
use Piwik\Log;
use Piwik\Period;
use Piwik\Piwik;
use Piwik\Segment;
use Piwik\Site;
/**
* Contains the analytics parameters for the reports that are currently being archived. The analytics
* parameters include the **website** the reports describe, the **period** of time the reports describe
* and the **segment** used to limit the visit set.
*/
class Parameters
{
/**
* @var Site
*/
private $site = null;
/**
* @var Period
*/
private $period = null;
/**
* @var Segment
*/
private $segment = null;
/**
* @var string Plugin name which triggered this archive processor
*/
private $requestedPlugin = false;
private $onlyArchiveRequestedPlugin = false;
/**
* @var bool
*/
private $isRootArchiveRequest = true;
/**
* Constructor.
*
* @ignore
*/
public function __construct(Site $site, Period $period, Segment $segment)
{
$this->site = $site;
$this->period = $period;
$this->segment = $segment;
}
/**
* @ignore
*/
public function setRequestedPlugin($plugin)
{
$this->requestedPlugin = $plugin;
}
/**
* @ignore
*/
public function onlyArchiveRequestedPlugin()
{
$this->onlyArchiveRequestedPlugin = true;
}
/**
* @ignore
*/
public function shouldOnlyArchiveRequestedPlugin()
{
return $this->onlyArchiveRequestedPlugin;
}
/**
* @ignore
*/
public function getRequestedPlugin()
{
return $this->requestedPlugin;
}
/**
* Returns the period we are computing statistics for.
*
* @return Period
* @api
*/
public function getPeriod()
{
return $this->period;
}
/**
* Returns the array of Period which make up this archive.
*
* @return \Piwik\Period[]
* @ignore
*/
public function getSubPeriods()
{
if ($this->getPeriod()->getLabel() == 'day') {
return array( $this->getPeriod() );
}
return $this->getPeriod()->getSubperiods();
}
/**
* @return array
* @ignore
*/
public function getIdSites()
{
$idSite = $this->getSite()->getId();
$idSites = array($idSite);
Piwik::postEvent('ArchiveProcessor.Parameters.getIdSites', array(&$idSites, $this->getPeriod()));
return $idSites;
}
/**
* Returns the site we are computing statistics for.
*
* @return Site
* @api
*/
public function getSite()
{
return $this->site;
}
/**
* The Segment used to limit the set of visits that are being aggregated.
*
* @return Segment
* @api
*/
public function getSegment()
{
return $this->segment;
}
/**
* Returns the end day of the period in the site's timezone.
*
* @return Date
*/
public function getDateEnd()
{
return $this->getPeriod()->getDateEnd()->setTimezone($this->getSite()->getTimezone());
}
/**
* Returns the start day of the period in the site's timezone.
*
* @return Date
*/
public function getDateStart()
{
return $this->getPeriod()->getDateStart()->setTimezone($this->getSite()->getTimezone());
}
/**
* Returns the start day of the period in the site's timezone (includes the time of day).
*
* @return Date
*/
public function getDateTimeStart()
{
return $this->getPeriod()->getDateTimeStart()->setTimezone($this->getSite()->getTimezone());
}
/**
* Returns the end day of the period in the site's timezone (includes the time of day).
*
* @return Date
*/
public function getDateTimeEnd()
{
return $this->getPeriod()->getDateTimeEnd()->setTimezone($this->getSite()->getTimezone());
}
/**
* @return bool
*/
public function isSingleSiteDayArchive()
{
return $this->isDayArchive() && $this->isSingleSite();
}
/**
* @return bool
*/
public function isDayArchive()
{
$period = $this->getPeriod();
$secondsInPeriod = $period->getDateEnd()->getTimestampUTC() - $period->getDateStart()->getTimestampUTC();
$oneDay = $secondsInPeriod < Date::NUM_SECONDS_IN_DAY;
return $oneDay;
}
public function isSingleSite()
{
return count($this->getIdSites()) == 1;
}
public function logStatusDebug($isTemporary)
{
$temporary = 'definitive archive';
if ($isTemporary) {
$temporary = 'temporary archive';
}
Log::debug(
"%s archive, idSite = %d (%s), segment '%s', report = '%s', UTC datetime [%s -> %s]",
$this->getPeriod()->getLabel(),
$this->getSite()->getId(),
$temporary,
$this->getSegment()->getString(),
$this->getRequestedPlugin(),
$this->getDateStart()->getDateStartUTC(),
$this->getDateEnd()->getDateEndUTC()
);
}
/**
* Returns `true` if these parameters are part of an initial archiving request.
* Returns `false` if these parameters are for an archiving request that was initiated
* during archiving.
*
* @return bool
*/
public function isRootArchiveRequest()
{
return $this->isRootArchiveRequest;
}
/**
* Sets whether these parameters are part of the initial archiving request or if they are
* for a request that was initiated during archiving.
*
* @param $isRootArchiveRequest
*/
public function setIsRootArchiveRequest($isRootArchiveRequest)
{
$this->isRootArchiveRequest = $isRootArchiveRequest;
}
}

View File

@ -0,0 +1,318 @@
<?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\ArchiveProcessor;
use Piwik\ArchiveProcessor;
use Piwik\Container\StaticContainer;
use Piwik\CronArchive\Performance\Logger;
use Piwik\DataAccess\ArchiveWriter;
use Piwik\DataAccess\LogAggregator;
use Piwik\DataTable\Manager;
use Piwik\Metrics;
use Piwik\Piwik;
use Piwik\Plugin\Archiver;
use Piwik\Log;
use Piwik\Timer;
use Exception;
/**
* This class creates the Archiver objects found in plugins and will trigger aggregation,
* so each plugin can process their reports.
*/
class PluginsArchiver
{
/**
* @param ArchiveProcessor $archiveProcessor
*/
public $archiveProcessor;
/**
* @var Parameters
*/
protected $params;
/**
* @var LogAggregator
*/
private $logAggregator;
/**
* Public only for tests. Won't be necessary after DI changes are complete.
*
* @var Archiver[] $archivers
*/
public static $archivers = array();
/**
* Defines if we should aggregate from raw data by using MySQL queries (when true) or aggregate archives (when false)
* @var bool
*/
private $shouldAggregateFromRawData;
public function __construct(Parameters $params, $isTemporaryArchive, ArchiveWriter $archiveWriter = null)
{
$this->params = $params;
$this->isTemporaryArchive = $isTemporaryArchive;
$this->archiveWriter = $archiveWriter ?: new ArchiveWriter($this->params, $this->isTemporaryArchive);
$this->archiveWriter->initNewArchive();
$this->logAggregator = new LogAggregator($params);
$this->archiveProcessor = new ArchiveProcessor($this->params, $this->archiveWriter, $this->logAggregator);
$shouldAggregateFromRawData = $this->params->isSingleSiteDayArchive();
/**
* Triggered to detect if the archiver should aggregate from raw data by using MySQL queries (when true)
* or by aggregate archives (when false). Typically, data is aggregated from raw data for "day" period, and
* aggregregated from archives for all other periods.
*
* @param bool $shouldAggregateFromRawData Set to true, to aggregate from raw data, or false to aggregate multiple reports.
* @param Parameters $params
* @ignore
* @deprecated
*
* In Matomo 4.0 we should maybe remove this event, and instead maybe always archive from raw data when it is daily archive,
* no matter if single site or not. We cannot do this in Matomo 3.X as some custom plugin archivers may not be able to handle multiple sites.
*/
Piwik::postEvent('ArchiveProcessor.shouldAggregateFromRawData', array(&$shouldAggregateFromRawData, $this->params));
$this->shouldAggregateFromRawData = $shouldAggregateFromRawData;
}
/**
* If period is day, will get the core metrics (including visits) from the logs.
* If period is != day, will sum the core metrics from the existing archives.
* @return array Core metrics
*/
public function callAggregateCoreMetrics()
{
$this->logAggregator->setQueryOriginHint('Core');
if ($this->shouldAggregateFromRawData) {
$metrics = $this->aggregateDayVisitsMetrics();
} else {
$metrics = $this->aggregateMultipleVisitsMetrics();
}
if (empty($metrics)) {
return array(
'nb_visits' => false,
'nb_visits_converted' => false
);
}
return array(
'nb_visits' => $metrics['nb_visits'],
'nb_visits_converted' => $metrics['nb_visits_converted']
);
}
/**
* Instantiates the Archiver class in each plugin that defines it,
* and triggers Aggregation processing on these plugins.
*/
public function callAggregateAllPlugins($visits, $visitsConverted, $forceArchivingWithoutVisits = false)
{
Log::debug("PluginsArchiver::%s: Initializing archiving process for all plugins [visits = %s, visits converted = %s]",
__FUNCTION__, $visits, $visitsConverted);
/** @var Logger $performanceLogger */
$performanceLogger = StaticContainer::get(Logger::class);
$this->archiveProcessor->setNumberOfVisits($visits, $visitsConverted);
$archivers = static::getPluginArchivers();
foreach ($archivers as $pluginName => $archiverClass) {
// We clean up below all tables created during this function call (and recursive calls)
$latestUsedTableId = Manager::getInstance()->getMostRecentTableId();
/** @var Archiver $archiver */
$archiver = $this->makeNewArchiverObject($archiverClass, $pluginName);
if (!$archiver->isEnabled()) {
Log::debug("PluginsArchiver::%s: Skipping archiving for plugin '%s' (disabled).", __FUNCTION__, $pluginName);
continue;
}
if (!$forceArchivingWithoutVisits && !$visits && !$archiver->shouldRunEvenWhenNoVisits()) {
Log::debug("PluginsArchiver::%s: Skipping archiving for plugin '%s' (no visits).", __FUNCTION__, $pluginName);
continue;
}
if ($this->shouldProcessReportsForPlugin($pluginName)) {
$this->logAggregator->setQueryOriginHint($pluginName);
try {
$timer = new Timer();
if ($this->shouldAggregateFromRawData) {
Log::debug("PluginsArchiver::%s: Archiving day reports for plugin '%s'.", __FUNCTION__, $pluginName);
$archiver->callAggregateDayReport();
} else {
Log::debug("PluginsArchiver::%s: Archiving period reports for plugin '%s'.", __FUNCTION__, $pluginName);
$archiver->callAggregateMultipleReports();
}
$this->logAggregator->setQueryOriginHint('');
$performanceLogger->logMeasurement('plugin', $pluginName, $this->params, $timer);
Log::debug("PluginsArchiver::%s: %s while archiving %s reports for plugin '%s' %s.",
__FUNCTION__,
$timer->getMemoryLeak(),
$this->params->getPeriod()->getLabel(),
$pluginName,
$this->params->getSegment() ? sprintf("(for segment = '%s')", $this->params->getSegment()->getString()) : ''
);
} catch (Exception $e) {
throw new PluginsArchiverException($e->getMessage() . " - in plugin $pluginName", $e->getCode(), $e);
}
} else {
Log::debug("PluginsArchiver::%s: Not archiving reports for plugin '%s'.", __FUNCTION__, $pluginName);
}
Manager::getInstance()->deleteAll($latestUsedTableId);
unset($archiver);
}
}
public function finalizeArchive()
{
$this->params->logStatusDebug($this->archiveWriter->isArchiveTemporary);
$this->archiveWriter->finalizeArchive();
return $this->archiveWriter->getIdArchive();
}
/**
* Returns if any plugin archiver archives without visits
*/
public static function doesAnyPluginArchiveWithoutVisits()
{
$archivers = static::getPluginArchivers();
foreach ($archivers as $pluginName => $archiverClass) {
if ($archiverClass::shouldRunEvenWhenNoVisits()) {
return true;
}
}
return false;
}
/**
* Loads Archiver class from any plugin that defines one.
*
* @return \Piwik\Plugin\Archiver[]
*/
protected static function getPluginArchivers()
{
if (empty(static::$archivers)) {
$pluginNames = \Piwik\Plugin\Manager::getInstance()->getActivatedPlugins();
$archivers = array();
foreach ($pluginNames as $pluginName) {
$archivers[$pluginName] = self::getPluginArchiverClass($pluginName);
}
static::$archivers = array_filter($archivers);
}
return static::$archivers;
}
private static function getPluginArchiverClass($pluginName)
{
$klassName = 'Piwik\\Plugins\\' . $pluginName . '\\Archiver';
if (class_exists($klassName)
&& is_subclass_of($klassName, 'Piwik\\Plugin\\Archiver')) {
return $klassName;
}
return false;
}
/**
* Whether the specified plugin's reports should be archived
* @param string $pluginName
* @return bool
*/
protected function shouldProcessReportsForPlugin($pluginName)
{
if ($this->params->getRequestedPlugin() == $pluginName) {
return true;
}
if ($this->params->shouldOnlyArchiveRequestedPlugin()) {
return false;
}
if (Rules::shouldProcessReportsAllPlugins(
$this->params->getIdSites(),
$this->params->getSegment(),
$this->params->getPeriod()->getLabel())) {
return true;
}
if (!\Piwik\Plugin\Manager::getInstance()->isPluginLoaded($this->params->getRequestedPlugin())) {
return true;
}
return false;
}
protected function aggregateDayVisitsMetrics()
{
$query = $this->archiveProcessor->getLogAggregator()->queryVisitsByDimension();
$data = $query->fetch();
$metrics = $this->convertMetricsIdToName($data);
$this->archiveProcessor->insertNumericRecords($metrics);
return $metrics;
}
protected function convertMetricsIdToName($data)
{
$metrics = array();
foreach ($data as $metricId => $value) {
$readableMetric = Metrics::$mappingFromIdToName[$metricId];
$metrics[$readableMetric] = $value;
}
return $metrics;
}
protected function aggregateMultipleVisitsMetrics()
{
$toSum = Metrics::getVisitsMetricNames();
$metrics = $this->archiveProcessor->aggregateNumericMetrics($toSum);
return $metrics;
}
/**
* @param $archiverClass
* @return Archiver
*/
private function makeNewArchiverObject($archiverClass, $pluginName)
{
$archiver = new $archiverClass($this->archiveProcessor);
/**
* Triggered right after a new **plugin archiver instance** is created.
* Subscribers to this event can configure the plugin archiver, for example prevent the archiving of a plugin's data
* by calling `$archiver->disable()` method.
*
* @param \Piwik\Plugin\Archiver &$archiver The newly created plugin archiver instance.
* @param string $pluginName The name of plugin of which archiver instance was created.
* @param array $this->params Array containing archive parameters (Site, Period, Date and Segment)
* @param bool $this->isTemporaryArchive Flag indicating whether the archive being processed is temporary (ie. the period isn't finished yet) or final (the period is already finished and in the past).
*/
Piwik::postEvent('Archiving.makeNewArchiverObject', array($archiver, $pluginName, $this->params, $this->isTemporaryArchive));
return $archiver;
}
}

View File

@ -0,0 +1,15 @@
<?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\ArchiveProcessor;
class PluginsArchiverException extends \Exception
{
}

View File

@ -0,0 +1,307 @@
<?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\ArchiveProcessor;
use Exception;
use Piwik\Config;
use Piwik\DataAccess\ArchiveWriter;
use Piwik\Date;
use Piwik\Log;
use Piwik\Option;
use Piwik\Piwik;
use Piwik\Plugins\CoreAdminHome\Controller;
use Piwik\Segment;
use Piwik\SettingsPiwik;
use Piwik\SettingsServer;
use Piwik\Site;
use Piwik\Tracker\Cache;
/**
* This class contains Archiving rules/logic which are used when creating and processing Archives.
*
*/
class Rules
{
const OPTION_TODAY_ARCHIVE_TTL = 'todayArchiveTimeToLive';
const OPTION_BROWSER_TRIGGER_ARCHIVING = 'enableBrowserTriggerArchiving';
const FLAG_TABLE_PURGED = 'lastPurge_';
/** Flag that will forcefully disable the archiving process (used in tests only) */
public static $archivingDisabledByTests = false;
/**
* Returns the name of the archive field used to tell the status of an archive, (ie,
* whether the archive was created successfully or not).
*
* @param array $idSites
* @param Segment $segment
* @param string $periodLabel
* @param string $plugin
* @return string
*/
public static function getDoneStringFlagFor(array $idSites, $segment, $periodLabel, $plugin)
{
if (!self::shouldProcessReportsAllPlugins($idSites, $segment, $periodLabel)) {
return self::getDoneFlagArchiveContainsOnePlugin($segment, $plugin);
}
return self::getDoneFlagArchiveContainsAllPlugins($segment);
}
public static function shouldProcessReportsAllPlugins(array $idSites, Segment $segment, $periodLabel)
{
if ($segment->isEmpty() && $periodLabel != 'range') {
return true;
}
return self::isSegmentPreProcessed($idSites, $segment);
}
/**
* @param $idSites
* @return array
*/
public static function getSegmentsToProcess($idSites)
{
$knownSegmentsToArchiveAllSites = SettingsPiwik::getKnownSegmentsToArchive();
$segmentsToProcess = $knownSegmentsToArchiveAllSites;
foreach ($idSites as $idSite) {
$segmentForThisWebsite = SettingsPiwik::getKnownSegmentsToArchiveForSite($idSite);
$segmentsToProcess = array_merge($segmentsToProcess, $segmentForThisWebsite);
}
$segmentsToProcess = array_unique($segmentsToProcess);
return $segmentsToProcess;
}
public static function getDoneFlagArchiveContainsOnePlugin(Segment $segment, $plugin)
{
return 'done' . $segment->getHash() . '.' . $plugin ;
}
public static function getDoneFlagArchiveContainsAllPlugins(Segment $segment)
{
return 'done' . $segment->getHash();
}
/**
* Return done flags used to tell how the archiving process for a specific archive was completed,
*
* @param array $plugins
* @param $segment
* @return array
*/
public static function getDoneFlags(array $plugins, Segment $segment)
{
$doneFlags = array();
$doneAllPlugins = self::getDoneFlagArchiveContainsAllPlugins($segment);
$doneFlags[$doneAllPlugins] = $doneAllPlugins;
$plugins = array_unique($plugins);
foreach ($plugins as $plugin) {
$doneOnePlugin = self::getDoneFlagArchiveContainsOnePlugin($segment, $plugin);
$doneFlags[$plugin] = $doneOnePlugin;
}
return $doneFlags;
}
public static function getMinTimeProcessedForTemporaryArchive(
Date $dateStart, \Piwik\Period $period, Segment $segment, Site $site)
{
$todayArchiveTimeToLive = self::getPeriodArchiveTimeToLiveDefault($period->getLabel());
$now = time();
$minimumArchiveTime = $now - $todayArchiveTimeToLive;
$idSites = array($site->getId());
$isArchivingDisabled = Rules::isArchivingDisabledFor($idSites, $segment, $period->getLabel());
if ($isArchivingDisabled) {
if ($period->getNumberOfSubperiods() == 0
&& $dateStart->getTimestamp() <= $now
) {
// Today: accept any recent enough archive
$minimumArchiveTime = false;
} else {
// This week, this month, this year:
// accept any archive that was processed today after 00:00:01 this morning
$timezone = $site->getTimezone();
$minimumArchiveTime = Date::factory(Date::factory('now', $timezone)->getDateStartUTC())->setTimezone($timezone)->getTimestamp();
}
}
return $minimumArchiveTime;
}
public static function setTodayArchiveTimeToLive($timeToLiveSeconds)
{
$timeToLiveSeconds = (int)$timeToLiveSeconds;
if ($timeToLiveSeconds <= 0) {
throw new Exception(Piwik::translate('General_ExceptionInvalidArchiveTimeToLive'));
}
Option::set(self::OPTION_TODAY_ARCHIVE_TTL, $timeToLiveSeconds, $autoLoad = true);
}
public static function getTodayArchiveTimeToLive()
{
$uiSettingIsEnabled = Controller::isGeneralSettingsAdminEnabled();
if ($uiSettingIsEnabled) {
$timeToLive = Option::get(self::OPTION_TODAY_ARCHIVE_TTL);
if ($timeToLive !== false) {
return $timeToLive;
}
}
return self::getTodayArchiveTimeToLiveDefault();
}
public static function getPeriodArchiveTimeToLiveDefault($periodLabel)
{
if (empty($periodLabel) || strtolower($periodLabel) === 'day') {
return self::getTodayArchiveTimeToLive();
}
$config = Config::getInstance();
$general = $config->General;
$key = sprintf('time_before_%s_archive_considered_outdated', $periodLabel);
if (isset($general[$key]) && is_numeric($general[$key]) && $general[$key] > 0) {
return $general[$key];
}
return self::getTodayArchiveTimeToLive();
}
public static function getTodayArchiveTimeToLiveDefault()
{
return Config::getInstance()->General['time_before_today_archive_considered_outdated'];
}
public static function isBrowserArchivingAvailableForSegments()
{
$generalConfig = Config::getInstance()->General;
return !$generalConfig['browser_archiving_disabled_enforce'];
}
public static function isArchivingDisabledFor(array $idSites, Segment $segment, $periodLabel)
{
$generalConfig = Config::getInstance()->General;
if ($periodLabel == 'range') {
if (!isset($generalConfig['archiving_range_force_on_browser_request'])
|| $generalConfig['archiving_range_force_on_browser_request'] != false
) {
return false;
}
Log::debug("Not forcing archiving for range period.");
$processOneReportOnly = false;
} else {
$processOneReportOnly = !self::shouldProcessReportsAllPlugins($idSites, $segment, $periodLabel);
}
$isArchivingEnabled = self::isRequestAuthorizedToArchive() && !self::$archivingDisabledByTests;
if ($processOneReportOnly) {
// When there is a segment, we disable archiving when browser_archiving_disabled_enforce applies
if (!$segment->isEmpty()
&& !$isArchivingEnabled
&& !self::isBrowserArchivingAvailableForSegments()
&& !SettingsServer::isArchivePhpTriggered() // Only applies when we are not running core:archive command
) {
Log::debug("Archiving is disabled because of config setting browser_archiving_disabled_enforce=1");
return true;
}
// Always allow processing one report
return false;
}
return !$isArchivingEnabled;
}
public static function isRequestAuthorizedToArchive()
{
return Rules::isBrowserTriggerEnabled() || SettingsServer::isArchivePhpTriggered();
}
public static function isBrowserTriggerEnabled()
{
$uiSettingIsEnabled = Controller::isGeneralSettingsAdminEnabled();
if ($uiSettingIsEnabled) {
$browserArchivingEnabled = Option::get(self::OPTION_BROWSER_TRIGGER_ARCHIVING);
if ($browserArchivingEnabled !== false) {
return (bool)$browserArchivingEnabled;
}
}
return (bool)Config::getInstance()->General['enable_browser_archiving_triggering'];
}
public static function setBrowserTriggerArchiving($enabled)
{
if (!is_bool($enabled)) {
throw new Exception('Browser trigger archiving must be set to true or false.');
}
Option::set(self::OPTION_BROWSER_TRIGGER_ARCHIVING, (int)$enabled, $autoLoad = true);
Cache::clearCacheGeneral();
}
/**
* Returns true if the archiving process should skip the calculation of unique visitors
* across several sites. The `[General] enable_processing_unique_visitors_multiple_sites`
* INI config option controls the value of this variable.
*
* @return bool
*/
public static function shouldSkipUniqueVisitorsCalculationForMultipleSites()
{
return Config::getInstance()->General['enable_processing_unique_visitors_multiple_sites'] != 1;
}
/**
* @param array $idSites
* @param Segment $segment
* @return bool
*/
public static function isSegmentPreProcessed(array $idSites, Segment $segment)
{
$segmentsToProcess = self::getSegmentsToProcess($idSites);
if (empty($segmentsToProcess)) {
return false;
}
// If the requested segment is one of the segments to pre-process
// we ensure that any call to the API will trigger archiving of all reports for this segment
$segment = $segment->getString();
// Turns out the getString() above returns the URL decoded segment string
$segmentsToProcessUrlDecoded = array_map('urldecode', $segmentsToProcess);
return in_array($segment, $segmentsToProcess)
|| in_array($segment, $segmentsToProcessUrlDecoded);
}
/**
* Returns done flag values allowed to be selected
*
* @return string
*/
public static function getSelectableDoneFlagValues()
{
$possibleValues = array(ArchiveWriter::DONE_OK, ArchiveWriter::DONE_OK_TEMPORARY);
if (!Rules::isRequestAuthorizedToArchive()) {
//If request is not authorized to archive then fetch also invalidated archives
$possibleValues[] = ArchiveWriter::DONE_INVALIDATED;
}
return $possibleValues;
}
}

View File

@ -0,0 +1,53 @@
<?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\Archiver;
class Request
{
/**
* If a request is aborted, the response of a CliMutli job will be a serialized array containing the
* key/value "aborted => 1".
*/
const ABORT = 'abort';
/**
* @var string
*/
private $url;
/**
* @var callable|null
*/
private $before;
/**
* @param string $url
*/
public function __construct($url)
{
$this->url = $url;
}
public function before($callable)
{
$this->before = $callable;
}
public function start()
{
if ($this->before) {
return call_user_func($this->before);
}
}
public function __toString()
{
return $this->url;
}
}

View File

@ -0,0 +1,427 @@
<?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;
use Exception;
use Piwik\AssetManager\UIAsset;
use Piwik\AssetManager\UIAsset\InMemoryUIAsset;
use Piwik\AssetManager\UIAsset\OnDiskUIAsset;
use Piwik\AssetManager\UIAssetCacheBuster;
use Piwik\AssetManager\UIAssetFetcher\JScriptUIAssetFetcher;
use Piwik\AssetManager\UIAssetFetcher\StaticUIAssetFetcher;
use Piwik\AssetManager\UIAssetFetcher\StylesheetUIAssetFetcher;
use Piwik\AssetManager\UIAssetFetcher;
use Piwik\AssetManager\UIAssetMerger\JScriptUIAssetMerger;
use Piwik\AssetManager\UIAssetMerger\StylesheetUIAssetMerger;
use Piwik\Container\StaticContainer;
use Piwik\Plugin\Manager;
/**
* AssetManager is the class used to manage the inclusion of UI assets:
* JavaScript and CSS files.
*
* It performs the following actions:
* - Identifies required assets
* - Includes assets in the rendered HTML page
* - Manages asset merging and minifying
* - Manages server-side cache
*
* Whether assets are included individually or as merged files is defined by
* the global option 'disable_merged_assets'. See the documentation in the global
* config for more information.
*
* @method static AssetManager getInstance()
*/
class AssetManager extends Singleton
{
const MERGED_CSS_FILE = "asset_manager_global_css.css";
const MERGED_CORE_JS_FILE = "asset_manager_core_js.js";
const MERGED_NON_CORE_JS_FILE = "asset_manager_non_core_js.js";
const CSS_IMPORT_DIRECTIVE = "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\" />\n";
const JS_IMPORT_DIRECTIVE = "<script type=\"text/javascript\" src=\"%s\"></script>\n";
const GET_CSS_MODULE_ACTION = "index.php?module=Proxy&action=getCss";
const GET_CORE_JS_MODULE_ACTION = "index.php?module=Proxy&action=getCoreJs";
const GET_NON_CORE_JS_MODULE_ACTION = "index.php?module=Proxy&action=getNonCoreJs";
/**
* @var UIAssetCacheBuster
*/
private $cacheBuster;
/**
* @var UIAssetFetcher
*/
private $minimalStylesheetFetcher;
/**
* @var Theme
*/
private $theme;
public function __construct()
{
$this->cacheBuster = UIAssetCacheBuster::getInstance();
$this->minimalStylesheetFetcher = new StaticUIAssetFetcher(array(), array(), $this->theme);
$theme = Manager::getInstance()->getThemeEnabled();
if (!empty($theme)) {
$this->theme = new Theme();
}
}
/**
* @param UIAssetCacheBuster $cacheBuster
*/
public function setCacheBuster($cacheBuster)
{
$this->cacheBuster = $cacheBuster;
}
/**
* @param UIAssetFetcher $minimalStylesheetFetcher
*/
public function setMinimalStylesheetFetcher($minimalStylesheetFetcher)
{
$this->minimalStylesheetFetcher = $minimalStylesheetFetcher;
}
/**
* @param Theme $theme
*/
public function setTheme($theme)
{
$this->theme = $theme;
}
/**
* Return CSS file inclusion directive(s) using the markup <link>
*
* @return string
*/
public function getCssInclusionDirective()
{
return sprintf(self::CSS_IMPORT_DIRECTIVE, self::GET_CSS_MODULE_ACTION);
}
/**
* Return JS file inclusion directive(s) using the markup <script>
*
* @return string
*/
public function getJsInclusionDirective()
{
$result = "<script type=\"text/javascript\">\n" . Translate::getJavascriptTranslations() . "\n</script>";
if ($this->isMergedAssetsDisabled()) {
$this->getMergedCoreJSAsset()->delete();
$this->getMergedNonCoreJSAsset()->delete();
$result .= $this->getIndividualCoreAndNonCoreJsIncludes();
} else {
$result .= sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_CORE_JS_MODULE_ACTION);
$result .= sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_NON_CORE_JS_MODULE_ACTION);
}
return $result;
}
/**
* Return the base.less compiled to css
*
* @return UIAsset
*/
public function getCompiledBaseCss()
{
$mergedAsset = new InMemoryUIAsset();
$assetMerger = new StylesheetUIAssetMerger($mergedAsset, $this->minimalStylesheetFetcher, $this->cacheBuster);
$assetMerger->generateFile();
return $mergedAsset;
}
/**
* Return the css merged file absolute location.
* If there is none, the generation process will be triggered.
*
* @return UIAsset
*/
public function getMergedStylesheet()
{
$mergedAsset = $this->getMergedStylesheetAsset();
$assetFetcher = new StylesheetUIAssetFetcher(Manager::getInstance()->getLoadedPluginsName(), $this->theme);
$assetMerger = new StylesheetUIAssetMerger($mergedAsset, $assetFetcher, $this->cacheBuster);
$assetMerger->generateFile();
return $mergedAsset;
}
/**
* Return the core js merged file absolute location.
* If there is none, the generation process will be triggered.
*
* @return UIAsset
*/
public function getMergedCoreJavaScript()
{
return $this->getMergedJavascript($this->getCoreJScriptFetcher(), $this->getMergedCoreJSAsset());
}
/**
* Return the non core js merged file absolute location.
* If there is none, the generation process will be triggered.
*
* @return UIAsset
*/
public function getMergedNonCoreJavaScript()
{
return $this->getMergedJavascript($this->getNonCoreJScriptFetcher(), $this->getMergedNonCoreJSAsset());
}
/**
* @param boolean $core
* @return string[]
*/
public function getLoadedPlugins($core)
{
$loadedPlugins = array();
foreach (Manager::getInstance()->getPluginsLoadedAndActivated() as $plugin) {
$pluginName = $plugin->getPluginName();
$pluginIsCore = Manager::getInstance()->isPluginBundledWithCore($pluginName);
if (($pluginIsCore && $core) || (!$pluginIsCore && !$core)) {
$loadedPlugins[] = $pluginName;
}
}
return $loadedPlugins;
}
/**
* Remove previous merged assets
*/
public function removeMergedAssets($pluginName = false)
{
$assetsToRemove = array($this->getMergedStylesheetAsset());
if ($pluginName) {
if ($this->pluginContainsJScriptAssets($pluginName)) {
if (Manager::getInstance()->isPluginBundledWithCore($pluginName)) {
$assetsToRemove[] = $this->getMergedCoreJSAsset();
} else {
$assetsToRemove[] = $this->getMergedNonCoreJSAsset();
}
}
} else {
$assetsToRemove[] = $this->getMergedCoreJSAsset();
$assetsToRemove[] = $this->getMergedNonCoreJSAsset();
}
$this->removeAssets($assetsToRemove);
}
/**
* Check if the merged file directory exists and is writable.
*
* @return string The directory location
* @throws Exception if directory is not writable.
*/
public function getAssetDirectory()
{
$mergedFileDirectory = StaticContainer::get('path.tmp') . '/assets';
if (!is_dir($mergedFileDirectory)) {
Filesystem::mkdir($mergedFileDirectory);
}
if (!is_writable($mergedFileDirectory)) {
throw new Exception("Directory " . $mergedFileDirectory . " has to be writable.");
}
return $mergedFileDirectory;
}
/**
* Return the global option disable_merged_assets
*
* @return boolean
*/
public function isMergedAssetsDisabled()
{
if (Config::getInstance()->Development['disable_merged_assets'] == 1) {
return true;
}
if (isset($_GET['disable_merged_assets']) && $_GET['disable_merged_assets'] == 1) {
return true;
}
return false;
}
/**
* @param UIAssetFetcher $assetFetcher
* @param UIAsset $mergedAsset
* @return UIAsset
*/
private function getMergedJavascript($assetFetcher, $mergedAsset)
{
$assetMerger = new JScriptUIAssetMerger($mergedAsset, $assetFetcher, $this->cacheBuster);
$assetMerger->generateFile();
return $mergedAsset;
}
/**
* Return individual JS file inclusion directive(s) using the markup <script>
*
* @return string
*/
private function getIndividualCoreAndNonCoreJsIncludes()
{
return
$this->getIndividualJsIncludesFromAssetFetcher($this->getCoreJScriptFetcher()) .
$this->getIndividualJsIncludesFromAssetFetcher($this->getNonCoreJScriptFetcher());
}
/**
* @param UIAssetFetcher $assetFetcher
* @return string
*/
private function getIndividualJsIncludesFromAssetFetcher($assetFetcher)
{
$jsIncludeString = '';
$assets = $assetFetcher->getCatalog()->getAssets();
foreach ($assets as $jsFile) {
$jsFile->validateFile();
$jsIncludeString = $jsIncludeString . sprintf(self::JS_IMPORT_DIRECTIVE, $jsFile->getRelativeLocation());
}
return $jsIncludeString;
}
private function getCoreJScriptFetcher()
{
return new JScriptUIAssetFetcher($this->getLoadedPlugins(true), $this->theme);
}
private function getNonCoreJScriptFetcher()
{
return new JScriptUIAssetFetcher($this->getLoadedPlugins(false), $this->theme);
}
/**
* @param string $pluginName
* @return boolean
*/
private function pluginContainsJScriptAssets($pluginName)
{
$fetcher = new JScriptUIAssetFetcher(array($pluginName), $this->theme);
try {
$assets = $fetcher->getCatalog()->getAssets();
} catch (\Exception $e) {
// This can happen when a plugin is not valid (eg. Piwik 1.x format)
// When posting the event to the plugin, it returns an exception "Plugin has not been loaded"
return false;
}
$pluginManager = Manager::getInstance();
$plugin = $pluginManager->getLoadedPlugin($pluginName);
if ($plugin->isTheme()) {
$theme = $pluginManager->getTheme($pluginName);
$javaScriptFiles = $theme->getJavaScriptFiles();
if (!empty($javaScriptFiles)) {
$assets = array_merge($assets, $javaScriptFiles);
}
}
return !empty($assets);
}
/**
* @param UIAsset[] $uiAssets
*/
public function removeAssets($uiAssets)
{
foreach ($uiAssets as $uiAsset) {
$uiAsset->delete();
}
}
/**
* @return UIAsset
*/
public function getMergedStylesheetAsset()
{
return $this->getMergedUIAsset(self::MERGED_CSS_FILE);
}
/**
* @return UIAsset
*/
private function getMergedCoreJSAsset()
{
return $this->getMergedUIAsset(self::MERGED_CORE_JS_FILE);
}
/**
* @return UIAsset
*/
private function getMergedNonCoreJSAsset()
{
return $this->getMergedUIAsset(self::MERGED_NON_CORE_JS_FILE);
}
/**
* @param string $fileName
* @return UIAsset
*/
private function getMergedUIAsset($fileName)
{
return new OnDiskUIAsset($this->getAssetDirectory(), $fileName);
}
public static function compileCustomStylesheets($files)
{
$assetManager = new AssetManager();
$fetcher = new StaticUIAssetFetcher($files, $priorityOrder = array(), $theme = null);
$assetManager->setMinimalStylesheetFetcher($fetcher);
return $assetManager->getCompiledBaseCss()->getContent();
}
public static function compileCustomJs($files)
{
$mergedAsset = new InMemoryUIAsset();
$fetcher = new StaticUIAssetFetcher($files, $priorityOrder = array(), $theme = null);
$cacheBuster = UIAssetCacheBuster::getInstance();
$assetMerger = new JScriptUIAssetMerger($mergedAsset, $fetcher, $cacheBuster);
$assetMerger->generateFile();
return $mergedAsset->getContent();
}
}

View File

@ -0,0 +1,61 @@
<?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\AssetManager;
use Exception;
abstract class UIAsset
{
abstract public function validateFile();
/**
* @return string
*/
abstract public function getAbsoluteLocation();
/**
* @return string
*/
abstract public function getRelativeLocation();
/**
* @return string
*/
abstract public function getBaseDirectory();
/**
* Removes the previous file if it exists.
* Also tries to remove compressed version of the file.
*
* @see ProxyStaticFile::serveStaticFile(serveFile
* @throws Exception if the file couldn't be deleted
*/
abstract public function delete();
/**
* @param string $content
* @throws \Exception
*/
abstract public function writeContent($content);
/**
* @return string
*/
abstract public function getContent();
/**
* @return boolean
*/
abstract public function exists();
/**
* @return int
*/
abstract public function getModificationDate();
}

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\AssetManager\UIAsset;
use Exception;
use Piwik\AssetManager\UIAsset;
class InMemoryUIAsset extends UIAsset
{
private $content;
public function validateFile()
{
return;
}
public function getAbsoluteLocation()
{
throw new Exception('invalid operation');
}
public function getRelativeLocation()
{
throw new Exception('invalid operation');
}
public function getBaseDirectory()
{
throw new Exception('invalid operation');
}
public function delete()
{
$this->content = null;
}
public function exists()
{
return false;
}
public function writeContent($content)
{
$this->content = $content;
}
public function getContent()
{
return $this->content;
}
public function getModificationDate()
{
throw new Exception('invalid operation');
}
}

View File

@ -0,0 +1,135 @@
<?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\AssetManager\UIAsset;
use Exception;
use Piwik\AssetManager\UIAsset;
use Piwik\Common;
use Piwik\Filesystem;
class OnDiskUIAsset extends UIAsset
{
/**
* @var string
*/
private $baseDirectory;
/**
* @var string
*/
private $relativeLocation;
/**
* @var string
*/
private $relativeRootDir;
/**
* @param string $baseDirectory
* @param string $fileLocation
*/
public function __construct($baseDirectory, $fileLocation, $relativeRootDir = '')
{
$this->baseDirectory = $baseDirectory;
$this->relativeLocation = $fileLocation;
if (!empty($relativeRootDir)
&& is_string($relativeRootDir)
&& !Common::stringEndsWith($relativeRootDir, '/')) {
$relativeRootDir .= '/';
}
$this->relativeRootDir = $relativeRootDir;
}
public function getAbsoluteLocation()
{
return $this->baseDirectory . '/' . $this->relativeLocation;
}
public function getRelativeLocation()
{
if (isset($this->relativeRootDir)) {
return $this->relativeRootDir . $this->relativeLocation;
}
return $this->relativeLocation;
}
public function getBaseDirectory()
{
return $this->baseDirectory;
}
public function validateFile()
{
if (!$this->assetIsReadable()) {
throw new Exception("The ui asset with 'href' = " . $this->getAbsoluteLocation() . " is not readable");
}
}
public function delete()
{
if ($this->exists()) {
try {
Filesystem::remove($this->getAbsoluteLocation());
} catch (Exception $e) {
throw new Exception("Unable to delete merged file : " . $this->getAbsoluteLocation() . ". Please delete the file and refresh");
}
// try to remove compressed version of the merged file.
Filesystem::remove($this->getAbsoluteLocation() . ".deflate", true);
Filesystem::remove($this->getAbsoluteLocation() . ".gz", true);
}
}
/**
* @param string $content
* @throws \Exception
*/
public function writeContent($content)
{
$this->delete();
$newFile = @fopen($this->getAbsoluteLocation(), "w");
if (!$newFile) {
throw new Exception("The file : " . $newFile . " can not be opened in write mode.");
}
fwrite($newFile, $content);
fclose($newFile);
}
/**
* @return string
*/
public function getContent()
{
return file_get_contents($this->getAbsoluteLocation());
}
public function exists()
{
return $this->assetIsReadable();
}
/**
* @return boolean
*/
private function assetIsReadable()
{
return is_readable($this->getAbsoluteLocation());
}
public function getModificationDate()
{
return filemtime($this->getAbsoluteLocation());
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
* @method static \Piwik\AssetManager\UIAssetCacheBuster getInstance()
*/
namespace Piwik\AssetManager;
use Piwik\Plugin\Manager;
use Piwik\Singleton;
use Piwik\Version;
class UIAssetCacheBuster extends Singleton
{
/**
* Cache buster based on
* - Piwik version
* - Loaded plugins (name and version)
* - Super user salt
* - Latest
*
* @param string[] $pluginNames
* @return string
*/
public function piwikVersionBasedCacheBuster($pluginNames = false)
{
static $cachedCacheBuster = null;
if (empty($cachedCacheBuster) || $pluginNames !== false) {
$masterFile = PIWIK_INCLUDE_PATH . '/.git/refs/heads/master';
$currentGitHash = file_exists($masterFile) ? @file_get_contents($masterFile) : null;
$plugins = !$pluginNames ? Manager::getInstance()->getLoadedPluginsName() : $pluginNames;
sort($plugins);
$pluginsInfo = '';
foreach ($plugins as $pluginName) {
$plugin = Manager::getInstance()->getLoadedPlugin($pluginName);
$pluginsInfo .= $plugin->getPluginName() . $plugin->getVersion() . ',';
}
$cacheBuster = md5($pluginsInfo . PHP_VERSION . Version::VERSION . trim($currentGitHash));
if ($pluginNames !== false) {
return $cacheBuster;
}
$cachedCacheBuster = $cacheBuster;
}
return $cachedCacheBuster;
}
/**
* @param string $content
* @return string
*/
public function md5BasedCacheBuster($content)
{
return md5($content);
}
}

View File

@ -0,0 +1,73 @@
<?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\AssetManager;
class UIAssetCatalog
{
/**
* @var UIAsset[]
*/
private $uiAssets = array();
/**
* @var UIAssetCatalogSorter
*/
private $catalogSorter;
/**
* @var string[] Absolute file locations
*/
private $existingAssetLocations = array();
/**
* @param UIAssetCatalogSorter $catalogSorter
*/
public function __construct($catalogSorter)
{
$this->catalogSorter = $catalogSorter;
}
/**
* @param UIAsset $uiAsset
*/
public function addUIAsset($uiAsset)
{
$location = $uiAsset->getAbsoluteLocation();
if (!$this->assetAlreadyInCatalog($location)) {
$this->existingAssetLocations[] = $location;
$this->uiAssets[] = $uiAsset;
}
}
/**
* @return UIAsset[]
*/
public function getAssets()
{
return $this->uiAssets;
}
/**
* @return UIAssetCatalog
*/
public function getSortedCatalog()
{
return $this->catalogSorter->sortUIAssetCatalog($this);
}
/**
* @param UIAsset $uiAsset
* @return boolean
*/
private function assetAlreadyInCatalog($location)
{
return in_array($location, $this->existingAssetLocations);
}
}

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\AssetManager;
class UIAssetCatalogSorter
{
/**
* @var string[]
*/
private $priorityOrder;
/**
* @param string[] $priorityOrder
*/
public function __construct($priorityOrder)
{
$this->priorityOrder = $priorityOrder;
}
/**
* @param UIAssetCatalog $uiAssetCatalog
* @return UIAssetCatalog
*/
public function sortUIAssetCatalog($uiAssetCatalog)
{
$sortedCatalog = new UIAssetCatalog($this);
foreach ($this->priorityOrder as $filePattern) {
$assetsMatchingPattern = array_filter($uiAssetCatalog->getAssets(), function ($uiAsset) use ($filePattern) {
return preg_match('~^' . $filePattern . '~', $uiAsset->getRelativeLocation());
});
foreach ($assetsMatchingPattern as $assetMatchingPattern) {
$sortedCatalog->addUIAsset($assetMatchingPattern);
}
}
$this->addUnmatchedAssets($uiAssetCatalog, $sortedCatalog);
return $sortedCatalog;
}
/**
* @param UIAssetCatalog $uiAssetCatalog
* @param UIAssetCatalog $sortedCatalog
*/
private function addUnmatchedAssets($uiAssetCatalog, $sortedCatalog)
{
foreach ($uiAssetCatalog->getAssets() as $uiAsset) {
$sortedCatalog->addUIAsset($uiAsset);
}
}
}

View File

@ -0,0 +1,150 @@
<?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\AssetManager;
use Piwik\AssetManager\UIAsset\OnDiskUIAsset;
use Piwik\Plugin\Manager;
use Piwik\Theme;
abstract class UIAssetFetcher
{
/**
* @var UIAssetCatalog
*/
protected $catalog;
/**
* @var string[]
*/
protected $fileLocations = array();
/**
* @var string[]
*/
protected $plugins;
/**
* @var Theme
*/
private $theme;
/**
* @param string[] $plugins
* @param Theme $theme
*/
public function __construct($plugins, $theme)
{
$this->plugins = $plugins;
$this->theme = $theme;
}
/**
* @return string[]
*/
public function getPlugins()
{
return $this->plugins;
}
/**
* $return UIAssetCatalog
*/
public function getCatalog()
{
if ($this->catalog == null) {
$this->createCatalog();
}
return $this->catalog;
}
abstract protected function retrieveFileLocations();
/**
* @return string[]
*/
abstract protected function getPriorityOrder();
private function createCatalog()
{
$this->retrieveFileLocations();
$this->initCatalog();
$this->populateCatalog();
$this->sortCatalog();
}
private function initCatalog()
{
$catalogSorter = new UIAssetCatalogSorter($this->getPriorityOrder());
$this->catalog = new UIAssetCatalog($catalogSorter);
}
private function populateCatalog()
{
$pluginBaseDir = Manager::getPluginsDirectory();
$pluginWebDirectories = Manager::getAlternativeWebRootDirectories();
$matomoRootDir = $this->getBaseDirectory();
foreach ($this->fileLocations as $fileLocation) {
$fileAbsolute = $matomoRootDir . '/' . $fileLocation;
$newUIAsset = new OnDiskUIAsset($this->getBaseDirectory(), $fileLocation);
if ($newUIAsset->exists()) {
$this->catalog->addUIAsset($newUIAsset);
continue;
}
$found = false;
if (strpos($fileAbsolute, $pluginBaseDir) === 0) {
// we iterate over all custom plugin directories only for plugin files, not libs files (not needed there)
foreach ($pluginWebDirectories as $pluginDirectory => $relative) {
$fileTest = str_replace($pluginBaseDir, $pluginDirectory, $fileAbsolute);
$newFileRelative = str_replace($pluginDirectory, '', $fileTest);
$testAsset = new OnDiskUIAsset($pluginDirectory, $newFileRelative, $relative);
if ($testAsset->exists()) {
$this->catalog->addUIAsset($testAsset);
$found = true;
break;
}
}
}
if (!$found) {
// we add it anyway so it'll trigger an error about the missing file
$this->catalog->addUIAsset($newUIAsset);
}
}
}
private function sortCatalog()
{
$this->catalog = $this->catalog->getSortedCatalog();
}
/**
* @return string
*/
private function getBaseDirectory()
{
// served by web server directly, so must be a public path
return PIWIK_DOCUMENT_ROOT;
}
/**
* @return Theme
*/
public function getTheme()
{
return $this->theme;
}
}

View File

@ -0,0 +1,88 @@
<?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\AssetManager\UIAssetFetcher;
use Piwik\AssetManager\UIAssetFetcher;
use Piwik\Piwik;
class JScriptUIAssetFetcher extends UIAssetFetcher
{
protected function retrieveFileLocations()
{
if (!empty($this->plugins)) {
/**
* Triggered when gathering the list of all JavaScript files needed by Piwik
* and its plugins.
*
* Plugins that have their own JavaScript should use this event to make those
* files load in the browser.
*
* JavaScript files should be placed within a **javascripts** subdirectory in your
* plugin's root directory.
*
* _Note: While you are developing your plugin you should enable the config setting
* `[Development] disable_merged_assets` so JavaScript files will be reloaded immediately
* after every change._
*
* **Example**
*
* public function getJsFiles(&$jsFiles)
* {
* $jsFiles[] = "plugins/MyPlugin/javascripts/myfile.js";
* $jsFiles[] = "plugins/MyPlugin/javascripts/anotherone.js";
* }
*
* @param string[] $jsFiles The JavaScript files to load.
*/
Piwik::postEvent('AssetManager.getJavaScriptFiles', array(&$this->fileLocations), null, $this->plugins);
}
$this->addThemeFiles();
}
protected function addThemeFiles()
{
$theme = $this->getTheme();
if (!$theme) {
return;
}
if (in_array($theme->getThemeName(), $this->plugins)) {
$jsInThemes = $this->getTheme()->getJavaScriptFiles();
if (!empty($jsInThemes)) {
foreach ($jsInThemes as $jsFile) {
$this->fileLocations[] = $jsFile;
}
}
}
}
protected function getPriorityOrder()
{
return array(
'libs/bower_components/jquery/dist/jquery.min.js',
'libs/bower_components/jquery-ui/ui/minified/jquery-ui.min.js',
'libs/jquery/jquery.browser.js',
'libs/',
'js/',
'piwik.js',
'matomo.js',
'plugins/CoreHome/javascripts/require.js',
'plugins/Morpheus/javascripts/piwikHelper.js',
'plugins/Morpheus/javascripts/',
'plugins/CoreHome/javascripts/uiControl.js',
'plugins/CoreHome/javascripts/broadcast.js',
'plugins/CoreHome/javascripts/', // load CoreHome JS before other plugins
'plugins/',
'tests/',
);
}
}

View File

@ -0,0 +1,36 @@
<?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\AssetManager\UIAssetFetcher;
use Piwik\AssetManager\UIAssetFetcher;
class StaticUIAssetFetcher extends UIAssetFetcher
{
/**
* @var string[]
*/
private $priorityOrder;
public function __construct($fileLocations, $priorityOrder, $theme)
{
parent::__construct(array(), $theme);
$this->fileLocations = $fileLocations;
$this->priorityOrder = $priorityOrder;
}
protected function retrieveFileLocations()
{
}
protected function getPriorityOrder()
{
return $this->priorityOrder;
}
}

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\AssetManager\UIAssetFetcher;
use Piwik\AssetManager\UIAssetFetcher;
use Piwik\Piwik;
class StylesheetUIAssetFetcher extends UIAssetFetcher
{
protected function getPriorityOrder()
{
$theme = $this->getTheme();
$themeName = $theme->getThemeName();
$order = array(
'plugins/Morpheus/stylesheets/base/bootstrap.css',
'plugins/Morpheus/stylesheets/base/icons.css',
'libs/',
'plugins/CoreHome/stylesheets/color_manager.css', // must be before other Piwik stylesheets
'plugins/Morpheus/stylesheets/base.less',
);
if ($themeName === 'Morpheus') {
$order[] = 'plugins\/((?!Morpheus).)*\/';
} else {
$order[] = sprintf('plugins\/((?!(Morpheus)|(%s)).)*\/', $themeName);
}
$order = array_merge(
$order,
array(
'plugins/Dashboard/stylesheets/dashboard.less',
'tests/',
)
);
return $order;
}
protected function retrieveFileLocations()
{
/**
* Triggered when gathering the list of all stylesheets (CSS and LESS) needed by
* Piwik and its plugins.
*
* Plugins that have stylesheets should use this event to make those stylesheets
* load.
*
* Stylesheets should be placed within a **stylesheets** subdirectory in your plugin's
* root directory.
*
* **Example**
*
* public function getStylesheetFiles(&$stylesheets)
* {
* $stylesheets[] = "plugins/MyPlugin/stylesheets/myfile.less";
* $stylesheets[] = "plugins/MyPlugin/stylesheets/myotherfile.css";
* }
*
* @param string[] &$stylesheets The list of stylesheet paths.
*/
Piwik::postEvent('AssetManager.getStylesheetFiles', array(&$this->fileLocations));
$this->addThemeFiles();
}
protected function addThemeFiles()
{
$theme = $this->getTheme();
if (!$theme) {
return;
}
$themeStylesheet = $theme->getStylesheet();
if ($themeStylesheet) {
$this->fileLocations[] = $themeStylesheet;
}
}
}

View File

@ -0,0 +1,193 @@
<?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\AssetManager;
abstract class UIAssetMerger
{
/**
* @var UIAssetFetcher
*/
private $assetFetcher;
/**
* @var UIAsset
*/
private $mergedAsset;
/**
* @var string
*/
protected $mergedContent;
/**
* @var UIAssetCacheBuster
*/
protected $cacheBuster;
/**
* @param UIAsset $mergedAsset
* @param UIAssetFetcher $assetFetcher
* @param UIAssetCacheBuster $cacheBuster
*/
public function __construct($mergedAsset, $assetFetcher, $cacheBuster)
{
$this->mergedAsset = $mergedAsset;
$this->assetFetcher = $assetFetcher;
$this->cacheBuster = $cacheBuster;
}
public function generateFile()
{
if (!$this->shouldGenerate()) {
return;
}
$this->mergedContent = $this->getMergedAssets();
$this->postEvent($this->mergedContent);
$this->adjustPaths();
$this->addPreamble();
$this->writeContentToFile();
}
/**
* @return string
*/
abstract protected function getMergedAssets();
/**
* @return string
*/
abstract protected function generateCacheBuster();
/**
* @return string
*/
abstract protected function getPreamble();
/**
* @return string
*/
abstract protected function getFileSeparator();
/**
* @param UIAsset $uiAsset
* @return string
*/
abstract protected function processFileContent($uiAsset);
/**
* @param string $mergedContent
*/
abstract protected function postEvent(&$mergedContent);
protected function getConcatenatedAssets()
{
if (empty($this->mergedContent)) {
$this->concatenateAssets();
}
return $this->mergedContent;
}
protected function concatenateAssets()
{
$mergedContent = '';
foreach ($this->getAssetCatalog()->getAssets() as $uiAsset) {
$uiAsset->validateFile();
$content = $this->processFileContent($uiAsset);
$mergedContent .= $this->getFileSeparator() . $content;
}
$this->mergedContent = $mergedContent;
}
/**
* @return string[]
*/
protected function getPlugins()
{
return $this->assetFetcher->getPlugins();
}
/**
* @return UIAssetCatalog
*/
protected function getAssetCatalog()
{
return $this->assetFetcher->getCatalog();
}
/**
* @return boolean
*/
private function shouldGenerate()
{
if (!$this->mergedAsset->exists()) {
return true;
}
return !$this->isFileUpToDate();
}
/**
* @return boolean
*/
private function isFileUpToDate()
{
$f = fopen($this->mergedAsset->getAbsoluteLocation(), 'r');
$firstLine = fgets($f);
fclose($f);
if (!empty($firstLine) && trim($firstLine) == trim($this->getCacheBusterValue())) {
return true;
}
// Some CSS file in the merge, has changed since last merged asset was generated
// Note: we do not detect changes in @import'ed LESS files
return false;
}
private function adjustPaths()
{
$theme = $this->assetFetcher->getTheme();
// During installation theme is not yet ready
if ($theme) {
$this->mergedContent = $this->assetFetcher->getTheme()->rewriteAssetsPathToTheme($this->mergedContent);
}
}
private function writeContentToFile()
{
$this->mergedAsset->writeContent($this->mergedContent);
}
/**
* @return string
*/
protected function getCacheBusterValue()
{
if (empty($this->cacheBusterValue)) {
$this->cacheBusterValue = $this->generateCacheBuster();
}
return $this->cacheBusterValue;
}
private function addPreamble()
{
$this->mergedContent = $this->getPreamble() . $this->mergedContent;
}
}

View File

@ -0,0 +1,88 @@
<?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\AssetManager\UIAssetMerger;
use Piwik\AssetManager\UIAsset;
use Piwik\AssetManager\UIAssetCacheBuster;
use Piwik\AssetManager\UIAssetFetcher\JScriptUIAssetFetcher;
use Piwik\AssetManager\UIAssetMerger;
use Piwik\AssetManager;
use Piwik\AssetManager\UIAssetMinifier;
use Piwik\Piwik;
class JScriptUIAssetMerger extends UIAssetMerger
{
/**
* @var UIAssetMinifier
*/
private $assetMinifier;
/**
* @param UIAsset $mergedAsset
* @param JScriptUIAssetFetcher $assetFetcher
* @param UIAssetCacheBuster $cacheBuster
*/
public function __construct($mergedAsset, $assetFetcher, $cacheBuster)
{
parent::__construct($mergedAsset, $assetFetcher, $cacheBuster);
$this->assetMinifier = UIAssetMinifier::getInstance();
}
protected function getMergedAssets()
{
return $this->getConcatenatedAssets();
}
protected function generateCacheBuster()
{
$cacheBuster = $this->cacheBuster->piwikVersionBasedCacheBuster($this->getPlugins());
return "/* Matomo Javascript - cb=" . $cacheBuster . "*/\n";
}
protected function getPreamble()
{
return $this->getCacheBusterValue();
}
protected function postEvent(&$mergedContent)
{
$plugins = $this->getPlugins();
if (!empty($plugins)) {
/**
* Triggered after all the JavaScript files Piwik uses are minified and merged into a
* single file, but before the merged JavaScript is written to disk.
*
* Plugins can use this event to modify merged JavaScript or do something else
* with it.
*
* @param string $mergedContent The minified and merged JavaScript.
*/
Piwik::postEvent('AssetManager.filterMergedJavaScripts', array(&$mergedContent), null, $plugins);
}
}
public function getFileSeparator()
{
return "\n";
}
protected function processFileContent($uiAsset)
{
$content = $uiAsset->getContent();
if (!$this->assetMinifier->isMinifiedJs($content)) {
$content = $this->assetMinifier->minifyJs($content);
}
return $content;
}
}

View File

@ -0,0 +1,260 @@
<?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\AssetManager\UIAssetMerger;
use Exception;
use lessc;
use Piwik\AssetManager\UIAsset;
use Piwik\AssetManager\UIAssetMerger;
use Piwik\Common;
use Piwik\Exception\StylesheetLessCompileException;
use Piwik\Piwik;
use Piwik\Plugin\Manager;
class StylesheetUIAssetMerger extends UIAssetMerger
{
/**
* @var lessc
*/
private $lessCompiler;
/**
* @var UIAsset[]
*/
private $cssAssetsToReplace = array();
public function __construct($mergedAsset, $assetFetcher, $cacheBuster)
{
parent::__construct($mergedAsset, $assetFetcher, $cacheBuster);
$this->lessCompiler = self::getLessCompiler();
}
protected function getMergedAssets()
{
// note: we're using setImportDir on purpose (not addImportDir)
$this->lessCompiler->setImportDir(PIWIK_DOCUMENT_ROOT);
$concatenatedAssets = $this->getConcatenatedAssets();
$this->lessCompiler->setFormatter('classic');
try {
$compiled = $this->lessCompiler->compile($concatenatedAssets);
} catch(\Exception $e) {
throw new StylesheetLessCompileException($e->getMessage());
}
foreach ($this->cssAssetsToReplace as $asset) {
// to fix #10173
$cssPath = $asset->getAbsoluteLocation();
$cssContent = $this->processFileContent($asset);
$compiled = str_replace($this->getCssStatementForReplacement($cssPath), $cssContent, $compiled);
}
$this->mergedContent = $compiled;
$this->cssAssetsToReplace = array();
return $compiled;
}
private function getCssStatementForReplacement($path)
{
return '.nonExistingSelectorOnlyForReplacementOfCssFiles { display:"' . $path . '"; }';
}
protected function concatenateAssets()
{
$concatenatedContent = '';
foreach ($this->getAssetCatalog()->getAssets() as $uiAsset) {
$uiAsset->validateFile();
try {
$path = $uiAsset->getAbsoluteLocation();
} catch (Exception $e) {
$path = null;
}
if (!empty($path) && Common::stringEndsWith($path, '.css')) {
// to fix #10173
$concatenatedContent .= "\n" . $this->getCssStatementForReplacement($path) . "\n";
$this->cssAssetsToReplace[] = $uiAsset;
} else {
$content = $this->processFileContent($uiAsset);
$concatenatedContent .= $this->getFileSeparator() . $content;
}
}
/**
* Triggered after all less stylesheets are concatenated into one long string but before it is
* minified and merged into one file.
*
* This event can be used to add less stylesheets that are not located in a file on the disc.
*
* @param string $concatenatedContent The content of all concatenated less files.
*/
Piwik::postEvent('AssetManager.addStylesheets', array(&$concatenatedContent));
$this->mergedContent = $concatenatedContent;
}
/**
* @return lessc
* @throws Exception
*/
private static function getLessCompiler()
{
if (!class_exists("lessc")) {
throw new Exception("Less was added to composer during 2.0. ==> Execute this command to update composer packages: \$ php composer.phar install");
}
$less = new lessc();
return $less;
}
protected function generateCacheBuster()
{
$fileHash = $this->cacheBuster->md5BasedCacheBuster($this->getConcatenatedAssets());
return "/* compile_me_once=$fileHash */";
}
protected function getPreamble()
{
return $this->getCacheBusterValue() . "\n"
. "/* Matomo CSS file is compiled with Less. You may be interested in writing a custom Theme for Matomo! */\n";
}
protected function postEvent(&$mergedContent)
{
/**
* Triggered after all less stylesheets are compiled to CSS, minified and merged into
* one file, but before the generated CSS is written to disk.
*
* This event can be used to modify merged CSS.
*
* @param string $mergedContent The merged and minified CSS.
*/
Piwik::postEvent('AssetManager.filterMergedStylesheets', array(&$mergedContent));
}
public function getFileSeparator()
{
return '';
}
protected function processFileContent($uiAsset)
{
$pathsRewriter = $this->getCssPathsRewriter($uiAsset);
$content = $uiAsset->getContent();
$content = $this->rewriteCssImagePaths($content, $pathsRewriter);
$content = $this->rewriteCssImportPaths($content, $pathsRewriter);
return $content;
}
/**
* Rewrite CSS url() directives
*
* @param string $content
* @param callable $pathsRewriter
* @return string
*/
private function rewriteCssImagePaths($content, $pathsRewriter)
{
$content = preg_replace_callback("/(url\(['\"]?)([^'\")]*)/", $pathsRewriter, $content);
return $content;
}
/**
* Rewrite CSS import directives
*
* @param string $content
* @param callable $pathsRewriter
* @return string
*/
private function rewriteCssImportPaths($content, $pathsRewriter)
{
$content = preg_replace_callback("/(@import \")([^\")]*)/", $pathsRewriter, $content);
return $content;
}
/**
* Rewrite CSS url directives
* - rewrites paths defined relatively to their css/less definition file
* - rewrite windows directory separator \\ to /
*
* @param UIAsset $uiAsset
* @return \Closure
*/
private function getCssPathsRewriter($uiAsset)
{
$baseDirectory = dirname($uiAsset->getRelativeLocation());
$webDirs = Manager::getAlternativeWebRootDirectories();
return function ($matches) use ($baseDirectory, $webDirs) {
$absolutePath = PIWIK_DOCUMENT_ROOT . "/$baseDirectory/" . $matches[2];
// Allow to import extension less file
if (strpos($matches[2], '.') === false) {
$absolutePath .= '.less';
}
// Prevent from rewriting full path
$absolutePathReal = realpath($absolutePath);
if ($absolutePathReal) {
$relativePath = $baseDirectory . "/" . $matches[2];
$relativePath = str_replace('\\', '/', $relativePath);
$publicPath = $matches[1] . $relativePath;
} else {
foreach ($webDirs as $absPath => $relativePath) {
if (strpos($baseDirectory, $relativePath) === 0) {
if (strpos($matches[2], '.') === 0) {
// eg ../images/ok.png
$fileRelative = $baseDirectory . '/' . $matches[2];
$fileAbsolute = $absPath . str_replace($relativePath, '', $fileRelative);
if (file_exists($fileAbsolute)) {
return $matches[1] . $fileRelative;
}
} elseif (strpos($matches[2], 'plugins/') === 0) {
// eg plugins/Foo/images/ok.png
$fileRelative = substr($matches[2], strlen('plugins/'));
$fileAbsolute = $absPath . $fileRelative;
if (file_exists($fileAbsolute)) {
return $matches[1] . $relativePath . $fileRelative;
}
} elseif ($matches[1] === '@import "') {
$fileRelative = $baseDirectory . '/' . $matches[2];
$fileAbsolute = $absPath . str_replace($relativePath, '', $fileRelative);
if (file_exists($fileAbsolute)) {
return $matches[1] . $baseDirectory . '/' . $matches[2];
}
}
}
}
$publicPath = $matches[1] . $matches[2];
}
return $publicPath;
};
}
/**
* @param UIAsset $uiAsset
* @return int
*/
protected function countDirectoriesInPathToRoot($uiAsset)
{
$rootDirectory = realpath($uiAsset->getBaseDirectory());
if ($rootDirectory != PATH_SEPARATOR
&& substr($rootDirectory, -strlen(PATH_SEPARATOR)) !== PATH_SEPARATOR) {
$rootDirectory .= PATH_SEPARATOR;
}
$rootDirectoryLen = strlen($rootDirectory);
return $rootDirectoryLen;
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
* @method static \Piwik\AssetManager\UIAssetMinifier getInstance()
*/
namespace Piwik\AssetManager;
use Exception;
use JShrink\Minifier;
use Piwik\Singleton;
class UIAssetMinifier extends Singleton
{
const MINIFIED_JS_RATIO = 100;
protected function __construct()
{
self::validateDependency();
parent::__construct();
}
/**
* Indicates if the provided JavaScript content has already been minified or not.
* The heuristic is based on a custom ratio : (size of file) / (number of lines).
* The threshold (100) has been found empirically on existing files :
* - the ratio never exceeds 50 for non-minified content and
* - it never goes under 150 for minified content.
*
* @param string $content Contents of the JavaScript file
* @return boolean
*/
public function isMinifiedJs($content)
{
$lineCount = substr_count($content, "\n");
if ($lineCount == 0) {
return true;
}
$contentSize = strlen($content);
$ratio = $contentSize / $lineCount;
return $ratio > self::MINIFIED_JS_RATIO;
}
/**
* @param string $content
* @return string
*/
public function minifyJs($content)
{
return Minifier::minify($content);
}
private static function validateDependency()
{
if (!class_exists("JShrink\\Minifier")) {
throw new Exception("JShrink could not be found, maybe you are using Matomo from git and need to update Composer. $ php composer.phar update");
}
}
}

View File

@ -0,0 +1,221 @@
<?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;
use Exception;
/**
* Base interface for authentication implementations.
*
* Plugins that provide Auth implementations must provide a class that implements
* this interface. Additionally, an instance of that class must be set in the
* container with the 'Piwik\Auth' key during the
* [Request.initAuthenticationObject](http://developer.piwik.org/api-reference/events#requestinitauthenticationobject)
* event.
*
* Authentication implementations must support authentication via username and
* clear-text password and authentication via username and token auth. They can
* additionally support authentication via username and an MD5 hash of a password. If
* they don't support it, then [formless authentication](http://piwik.org/faq/how-to/faq_30/) will fail.
*
* Derived implementations should favor authenticating by password over authenticating
* by token auth. That is to say, if a token auth and a password are set, password
* authentication should be used.
*
* ### Examples
*
* **How an Auth implementation will be used**
*
* // authenticating by password
* $auth = StaticContainer::get('Piwik\Auth');
* $auth->setLogin('user');
* $auth->setPassword('password');
* $result = $auth->authenticate();
*
* // authenticating by token auth
* $auth = StaticContainer::get('Piwik\Auth');
* $auth->setLogin('user');
* $auth->setTokenAuth('...');
* $result = $auth->authenticate();
*
* @api
*/
interface Auth
{
/**
* Must return the Authentication module's name, e.g., `"Login"`.
*
* @return string
*/
public function getName();
/**
* Sets the authentication token to authenticate with.
*
* @param string $token_auth authentication token
*/
public function setTokenAuth($token_auth);
/**
* Returns the login of the user being authenticated.
*
* @return string
*/
public function getLogin();
/**
* Returns the secret used to calculate a user's token auth.
*
* A users token auth is generated using the user's login and this secret. The secret
* should be specific to the user and not easily guessed. Piwik's default Auth implementation
* uses an MD5 hash of a user's password.
*
* @return string
* @throws Exception if the token auth secret does not exist or cannot be obtained.
*/
public function getTokenAuthSecret();
/**
* Sets the login name to authenticate with.
*
* @param string $login The username.
*/
public function setLogin($login);
/**
* Sets the password to authenticate with.
*
* @param string $password Password (not hashed).
*/
public function setPassword($password);
/**
* Sets the hash of the password to authenticate with. The hash will be an MD5 hash.
*
* @param string $passwordHash The hashed password.
* @throws Exception if authentication by hashed password is not supported.
*/
public function setPasswordHash($passwordHash);
/**
* Authenticates a user using the login and password set using the setters. Can also authenticate
* via token auth if one is set and no password is set.
*
* Note: this method must successfully authenticate if the token auth supplied is a special hash
* of the user's real token auth. This is because the SessionInitializer class stores a
* hash of the token auth in the session cookie. You can calculate the token auth hash using the
* {@link Piwik\Plugins\Login\SessionInitializer::getHashTokenAuth()} method.
*
* @return AuthResult
* @throws Exception if the Auth implementation has an invalid state (ie, no login
* was specified). Note: implementations are not **required** to throw
* exceptions for invalid state, but they are allowed to.
*/
public function authenticate();
}
/**
* Authentication result. This is what is returned by authentication attempts using {@link Auth}
* implementations.
*
* @api
*/
class AuthResult
{
const FAILURE = 0;
const SUCCESS = 1;
const SUCCESS_SUPERUSER_AUTH_CODE = 42;
/**
* token_auth parameter used to authenticate in the API
*
* @var string
*/
protected $tokenAuth = null;
/**
* The login used to authenticate.
*
* @var string
*/
protected $login = null;
/**
* The authentication result code. Can be self::FAILURE, self::SUCCESS, or
* self::SUCCESS_SUPERUSER_AUTH_CODE.
*
* @var int
*/
protected $code = null;
/**
* Constructor for AuthResult
*
* @param int $code
* @param string $login identity
* @param string $tokenAuth
*/
public function __construct($code, $login, $tokenAuth)
{
$this->code = (int)$code;
$this->login = $login;
$this->tokenAuth = $tokenAuth;
}
/**
* Returns the login used to authenticate.
*
* @return string
*/
public function getIdentity()
{
return $this->login;
}
/**
* Returns the token_auth to authenticate the current user in the API
*
* @return string
*/
public function getTokenAuth()
{
return $this->tokenAuth;
}
/**
* Returns the authentication result code.
*
* @return int
*/
public function getCode()
{
return $this->code;
}
/**
* Returns true if the user has Super User access, false otherwise.
*
* @return bool
*/
public function hasSuperUserAccess()
{
return $this->getCode() == self::SUCCESS_SUPERUSER_AUTH_CODE;
}
/**
* Returns true if this result was successfully authentication.
*
* @return bool
*/
public function wasAuthenticationSuccessful()
{
return $this->code > self::FAILURE;
}
}

View File

@ -0,0 +1,69 @@
<?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\Auth;
/**
* Main class to handle actions related to password hashing and verification.
*
* @api
*/
class Password
{
/**
* Hashes a password with the configured algorithm.
*
* @param string $password
* @return string
*/
public function hash($password)
{
return password_hash($password, PASSWORD_BCRYPT);
}
/**
* Returns information about a hashed password (algo, options, ...).
*
* Can be used to verify whether a string is compatible with password_hash().
*
* @param string
* @return array
*/
public function info($hash)
{
return password_get_info($hash);
}
/**
* Rehashes a user's password if necessary.
*
* This method expects the password to be pre-hashed by
* \Piwik\Plugins\UsersManager\UsersManager::getPasswordHash().
*
* @param string $hash
* @return boolean
*/
public function needsRehash($hash)
{
return password_needs_rehash($hash, PASSWORD_BCRYPT);
}
/**
* Verifies a user's password against the provided hash.
*
* This method expects the password to be pre-hashed by
* \Piwik\Plugins\UsersManager\UsersManager::getPasswordHash().
*
* @param string $password
* @param string $hash
* @return boolean
*/
public function verify($password, $hash)
{
return password_verify($password, $hash);
}
}

View File

@ -0,0 +1,65 @@
<?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;
use Exception;
/**
* Base class for all factory types.
*
* Factory types are base classes that contain a **factory** method. This method is used to instantiate
* concrete instances by a specified string ID. Fatal errors do not occur if a class does not exist.
* Instead an exception is thrown.
*
* Derived classes should override the **getClassNameFromClassId** and **getInvalidClassIdExceptionMessage**
* static methods.
*/
abstract class BaseFactory
{
/**
* Creates a new instance of a class using a string ID.
*
* @param string $classId The ID of the class.
* @return \Piwik\DataTable\Renderer
* @throws Exception if $classId is invalid.
*/
public static function factory($classId)
{
$className = static::getClassNameFromClassId($classId);
if (!class_exists($className)) {
self::sendPlainHeader();
throw new Exception(static::getInvalidClassIdExceptionMessage($classId));
}
return new $className;
}
private static function sendPlainHeader()
{
Common::sendHeader('Content-Type: text/plain; charset=utf-8');
}
/**
* Should return a class name based on the class's associated string ID.
*/
protected static function getClassNameFromClassId($id)
{
return $id;
}
/**
* Should return a message to use in an Exception when an invalid class ID is supplied to
* {@link factory()}.
*/
protected static function getInvalidClassIdExceptionMessage($id)
{
return "Invalid class ID '$id' for " . get_called_class() . "::factory().";
}
}

View File

@ -0,0 +1,117 @@
<?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;
use Piwik\Cache\Backend;
use Piwik\Container\StaticContainer;
class Cache
{
/**
* This can be considered as the default cache to use in case you don't know which one to pick. It does not support
* the caching of any objects though. Only boolean, numbers, strings and arrays are supported. Whenever you request
* an entry from the cache it will fetch the entry. Cache entries might be persisted but not necessarily. It
* depends on the configured backend.
*
* @return Cache\Lazy
*/
public static function getLazyCache()
{
return StaticContainer::get('Piwik\Cache\Lazy');
}
/**
* This class is used to cache any data during one request. It won't be persisted between requests and it can
* cache all kind of data, even objects or resources. This cache is very fast.
*
* @return Cache\Transient
*/
public static function getTransientCache()
{
return StaticContainer::get('Piwik\Cache\Transient');
}
/**
* This cache stores all its cache entries under one "cache" entry in a configurable backend.
*
* This comes handy for things that you need very often, nearly in every request. For example plugin metadata, the
* list of tracker plugins, the list of available languages, ...
* Instead of having to read eg. a hundred cache entries from files (or any other backend) it only loads one cache
* entry which contains the hundred keys. Should be used only for things that you need very often and only for
* cache entries that are not too large to keep loading and parsing the single cache entry fast.
* All cache entries it contains have the same life time. For fast performance it won't validate any cache ids.
* It is not possible to cache any objects using this cache.
*
* @return Cache\Eager
*/
public static function getEagerCache()
{
return StaticContainer::get('Piwik\Cache\Eager');
}
public static function flushAll()
{
self::getLazyCache()->flushAll();
self::getTransientCache()->flushAll();
self::getEagerCache()->flushAll();
}
/**
* @param $type
* @return Cache\Backend
*/
public static function buildBackend($type)
{
$factory = new Cache\Backend\Factory();
$options = self::getOptions($type);
$backend = $factory->buildBackend($type, $options);
return $backend;
}
private static function getOptions($type)
{
$options = self::getBackendOptions($type);
switch ($type) {
case 'file':
$options = array('directory' => StaticContainer::get('path.cache'));
break;
case 'chained':
foreach ($options['backends'] as $backend) {
$options[$backend] = self::getOptions($backend);
}
break;
case 'redis':
if (!empty($options['timeout'])) {
$options['timeout'] = (float)Common::forceDotAsSeparatorForDecimalPoint($options['timeout']);
}
break;
}
return $options;
}
private static function getBackendOptions($backend)
{
$key = ucfirst($backend) . 'Cache';
$options = Config::getInstance()->$key;
return $options;
}
}

View File

@ -0,0 +1,87 @@
<?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;
use Piwik\Plugin\Manager;
class CacheId
{
public static function languageAware($cacheId)
{
return $cacheId . '-' . Translate::getLanguageLoaded();
}
public static function pluginAware($cacheId)
{
$pluginManager = Manager::getInstance();
$pluginNames = $pluginManager->getLoadedPluginsName();
$cacheId = $cacheId . '-' . md5(implode('', $pluginNames));
$cacheId = self::languageAware($cacheId);
return $cacheId;
}
public static function siteAware($cacheId, array $idSites = null)
{
if ($idSites === null) {
$idSites = self::getIdSiteList('idSite');
$cacheId .= self::idSiteListCacheKey($idSites);
$idSites = self::getIdSiteList('idSites');
$cacheId .= self::idSiteListCacheKey($idSites);
$idSites = self::getIdSiteList('idsite'); // tracker param
$cacheId .= self::idSiteListCacheKey($idSites);
} else {
$cacheId .= self::idSiteListCacheKey($idSites);
}
return $cacheId;
}
private static function getIdSiteList($queryParamName)
{
if (empty($_GET[$queryParamName])
&& empty($_POST[$queryParamName])
) {
return [];
}
$idSiteGetParam = [];
if (!empty($_GET[$queryParamName])) {
$value = $_GET[$queryParamName];
$idSiteGetParam = is_array($value) ? $value : explode(',', $value);
}
$idSitePostParam = [];
if (!empty($_POST[$queryParamName])) {
$value = $_POST[$queryParamName];
$idSitePostParam = is_array($value) ? $value : explode(',', $value);
}
$idSiteList = array_merge($idSiteGetParam, $idSitePostParam);
$idSiteList = array_map('intval', $idSiteList);
$idSiteList = array_unique($idSiteList);
sort($idSiteList);
return $idSiteList;
}
private static function idSiteListCacheKey($idSites)
{
if (empty($idSites)) {
return '';
}
if (count($idSites) <= 5) {
return '-' . implode('_', $idSites); // we keep the cache key readable when possible
} else {
return '-' . md5(implode('_', $idSites)); // we need to shorten it
}
}
}

View File

@ -0,0 +1,124 @@
<?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\Category;
use Piwik\Piwik;
/**
* Base type for category. lets you change the name for a categoryId and specifiy a different order
* so the category appears eg at a different order in the reporting menu.
*
* This class is for now not exposed as public API until needed. Categories of plugins will be automatically
* displayed in the menu at the very right after all core categories.
*/
class Category
{
/**
* The id of the category as specified eg in {@link Piwik\Widget\WidgetConfig::setCategoryId()`} or
* {@link Piwik\Report\getCategoryId()}. The id is used as the name in the menu and will be visible in the
* URL.
*
* @var string Should be a translation key, eg 'General_Vists'
*/
protected $id = '';
/**
* @var Subcategory[]
*/
protected $subcategories = array();
/**
* The order of the category. The lower the value the further left the category will appear in the menu.
* @var int
*/
protected $order = 99;
/**
* The icon for this category, eg 'icon-user'
* @var int
*/
protected $icon = '';
/**
* @param int $order
* @return static
*/
public function setOrder($order)
{
$this->order = (int) $order;
return $this;
}
public function getOrder()
{
return $this->order;
}
public function setId($id)
{
$this->id = $id;
return $this;
}
public function getId()
{
return $this->id;
}
public function getDisplayName()
{
return Piwik::translate($this->getId());
}
public function addSubcategory(Subcategory $subcategory)
{
$subcategoryId = $subcategory->getId();
if ($this->hasSubcategory($subcategoryId)) {
throw new \Exception(sprintf('Subcategory %s already exists', $subcategoryId));
}
$this->subcategories[$subcategoryId] = $subcategory;
}
public function hasSubcategory($subcategoryId)
{
return isset($this->subcategories[$subcategoryId]);
}
public function getSubcategory($subcategoryId)
{
if ($this->hasSubcategory($subcategoryId)) {
return $this->subcategories[$subcategoryId];
}
}
/**
* @return Subcategory[]
*/
public function getSubcategories()
{
return array_values($this->subcategories);
}
public function hasSubCategories()
{
return !empty($this->subcategories);
}
public function setIcon($icon)
{
$this->icon = $icon;
return $this;
}
public function getIcon()
{
return $this->icon;
}
}

View File

@ -0,0 +1,95 @@
<?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\Category;
use Piwik\Container\StaticContainer;
use Piwik\Plugin;
/**
* Base type for category. lets you change the name for a categoryId and specifiy a different order
* so the category appears eg at a different order in the reporting menu.
*
* This class is for now not exposed as public API until needed. Categories of plugins will be automatically
* displayed in the menu at the very right after all core categories.
*/
class CategoryList
{
/**
* @var Category[] indexed by categoryId
*/
private $categories = array();
public function addCategory(Category $category)
{
$categoryId = $category->getId();
if ($this->hasCategory($categoryId)) {
throw new \Exception(sprintf('Category %s already exists', $categoryId));
}
$this->categories[$categoryId] = $category;
}
public function getCategories()
{
return $this->categories;
}
public function hasCategory($categoryId)
{
return isset($this->categories[$categoryId]);
}
/**
* Get the category having the given id, if possible.
*
* @param string $categoryId
* @return Category|null
*/
public function getCategory($categoryId)
{
if ($this->hasCategory($categoryId)) {
return $this->categories[$categoryId];
}
}
/**
* @return CategoryList
*/
public static function get()
{
$list = new CategoryList();
$categories = StaticContainer::get('Piwik\Plugin\Categories');
foreach ($categories->getAllCategories() as $category) {
$list->addCategory($category);
}
// move subcategories into categories
foreach ($categories->getAllSubcategories() as $subcategory) {
$categoryId = $subcategory->getCategoryId();
if (!$categoryId) {
continue;
}
if ($list->hasCategory($categoryId)) {
$category = $list->getCategory($categoryId);
} else {
$category = new Category();
$category->setId($categoryId);
$list->addCategory($category);
}
$category->addSubcategory($subcategory);
}
return $list;
}
}

View File

@ -0,0 +1,146 @@
<?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\Category;
/**
* Base type for subcategories.
*
* All widgets within a subcategory will be rendered in the Piwik reporting UI under the same page. By default
* you do not have to specify any subcategory as they are created automatically. Only create a subcategory if you
* want to change the name for a specific subcategoryId or if you want to specifiy a different order so the subcategory
* appears eg at a different order in the reporting menu. It also affects the order of reports in
* `API.getReportMetadata` and wherever we display any reports.
*
* To define a subcategory just place a subclass within the `Categories` folder of your plugin.
*
* Subcategories can also be added through the {@hook Subcategory.addSubcategories} event.
*
* @api since Piwik 3.0.0
*/
class Subcategory
{
/**
* The id of the subcategory, see eg {@link Piwik\Widget\WidgetConfig::setSubcategoryId()`} or
* {@link Piwik\Report\getSubcategoryId()}. The id will be used in the Piwik reporting URL and as the name
* in the Piwik reporting submenu. If you want to define a different URL and name, specify a {@link $name}.
* For example you might want to have the actual GoalId (eg '4') in the URL but the actual goal name in the
* submenu (eg 'Downloads'). In this case one should specify `$id=4;$name='Downloads'`.
*
* @var string eg 'General_Overview' or 'VisitTime_ByServerTimeWidgetName'.
*/
protected $id = '';
/**
* The id of the category the subcategory belongs to, must be specified.
* See {@link Piwik\Widget\WidgetConfig::setCategoryId()`} or {@link Piwik\Report\getCategoryId()}.
*
* @var string A translation key eg 'General_Visits' or 'Goals_Goals'
*/
protected $categoryId = '';
/**
* The name that shall be used in the menu etc, defaults to the specified {@link $id}. See {@link $id}.
* @var string
*/
protected $name = '';
/**
* The order of the subcategory. The lower the value the earlier a widget or a report will be displayed.
* @var int
*/
protected $order = 99;
/**
* Sets (overwrites) the id of the subcategory see {@link $id}.
*
* @param string $id A translation key eg 'General_Overview'.
* @return static
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Get the id of the subcategory.
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* Get the specifed categoryId see {@link $categoryId}.
*
* @return string
*/
public function getCategoryId()
{
return $this->categoryId;
}
/**
* Sets (overwrites) the categoryId see {@link $categoryId}.
*
* @param string $categoryId
* @return static
*/
public function setCategoryId($categoryId)
{
$this->categoryId = $categoryId;
return $this;
}
/**
* Sets (overwrites) the name see {@link $name} and {@link $id}.
*
* @param string $name A translation key eg 'General_Overview'.
* @return static
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get the name of the subcategory.
* @return string
*/
public function getName()
{
if (!empty($this->name)) {
return $this->name;
}
return $this->id;
}
/**
* Sets (overwrites) the order see {@link $order}.
*
* @param int $order
* @return static
*/
public function setOrder($order)
{
$this->order = (int) $order;
return $this;
}
/**
* Get the order of the subcategory.
* @return int
*/
public function getOrder()
{
return $this->order;
}
}

View File

@ -0,0 +1,455 @@
<?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;
use Piwik\Archiver\Request;
use Piwik\CliMulti\CliPhp;
use Piwik\CliMulti\Output;
use Piwik\CliMulti\Process;
use Piwik\Container\StaticContainer;
/**
* Class CliMulti.
*/
class CliMulti
{
const BASE_WAIT_TIME = 250000; // 250 * 1000 = 250ms
/**
* If set to true or false it will overwrite whether async is supported or not.
*
* @var null|bool
*/
public $supportsAsync = null;
/**
* @var Process[]
*/
private $processes = array();
/**
* If set it will issue at most concurrentProcessesLimit requests
* @var int
*/
private $concurrentProcessesLimit = null;
/**
* @var Output[]
*/
private $outputs = array();
private $acceptInvalidSSLCertificate = false;
/**
* @var bool
*/
private $runAsSuperUser = false;
/**
* Only used when doing synchronous curl requests.
*
* @var string
*/
private $urlToPiwik = null;
private $phpCliOptions = '';
/**
* @var callable
*/
private $onProcessFinish = null;
public function __construct()
{
$this->supportsAsync = $this->supportsAsync();
}
/**
* It will request all given URLs in parallel (async) using the CLI and wait until all requests are finished.
* If multi cli is not supported (eg windows) it will initiate an HTTP request instead (not async).
*
* @param string[] $piwikUrls An array of urls, for instance:
*
* `array('http://www.example.com/piwik?module=API...')`
*
* **Make sure query parameter values are properly encoded in the URLs.**
*
* @return array The response of each URL in the same order as the URLs. The array can contain null values in case
* there was a problem with a request, for instance if the process died unexpected.
*/
public function request(array $piwikUrls)
{
$chunks = array($piwikUrls);
if ($this->concurrentProcessesLimit) {
$chunks = array_chunk($piwikUrls, $this->concurrentProcessesLimit);
}
$results = array();
foreach ($chunks as $urlsChunk) {
$results = array_merge($results, $this->requestUrls($urlsChunk));
}
return $results;
}
/**
* Forwards the given configuration options to the PHP cli command.
* @param string $phpCliOptions eg "-d memory_limit=8G -c=path/to/php.ini"
*/
public function setPhpCliConfigurationOptions($phpCliOptions)
{
$this->phpCliOptions = (string) $phpCliOptions;
}
/**
* Ok, this sounds weird. Why should we care about ssl certificates when we are in CLI mode? It is needed for
* our simple fallback mode for Windows where we initiate HTTP requests instead of CLI.
* @param $acceptInvalidSSLCertificate
*/
public function setAcceptInvalidSSLCertificate($acceptInvalidSSLCertificate)
{
$this->acceptInvalidSSLCertificate = $acceptInvalidSSLCertificate;
}
/**
* @param $limit int Maximum count of requests to issue in parallel
*/
public function setConcurrentProcessesLimit($limit)
{
$this->concurrentProcessesLimit = $limit;
}
public function runAsSuperUser($runAsSuperUser = true)
{
$this->runAsSuperUser = $runAsSuperUser;
}
private function start($piwikUrls)
{
foreach ($piwikUrls as $index => $url) {
$shouldStart = null;
if ($url instanceof Request) {
$shouldStart = $url->start();
}
$cmdId = $this->generateCommandId($url) . $index;
if ($shouldStart === Request::ABORT) {
// output is needed to ensure same order of url to response
$output = new Output($cmdId);
$output->write(serialize(array('aborted' => '1')));
$this->outputs[] = $output;
} else {
$this->executeUrlCommand($cmdId, $url);
}
}
}
private function executeUrlCommand($cmdId, $url)
{
$output = new Output($cmdId);
if ($this->supportsAsync) {
$this->executeAsyncCli($url, $output, $cmdId);
} else {
$this->executeNotAsyncHttp($url, $output);
}
$this->outputs[] = $output;
}
private function buildCommand($hostname, $query, $outputFile, $doEsacpeArg = true)
{
$bin = $this->findPhpBinary();
$superuserCommand = $this->runAsSuperUser ? "--superuser" : "";
if ($doEsacpeArg) {
$hostname = escapeshellarg($hostname);
$query = escapeshellarg($query);
}
return sprintf('%s %s %s/console climulti:request -q --matomo-domain=%s %s %s > %s 2>&1 &',
$bin, $this->phpCliOptions, PIWIK_INCLUDE_PATH, $hostname, $superuserCommand, $query, $outputFile);
}
private function getResponse()
{
$response = array();
foreach ($this->outputs as $output) {
$response[] = $output->get();
}
return $response;
}
private function hasFinished()
{
foreach ($this->processes as $index => $process) {
$hasStarted = $process->hasStarted();
if (!$hasStarted && 8 <= $process->getSecondsSinceCreation()) {
// if process was created more than 8 seconds ago but still not started there must be something wrong.
// ==> declare the process as finished
$process->finishProcess();
continue;
} elseif (!$hasStarted) {
return false;
}
if ($process->isRunning()) {
return false;
}
$pid = $process->getPid();
foreach ($this->outputs as $output) {
if ($output->getOutputId() === $pid && $output->isAbnormal()) {
$process->finishProcess();
return true;
}
}
if ($process->hasFinished()) {
// prevent from checking this process over and over again
unset($this->processes[$index]);
if ($this->onProcessFinish) {
$onProcessFinish = $this->onProcessFinish;
$onProcessFinish($pid);
}
}
}
return true;
}
private function generateCommandId($command)
{
return substr(Common::hash($command . microtime(true) . rand(0, 99999)), 0, 100);
}
/**
* What is missing under windows? Detection whether a process is still running in Process::isProcessStillRunning
* and how to send a process into background in start()
*/
public function supportsAsync()
{
return Process::isSupported() && !Common::isPhpCgiType() && $this->findPhpBinary();
}
private function findPhpBinary()
{
$cliPhp = new CliPhp();
return $cliPhp->findPhpBinary();
}
private function cleanup()
{
foreach ($this->processes as $pid) {
$pid->finishProcess();
}
foreach ($this->outputs as $output) {
$output->destroy();
}
$this->processes = array();
$this->outputs = array();
}
/**
* Remove files older than one week. They should be cleaned up automatically after each request but for whatever
* reason there can be always some files left.
*/
public static function cleanupNotRemovedFiles()
{
$timeOneWeekAgo = strtotime('-1 week');
$files = _glob(self::getTmpPath() . '/*');
if (empty($files)) {
return;
}
foreach ($files as $file) {
if (file_exists($file)) {
$timeLastModified = filemtime($file);
if ($timeLastModified !== false && $timeOneWeekAgo > $timeLastModified) {
unlink($file);
}
}
}
}
public static function getTmpPath()
{
return StaticContainer::get('path.tmp') . '/climulti';
}
public function isCommandAlreadyRunning($url)
{
if (defined('PIWIK_TEST_MODE')) {
return false; // skip check in tests as it might result in random failures
}
if (!$this->supportsAsync) {
// we cannot detect if web archive is still running
return false;
}
$query = UrlHelper::getQueryFromUrl($url, array('pid' => 'removeme'));
$hostname = Url::getHost($checkIfTrusted = false);
$commandToCheck = $this->buildCommand($hostname, $query, $output = '', $escape = false);
$currentlyRunningJobs = `ps aux`;
$posStart = strpos($commandToCheck, 'console climulti');
$posPid = strpos($commandToCheck, '&pid='); // the pid is random each time so we need to ignore it.
$shortendCommand = substr($commandToCheck, $posStart, $posPid - $posStart);
// equals eg console climulti:request -q --matomo-domain= --superuser module=API&method=API.get&idSite=1&period=month&date=2018-04-08,2018-04-30&format=php&trigger=archivephp
$shortendCommand = preg_replace("/([&])date=.*?(&|$)/", "", $shortendCommand);
$currentlyRunningJobs = preg_replace("/([&])date=.*?(&|$)/", "", $currentlyRunningJobs);
if (strpos($currentlyRunningJobs, $shortendCommand) !== false) {
Log::debug($shortendCommand . ' is already running');
return true;
}
return false;
}
private function executeAsyncCli($url, Output $output, $cmdId)
{
$this->processes[] = new Process($cmdId);
$url = $this->appendTestmodeParamToUrlIfNeeded($url);
$query = UrlHelper::getQueryFromUrl($url, array('pid' => $cmdId, 'runid' => getmypid()));
$hostname = Url::getHost($checkIfTrusted = false);
$command = $this->buildCommand($hostname, $query, $output->getPathToFile());
Log::debug($command);
shell_exec($command);
}
private function executeNotAsyncHttp($url, Output $output)
{
$piwikUrl = $this->urlToPiwik ?: SettingsPiwik::getPiwikUrl();
if (empty($piwikUrl)) {
$piwikUrl = 'http://' . Url::getHost() . '/';
}
$url = $piwikUrl . $url;
if (Config::getInstance()->General['force_ssl'] == 1) {
$url = str_replace("http://", "https://", $url);
}
if ($this->runAsSuperUser) {
$tokenAuths = self::getSuperUserTokenAuths();
$tokenAuth = reset($tokenAuths);
if (strpos($url, '?') === false) {
$url .= '?';
} else {
$url .= '&';
}
$url .= 'token_auth=' . $tokenAuth;
}
try {
Log::debug("Execute HTTP API request: " . $url);
$response = Http::sendHttpRequestBy('curl', $url, $timeout = 0, $userAgent = null, $destinationPath = null, $file = null, $followDepth = 0, $acceptLanguage = false, $this->acceptInvalidSSLCertificate);
$output->write($response);
} catch (\Exception $e) {
$message = "Got invalid response from API request: $url. ";
if (isset($response) && empty($response)) {
$message .= "The response was empty. This usually means a server error. This solution to this error is generally to increase the value of 'memory_limit' in your php.ini file. Please check your Web server Error Log file for more details.";
} else {
$message .= "Response was '" . $e->getMessage() . "'";
}
$output->write($message);
Log::debug($e);
}
}
private function appendTestmodeParamToUrlIfNeeded($url)
{
$isTestMode = defined('PIWIK_TEST_MODE');
if ($isTestMode && false === strpos($url, '?')) {
$url .= "?testmode=1";
} elseif ($isTestMode) {
$url .= "&testmode=1";
}
return $url;
}
/**
* @param array $piwikUrls
* @return array
*/
private function requestUrls(array $piwikUrls)
{
$this->start($piwikUrls);
$startTime = time();
do {
$elapsed = time() - $startTime;
$timeToWait = $this->getTimeToWaitBeforeNextCheck($elapsed);
usleep($timeToWait);
} while (!$this->hasFinished());
$results = $this->getResponse();
$this->cleanup();
self::cleanupNotRemovedFiles();
return $results;
}
private static function getSuperUserTokenAuths()
{
$tokens = array();
/**
* Used to be in CronArchive, moved to CliMulti.
*
* @ignore
*/
Piwik::postEvent('CronArchive.getTokenAuth', array(&$tokens));
return $tokens;
}
public function setUrlToPiwik($urlToPiwik)
{
$this->urlToPiwik = $urlToPiwik;
}
public function onProcessFinish(callable $callback)
{
$this->onProcessFinish = $callback;
}
// every minute that passes adds an extra 100ms to the wait time. so 5 minutes results in 500ms extra, 20mins results in 2s extra.
private function getTimeToWaitBeforeNextCheck($elapsed)
{
$minutes = floor($elapsed / 60);
return self::BASE_WAIT_TIME + $minutes * 100000; // 100 * 1000 = 100ms
}
public static function isCliMultiRequest()
{
return Common::getRequestVar('pid', false) !== false;
}
}

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\CliMulti;
use Piwik\Common;
class CliPhp
{
public function findPhpBinary()
{
if (defined('PHP_BINARY')) {
if ($this->isHhvmBinary(PHP_BINARY)) {
return PHP_BINARY . ' --php';
}
if ($this->isValidPhpType(PHP_BINARY)) {
return PHP_BINARY . ' -q';
}
}
$bin = '';
if (!empty($_SERVER['_']) && Common::isPhpCliMode()) {
$bin = $this->getPhpCommandIfValid($_SERVER['_']);
}
if (empty($bin) && !empty($_SERVER['argv'][0]) && Common::isPhpCliMode()) {
$bin = $this->getPhpCommandIfValid($_SERVER['argv'][0]);
}
if (!$this->isValidPhpType($bin)) {
$bin = shell_exec('which php');
}
if (!$this->isValidPhpType($bin)) {
$bin = shell_exec('which php5');
}
if (!$this->isValidPhpType($bin)) {
return false;
}
$bin = trim($bin);
if (!$this->isValidPhpVersion($bin)) {
return false;
}
$bin .= ' -q';
return $bin;
}
private function isHhvmBinary($bin)
{
return false !== strpos($bin, 'hhvm');
}
private function isValidPhpVersion($bin)
{
global $piwik_minimumPHPVersion;
$cliVersion = $this->getPhpVersion($bin);
$isCliVersionValid = version_compare($piwik_minimumPHPVersion, $cliVersion) <= 0;
return $isCliVersionValid;
}
private function isValidPhpType($path)
{
return !empty($path)
&& false === strpos($path, 'fpm')
&& false === strpos($path, 'cgi')
&& false === strpos($path, 'phpunit');
}
private function getPhpCommandIfValid($path)
{
if (!empty($path) && is_executable($path)) {
if (0 === strpos($path, PHP_BINDIR) && $this->isValidPhpType($path)) {
return $path;
}
}
return null;
}
/**
* @param string $bin PHP binary
* @return string
*/
private function getPhpVersion($bin)
{
$command = sprintf("%s -r 'echo phpversion();'", $bin);
$version = shell_exec($command);
return $version;
}
}

View File

@ -0,0 +1,68 @@
<?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\CliMulti;
use Piwik\CliMulti;
use Piwik\Filesystem;
class Output
{
private $tmpFile = '';
private $outputId = null;
public function __construct($outputId)
{
if (!Filesystem::isValidFilename($outputId)) {
throw new \Exception('The given output id has an invalid format');
}
$dir = CliMulti::getTmpPath();
Filesystem::mkdir($dir);
$this->tmpFile = $dir . '/' . $outputId . '.output';
$this->outputId = $outputId;
}
public function getOutputId()
{
return $this->outputId;
}
public function write($content)
{
file_put_contents($this->tmpFile, $content);
}
public function getPathToFile()
{
return $this->tmpFile;
}
public function isAbnormal()
{
$size = Filesystem::getFileSize($this->tmpFile, 'MB');
return $size !== null && $size >= 100;
}
public function exists()
{
return file_exists($this->tmpFile);
}
public function get()
{
return @file_get_contents($this->tmpFile);
}
public function destroy()
{
Filesystem::deleteFileIfExists($this->tmpFile);
}
}

View File

@ -0,0 +1,266 @@
<?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\CliMulti;
use Piwik\CliMulti;
use Piwik\Filesystem;
use Piwik\SettingsServer;
/**
* There are three different states
* - PID file exists with empty content: Process is created but not started
* - PID file exists with the actual process PID as content: Process is running
* - PID file does not exist: Process is marked as finished
*
* Class Process
*/
class Process
{
private $pidFile = '';
private $timeCreation = null;
private $isSupported = null;
private $pid = null;
public function __construct($pid)
{
if (!Filesystem::isValidFilename($pid)) {
throw new \Exception('The given pid has an invalid format');
}
$pidDir = CliMulti::getTmpPath();
Filesystem::mkdir($pidDir);
$this->isSupported = self::isSupported();
$this->pidFile = $pidDir . '/' . $pid . '.pid';
$this->timeCreation = time();
$this->pid = $pid;
$this->markAsNotStarted();
}
public function getPid()
{
return $this->pid;
}
private function markAsNotStarted()
{
$content = $this->getPidFileContent();
if ($this->doesPidFileExist($content)) {
return;
}
$this->writePidFileContent('');
}
public function hasStarted($content = null)
{
if (is_null($content)) {
$content = $this->getPidFileContent();
}
if (!$this->doesPidFileExist($content)) {
// process is finished, this means there was a start before
return true;
}
if ('' === trim($content)) {
// pid file is overwritten by startProcess()
return false;
}
// process is probably running or pid file was not removed
return true;
}
public function hasFinished()
{
$content = $this->getPidFileContent();
return !$this->doesPidFileExist($content);
}
public function getSecondsSinceCreation()
{
return time() - $this->timeCreation;
}
public function startProcess()
{
$this->writePidFileContent(getmypid());
}
public function isRunning()
{
$content = $this->getPidFileContent();
if (!$this->doesPidFileExist($content)) {
return false;
}
if (!$this->pidFileSizeIsNormal()) {
$this->finishProcess();
return false;
}
if ($this->isProcessStillRunning($content)) {
return true;
}
if ($this->hasStarted($content)) {
$this->finishProcess();
}
return false;
}
private function pidFileSizeIsNormal()
{
$size = Filesystem::getFileSize($this->pidFile);
return $size !== null && $size < 500;
}
public function finishProcess()
{
Filesystem::deleteFileIfExists($this->pidFile);
}
private function doesPidFileExist($content)
{
return false !== $content;
}
private function isProcessStillRunning($content)
{
if (!$this->isSupported) {
return true;
}
$lockedPID = trim($content);
$runningPIDs = self::getRunningProcesses();
return !empty($lockedPID) && in_array($lockedPID, $runningPIDs);
}
private function getPidFileContent()
{
return @file_get_contents($this->pidFile);
}
private function writePidFileContent($content)
{
file_put_contents($this->pidFile, $content);
}
public static function isSupported()
{
if (SettingsServer::isWindows()) {
return false;
}
if (self::shellExecFunctionIsDisabled()) {
return false;
}
if (self::isSystemNotSupported()) {
return false;
}
if (!self::commandExists('ps') || !self::returnsSuccessCode('ps') || !self::commandExists('awk')) {
return false;
}
if (!in_array(getmypid(), self::getRunningProcesses())) {
return false;
}
if (!self::isProcFSMounted()) {
return false;
}
return true;
}
private static function isSystemNotSupported()
{
$uname = @shell_exec('uname -a 2> /dev/null');
if (empty($uname)) {
$uname = php_uname();
}
if (strpos($uname, 'synology') !== false) {
return true;
}
return false;
}
private static function shellExecFunctionIsDisabled()
{
$command = 'shell_exec';
$disabled = explode(',', ini_get('disable_functions'));
$disabled = array_map('trim', $disabled);
return in_array($command, $disabled) || !function_exists($command);
}
private static function returnsSuccessCode($command)
{
$exec = $command . ' > /dev/null 2>&1; echo $?';
$returnCode = shell_exec($exec);
$returnCode = trim($returnCode);
return 0 == (int) $returnCode;
}
private static function commandExists($command)
{
$result = @shell_exec('which ' . escapeshellarg($command) . ' 2> /dev/null');
return !empty($result);
}
/**
* ps -e requires /proc
* @return bool
*/
private static function isProcFSMounted()
{
if (is_resource(@fopen('/proc', 'r'))) {
return true;
}
// Testing if /proc is a resource with @fopen fails on systems with open_basedir set.
// by using stat we not only test the existence of /proc but also confirm it's a 'proc' filesystem
$type = @shell_exec('stat -f -c "%T" /proc 2>/dev/null');
return strpos($type, 'proc') === 0;
}
public static function getListOfRunningProcesses()
{
$processes = `ps ex 2>/dev/null`;
if (empty($processes)) {
return array();
}
return explode("\n", $processes);
}
/**
* @return int[] The ids of the currently running processes
*/
public static function getRunningProcesses()
{
$ids = explode("\n", trim(`ps ex 2>/dev/null | awk '! /defunct/ {print $1}' 2>/dev/null`));
$ids = array_map('intval', $ids);
$ids = array_filter($ids, function ($id) {
return $id > 0;
});
return $ids;
}
}

View File

@ -0,0 +1,129 @@
<?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\CliMulti;
use Piwik\Application\Environment;
use Piwik\Access;
use Piwik\Container\StaticContainer;
use Piwik\Db;
use Piwik\Log;
use Piwik\Option;
use Piwik\Plugin\ConsoleCommand;
use Piwik\Url;
use Piwik\UrlHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* RequestCommand
*/
class RequestCommand extends ConsoleCommand
{
/**
* @var Environment
*/
private $environment;
protected function configure()
{
$this->setName('climulti:request');
$this->setDescription('Parses and executes the given query. See Piwik\CliMulti. Intended only for system usage.');
$this->addArgument('url-query', InputArgument::REQUIRED, 'Matomo URL query string, for instance: "module=API&method=API.getPiwikVersion&token_auth=123456789"');
$this->addOption('superuser', null, InputOption::VALUE_NONE, 'If supplied, runs the code as superuser.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->recreateContainerWithWebEnvironment();
$this->initHostAndQueryString($input);
if ($this->isTestModeEnabled()) {
$indexFile = '/tests/PHPUnit/proxy/';
$this->resetDatabase();
} else {
$indexFile = '/';
}
$indexFile .= 'index.php';
if (!empty($_GET['pid'])) {
$process = new Process($_GET['pid']);
if ($process->hasFinished()) {
return;
}
$process->startProcess();
}
if ($input->getOption('superuser')) {
StaticContainer::addDefinitions(array(
'observers.global' => \DI\add(array(
array('Environment.bootstrapped', function () {
Access::getInstance()->setSuperUserAccess(true);
})
)),
));
}
require_once PIWIK_INCLUDE_PATH . $indexFile;
if (!empty($process)) {
$process->finishProcess();
}
}
private function isTestModeEnabled()
{
return !empty($_GET['testmode']);
}
/**
* @param InputInterface $input
*/
protected function initHostAndQueryString(InputInterface $input)
{
$_GET = array();
// @todo remove piwik-domain fallback in Matomo 4
$hostname = $input->getOption('matomo-domain') ?: $input->getOption('piwik-domain');
Url::setHost($hostname);
$query = $input->getArgument('url-query');
$query = UrlHelper::getArrayFromQueryString($query); // NOTE: this method can create the StaticContainer now
foreach ($query as $name => $value) {
$_GET[$name] = $value;
}
}
/**
* We will be simulating an HTTP request here (by including index.php).
*
* To avoid weird side-effects (e.g. the logging output messing up the HTTP response on the CLI output)
* we need to recreate the container with the default environment instead of the CLI environment.
*/
private function recreateContainerWithWebEnvironment()
{
StaticContainer::clearContainer();
Log::unsetInstance();
$this->environment = new Environment(null);
$this->environment->init();
}
private function resetDatabase()
{
Option::clearCache();
Db::destroyDatabaseObject();
}
}

View File

@ -0,0 +1,57 @@
<?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\Columns;
use Piwik\Piwik;
use Piwik\Plugin\ArchivedMetric;
use Piwik\Plugin\ComputedMetric;
use Piwik\Plugin\Report;
/**
* A factory to create computed metrics.
*
* @api since Piwik 3.2.0
*/
class ComputedMetricFactory
{
/**
* @var MetricsList
*/
private $metricsList = null;
/**
* Generates a new report metric factory.
* @param MetricsList $list A report list instance
* @ignore
*/
public function __construct(MetricsList $list)
{
$this->metricsList = $list;
}
/**
* @return \Piwik\Plugin\ComputedMetric
*/
public function createComputedMetric($metricName1, $metricName2, $aggregation)
{
$metric1 = $this->metricsList->getMetric($metricName1);
if (!$metric1 instanceof ArchivedMetric || !$metric1->getDimension()) {
throw new \Exception('Only possible to create computed metric for an archived metric with a dimension');
}
$dimension1 = $metric1->getDimension();
$metric = new ComputedMetric($metricName1, $metricName2, $aggregation);
$metric->setCategory($dimension1->getCategoryId());
return $metric;
}
}

View File

@ -0,0 +1,907 @@
<?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\Columns;
use Piwik\Common;
use Piwik\Db;
use Piwik\Piwik;
use Piwik\Plugin;
use Piwik\Plugin\ArchivedMetric;
use Piwik\Plugin\ComponentFactory;
use Piwik\Plugin\Segment;
use Exception;
use Piwik\CacheId;
use Piwik\Cache as PiwikCache;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Metrics\Formatter;
/**
* @api
* @since 3.1.0
*/
abstract class Dimension
{
const COMPONENT_SUBNAMESPACE = 'Columns';
/**
* Segment type 'dimension'. Can be used along with {@link setType()}.
* @api
*/
const TYPE_DIMENSION = 'dimension';
const TYPE_BINARY = 'binary';
const TYPE_TEXT = 'text';
const TYPE_ENUM = 'enum';
const TYPE_MONEY = 'money';
const TYPE_BYTE = 'byte';
const TYPE_DURATION_MS = 'duration_ms';
const TYPE_DURATION_S = 'duration_s';
const TYPE_NUMBER = 'number';
const TYPE_FLOAT = 'float';
const TYPE_URL = 'url';
const TYPE_DATE = 'date';
const TYPE_TIME = 'time';
const TYPE_DATETIME = 'datetime';
const TYPE_TIMESTAMP = 'timestamp';
const TYPE_BOOL = 'bool';
const TYPE_PERCENT = 'percent';
/**
* This will be the name of the column in the database table if a $columnType is specified.
* @var string
* @api
*/
protected $columnName = '';
/**
* If a columnType is defined, we will create a column in the MySQL table having this type. Please make sure
* MySQL understands this type. Once you change the column type the Piwik platform will notify the user to
* perform an update which can sometimes take a long time so be careful when choosing the correct column type.
* @var string
* @api
*/
protected $columnType = '';
/**
* Holds an array of segment instances
* @var Segment[]
*/
protected $segments = array();
/**
* Defines what kind of data type this dimension holds. By default the type is auto-detected based on
* `$columnType` but sometimes it may be needed to correct this value. Depending on this type, a dimension will be
* formatted differently for example.
* @var string
* @api since Piwik 3.2.0
*/
protected $type = '';
/**
* Translation key for name singular
* @var string
*/
protected $nameSingular = '';
/**
* Translation key for name plural
* @var string
* @api since Piwik 3.2.0
*/
protected $namePlural = '';
/**
* Translation key for category
* @var string
*/
protected $category = '';
/**
* By defining a segment name a user will be able to filter their visitors by this column. If you do not want to
* define a segment for this dimension, simply leave the name empty.
* @api since Piwik 3.2.0
*/
protected $segmentName = '';
/**
* Sets a callback which will be executed when user will call for suggested values for segment.
*
* @var callable
* @api since Piwik 3.2.0
*/
protected $suggestedValuesCallback;
/**
* Here you should explain which values are accepted/useful for your segment, for example:
* "1, 2, 3, etc." or "comcast.net, proxad.net, etc.". If the value needs any special encoding you should mention
* this as well. For example "Any URL including protocol. The URL must be URL encoded."
*
* @var string
* @api since Piwik 3.2.0
*/
protected $acceptValues;
/**
* Defines to which column in the MySQL database the segment belongs (if one is conifugred). Defaults to
* `$this.dbTableName . '.'. $this.columnName` but you can customize it eg like `HOUR(log_visit.visit_last_action_time)`.
*
* @param string $sqlSegment
* @api since Piwik 3.2.0
*/
protected $sqlSegment;
/**
* Interesting when specifying a segment. Sometimes you want users to set segment values that differ from the way
* they are actually stored. For instance if you want to allow to filter by any URL than you might have to resolve
* this URL to an action id. Or a country name maybe has to be mapped to a 2 letter country code. You can do this by
* specifing either a callable such as `array('Classname', 'methodName')` or by passing a closure.
* There will be four values passed to the given closure or callable: `string $valueToMatch`, `string $segment`
* (see {@link setSegment()}), `string $matchType` (eg SegmentExpression::MATCH_EQUAL or any other match constant
* of this class) and `$segmentName`.
*
* If the closure returns NULL, then Piwik assumes the segment sub-string will not match any visitor.
*
* @var string|\Closure
* @api since Piwik 3.2.0
*/
protected $sqlFilter;
/**
* Similar to {@link $sqlFilter} you can map a given segment value to another value. For instance you could map
* "new" to 0, 'returning' to 1 and any other value to '2'. You can either define a callable or a closure. There
* will be only one value passed to the closure or callable which contains the value a user has set for this
* segment.
* @var string|array
* @api since Piwik 3.2.0
*/
protected $sqlFilterValue;
/**
* Defines whether this dimension (and segment based on this dimension) is available to anonymous users.
* @var bool
* @api since Piwik 3.2.0
*/
protected $allowAnonymous = true;
/**
* The name of the database table this dimension refers to
* @var string
* @api
*/
protected $dbTableName = '';
/**
* By default the metricId is automatically generated based on the dimensionId. This might sometimes not be as
* readable and quite long. If you want more expressive metric names like `nb_visits` compared to
* `nb_corehomevisitid`, you can eg set a metricId `visit`.
*
* @var string
* @api since Piwik 3.2.0
*/
protected $metricId = '';
/**
* To be implemented when a column references another column
* @return Join|null
* @api since Piwik 3.2.0
*/
public function getDbColumnJoin()
{
return null;
}
/**
* @return Discriminator|null
* @api since Piwik 3.2.0
*/
public function getDbDiscriminator()
{
return null;
}
/**
* To be implemented when a column represents an enum.
* @return array
* @api since Piwik 3.2.0
*/
public function getEnumColumnValues()
{
return array();
}
/**
* Get the metricId which is used to generate metric names based on this dimension.
* @return string
*/
public function getMetricId()
{
if (!empty($this->metricId)) {
return $this->metricId;
}
$id = $this->getId();
return str_replace(array('.', ' ', '-'), '_', strtolower($id));
}
/**
* Installs the action dimension in case it is not installed yet. The installation is already implemented based on
* the {@link $columnName} and {@link $columnType}. If you want to perform additional actions beside adding the
* column to the database - for instance adding an index - you can overwrite this method. We recommend to call
* this parent method to get the minimum required actions and then add further custom actions since this makes sure
* the column will be installed correctly. We also recommend to change the default install behavior only if really
* needed. FYI: We do not directly execute those alter table statements here as we group them together with several
* other alter table statements do execute those changes in one step which results in a faster installation. The
* column will be added to the `log_link_visit_action` MySQL table.
*
* Example:
* ```
public function install()
{
$changes = parent::install();
$changes['log_link_visit_action'][] = "ADD INDEX index_idsite_servertime ( idsite, server_time )";
return $changes;
}
```
*
* @return array An array containing the table name as key and an array of MySQL alter table statements that should
* be executed on the given table. Example:
* ```
array(
'log_link_visit_action' => array("ADD COLUMN `$this->columnName` $this->columnType", "ADD INDEX ...")
);
```
* @api
*/
public function install()
{
if (empty($this->columnName) || empty($this->columnType) || empty($this->dbTableName)) {
return array();
}
// TODO if table does not exist, create it with a primary key, but at this point we cannot really create it
// cause we need to show the query in the UI first and user needs to be able to create table manually.
// we cannot return something like "create table " here as it would be returned for each table etc.
// we need to do this in column updater etc!
return array(
$this->dbTableName => array("ADD COLUMN `$this->columnName` $this->columnType")
);
}
/**
* Updates the action dimension in case the {@link $columnType} has changed. The update is already implemented based
* on the {@link $columnName} and {@link $columnType}. This method is intended not to overwritten by plugin
* developers as it is only supposed to make sure the column has the correct type. Adding additional custom "alter
* table" actions would not really work since they would be executed with every {@link $columnType} change. So
* adding an index here would be executed whenever the columnType changes resulting in an error if the index already
* exists. If an index needs to be added after the first version is released a plugin update class should be
* created since this makes sure it is only executed once.
*
* @return array An array containing the table name as key and an array of MySQL alter table statements that should
* be executed on the given table. Example:
* ```
array(
'log_link_visit_action' => array("MODIFY COLUMN `$this->columnName` $this->columnType", "DROP COLUMN ...")
);
```
* @ignore
*/
public function update()
{
if (empty($this->columnName) || empty($this->columnType) || empty($this->dbTableName)) {
return array();
}
return array(
$this->dbTableName => array("MODIFY COLUMN `$this->columnName` $this->columnType")
);
}
/**
* Uninstalls the dimension if a {@link $columnName} and {@link columnType} is set. In case you perform any custom
* actions during {@link install()} - for instance adding an index - you should make sure to undo those actions by
* overwriting this method. Make sure to call this parent method to make sure the uninstallation of the column
* will be done.
* @throws Exception
* @api
*/
public function uninstall()
{
if (empty($this->columnName) || empty($this->columnType) || empty($this->dbTableName)) {
return;
}
try {
$sql = "ALTER TABLE `" . Common::prefixTable($this->dbTableName) . "` DROP COLUMN `$this->columnName`";
Db::exec($sql);
} catch (Exception $e) {
if (!Db::get()->isErrNo($e, '1091')) {
throw $e;
}
}
}
/**
* Returns the ID of the category (typically a translation key).
* @return string
*/
public function getCategoryId()
{
return $this->category;
}
/**
* Returns the translated name of this dimension which is typically in singular.
*
* @return string
*/
public function getName()
{
if (!empty($this->nameSingular)) {
return Piwik::translate($this->nameSingular);
}
return $this->nameSingular;
}
/**
* Returns a translated name in plural for this dimension.
* @return string
* @api since Piwik 3.2.0
*/
public function getNamePlural()
{
if (!empty($this->namePlural)) {
return Piwik::translate($this->namePlural);
}
return $this->getName();
}
/**
* Defines whether an anonymous user is allowed to view this dimension
* @return bool
* @api since Piwik 3.2.0
*/
public function isAnonymousAllowed()
{
return $this->allowAnonymous;
}
/**
* Sets (overwrites) the SQL segment
* @param $segment
* @api since Piwik 3.2.0
*/
public function setSqlSegment($segment)
{
$this->sqlSegment = $segment;
}
/**
* Sets (overwrites the dimension type)
* @param $type
* @api since Piwik 3.2.0
*/
public function setType($type)
{
$this->type = $type;
}
/**
* A dimension should group values by using this method. Otherwise the same row may appear several times.
*
* @param mixed $value
* @param int $idSite
* @return mixed
* @api since Piwik 3.2.0
*/
public function groupValue($value, $idSite)
{
switch ($this->type) {
case Dimension::TYPE_URL:
return str_replace(array('http://', 'https://'), '', $value);
case Dimension::TYPE_BOOL:
return !empty($value) ? '1' : '0';
case Dimension::TYPE_DURATION_MS:
return number_format($value / 1000, 2); // because we divide we need to group them and cannot do this in formatting step
}
return $value;
}
/**
* Formats the dimension value. By default, the dimension is formatted based on the set dimension type.
*
* @param mixed $value
* @param int $idSite
* @param Formatter $formatter
* @return mixed
* @api since Piwik 3.2.0
*/
public function formatValue($value, $idSite, Formatter $formatter)
{
switch ($this->type) {
case Dimension::TYPE_BOOL:
if (empty($value)) {
return Piwik::translate('General_No');
}
return Piwik::translate('General_Yes');
case Dimension::TYPE_ENUM:
$values = $this->getEnumColumnValues();
if (isset($values[$value])) {
return $values[$value];
}
break;
case Dimension::TYPE_MONEY:
return $formatter->getPrettyMoney($value, $idSite);
case Dimension::TYPE_FLOAT:
return $formatter->getPrettyNumber((float) $value, $precision = 2);
case Dimension::TYPE_NUMBER:
return $formatter->getPrettyNumber($value);
case Dimension::TYPE_DURATION_S:
return $formatter->getPrettyTimeFromSeconds($value, $displayAsSentence = false);
case Dimension::TYPE_DURATION_MS:
return $formatter->getPrettyTimeFromSeconds($value, $displayAsSentence = true);
case Dimension::TYPE_PERCENT:
return $formatter->getPrettyPercentFromQuotient($value);
case Dimension::TYPE_BYTE:
return $formatter->getPrettySizeFromBytes($value);
}
return $value;
}
/**
* Overwrite this method to configure segments. To do so just create an instance of a {@link \Piwik\Plugin\Segment}
* class, configure it and call the {@link addSegment()} method. You can add one or more segments for this
* dimension. Example:
*
* ```
* $segment = new Segment();
* $segment->setSegment('exitPageUrl');
* $segment->setName('Actions_ColumnExitPageURL');
* $segment->setCategory('General_Visit');
* $this->addSegment($segment);
* ```
*/
protected function configureSegments()
{
if ($this->segmentName && $this->category
&& ($this->sqlSegment || ($this->columnName && $this->dbTableName))
&& $this->nameSingular) {
$segment = new Segment();
$this->addSegment($segment);
}
}
/**
* Configures metrics for this dimension.
*
* For certain dimension types, some metrics will be added automatically.
*
* @param MetricsList $metricsList
* @param DimensionMetricFactory $dimensionMetricFactory
*/
public function configureMetrics(MetricsList $metricsList, DimensionMetricFactory $dimensionMetricFactory)
{
if ($this->getMetricId() && $this->dbTableName && $this->columnName && $this->getNamePlural()) {
if (in_array($this->getType(), array(self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME, self::TYPE_TIMESTAMP))) {
// we do not generate any metrics from these types
return;
} elseif (in_array($this->getType(), array(self::TYPE_URL, self::TYPE_TEXT, self::TYPE_BINARY, self::TYPE_ENUM))) {
$metric = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_UNIQUE);
$metricsList->addMetric($metric);
} elseif (in_array($this->getType(), array(self::TYPE_BOOL))) {
$metric = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_SUM);
$metricsList->addMetric($metric);
} else {
$metric = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_SUM);
$metricsList->addMetric($metric);
$metric = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MAX);
$metricsList->addMetric($metric);
}
}
}
/**
* Check whether a dimension has overwritten a specific method.
* @param $method
* @return bool
* @ignore
*/
public function hasImplementedEvent($method)
{
$method = new \ReflectionMethod($this, $method);
$declaringClass = $method->getDeclaringClass();
return 0 === strpos($declaringClass->name, 'Piwik\Plugins');
}
/**
* Adds a new segment. It automatically sets the SQL segment depending on the column name in case none is set
* already.
* @see \Piwik\Columns\Dimension::addSegment()
* @param Segment $segment
* @api
*/
protected function addSegment(Segment $segment)
{
if (!$segment->getSegment() && $this->segmentName) {
$segment->setSegment($this->segmentName);
}
if (!$segment->getType()) {
$metricTypes = array(self::TYPE_NUMBER, self::TYPE_FLOAT, self::TYPE_MONEY, self::TYPE_DURATION_S, self::TYPE_DURATION_MS);
if (in_array($this->getType(), $metricTypes, $strict = true)) {
$segment->setType(Segment::TYPE_METRIC);
} else {
$segment->setType(Segment::TYPE_DIMENSION);
}
}
if (!$segment->getCategoryId() && $this->category) {
$segment->setCategory($this->category);
}
if (!$segment->getName() && $this->nameSingular) {
$segment->setName($this->nameSingular);
}
$sqlSegment = $segment->getSqlSegment();
if (empty($sqlSegment) && !$segment->getUnionOfSegments()) {
if (!empty($this->sqlSegment)) {
$segment->setSqlSegment($this->sqlSegment);
} elseif ($this->dbTableName && $this->columnName) {
$segment->setSqlSegment($this->dbTableName . '.' . $this->columnName);
} else {
throw new Exception('Segment cannot be added because no sql segment is set');
}
}
if (!$this->suggestedValuesCallback) {
// we can generate effecient value callback for enums automatically
$enum = $this->getEnumColumnValues();
if (!empty($enum)) {
$this->suggestedValuesCallback = function ($idSite, $maxValuesToReturn) use ($enum) {
$values = array_values($enum);
return array_slice($values, 0, $maxValuesToReturn);
};
}
}
if (!$this->acceptValues) {
// we can generate accept values for enums automatically
$enum = $this->getEnumColumnValues();
if (!empty($enum)) {
$enumValues = array_values($enum);
$enumValues = array_slice($enumValues, 0, 20);
$this->acceptValues = 'Eg. ' . implode(', ', $enumValues);
};
}
if ($this->acceptValues && !$segment->getAcceptValues()) {
$segment->setAcceptedValues($this->acceptValues);
}
if (!$this->sqlFilterValue && !$segment->getSqlFilter() && !$segment->getSqlFilterValue()) {
// no sql filter configured, we try to configure automatically for enums
$enum = $this->getEnumColumnValues();
if (!empty($enum)) {
$this->sqlFilterValue = function ($value, $sqlSegmentName) use ($enum) {
if (isset($enum[$value])) {
return $value;
}
$id = array_search($value, $enum);
if ($id === false) {
$id = array_search(strtolower(trim(urldecode($value))), $enum);
if ($id === false) {
throw new \Exception("Invalid '$sqlSegmentName' segment value $value");
}
}
return $id;
};
};
}
if ($this->suggestedValuesCallback && !$segment->getSuggestedValuesCallback()) {
$segment->setSuggestedValuesCallback($this->suggestedValuesCallback);
}
if ($this->sqlFilterValue && !$segment->getSqlFilterValue()) {
$segment->setSqlFilterValue($this->sqlFilterValue);
}
if ($this->sqlFilter && !$segment->getSqlFilter()) {
$segment->setSqlFilter($this->sqlFilter);
}
if (!$this->allowAnonymous) {
$segment->setRequiresAtLeastViewAccess(true);
}
$this->segments[] = $segment;
}
/**
* Get the list of configured segments.
* @return Segment[]
* @ignore
*/
public function getSegments()
{
if (empty($this->segments)) {
$this->configureSegments();
}
return $this->segments;
}
/**
* Returns the name of the segment that this dimension defines
* @return string
* @api since Piwik 3.2.0
*/
public function getSegmentName()
{
return $this->segmentName;
}
/**
* Get the name of the dimension column.
* @return string
* @ignore
*/
public function getColumnName()
{
return $this->columnName;
}
/**
* Returns a sql segment expression for this dimension.
* @return string
* @api since Piwik 3.2.0
*/
public function getSqlSegment()
{
if (!empty($this->sqlSegment)) {
return $this->sqlSegment;
}
if ($this->dbTableName && $this->columnName) {
return $this->dbTableName . '.' . $this->columnName;
}
}
/**
* Check whether the dimension has a column type configured
* @return bool
* @ignore
*/
public function hasColumnType()
{
return !empty($this->columnType);
}
/**
* Returns the name of the database table this dimension belongs to.
* @return string
* @api since Piwik 3.2.0
*/
public function getDbTableName()
{
return $this->dbTableName;
}
/**
* Returns a unique string ID for this dimension. The ID is built using the namespaced class name
* of the dimension, but is modified to be more human readable.
*
* @return string eg, `"Referrers.Keywords"`
* @throws Exception if the plugin and simple class name of this instance cannot be determined.
* This would only happen if the dimension is located in the wrong directory.
* @api
*/
public function getId()
{
$className = get_class($this);
return $this->generateIdFromClass($className);
}
/**
* @param string $className
* @return string
* @throws Exception
* @ignore
*/
protected function generateIdFromClass($className)
{
// parse plugin name & dimension name
$regex = "/Piwik\\\\Plugins\\\\([^\\\\]+)\\\\" . self::COMPONENT_SUBNAMESPACE . "\\\\([^\\\\]+)/";
if (!preg_match($regex, $className, $matches)) {
throw new Exception("'$className' is located in the wrong directory.");
}
$pluginName = $matches[1];
$dimensionName = $matches[2];
return $pluginName . '.' . $dimensionName;
}
/**
* Gets an instance of all available visit, action and conversion dimension.
* @return Dimension[]
*/
public static function getAllDimensions()
{
$cacheId = CacheId::siteAware(CacheId::pluginAware('AllDimensions'));
$cache = PiwikCache::getTransientCache();
if (!$cache->contains($cacheId)) {
$plugins = PluginManager::getInstance()->getPluginsLoadedAndActivated();
$instances = array();
/**
* Triggered to add new dimensions that cannot be picked up automatically by the platform.
* This is useful if the plugin allows a user to create reports / dimensions dynamically. For example
* CustomDimensions or CustomVariables. There are a variable number of dimensions in this case and it
* wouldn't be really possible to create a report file for one of these dimensions as it is not known
* how many Custom Dimensions will exist.
*
* **Example**
*
* public function addDimension(&$dimensions)
* {
* $dimensions[] = new MyCustomDimension();
* }
*
* @param Dimension[] $reports An array of dimensions
*/
Piwik::postEvent('Dimension.addDimensions', array(&$instances));
foreach ($plugins as $plugin) {
foreach (self::getDimensions($plugin) as $instance) {
$instances[] = $instance;
}
}
/**
* Triggered to filter / restrict dimensions.
*
* **Example**
*
* public function filterDimensions(&$dimensions)
* {
* foreach ($dimensions as $index => $dimension) {
* if ($dimension->getName() === 'Page URL') {}
* unset($dimensions[$index]); // remove this dimension
* }
* }
* }
*
* @param Dimension[] $dimensions An array of dimensions
*/
Piwik::postEvent('Dimension.filterDimensions', array(&$instances));
$cache->save($cacheId, $instances);
}
return $cache->fetch($cacheId);
}
public static function getDimensions(Plugin $plugin)
{
$columns = $plugin->findMultipleComponents('Columns', '\\Piwik\\Columns\\Dimension');
$instances = array();
foreach ($columns as $colum) {
$instances[] = new $colum();
}
return $instances;
}
/**
* Creates a Dimension instance from a string ID (see {@link getId()}).
*
* @param string $dimensionId See {@link getId()}.
* @return Dimension|null The created instance or null if there is no Dimension for
* $dimensionId or if the plugin that contains the Dimension is
* not loaded.
* @api
* @deprecated Please use DimensionsProvider::factory instead
*/
public static function factory($dimensionId)
{
list($module, $dimension) = explode('.', $dimensionId);
return ComponentFactory::factory($module, $dimension, __CLASS__);
}
/**
* Returns the name of the plugin that contains this Dimension.
*
* @return string
* @throws Exception if the Dimension is not located within a Plugin module.
* @api
*/
public function getModule()
{
$id = $this->getId();
if (empty($id)) {
throw new Exception("Invalid dimension ID: '$id'.");
}
$parts = explode('.', $id);
return reset($parts);
}
/**
* Returns the type of the dimension which defines what kind of value this dimension stores.
* @return string
* @api since Piwik 3.2.0
*/
public function getType()
{
if (!empty($this->type)) {
return $this->type;
}
if ($this->getDbColumnJoin()) {
// best guess
return self::TYPE_TEXT;
}
if ($this->getEnumColumnValues()) {
// best guess
return self::TYPE_ENUM;
}
if (!empty($this->columnType)) {
// best guess
$type = strtolower($this->columnType);
if (strpos($type, 'datetime') !== false) {
return self::TYPE_DATETIME;
} elseif (strpos($type, 'timestamp') !== false) {
return self::TYPE_TIMESTAMP;
} elseif (strpos($type, 'date') !== false) {
return self::TYPE_DATE;
} elseif (strpos($type, 'time') !== false) {
return self::TYPE_TIME;
} elseif (strpos($type, 'float') !== false) {
return self::TYPE_FLOAT;
} elseif (strpos($type, 'decimal') !== false) {
return self::TYPE_FLOAT;
} elseif (strpos($type, 'int') !== false) {
return self::TYPE_NUMBER;
} elseif (strpos($type, 'binary') !== false) {
return self::TYPE_BINARY;
}
}
return self::TYPE_TEXT;
}
/**
* Get the version of the dimension which is used for update checks.
* @return string
* @ignore
*/
public function getVersion()
{
return $this->columnType;
}
}

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\Columns;
use Piwik\Piwik;
use Piwik\Plugin\ArchivedMetric;
use Piwik\Plugin\ComputedMetric;
use Piwik\Plugin\Report;
/**
* A factory to create metrics from a dimension.
*
* @api since Piwik 3.2.0
*/
class DimensionMetricFactory
{
/**
* @var Dimension
*/
private $dimension = null;
/**
* Generates a new dimension metric factory.
* @param Dimension $dimension A dimension instance the created metrics should be based on.
*/
public function __construct(Dimension $dimension)
{
$this->dimension = $dimension;
}
/**
* @return ArchivedMetric
*/
public function createCustomMetric($metricName, $readableName, $aggregation, $documentation = '')
{
if (!$this->dimension->getDbTableName() || !$this->dimension->getColumnName()) {
throw new \Exception(sprintf('Cannot make metric from dimension %s because DB table or column missing', $this->dimension->getId()));
}
$metric = new ArchivedMetric($this->dimension, $aggregation);
$metric->setType($this->dimension->getType());
$metric->setName($metricName);
$metric->setTranslatedName($readableName);
$metric->setDocumentation($documentation);
$metric->setCategory($this->dimension->getCategoryId());
return $metric;
}
/**
* @return \Piwik\Plugin\ComputedMetric
*/
public function createComputedMetric($metricName1, $metricName2, $aggregation)
{
// We cannot use reuse ComputedMetricFactory here as it would result in an endless loop since ComputedMetricFactory
// requires a MetricsList which is just being built here...
$metric = new ComputedMetric($metricName1, $metricName2, $aggregation);
$metric->setCategory($this->dimension->getCategoryId());
return $metric;
}
/**
* @return ArchivedMetric
*/
public function createMetric($aggregation)
{
$dimension = $this->dimension;
if (!$dimension->getNamePlural()) {
throw new \Exception(sprintf('No metric can be created for this dimension %s automatically because no $namePlural is set.', $dimension->getId()));
}
$prefix = '';
$translatedName = $dimension->getNamePlural();
$documentation = '';
switch ($aggregation) {
case ArchivedMetric::AGGREGATION_COUNT;
$prefix = ArchivedMetric::AGGREGATION_COUNT_PREFIX;
$translatedName = $dimension->getNamePlural();
$documentation = Piwik::translate('General_ComputedMetricCountDocumentation', $dimension->getNamePlural());
break;
case ArchivedMetric::AGGREGATION_SUM;
$prefix = ArchivedMetric::AGGREGATION_SUM_PREFIX;
$translatedName = Piwik::translate('General_ComputedMetricSum', $dimension->getNamePlural());
$documentation = Piwik::translate('General_ComputedMetricSumDocumentation', $dimension->getNamePlural());
break;
case ArchivedMetric::AGGREGATION_MAX;
$prefix = ArchivedMetric::AGGREGATION_MAX_PREFIX;
$translatedName = Piwik::translate('General_ComputedMetricMax', $dimension->getNamePlural());
$documentation = Piwik::translate('General_ComputedMetricMaxDocumentation', $dimension->getNamePlural());
break;
case ArchivedMetric::AGGREGATION_MIN;
$prefix = ArchivedMetric::AGGREGATION_MIN_PREFIX;
$translatedName = Piwik::translate('General_ComputedMetricMin', $dimension->getNamePlural());
$documentation = Piwik::translate('General_ComputedMetricMinDocumentation', $dimension->getNamePlural());
break;
case ArchivedMetric::AGGREGATION_UNIQUE;
$prefix = ArchivedMetric::AGGREGATION_UNIQUE_PREFIX;
$translatedName = Piwik::translate('General_ComputedMetricUniqueCount', $dimension->getNamePlural());
$documentation = Piwik::translate('General_ComputedMetricUniqueCountDocumentation', $dimension->getNamePlural());
break;
case ArchivedMetric::AGGREGATION_COUNT_WITH_NUMERIC_VALUE;
$prefix = ArchivedMetric::AGGREGATION_COUNT_WITH_NUMERIC_VALUE_PREFIX;
$translatedName = Piwik::translate('General_ComputedMetricCountWithValue', $dimension->getName());
$documentation = Piwik::translate('General_ComputedMetricCountWithValueDocumentation', $dimension->getName());
break;
}
$metricId = strtolower($dimension->getMetricId());
return $this->createCustomMetric($prefix . $metricId, $translatedName, $aggregation, $documentation);
}
}

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\Columns;
use Piwik\CacheId;
use Piwik\Cache as PiwikCache;
class DimensionsProvider
{
/**
* @param $dimensionId
* @return Dimension
*/
public function factory($dimensionId)
{
$listDimensions = self::getMapOfNameToDimension();
if (empty($listDimensions) || !is_array($listDimensions) || !$dimensionId || !array_key_exists($dimensionId, $listDimensions)) {
return null;
}
return $listDimensions[$dimensionId];
}
private static function getMapOfNameToDimension()
{
$cacheId = CacheId::siteAware(CacheId::pluginAware('DimensionFactoryMap'));
$cache = PiwikCache::getTransientCache();
if ($cache->contains($cacheId)) {
$mapIdToDimension = $cache->fetch($cacheId);
} else {
$dimensions = new static();
$dimensions = $dimensions->getAllDimensions();
$mapIdToDimension = array();
foreach ($dimensions as $dimension) {
$mapIdToDimension[$dimension->getId()] = $dimension;
}
$cache->save($cacheId, $mapIdToDimension);
}
return $mapIdToDimension;
}
/**
* Returns a list of all available dimensions.
* @return Dimension[]
*/
public function getAllDimensions()
{
return Dimension::getAllDimensions();
}
}

View File

@ -0,0 +1,75 @@
<?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\Columns;
use Exception;
use Piwik\Plugins\Actions\Actions\ActionSiteSearch;
/**
* @api
* @since 3.1.0
*/
class Discriminator
{
private $table;
private $discriminatorColumn;
private $discriminatorValue;
/**
* Join constructor.
* @param string $table unprefixed table name
* @param null|string $discriminatorColumn
* @param null|int $discriminatorValue should be only hard coded, safe values.
* @throws Exception
*/
public function __construct($table, $discriminatorColumn = null, $discriminatorValue = null)
{
if (empty($discriminatorColumn) || !isset($discriminatorValue)) {
throw new Exception('Both discriminatorColumn and discriminatorValue need to be defined');
}
$this->table = $table;
$this->discriminatorColumn = $discriminatorColumn;
$this->discriminatorValue = $discriminatorValue;
if (!$this->isValid()) {
// if adding another string value please post an event instead to get a list of allowed values
throw new Exception('$discriminatorValue needs to be null or numeric');
}
}
public function isValid()
{
return isset($this->discriminatorColumn)
&& (is_numeric($this->discriminatorValue) || $this->discriminatorValue == ActionSiteSearch::CVAR_KEY_SEARCH_CATEGORY);
}
/**
* @return string
*/
public function getTable()
{
return $this->table;
}
/**
* @return string
*/
public function getColumn()
{
return $this->discriminatorColumn;
}
/**
* @return int|null
*/
public function getValue()
{
return $this->discriminatorValue;
}
}

View File

@ -0,0 +1,61 @@
<?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\Columns;
use Exception;
/**
* @api
* @since 3.1.0
*/
class Join
{
private $table;
private $column;
private $targetColumn;
/**
* Join constructor.
* @param $table
* @param $column
* @param $targetColumn
* @throws Exception
*/
public function __construct($table, $column, $targetColumn)
{
$this->table = $table;
$this->column = $column;
$this->targetColumn = $targetColumn;
}
/**
* @return string
*/
public function getTable()
{
return $this->table;
}
/**
* @return string
*/
public function getColumn()
{
return $this->column;
}
/**
* @return string
*/
public function getTargetColumn()
{
return $this->targetColumn;
}
}

View File

@ -0,0 +1,24 @@
<?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\Columns\Join;
use Piwik\Columns;
/**
* @api
* @since 3.1.0
*/
class ActionNameJoin extends Columns\Join
{
public function __construct()
{
return parent::__construct('log_action', 'idaction', 'name');
}
}

View File

@ -0,0 +1,24 @@
<?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\Columns\Join;
use Piwik\Columns;
/**
* @api
* @since 3.1.0
*/
class GoalNameJoin extends Columns\Join
{
public function __construct()
{
return parent::__construct('goal', 'idgoal', 'name');
}
}

View File

@ -0,0 +1,24 @@
<?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\Columns\Join;
use Piwik\Columns;
/**
* @api
* @since 3.1.0
*/
class SiteNameJoin extends Columns\Join
{
public function __construct()
{
return parent::__construct('site', 'idsite', 'name');
}
}

View File

@ -0,0 +1,191 @@
<?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\Columns;
use Piwik\Cache;
use Piwik\CacheId;
use Piwik\Piwik;
use Piwik\Plugin\ArchivedMetric;
use Piwik\Plugin\Metric;
use Piwik\Plugin\ProcessedMetric;
/**
* Manages the global list of metrics that can be used in reports.
*
* Metrics are added automatically by dimensions as well as through the {@hook Metric.addMetrics} and
* {@hook Metric.addComputedMetrics} and filtered through the {@hook Metric.filterMetrics} event.
* Observers for this event should call the {@link addMetric()} method to add metrics or use any of the other
* methods to remove metrics.
*
* @api since Piwik 3.2.0
*/
class MetricsList
{
/**
* List of metrics
*
* @var Metric[]
*/
private $metrics = array();
private $metricsByNameCache = array();
/**
* @param Metric $metric
*/
public function addMetric(Metric $metric)
{
$this->metrics[] = $metric;
$this->metricsByNameCache = array();
}
/**
* Get all available metrics.
*
* @return Metric[]
*/
public function getMetrics()
{
return $this->metrics;
}
/**
* Removes one or more metrics from the metrics list.
*
* @param string $metricCategory The metric category id. Can be a translation token eg 'General_Visits'
* see {@link Metric::getCategory()}.
* @param string|false $metricName The name of the metric to remove eg 'nb_visits'.
* If not supplied, all metrics within that category will be removed.
*/
public function remove($metricCategory, $metricName = false)
{
foreach ($this->metrics as $index => $metric) {
if ($metric->getCategoryId() === $metricCategory) {
if (!$metricName || $metric->getName() === $metricName) {
unset($this->metrics[$index]);
$this->metricsByNameCache = array();
}
}
}
}
/**
* @param string $metricName
* @return Metric|ArchivedMetric|null
*/
public function getMetric($metricName)
{
if (empty($this->metricsByNameCache)) {
// this method might be called quite often... eg when having heaps of goals... need to cache it
foreach ($this->metrics as $index => $metric) {
$this->metricsByNameCache[$metric->getName()] = $metric;
}
}
if (!empty($this->metricsByNameCache[$metricName])) {
return $this->metricsByNameCache[$metricName];
}
return null;
}
/**
* Get all metrics defined in the Piwik platform.
* @ignore
* @return static
*/
public static function get()
{
$cache = Cache::getTransientCache();
$cacheKey = CacheId::siteAware('MetricsList');
if ($cache->contains($cacheKey)) {
return $cache->fetch($cacheKey);
}
$list = new static;
/**
* Triggered to add new metrics that cannot be picked up automatically by the platform.
* This is useful if the plugin allows a user to create metrics dynamically. For example
* CustomDimensions or CustomVariables.
*
* **Example**
*
* public function addMetric(&$list)
* {
* $list->addMetric(new MyCustomMetric());
* }
*
* @param MetricsList $list An instance of the MetricsList. You can add metrics to the list this way.
*/
Piwik::postEvent('Metric.addMetrics', array($list));
$dimensions = Dimension::getAllDimensions();
foreach ($dimensions as $dimension) {
$factory = new DimensionMetricFactory($dimension);
$dimension->configureMetrics($list, $factory);
}
$computedFactory = new ComputedMetricFactory($list);
/**
* Triggered to add new metrics that cannot be picked up automatically by the platform.
* This is useful if the plugin allows a user to create metrics dynamically. For example
* CustomDimensions or CustomVariables.
*
* **Example**
*
* public function addMetric(&$list)
* {
* $list->addMetric(new MyCustomMetric());
* }
*
* @param MetricsList $list An instance of the MetricsList. You can add metrics to the list this way.
*/
Piwik::postEvent('Metric.addComputedMetrics', array($list, $computedFactory));
/**
* Triggered to filter metrics.
*
* **Example**
*
* public function removeMetrics(Piwik\Columns\MetricsList $list)
* {
* $list->remove($category='General_Visits'); // remove all metrics having this category
* }
*
* @param MetricsList $list An instance of the MetricsList. You can change the list of metrics this way.
*/
Piwik::postEvent('Metric.filterMetrics', array($list));
$availableMetrics = array();
foreach ($list->getMetrics() as $metric) {
$availableMetrics[] = $metric->getName();
}
foreach ($list->metrics as $index => $metric) {
if ($metric instanceof ProcessedMetric) {
$depMetrics = $metric->getDependentMetrics();
if (is_array($depMetrics)) {
foreach ($depMetrics as $depMetric) {
if (!in_array($depMetric, $availableMetrics, $strict = true)) {
unset($list->metrics[$index]); // not resolvable metric
}
}
}
}
}
$cache->save($cacheKey, $list);
return $list;
}
}

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