diff --git a/.hgignore b/.hgignore index 94d6c873..29a79b09 100644 --- a/.hgignore +++ b/.hgignore @@ -6,5 +6,5 @@ syntax: regexp ^seminarymedia/* ^seminaryuploads/* ^www/analytics/config/config.ini.php* -^www/analytics/temp/* +^www/analytics/tmp/* ^app/lib/phpqrcode/cache/* diff --git a/views/html/html.tpl b/views/html/html.tpl index 24a34e3a..d46631e0 100644 --- a/views/html/html.tpl +++ b/views/html/html.tpl @@ -128,6 +128,7 @@ + + diff --git a/www/analytics/CHANGELOG.md b/www/analytics/CHANGELOG.md new file mode 100644 index 00000000..f80c4a0b --- /dev/null +++ b/www/analytics/CHANGELOG.md @@ -0,0 +1,411 @@ +# Piwik Platform Changelog + +This is a changelog for Piwik platform developers. All changes for our HTTP API's, Plugins, Themes, etc will be listed here. + +## 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.piwik.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, '*.piwik.org/website1'])` all link that goes to the same domain `piwik.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 specifiy 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 overriden 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/piwik/searchengine-and-social-list](https://github.com/piwik/searchengine-and-social-list)). + * In [UI screenshot tests](https://developer.piwik.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://piwik.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://piwik.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](http://developer.piwik.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 3.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 `�` +* 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 explicitely 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/piwik/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/piwik/referrer-spam-blacklist](https://github.com/piwik/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/piwik/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/piwik/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.piwik.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/piwik/tracker-proxy](https://github.com/piwik/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.piwik.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.piwik.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/piwik/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/piwik/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.piwik.org/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/piwik/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.piwik.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` Let's 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://piwik.org/docs/user-id/) is set. +* New APIs have been added for [Content Tracking](http://piwik.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/piwik/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/piwik/piwik/issues/6140), [urlForDefaultAction()](http://developer.piwik.org/api-reference/Piwik/Plugin/Menu#urlfordefaultaction), [urlForAction()](http://developer.piwik.org/api-reference/Piwik/Plugin/Menu#urlforaction), [urlForModuleAction()](http://developer.piwik.org/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` Let's 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.piwik.org/api-reference/tracking-javascript). +* The [settings](http://developer.piwik.org/guides/piwik-configuration) API will receive the actual entered value and will no longer convert characters like `&` to `&`. 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/piwik/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/piwik/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.piwik.org/api-reference/Piwik/Plugin/Report) class instead to define new reports. There is an updated guide as well [Part1](http://developer.piwik.org/guides/getting-started-part-1) +* `WidgetsList.addWidgets`: use [Widgets](http://developer.piwik.org/api-reference/Piwik/Plugin/Widgets) class instead to define new widgets +* `Menu.Admin.addItems`, `Menu.Reporting.addItems`, `Menu.Top.addItems`: use [Menu](http://developer.piwik.org/api-reference/Piwik/Plugin/Menu) class instead +* `TaskScheduler.getScheduledTasks`: use [Tasks](http://developer.piwik.org/api-reference/Piwik/Plugin/Tasks) class instead to define new tasks +* `Tracker.recordEcommerceGoal`, `Tracker.recordStandardGoals`, `Tracker.newConversionInformation`: use [Conversion Dimension](http://developer.piwik.org/api-reference/Piwik/Plugin/Dimension/ConversionDimension) class instead +* `Tracker.existingVisitInformation`, `Tracker.newVisitorInformation`, `Tracker.getVisitFieldsToPersist`: use [Visit Dimension](http://developer.piwik.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.piwik.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.piwik.org/api-reference/classes). + +### New APIs +* [Report](http://developer.piwik.org/api-reference/Piwik/Plugin/Report) to add a new report +* [Action Dimension](http://developer.piwik.org/api-reference/Piwik/Plugin/Dimension/ActionDimension) to add a dimension that tracks action related information +* [Visit Dimension](http://developer.piwik.org/api-reference/Piwik/Plugin/Dimension/VisitDimension) to add a dimension that tracks visit related information +* [Conversion Dimension](http://developer.piwik.org/api-reference/Piwik/Plugin/Dimension/ConversionDimension) to add a dimension that tracks conversion related information +* [Dimension](http://developer.piwik.org/api-reference/Piwik/Columns/Dimension) to add a basic non tracking dimension that can be used in `Reports` +* [Widgets](http://developer.piwik.org/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.piwik.org/api-reference/Piwik/Menu/MenuAdmin) to add or modify admin menu items. + * [MenuReporting](http://developer.piwik.org/api-reference/Piwik/Menu/MenuReporting) to add or modify reporting menu items + * [MenuUser](http://developer.piwik.org/api-reference/Piwik/Menu/MenuUser) to add or modify user menu items +* [Tasks](http://developer.piwik.org/api-reference/Piwik/Plugin/Tasks) to add scheduled tasks + +### New commands +* `generate:theme` Let's you easily generate a new theme and customize colors, see the [Theming guide](http://developer.piwik.org/guides/theming) +* `generate:update` Let's you generate an update file +* `generate:report` Let's you generate a report +* `generate:dimension` Let's you enhance the tracking by adding new dimensions +* `generate:menu` Let's you generate a menu class to add or modify menu items +* `generate:widgets` Let's you generate a widgets class to add or modify widgets +* `generate:tasks` Let's you generate a tasks class to add or modify tasks +* `development:enable` Let's 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` Let's you disable the development mode + + + +Find the general Piwik Changelogs for each release at [piwik.org/changelog](http://piwik.org/changelog/) + diff --git a/www/analytics/CONTRIBUTING.md b/www/analytics/CONTRIBUTING.md new file mode 100644 index 00000000..f24e51b4 --- /dev/null +++ b/www/analytics/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# 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! + +http://developer.piwik.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](http://developer.piwik.org/guides/core-team-workflow#submitting-a-bug-report) or [feature request](http://developer.piwik.org/guides/core-team-workflow#submitting-a-feature-request). + +## How to suggest improvements to translations? + +You can help improve translations in Piwik, please read [contribute to translations](https://github.com/piwik/piwik/blob/master/lang/README.md). diff --git a/www/analytics/LEGALNOTICE b/www/analytics/LEGALNOTICE index e95df846..1ab89f00 100644 --- a/www/analytics/LEGALNOTICE +++ b/www/analytics/LEGALNOTICE @@ -1,10 +1,10 @@ COPYRIGHT - Piwik - Open Source Web Analytics + Piwik - free/libre analytics platform The software package is: - Copyright (C) 2013 Matthieu Aubry + Copyright (C) 2014 Matthieu Aubry Individual contributions, components, and libraries are copyright of their respective authors. @@ -40,7 +40,7 @@ CREDITS For detailed contribution history, refer to the source, tickets, patches, and Git revision history, available at - http://dev.piwik.org/trac/ + https://github.com/piwik/piwik/issues https://github.com/piwik/piwik @@ -65,9 +65,17 @@ SEPARATELY LICENSED COMPONENTS AND LIBRARIES Link: https://github.com/piwik/piwik/blob/master/libs/PiwikTracker/ License: New BSD - Name: UserAgentParser - Link: https://github.com/piwik/piwik/blob/master/libs/UserAgentParser/ - License: New BSD + Name: DeviceDetector + Link: https://github.com/piwik/device-detector + License: LGPL + + Name: Piwik/Decompress + Link: https://github.com/piwik/component-decompress + License: LGPL v3.0 + + Name: Piwik/Network + Link: https://github.com/piwik/component-network + License: LGPL v3.0 THIRD-PARTY COMPONENTS AND LIBRARIES @@ -77,40 +85,40 @@ THIRD-PARTY COMPONENTS AND LIBRARIES Name: jqPlot Link: http://www.jqplot.com/ - License: Dual-licensed: MIT or GPL v2 - + License: Dual-licensed: MIT (Expat) or GPL v2 + Name: jQuery Link: http://jquery.com/ - License: Dual-licensed: MIT or GPL + 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, New BSD, or GPL [v2] + - includes Sizzle.js - multi-licensed: MIT (Expat), New BSD, or GPL [v2] Name: jQuery UI Link: http://jqueryui.com/ - License: Dual-licensed: MIT or GPL + 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: http://tkyk.github.com/jquery-history-plugin/ - License: MIT + License: MIT (Expat) Name: jquery.scrollTo Link: http://plugins.jquery.com/project/ScrollTo - License: Dual licensed: MIT or GPL + License: Dual licensed: MIT (Expat) or GPL Name: jquery Tooltip Link: http://bassistance.de/jquery-plugins/jquery-plugin-tooltip/ - License: Dual licensed: MIT or GPL + License: Dual licensed: MIT (Expat) or GPL Name: jquery placeholder Link: http://mths.be/placeholder - License: Dual licensed: MIT or GPL + License: Dual licensed: MIT (Expat) or GPL Name: jquery smartbanner Link: https://github.com/jasny/jquery.smartbanner - License: Dual licensed: MIT + License: Dual licensed: MIT (Expat) Name: json2.js Link: http://json.org/ @@ -187,8 +195,8 @@ THIRD-PARTY COMPONENTS AND LIBRARIES Name: Zend Framework Link: http://www.zendframework.com/ License: New BSD - - Name: pChart 2.1.3 + + Name: pChart 2.1.4 Link: http://www.pchart.net License: GPL v3 @@ -206,15 +214,27 @@ THIRD-PARTY COMPONENTS AND LIBRARIES Name: Raphaël - JavaScript Vector Library Link: http://raphaeljs.com/ - License: MIT + License: MIT (Expat) Name: lessphp Link: http://leafo.net/lessphp - License: GPL3/MIT + License: GPL3, MIT (Expat) Name: Symfony Console Component Link: https://github.com/symfony/Console - License: MIT + 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 @@ -235,10 +255,6 @@ THIRD-PARTY CONTENT Notes: - used in ImageGraph plugin - Name: plugins/CorePluginsAdmin/images/themes.png - Link: https://www.iconfinder.com/icons/17022/colors_draw_paint_icon - License: Free for commercial use - 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) @@ -247,10 +263,6 @@ THIRD-PARTY CONTENT Link: https://www.iconfinder.com/icons/216429/thumbs_up_icon License: Creative Commons (Attribution-Share Alike 3.0 Unported) - Name: plugins/CorePluginsAdmin/images/plugins.png - Link: http://findicons.com/icon/94051/tools_wizard?id=396912 - License: GNU/GPL - Name: plugins/Insights/images/idea.png Link: https://www.iconfinder.com/icons/6074/brainstorm_bulb_idea_jabber_light_icon License: GPL @@ -259,7 +271,6 @@ THIRD-PARTY CONTENT Notes: - the "New BSD" license refers to either the "Modified BSD" and "Simplified BSD" licenses (2- or 3-clause), which are GPL compatible. -- the "MIT" license is also referred to as the "X11" license - icons for browsers, operating systems, browser plugins, search engines, and and flags of countries are nominative use of third-party trademarks when referring to the corresponding product or entity diff --git a/www/analytics/PRIVACY.md b/www/analytics/PRIVACY.md new file mode 100644 index 00000000..ecccfe8f --- /dev/null +++ b/www/analytics/PRIVACY.md @@ -0,0 +1,60 @@ +# Privacy +This is a summary of all of the components within Piwik 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 Piwik +In this section we document how to protect the privacy of visitors who are tracked by your Piwik analytics service. + +### Anonymise visitor IP addresses +By default, Piwik 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 Piwik server. You can anonymize IP addresses to ensure visitors cannot +be tracked this way: [How to anonymise IP addresses.](http://piwik.org/docs/privacy/#step-1-automatically-anonymize-visitor-ips) + +### Delete old visitors logs +By default, Piwik stores tracked data forever. To better respect the privacy of your users, it is recommended to regularly +purge old data. You can configure Piwik to automatically delete log data older than a specified number of months: +[How to delete old visitors log data.](http://piwik.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 Piwik. +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 Piwik in the future: +[How to include a tracking opt-out iframe.](http://piwik.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 Piwik to respect it. By default, +Piwik is configured to ignore visitors that have enabled it: +[How to check if your Piwik respects DoNotTrack.] (http://piwik.org/docs/privacy/#step-4-respect-donottrack-preference) + +### Disable tracking cookies +A cookie is a collection of information that a website stores on a visitor’s computer and accesses each time the visitor +returns. By default, Piwik 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.](http://piwik.org/faq/general/faq_157/) + +### Keep your visitors details private +Any user that has at least `view` access (the default access level) to Piwik can view detailed information for all users +tracked in Piwik (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 Piwik 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 Piwik admins and website owners +In this section we document how a Piwik administrator can better protect their own privacy. + +### Keep your Piwik server URL private +By default, the Piwik Javascript code on all tracked websites contains the Piwik server URL. In some cases you might +want to hide this Piwik URL completely while still tracking all websites in your Piwik instance. To hide your Piwik +server's URL, you can modify the Javascript Tracking code and point it to a proxy piwik.php script instead of your actual +Piwik server: [How to keep Piwik server URL private.](http://piwik.org/faq/how-to/faq_132/) + +### Automatic update check +From time to time, Piwik uses `api.piwik.org` to check if the current version of Piwik is the latest version of Piwik. +If an update is available, a notification is displayed allowing you to upgrade Piwik. To disable the update check, +and stop your instance from sending HTTP requests to `api.piwik.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 Piwik](http://piwik.org/privacy/). diff --git a/www/analytics/README.md b/www/analytics/README.md index 7894effd..0b01769e 100644 --- a/www/analytics/README.md +++ b/www/analytics/README.md @@ -1,14 +1,27 @@ -# Piwik - piwik.org +# Piwik - piwik.org + +[![Latest Stable Version](https://poser.pugx.org/piwik/piwik/v/stable)](https://packagist.org/packages/piwik/piwik) +[![Latest Unstable Version](https://poser.pugx.org/piwik/piwik/v/unstable)](https://packagist.org/packages/piwik/piwik) +[![Total Downloads](https://poser.pugx.org/piwik/piwik/downloads)](https://packagist.org/packages/piwik/piwik) +[![License](https://poser.pugx.org/piwik/piwik/license)](https://packagist.org/packages/piwik/piwik) + +## Code Status + +[![Build Status](https://travis-ci.org/piwik/piwik.svg?branch=master)](https://travis-ci.org/piwik/piwik) +[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/piwik/piwik.svg)](https://scrutinizer-ci.com/g/piwik/piwik?branch=master) +[![Code Coverage](https://scrutinizer-ci.com/g/piwik/piwik/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/piwik/piwik/?branch=master) +[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/piwik/piwik.svg)](http://isitmaintained.com/project/piwik/piwik "Average time to resolve an issue") +[![Percentage of issues still open](http://isitmaintained.com/badge/open/piwik/piwik.svg)](http://isitmaintained.com/project/piwik/piwik "Percentage of issues still open") ## Description -Piwik is the leading Free/Libre open source Web Analytics platform. +Piwik is the leading Free/Libre open analytics platform. Piwik 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. -Piwik aims to be a Free software alternative to Google Analytics, and is already used on more than 1,000,000 websites. +Piwik aims to be a Free software alternative to Google Analytics, and is already used on more than 1,000,000 websites. Privacy is built-in! ## Mission Statement @@ -21,17 +34,27 @@ Or in short: Piwik is released under the GPL v3 (or later) license, see [misc/gpl-3.0.txt](misc/gpl-3.0.txt) +## We’re seeking a talented Software Engineer + +Are you looking for a new challenge? We are currently seeking a software engineer or software developer who is passionate about data processing, security, privacy, the open source and free/libre philosophy and usable interface design. + +[View Job Description](https://piwik.org/blog/2015/01/piwik-expanding-seeking-talented-software-engineer-new-zealand-poland/) - [Apply online](http://piwik.org/jobs/) + +This is for a full time position to work on the open source Piwik platform, either remotely or we can help the right candidate relocate to beautiful New Zealand (Wellington) or Poland (Wroclaw). + +We are grateful if you can share the job description with your friends and wider network! + ## Requirements - * PHP 5.3.2 or greater + * PHP 5.3.3 or greater * MySQL 4.1 or greater, and either MySQLi or PDO library must be enabled * Piwik is OS / server independent See http://piwik.org/docs/requirements/ -## Install +## Install - * Upload piwik to your webserver + * Upload piwik to your webserver * Point your browser to the directory * Follow the steps * Add the given javascript code to your pages @@ -43,7 +66,7 @@ If you do not have a server, consider our Piwik Hosting partner: http://piwik.or ## Changelog -For the list of all tickets closed in the current and past releases, see http://piwik.org/changelog/ +For the list of all tickets closed in the current and past releases, see http://piwik.org/changelog/. For the list of technical changes in the Piwik platform, see [http://developer.piwik.org/changelog](http://developer.piwik.org/changelog). ## Participate! @@ -64,7 +87,7 @@ About us: http://piwik.org/the-piwik-team/ What makes Piwik unique from the competition: - * Real time web analytics reports: in Piwik, reports are by default generated in real time. + * Real time web analytics reports: in Piwik, 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 Piwik is installed on your server, the data is stored in your own database and you can get all the statistics @@ -74,19 +97,12 @@ What makes Piwik unique from the competition: * Modern, easy to use User Interface: you can fully customize your dashboard, drag and drop widgets and more. - * Piwik features are built inside plugins: you can add new features and remove the ones you don’t need. + * Piwik features are built inside plugins: you can add new features and remove the ones you don’t need. You can build your own web analytics plugins or hire a consultant to have your custom feature built in Piwik * Vibrant international Open community of more than 200,000 active users (tracking even more websites!) - * Advanced Web Analytics capabilities such as Ecommerce Tracking, Goal tracking, Campaign tracking, + * Advanced Web Analytics capabilities such as Ecommerce Tracking, Goal tracking, Campaign tracking, Custom Variables, Email Reports, Custom Segment Editor, Geo Location, Real time maps, and more! Documentation and more info on http://piwik.org - -## Code Status -The Piwik project uses an ever-expanding comprehensive set of thousands of unit tests and dozens of integration [tests](https://github.com/piwik/piwik/tree/master/tests), - running on the hosted distributed continuous integration platform Travis-CI. - -Build status (master branch) [![Build Status](https://travis-ci.org/piwik/piwik.png?branch=master)](https://travis-ci.org/piwik/piwik) - Screenshot tests Build [![Build Status](https://travis-ci.org/piwik/piwik-ui-tests.png?branch=master)](https://travis-ci.org/piwik/piwik-ui-tests) - diff --git a/www/analytics/SECURITY.md b/www/analytics/SECURITY.md new file mode 100644 index 00000000..7834108a --- /dev/null +++ b/www/analytics/SECURITY.md @@ -0,0 +1,21 @@ +# Reporting Security Issues + +## Security Bug Bounty Program + +The Piwik Security Bug Bounty Program is designed to encourage security research in Piwik 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 Piwik please read [our security notes](http://piwik.org/security/) regarding responsible disclosures. + +[Email your Report Vulnerability to the Piwik Security team](mailto:security@piwik.org?subject=Reporting%20Vulnerability%20in%20Piwik) + + +## Improve your Piwik Server Security + +[Secure Piwik server](http://piwik.org/docs/how-to-secure-piwik/): follow these steps to keep your Piwik data safe. + +## Security announcements + +Please subscribe to [the Changelog](http://piwik.org/changelog/) ([rss feed](http://piwik.org/changelog/feed/)) to be notified of new releases (including security releases). diff --git a/www/analytics/bower.json b/www/analytics/bower.json new file mode 100644 index 00000000..49252d93 --- /dev/null +++ b/www/analytics/bower.json @@ -0,0 +1,41 @@ +{ + "name": "Piwik", + "main": "piwik.js", + "homepage": "http://piwik.org", + "authors": [ + "Piwik.org " + ], + "description": "the leading free/libre analytics platform", + "private": true, + "keywords": [ + "piwik", + "web", + "analytics" + ], + "dependencies": { + "jquery-ui": "1.10.4", + "jquery": "~1.11.0", + "angular": "~1.2.0", + "angular-sanitize": "~1.2.0", + "angular-animate": "~1.2.0", + "angular-cookies": "~1.2.0", + "angular-mocks": "~1.2.0", + "ngDialog": "~0.2.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" + ] +} diff --git a/www/analytics/composer.json b/www/analytics/composer.json index 8c3fc620..3c6521b8 100644 --- a/www/analytics/composer.json +++ b/www/analytics/composer.json @@ -1,7 +1,7 @@ { "name": "piwik/piwik", "type": "application", - "description": "Open Source Real Time Web Analytics Platform", + "description": "the leading free/libre analytics platform", "keywords": ["piwik","web","analytics"], "homepage": "http://piwik.org", "license": "GPL-3.0+", @@ -14,17 +14,98 @@ ], "support": { "forum": "http://forum.piwik.org/", - "issues": "http://dev.piwik.org/trac/roadmap", - "wiki": "http://dev.piwik.org/", + "issues": "https://github.com/piwik/piwik/issues", + "wiki": "https://github.com/piwik/piwik/wiki", "source": "https://github.com/piwik/piwik" }, + "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/" + } + }, "require": { - "php": ">=5.3.2", - "twig/twig": "1.*", - "leafo/lessphp": "~0.3", - "symfony/console": ">=v2.3.5", - "tedivm/jshrink": "v0.5.1", - "mustangostang/spyc": "0.5.*", - "piwik/device-detector": "*" + "php": ">=5.3.3", + "twig/twig": "~1.0", + "leafo/lessphp": "~0.5.0", + "symfony/console": "~2.6.0", + "tedivm/jshrink": "~0.5.1", + "mustangostang/spyc": "~0.5.0", + "piwik/device-detector": "~3.0", + "piwik/decompress": "~1.0", + "piwik/network": "~0.1.0", + "piwik/cache": "~0.2.5", + "piwik/ini": "^1.0.6", + "php-di/php-di": "5.0.0-beta1", + "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", + "piwik/referrer-spam-blacklist": "~1.0", + "piwik/searchengine-and-social-list": "~1.0", + "tecnickcom/tcpdf": "~6.0", + "piwik/piwik-php-tracker": "^1.0" + }, + "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" + } + } + } + ], + "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" + ] } } diff --git a/www/analytics/composer.lock b/www/analytics/composer.lock index c31eafe5..32d55198 100644 --- a/www/analytics/composer.lock +++ b/www/analytics/composer.lock @@ -1,28 +1,249 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" ], - "hash": "69246584e6b57bfbc8d39799cd3b9213", + "hash": "de61be52972a0fe8fe751306c271f4b8", + "content-hash": "68130b067cdceef8346b47d858b763a3", "packages": [ { - "name": "leafo/lessphp", - "version": "v0.4.0", + "name": "container-interop/container-interop", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/leafo/lessphp.git", - "reference": "51f3f06f0fe78a722dabfd14578444bdd078d9de" + "url": "https://github.com/container-interop/container-interop.git", + "reference": "fc08354828f8fd3245f77a66b9e23a6bca48297e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/leafo/lessphp/zipball/51f3f06f0fe78a722dabfd14578444bdd078d9de", - "reference": "51f3f06f0fe78a722dabfd14578444bdd078d9de", + "url": "https://api.github.com/repos/container-interop/container-interop/zipball/fc08354828f8fd3245f77a66b9e23a6bca48297e", + "reference": "fc08354828f8fd3245f77a66b9e23a6bca48297e", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/Interop/Container/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", + "time": "2014-12-30 15:22:37" + }, + { + "name": "doctrine/annotations", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/f25c8aab83e0c3e976fd7d19875f198ccf2f7535", + "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "php": ">=5.3.2" + }, + "require-dev": { + "doctrine/cache": "1.*", + "phpunit/phpunit": "4.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Annotations\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "time": "2015-08-31 12:32:49" + }, + { + "name": "doctrine/cache", + "version": "v1.4.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "8c434000f420ade76a07c64cbe08ca47e5c101ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/8c434000f420ade76a07c64cbe08ca47e5c101ca", + "reference": "8c434000f420ade76a07c64cbe08ca47e5c101ca", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "phpunit/phpunit": ">=3.7", + "predis/predis": "~1.0", + "satooshi/php-coveralls": "~0.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Cache\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Caching library offering an object-oriented API for many cache backends", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "cache", + "caching" + ], + "time": "2015-08-31 12:36:41" + }, + { + "name": "doctrine/lexer", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", + "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Lexer\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "lexer", + "parser" + ], + "time": "2014-09-09 13:34:57" + }, + { + "name": "leafo/lessphp", + "version": "v0.5.0", + "source": { + "type": "git", + "url": "https://github.com/leafo/lessphp.git", + "reference": "0f5a7f5545d2bcf4e9fad9a228c8ad89cc9aa283" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/leafo/lessphp/zipball/0f5a7f5545d2bcf4e9fad9a228c8ad89cc9aa283", + "reference": "0f5a7f5545d2bcf4e9fad9a228c8ad89cc9aa283", "shasum": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.3-dev" + "dev-master": "0.4.x-dev" } }, "autoload": { @@ -44,7 +265,119 @@ ], "description": "lessphp is a compiler for LESS written in PHP.", "homepage": "http://leafo.net/lessphp/", - "time": "2013-08-09 17:09:19" + "time": "2014-11-24 18:39:20" + }, + { + "name": "mnapoli/phpdocreader", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/mnapoli/PhpDocReader.git", + "reference": "8a6e123fd1ce54f7fcbd71747b3bf04e465da229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mnapoli/PhpDocReader/zipball/8a6e123fd1ce54f7fcbd71747b3bf04e465da229", + "reference": "8a6e123fd1ce54f7fcbd71747b3bf04e465da229", + "shasum": "" + }, + "require": { + "doctrine/annotations": "1.*", + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "PhpDocReader": "src/", + "UnitTest": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "keywords": [ + "phpdoc", + "reflection" + ], + "time": "2014-08-21 08:20:45" + }, + { + "name": "monolog/monolog", + "version": "1.17.2", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "bee7f0dc9c3e0b69a6039697533dca1e845c8c24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bee7f0dc9c3e0b69a6039697533dca1e845c8c24", + "reference": "bee7f0dc9c3e0b69a6039697533dca1e845c8c24", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "jakub-onderka/php-parallel-lint": "0.9", + "php-console/php-console": "^3.1.3", + "phpunit/phpunit": "~4.5", + "phpunit/phpunit-mock-objects": "2.3.0", + "raven/raven": "^0.13", + "ruflin/elastica": ">=0.90 <3.0", + "swiftmailer/swiftmailer": "~5.3", + "videlalvaro/php-amqplib": "~2.4" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "raven/raven": "Allow sending log messages to a Sentry server", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "videlalvaro/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.16.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "time": "2015-10-14 12:51:02" }, { "name": "mustangostang/spyc", @@ -94,32 +427,347 @@ "time": "2013-02-21 10:52:01" }, { - "name": "piwik/device-detector", - "version": "1.0", + "name": "pear/archive_tar", + "version": "1.4.1", "source": { "type": "git", - "url": "https://github.com/piwik/device-detector.git", - "reference": "ea7c5d8b76def0d8345a4eba59c5f98ec0109de6" + "url": "https://github.com/pear/Archive_Tar.git", + "reference": "fc2937c0e5a2a1c62a378d16394893172f970064" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/piwik/device-detector/zipball/ea7c5d8b76def0d8345a4eba59c5f98ec0109de6", - "reference": "ea7c5d8b76def0d8345a4eba59c5f98ec0109de6", + "url": "https://api.github.com/repos/pear/Archive_Tar/zipball/fc2937c0e5a2a1c62a378d16394893172f970064", + "reference": "fc2937c0e5a2a1c62a378d16394893172f970064", "shasum": "" }, "require": { - "mustangostang/spyc": "*", - "php": ">=5.3.1" + "pear/pear-core-minimal": "^1.10.0alpha2", + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-bz2": "bz2 compression support.", + "ext-xz": "lzma2 compression support.", + "ext-zlib": "Gzip compression support." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "Archive_Tar": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "./" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Vincent Blavet", + "email": "vincent@phpconcept.net" + }, + { + "name": "Greg Beaver", + "email": "greg@chiaraquartet.net" + }, + { + "name": "Michiel Rook", + "email": "mrook@php.net" + } + ], + "description": "Tar file management class", + "homepage": "https://github.com/pear/Archive_Tar", + "keywords": [ + "archive", + "tar" + ], + "time": "2015-08-05 12:31:03" + }, + { + "name": "pear/console_getopt", + "version": "v1.4.1", + "source": { + "type": "git", + "url": "https://github.com/pear/Console_Getopt.git", + "reference": "82f05cd1aa3edf34e19aa7c8ca312ce13a6a577f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/Console_Getopt/zipball/82f05cd1aa3edf34e19aa7c8ca312ce13a6a577f", + "reference": "82f05cd1aa3edf34e19aa7c8ca312ce13a6a577f", + "shasum": "" }, "type": "library", "autoload": { + "psr-0": { + "Console": "./" + } + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "./" + ], + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Greg Beaver", + "email": "cellog@php.net", + "role": "Helper" + }, + { + "name": "Andrei Zmievski", + "email": "andrei@php.net", + "role": "Lead" + }, + { + "name": "Stig Bakken", + "email": "stig@php.net", + "role": "Developer" + } + ], + "description": "More info available on: http://pear.php.net/package/Console_Getopt", + "time": "2015-07-20 20:28:12" + }, + { + "name": "pear/pear-core-minimal", + "version": "v1.10.1", + "source": { + "type": "git", + "url": "https://github.com/pear/pear-core-minimal.git", + "reference": "cae0f1ce0cb5bddb611b0a652d322905a65a5896" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/pear-core-minimal/zipball/cae0f1ce0cb5bddb611b0a652d322905a65a5896", + "reference": "cae0f1ce0cb5bddb611b0a652d322905a65a5896", + "shasum": "" + }, + "require": { + "pear/console_getopt": "~1.3", + "pear/pear_exception": "~1.0" + }, + "replace": { + "rsky/pear-core-min": "self.version" + }, + "type": "library", + "autoload": { + "psr-0": { + "": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "src/" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@php.net", + "role": "Lead" + } + ], + "description": "Minimal set of PEAR core files to be used as composer dependency", + "time": "2015-10-17 11:41:19" + }, + { + "name": "pear/pear_exception", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/pear/PEAR_Exception.git", + "reference": "8c18719fdae000b690e3912be401c76e406dd13b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/PEAR_Exception/zipball/8c18719fdae000b690e3912be401c76e406dd13b", + "reference": "8c18719fdae000b690e3912be401c76e406dd13b", + "shasum": "" + }, + "require": { + "php": ">=4.4.0" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "type": "class", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "PEAR": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "." + ], + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Helgi Thormar", + "email": "dufuz@php.net" + }, + { + "name": "Greg Beaver", + "email": "cellog@php.net" + } + ], + "description": "The PEAR Exception base class.", + "homepage": "https://github.com/pear/PEAR_Exception", + "keywords": [ + "exception" + ], + "time": "2015-02-10 20:07:52" + }, + { + "name": "php-di/invoker", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "9949fff87fcf14e8f2ccfbe36dac1e5921944c48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/9949fff87fcf14e8f2ccfbe36dac1e5921944c48", + "reference": "9949fff87fcf14e8f2ccfbe36dac1e5921944c48", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "~1.1" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "phpunit/phpunit": "~4.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Invoker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", + "keywords": [ + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" + ], + "time": "2015-10-22 19:49:23" + }, + { + "name": "php-di/php-di", + "version": "5.0.0-beta1", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "2325afb15d74728f52cb9721c9e184829f8f343a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/2325afb15d74728f52cb9721c9e184829f8f343a", + "reference": "2325afb15d74728f52cb9721c9e184829f8f343a", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "~1.0", + "doctrine/annotations": "~1.2", + "doctrine/cache": "~1.0", + "mnapoli/phpdocreader": "~1.3", + "php": ">=5.3.3", + "php-di/invoker": "~1.0" + }, + "require-dev": { + "mnapoli/phpunit-easymock": "~0.1.4", + "ocramius/proxy-manager": "~0.5", + "phpunit/phpunit": "~4.5" + }, + "suggest": { + "ocramius/proxy-manager": "Install it if you want to use lazy injection (version ~0.5)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "DI\\": "src/DI/" + }, "files": [ - "DeviceDetector.php" + "src/DI/functions.php" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "GPL-3.0+" + "MIT" + ], + "description": "PHP-DI is a Container that makes Dependency Injection as practical as possible in PHP", + "homepage": "http://mnapoli.github.com/PHP-DI/", + "keywords": [ + "container", + "dependency injection", + "di" + ], + "time": "2015-04-25 02:05:04" + }, + { + "name": "piwik/cache", + "version": "0.2.6", + "source": { + "type": "git", + "url": "https://github.com/piwik/component-cache.git", + "reference": "b8f2d18069c77726862f67d0199896d13073a831" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/piwik/component-cache/zipball/b8f2d18069c77726862f67d0199896d13073a831", + "reference": "b8f2d18069c77726862f67d0199896d13073a831", + "shasum": "" + }, + "require": { + "doctrine/cache": "~1.4", + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Piwik\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" ], "authors": [ { @@ -128,43 +776,322 @@ "homepage": "http://piwik.org/the-piwik-team/" } ], - "description": "The Universal Device Detection library, that parses User Agents and detects devices (desktop, tablet, mobile, tv, cars, console, etc.), and detects browsers, operating systems, devices, brands and models.", + "description": "PHP caching library based on Doctrine cache", + "keywords": [ + "array", + "cache", + "file", + "redis" + ], + "time": "2015-09-29 16:50:32" + }, + { + "name": "piwik/decompress", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/piwik/component-decompress.git", + "reference": "deca40d71d29d6140aad39db007aea82676b7631" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/piwik/component-decompress/zipball/deca40d71d29d6140aad39db007aea82676b7631", + "reference": "deca40d71d29d6140aad39db007aea82676b7631", + "shasum": "" + }, + "require": { + "pear/archive_tar": "~1.3,>=1.3.15", + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Piwik\\Decompress\\": "src/" + }, + "classmap": [ + "libs/PclZip" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "time": "2015-09-22 10:58:19" + }, + { + "name": "piwik/device-detector", + "version": "3.5.1", + "source": { + "type": "git", + "url": "https://github.com/piwik/device-detector.git", + "reference": "29830f9bd67c8300e37828db0688161dd6f5f7a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/piwik/device-detector/zipball/29830f9bd67c8300e37828db0688161dd6f5f7a5", + "reference": "29830f9bd67c8300e37828db0688161dd6f5f7a5", + "shasum": "" + }, + "require": { + "mustangostang/spyc": "*", + "php": ">=5.3.2" + }, + "require-dev": { + "fabpot/php-cs-fixer": "~1.7", + "phpunit/phpunit": "4.1.*" + }, + "suggest": { + "doctrine/cache": "Can directly be used for caching purpose" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeviceDetector\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0+" + ], + "authors": [ + { + "name": "The Piwik Team", + "email": "hello@piwik.org", + "homepage": "http://piwik.org/the-piwik-team/" + } + ], + "description": "The Universal Device Detection library, that parses User Agents and detects devices (desktop, tablet, mobile, tv, cars, console, etc.), clients (browsers, media players, mobile apps, feed readers, libraries, etc), operating systems, devices, brands and models.", "homepage": "http://piwik.org", "keywords": [ "devicedetection", "parser", "useragent" ], - "time": "2014-04-03 08:59:48" + "time": "2016-01-21 22:26:37" }, { - "name": "symfony/console", - "version": "v2.4.3", - "target-dir": "Symfony/Component/Console", + "name": "piwik/ini", + "version": "1.0.6", "source": { "type": "git", - "url": "https://github.com/symfony/Console.git", - "reference": "ef20f1f58d7f693ee888353962bd2db336e3bbcb" + "url": "https://github.com/piwik/component-ini.git", + "reference": "bd2711ba4d5e20e4ca09b6829dc2831576b59dc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/ef20f1f58d7f693ee888353962bd2db336e3bbcb", - "reference": "ef20f1f58d7f693ee888353962bd2db336e3bbcb", + "url": "https://api.github.com/repos/piwik/component-ini/zipball/bd2711ba4d5e20e4ca09b6829dc2831576b59dc3", + "reference": "bd2711ba4d5e20e4ca09b6829dc2831576b59dc3", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "symfony/event-dispatcher": "~2.1" + "athletic/athletic": "0.1.*", + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Piwik\\Ini\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "time": "2016-01-14 21:13:33" + }, + { + "name": "piwik/network", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/piwik/component-network.git", + "reference": "9037fa29509f86767e02ba58a57d4deb1d01a844" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/piwik/component-network/zipball/9037fa29509f86767e02ba58a57d4deb1d01a844", + "reference": "9037fa29509f86767e02ba58a57d4deb1d01a844", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Piwik\\Network\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "time": "2014-10-23 03:30:23" + }, + { + "name": "piwik/piwik-php-tracker", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/piwik/piwik-php-tracker.git", + "reference": "ac3e26bb3e2c8a428ccbf6ca663c2ef37fa47a5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/piwik/piwik-php-tracker/zipball/ac3e26bb3e2c8a428ccbf6ca663c2ef37fa47a5e", + "reference": "ac3e26bb3e2c8a428ccbf6ca663c2ef37fa47a5e", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "." + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "The Piwik Team", + "email": "hello@piwik.org", + "homepage": "http://piwik.org/the-piwik-team/" + } + ], + "description": "PHP Client for Piwik Analytics Tracking API", + "homepage": "http://piwik.org", + "keywords": [ + "analytics", + "piwik", + "tracker" + ], + "time": "2015-11-11 02:55:37" + }, + { + "name": "piwik/referrer-spam-blacklist", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/piwik/referrer-spam-blacklist.git", + "reference": "85db74cfc7249cb34ff59eba22edeb6704fd69b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/piwik/referrer-spam-blacklist/zipball/85db74cfc7249cb34ff59eba22edeb6704fd69b8", + "reference": "85db74cfc7249cb34ff59eba22edeb6704fd69b8", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Public Domain" + ], + "description": "Community-contributed list of referrer spammers", + "time": "2016-01-05 17:31:58" + }, + { + "name": "piwik/searchengine-and-social-list", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/piwik/searchengine-and-social-list.git", + "reference": "5b6763e77dadf24e579f03a7a0e79f1827b5db8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/piwik/searchengine-and-social-list/zipball/5b6763e77dadf24e579f03a7a0e79f1827b5db8a", + "reference": "5b6763e77dadf24e579f03a7a0e79f1827b5db8a", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Public Domain" + ], + "description": "Search engine and social network definitions used by Piwik", + "time": "2015-11-16 22:24:23" + }, + { + "name": "psr/log", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-0": { + "Psr\\Log\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2012-12-21 11:40:51" + }, + { + "name": "symfony/console", + "version": "v2.6.11", + "target-dir": "Symfony/Component/Console", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0e5e18ae09d3f5c06367759be940e9ed3f568359" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0e5e18ae09d3f5c06367759be940e9ed3f568359", + "reference": "0e5e18ae09d3f5c06367759be940e9ed3f568359", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.1", + "symfony/phpunit-bridge": "~2.7", + "symfony/process": "~2.1" }, "suggest": { - "symfony/event-dispatcher": "" + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/process": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.6-dev" } }, "autoload": { @@ -179,31 +1106,210 @@ "authors": [ { "name": "Fabien Potencier", - "email": "fabien@symfony.com", - "homepage": "http://fabien.potencier.org", - "role": "Lead Developer" + "email": "fabien@symfony.com" }, { "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" + "homepage": "https://symfony.com/contributors" } ], "description": "Symfony Console Component", - "homepage": "http://symfony.com", - "time": "2014-03-01 17:35:04" + "homepage": "https://symfony.com", + "time": "2015-07-26 09:08:40" }, { - "name": "tedivm/jshrink", - "version": "v0.5.1", + "name": "symfony/event-dispatcher", + "version": "v2.6.11", + "target-dir": "Symfony/Component/EventDispatcher", "source": { "type": "git", - "url": "https://github.com/tedivm/JShrink.git", - "reference": "2d3f1a7d336ad54bdf2180732b806c768a791cbf" + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "672593bc4b0043a0acf91903bb75a1c82d8f2e02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tedivm/JShrink/zipball/2d3f1a7d336ad54bdf2180732b806c768a791cbf", - "reference": "2d3f1a7d336ad54bdf2180732b806c768a791cbf", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/672593bc4b0043a0acf91903bb75a1c82d8f2e02", + "reference": "672593bc4b0043a0acf91903bb75a1c82d8f2e02", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.0,>=2.0.5", + "symfony/dependency-injection": "~2.6", + "symfony/expression-language": "~2.6", + "symfony/phpunit-bridge": "~2.7", + "symfony/stopwatch": "~2.3" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2015-05-02 15:18:45" + }, + { + "name": "symfony/monolog-bridge", + "version": "v2.6.11", + "target-dir": "Symfony/Bridge/Monolog", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "ba66eeabaa004e3ab70764cab59b056b182aa535" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/ba66eeabaa004e3ab70764cab59b056b182aa535", + "reference": "ba66eeabaa004e3ab70764cab59b056b182aa535", + "shasum": "" + }, + "require": { + "monolog/monolog": "~1.11", + "php": ">=5.3.3" + }, + "require-dev": { + "symfony/console": "~2.4", + "symfony/event-dispatcher": "~2.2", + "symfony/http-kernel": "~2.4", + "symfony/phpunit-bridge": "~2.7" + }, + "suggest": { + "symfony/console": "For the possibility to show log messages in console commands depending on verbosity settings. You need version ~2.3 of the console for it.", + "symfony/event-dispatcher": "Needed when using log messages in console commands", + "symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel." + }, + "type": "symfony-bridge", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Bridge\\Monolog\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Monolog Bridge", + "homepage": "https://symfony.com", + "time": "2015-06-25 11:21:15" + }, + { + "name": "tecnickcom/tcpdf", + "version": "6.2.12", + "source": { + "type": "git", + "url": "https://github.com/tecnickcom/TCPDF.git", + "reference": "2f732eaa91b5665274689b1d40b285a7bacdc37f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/2f732eaa91b5665274689b1d40b285a7bacdc37f", + "reference": "2f732eaa91b5665274689b1d40b285a7bacdc37f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "fonts", + "config", + "include", + "tcpdf.php", + "tcpdf_parser.php", + "tcpdf_import.php", + "tcpdf_barcodes_1d.php", + "tcpdf_barcodes_2d.php", + "include/tcpdf_colors.php", + "include/tcpdf_filters.php", + "include/tcpdf_font_data.php", + "include/tcpdf_fonts.php", + "include/tcpdf_images.php", + "include/tcpdf_static.php", + "include/barcodes/datamatrix.php", + "include/barcodes/pdf417.php", + "include/barcodes/qrcode.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPLv3" + ], + "authors": [ + { + "name": "Nicola Asuni", + "email": "info@tecnick.com", + "homepage": "http://nicolaasuni.tecnick.com" + } + ], + "description": "TCPDF is a PHP class for generating PDF documents and barcodes.", + "homepage": "http://www.tcpdf.org/", + "keywords": [ + "PDFD32000-2008", + "TCPDF", + "barcodes", + "datamatrix", + "pdf", + "pdf417", + "qrcode" + ], + "time": "2015-09-12 10:08:34" + }, + { + "name": "tedivm/jshrink", + "version": "v0.5.2", + "source": { + "type": "git", + "url": "https://github.com/tedious/JShrink.git", + "reference": "4b48e3d051cf0ab145db9df20d3292d91485bb60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tedious/JShrink/zipball/4b48e3d051cf0ab145db9df20d3292d91485bb60", + "reference": "4b48e3d051cf0ab145db9df20d3292d91485bb60", "shasum": "" }, "require": { @@ -231,29 +1337,33 @@ "javascript", "minifier" ], - "time": "2012-11-26 04:48:55" + "time": "2014-01-14 22:23:53" }, { "name": "twig/twig", - "version": "v1.15.1", + "version": "v1.22.3", "source": { "type": "git", - "url": "https://github.com/fabpot/Twig.git", - "reference": "1fb5784662f438d7d96a541e305e28b812e2eeed" + "url": "https://github.com/twigphp/Twig.git", + "reference": "ebfc36b7e77b0c1175afe30459cf943010245540" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fabpot/Twig/zipball/1fb5784662f438d7d96a541e305e28b812e2eeed", - "reference": "1fb5784662f438d7d96a541e305e28b812e2eeed", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/ebfc36b7e77b0c1175afe30459cf943010245540", + "reference": "ebfc36b7e77b0c1175afe30459cf943010245540", "shasum": "" }, "require": { - "php": ">=5.2.4" + "php": ">=5.2.7" + }, + "require-dev": { + "symfony/debug": "~2.7", + "symfony/phpunit-bridge": "~2.7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.22-dev" } }, "autoload": { @@ -279,7 +1389,7 @@ }, { "name": "Twig Team", - "homepage": "https://github.com/fabpot/Twig/graphs/contributors", + "homepage": "http://twig.sensiolabs.org/contributors", "role": "Contributors" } ], @@ -288,23 +1398,1322 @@ "keywords": [ "templating" ], - "time": "2014-02-13 10:19:29" + "time": "2015-10-13 07:07:02" } ], "packages-dev": [ - - ], - "aliases": [ - + { + "name": "aws/aws-sdk-php", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "937a39ca3cee98d31a7410a17db24e0496c41494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/937a39ca3cee98d31a7410a17db24e0496c41494", + "reference": "937a39ca3cee98d31a7410a17db24e0496c41494", + "shasum": "" + }, + "require": { + "guzzle/guzzle": "~3.7", + "php": ">=5.3.3" + }, + "require-dev": { + "doctrine/cache": "~1.0", + "ext-openssl": "*", + "monolog/monolog": "~1.4", + "phpunit/phpunit": "~4.0", + "symfony/yaml": "~2.1" + }, + "suggest": { + "doctrine/cache": "Adds support for caching of credentials and responses", + "ext-apc": "Allows service description opcode caching, request and response caching, and credentials caching", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "monolog/monolog": "Adds support for logging HTTP requests and responses", + "symfony/yaml": "Eases the ability to write manifests for creating jobs in AWS Import/Export" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-0": { + "Aws": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "time": "2014-10-16 21:37:55" + }, + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14 21:17:01" + }, + { + "name": "facebook/xhprof", + "version": "master", + "source": { + "type": "git", + "url": "https://github.com/phacility/xhprof", + "reference": "master" + }, + "require": { + "php": ">=5.2.0" + }, + "type": "library", + "autoload": { + "files": [ + "xhprof_lib/utils/xhprof_lib.php", + "xhprof_lib/utils/xhprof_runs.php" + ] + }, + "license": [ + "Apache-2.0" + ], + "description": "XHProf: A Hierarchical Profiler for PHP", + "homepage": "http://pecl.php.net/package/xhprof", + "keywords": [ + "performance", + "profiling" + ], + "time": "2015-02-26 14:37:51" + }, + { + "name": "guzzle/guzzle", + "version": "v3.9.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle3.git", + "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/0645b70d953bc1c067bbc8d5bc53194706b628d9", + "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.3", + "symfony/event-dispatcher": "~2.1" + }, + "replace": { + "guzzle/batch": "self.version", + "guzzle/cache": "self.version", + "guzzle/common": "self.version", + "guzzle/http": "self.version", + "guzzle/inflection": "self.version", + "guzzle/iterator": "self.version", + "guzzle/log": "self.version", + "guzzle/parser": "self.version", + "guzzle/plugin": "self.version", + "guzzle/plugin-async": "self.version", + "guzzle/plugin-backoff": "self.version", + "guzzle/plugin-cache": "self.version", + "guzzle/plugin-cookie": "self.version", + "guzzle/plugin-curlauth": "self.version", + "guzzle/plugin-error-response": "self.version", + "guzzle/plugin-history": "self.version", + "guzzle/plugin-log": "self.version", + "guzzle/plugin-md5": "self.version", + "guzzle/plugin-mock": "self.version", + "guzzle/plugin-oauth": "self.version", + "guzzle/service": "self.version", + "guzzle/stream": "self.version" + }, + "require-dev": { + "doctrine/cache": "~1.3", + "monolog/monolog": "~1.0", + "phpunit/phpunit": "3.7.*", + "psr/log": "~1.0", + "symfony/class-loader": "~2.1", + "zendframework/zend-cache": "2.*,<2.3", + "zendframework/zend-log": "2.*,<2.3" + }, + "suggest": { + "guzzlehttp/guzzle": "Guzzle 5 has moved to a new package name. The package you have installed, Guzzle 3, is deprecated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.9-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle": "src/", + "Guzzle\\Tests": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Guzzle Community", + "homepage": "https://github.com/guzzle/guzzle/contributors" + } + ], + "description": "PHP HTTP client. This library is deprecated in favor of https://packagist.org/packages/guzzlehttp/guzzle", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2015-03-18 18:23:50" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "time": "2015-02-03 12:10:50" + }, + { + "name": "phpseclib/phpseclib", + "version": "0.3.10", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "d15bba1edcc7c89e09cc74c5d961317a8b947bf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d15bba1edcc7c89e09cc74c5d961317a8b947bf4", + "reference": "d15bba1edcc7c89e09cc74c5d961317a8b947bf4", + "shasum": "" + }, + "require": { + "php": ">=5.0.0" + }, + "require-dev": { + "phing/phing": "~2.7", + "phpunit/phpunit": "~4.0", + "sami/sami": "~2.0", + "squizlabs/php_codesniffer": "~1.5" + }, + "suggest": { + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a wide variety of cryptographic operations.", + "pear-pear/PHP_Compat": "Install PHP_Compat to get phpseclib working on PHP < 4.3.3." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.3-dev" + } + }, + "autoload": { + "psr-0": { + "Crypt": "phpseclib/", + "File": "phpseclib/", + "Math": "phpseclib/", + "Net": "phpseclib/", + "System": "phpseclib/" + }, + "files": [ + "phpseclib/Crypt/Random.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "phpseclib/" + ], + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "time": "2015-01-28 21:50:33" + }, + { + "name": "phpspec/prophecy", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4745ded9307786b730d7a60df5cb5a6c43cf95f7", + "reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "phpdocumentor/reflection-docblock": "~2.0", + "sebastian/comparator": "~1.1" + }, + "require-dev": { + "phpspec/phpspec": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2015-08-13 10:07:40" + }, + { + "name": "phpunit/php-code-coverage", + "version": "2.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "~1.3", + "sebastian/environment": "^1.3.2", + "sebastian/version": "~1.0" + }, + "require-dev": { + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "~4" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.2.1", + "ext-xmlwriter": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2015-10-06 15:47:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2015-06-21 13:08:43" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21 13:50:34" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b", + "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2015-06-21 08:01:12" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.4.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", + "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2015-09-15 10:49:45" + }, + { + "name": "phpunit/phpunit", + "version": "4.8.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "625f8c345606ed0f3a141dfb88f4116f0e22978e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/625f8c345606ed0f3a141dfb88f4116f0e22978e", + "reference": "625f8c345606ed0f3a141dfb88f4116f0e22978e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.3.3", + "phpspec/prophecy": "^1.3.1", + "phpunit/php-code-coverage": "~2.1", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": ">=1.0.6", + "phpunit/phpunit-mock-objects": "~2.3", + "sebastian/comparator": "~1.1", + "sebastian/diff": "~1.2", + "sebastian/environment": "~1.3", + "sebastian/exporter": "~1.2", + "sebastian/global-state": "~1.0", + "sebastian/version": "~1.0", + "symfony/yaml": "~2.1|~3.0" + }, + "suggest": { + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.8.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2015-10-23 06:48:33" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "2.3.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": ">=5.3.3", + "phpunit/php-text-template": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2015-10-02 06:51:40" + }, + { + "name": "sebastian/comparator", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2015-07-26 15:48:44" + }, + { + "name": "sebastian/diff", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/863df9687835c62aa423a22412d26fa2ebde3fd3", + "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "http://www.github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-02-22 15:13:53" + }, + { + "name": "sebastian/environment", + "version": "1.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6324c907ce7a52478eeeaede764f48733ef5ae44", + "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2015-08-03 06:14:51" + }, + { + "name": "sebastian/exporter", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "7ae5513327cb536431847bcc0c10edba2701064e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e", + "reference": "7ae5513327cb536431847bcc0c10edba2701064e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2015-06-21 07:55:53" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12 03:26:01" + }, + { + "name": "sebastian/recursion-context", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "994d4a811bafe801fb06dccbee797863ba2792ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/994d4a811bafe801fb06dccbee797863ba2792ba", + "reference": "994d4a811bafe801fb06dccbee797863ba2792ba", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2015-06-21 08:04:50" + }, + { + "name": "sebastian/version", + "version": "1.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2015-06-21 13:59:46" + }, + { + "name": "symfony/var-dumper", + "version": "v2.6.11", + "target-dir": "Symfony/Component/VarDumper", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "5fba957a30161d8724aade093593cd22f815bea2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/5fba957a30161d8724aade093593cd22f815bea2", + "reference": "5fba957a30161d8724aade093593cd22f815bea2", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "suggest": { + "ext-symfony_debug": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-0": { + "Symfony\\Component\\VarDumper\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony mechanism for exploring and dumping PHP variables", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "time": "2015-07-01 10:03:42" + }, + { + "name": "symfony/yaml", + "version": "v2.6.11", + "target-dir": "Symfony/Component/Yaml", + "source": { + "type": "git", + "url": "https://github.com/symfony/Yaml.git", + "reference": "c044d1744b8e91aaaa0d9bac683ab87ec7cbf359" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/c044d1744b8e91aaaa0d9bac683ab87ec7cbf359", + "reference": "c044d1744b8e91aaaa0d9bac683ab87ec7cbf359", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\Yaml\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2015-07-26 08:59:42" + } ], + "aliases": [], "minimum-stability": "stable", - "stability-flags": [ - - ], - "platform": { - "php": ">=5.3.2" + "stability-flags": { + "php-di/php-di": 10, + "facebook/xhprof": 20 }, - "platform-dev": [ - - ] + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.3.3" + }, + "platform-dev": [] } diff --git a/www/analytics/config/.htaccess b/www/analytics/config/.htaccess index 6cd2e134..f4e970ee 100644 --- a/www/analytics/config/.htaccess +++ b/www/analytics/config/.htaccess @@ -1,13 +1,8 @@ - -Deny from all - - - -Deny from all - - - -Deny from all - + + Deny from all + + = 2.4> + Require all denied + diff --git a/www/analytics/config/environment/dev.php b/www/analytics/config/environment/dev.php new file mode 100644 index 00000000..a8bedecc --- /dev/null +++ b/www/analytics/config/environment/dev.php @@ -0,0 +1,12 @@ + 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')), + +); diff --git a/www/analytics/config/environment/test.php b/www/analytics/config/environment/test.php new file mode 100644 index 00000000..702bebae --- /dev/null +++ b/www/analytics/config/environment/test.php @@ -0,0 +1,97 @@ + DI\object('Psr\Log\NullLogger'), + + 'Piwik\Cache\Backend' => function () { + return \Piwik\Cache::buildBackend('file'); + }, + 'cache.eager.cache_id' => 'eagercache-test-', + + // 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'); + $access = new FakeAccess(); + if (!empty($idSitesAdmin)) { + FakeAccess::$superUser = false; + FakeAccess::$idSitesAdmin = $idSitesAdmin; + FakeAccess::$identity = 'adminUserLogin'; + } else { + FakeAccess::$superUser = true; + FakeAccess::$superUserLogin = 'superUserLogin'; + } + 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)); + }), + )), +); diff --git a/www/analytics/config/environment/ui-test.php b/www/analytics/config/environment/ui-test.php new file mode 100644 index 00000000..b5ef0fed --- /dev/null +++ b/www/analytics/config/environment/ui-test.php @@ -0,0 +1,62 @@ + 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(array( + + // 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']) + && !empty($request['action']) + ) { + $controllerAction = $request['module'] . '.' . $request['action']; + 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.ExampleRssWidget.rssPiwik.end', function (&$result, $parameters) { + $result = ""; + }), + )), + +); diff --git a/www/analytics/config/global.ini.php b/www/analytics/config/global.ini.php index 9d76c4be..6ac4feac 100644 --- a/www/analytics/config/global.ini.php +++ b/www/analytics/config/global.ini.php @@ -18,25 +18,46 @@ password = dbname = tables_prefix = port = 3306 -adapter = PDO_MYSQL +adapter = PDO\MYSQL type = InnoDB schema = Mysql - ; if charset is set to utf8, Piwik will ensure that it is storing its data using UTF8 charset. ; it will add a sql query SET at each page view. -; Piwik should work correctly without this setting. -;charset = utf8 +; Piwik should work correctly without this setting but we recommend to have a charset set. +charset = utf8 [database_tests] host = localhost -username = root +username = "@USERNAME@" password = dbname = piwik_tests tables_prefix = piwiktests_ port = 3306 -adapter = PDO_MYSQL +adapter = PDO\MYSQL type = InnoDB schema = Mysql +charset = utf8 + +[tests] +; needed in order to run tests. +; if Piwik is available at http://localhost/dev/piwik/ replace @REQUEST_URI@ with /dev/piwik/ +; 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 = + +; 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 = "" +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 @@ -44,20 +65,36 @@ 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: -; NONE, ERROR, WARN, INFO, DEBUG, VERBOSE +; ERROR, WARN, INFO, DEBUG log_level = WARN -; if set to 1, only requests done in CLI mode (eg. the archive.php cron run) will be logged -; NOTE: log_only_when_debug_parameter will also be checked for -log_only_when_cli = 0 - -; if set to 1, only requests with "&debug" parameter will be logged -; NOTE: log_only_when_cli will also be checked for -log_only_when_debug_parameter = 0 - ; if configured to log in a file, log entries will be made to this file logger_file_path = tmp/logs/piwik.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 Piwik 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 +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 @@ -72,40 +109,68 @@ always_archive_data_range = 0; ; 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, a Piwik tracking code will be included in the Piwik UI footer and will track visits, pages, etc. to idsite = 1 -; this is useful for Piwik developers as an easy way to create data in their local Piwik -track_visits_inside_piwik_ui = 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 -disable_merged_assets = 0 - ; If set to 1, all requests to piwik.php will be forced to be 'new visitors' tracker_always_new_visitor = 0 -; Allow automatic upgrades to Beta or RC releases -allow_upgrades_to_beta = 0 +; if set to 1, all SQL queries will be logged using the DEBUG log level +log_sql_queries = 0 [DebugTests] -; Set to 1 by default. If you set to 0, the standalone plugins (with their own git repositories) -; will not be loaded when executing tests. -enable_load_standalone_plugins_during_tests = 1 +; 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 Piwik URL +disable_merged_assets = 0 [General] -; the following settings control whether Unique Visitors will be processed for different period types. + +; 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 Piwik instances ; if you set it to 1 and want the Unique Visitors to be re-processed for reports in the past, drop all piwik_archive_* tables -; it is recommended to always enable Unique Visitors processing for 'day' periods +; 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 Piwik 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 Piwik 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 @@ -143,10 +208,36 @@ browser_archiving_disabled_enforce = 0 ; 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. -; Note: 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. +; 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 Piwik 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 Piwik 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 @@ -158,6 +249,11 @@ 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'. @@ -175,21 +271,32 @@ default_day = yesterday default_period = day ; Time in seconds after which an archive will be computed again. This setting is used only for today's statistics. -; Defaults to 10 seconds so that by default, Piwik provides real time reporting. ; 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 = 10 +time_before_today_archive_considered_outdated = 150 ; This setting is overriden in the UI, under "General Settings". ; The default value is to allow browsers to trigger the Piwik archiving process. ; This setting is only used if it hasn't been overriden via the UI yet, or if enable_general_settings_admin=0 enable_browser_archiving_triggering = 1 +; By default, Piwik 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 setting +; enable_browser_archiving_triggering to 0 will make sure ranges are not archived on browser request. Since the cron +; archiver does not archive ranges, you must either disable ranges or make sure the ranges users' want to see will be +; processed somehow. +archiving_range_force_on_browser_request = 1 + ; By default Piwik runs OPTIMIZE TABLE SQL queries to free spaces after deleting some data. ; If your Piwik 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 Piwik 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 @@ -200,7 +307,7 @@ minimum_pgsql_version = 8.3 ; Minimum adviced memory limit in php.ini file (see memory_limit value) minimum_memory_limit = 128 -; Minimum memory limit enforced when archived via misc/cron/archive.php +; Minimum memory limit enforced when archived via ./console core:archive minimum_memory_limit_when_archiving = 768 ; Piwik will check that usernames and password have a minimum length, and will check that characters are "allowed" @@ -217,14 +324,15 @@ session_save_handler = files ; If set to 1, Piwik will automatically redirect all http:// requests to https:// ; If SSL / https is not correctly configured on the server, this will break Piwik -; If you set this to 1, and your SSL configuration breaks later on, you can always edit this back to 0 +; 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 Piwik over https force_ssl = 0 ; login cookie name login_cookie_name = piwik_auth -; login cookie expiration (14 days) +; 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 ; The path on the server in which the cookie will be available on. @@ -237,6 +345,12 @@ login_password_recovery_email_address = "password-recovery@{DOMAIN}" ; name that appears as a Sender in the password recovery email login_password_recovery_email_name = Piwik +; email address that appears as a Repy-to in the password recovery email +; if specified, {DOMAIN} will be replaced by the current Piwik 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" + ; By default when user logs out he is redirected to Piwik "homepage" usually the Login form. ; Uncomment the next line to set a URL to redirect the user to after he logs out of Piwik. ; login_logout_url = http://... @@ -255,10 +369,21 @@ language_cookie_name = piwik_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 = "" + ; feedback email address; ; when testing, use your own email address or "nobody" feedback_email_address = "feedback@piwik.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, Piwik 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 @@ -347,9 +472,20 @@ enable_trusted_host_check = 1 ;trusted_hosts[] = example.com ;trusted_hosts[] = stats.example.com -; The release server is an essential part of the Piwik infrastructure/ecosystem -; to provide the latest software version. -latest_version_url = http://builds.piwik.org/latest.zip +; List of Cross-origin resource sharing domains (eg domain or subdomain names) when generating absolute URLs. +; Described here: http://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 Piwik instance over multiple hostnames, Piwik will need to know +; a unique instance_id for this instance, so that Piwik can serve the right custom logo and tmp/* assets, +; independently of the hostname Piwik is currently running under. +; instance_id = stats.example.com ; The API server is an essential part of the Piwik infrastructure/ecosystem to ; provide services to Piwik installations, e.g., getLatestVersion and @@ -362,6 +498,10 @@ api_service_url = http://api.piwik.org ; 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 + ; 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 @@ -411,7 +551,29 @@ enable_auto_update = 1 ; 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 +; 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 Piwik Pro advertisements in some places. For example in the installation screen, the +; Piwik Pro Ad widget will be removed etc. +piwik_pro_ads_enabled = 1 + [Tracker] + +; Piwik uses "Privacy by default" model. When one of your users visit multiple of your websites tracked in this Piwik, +; Piwik will create for this user a fingerprint that will be different across the multiple websites. +; If you want to track unique users across websites (for example when using the InterSites plugin) you may set this setting to 1. +; Note: setting this to 0 increases your users' privacy. +enable_fingerprinting_across_websites = 0 + ; Piwik uses first party cookies by default. If set to 1, ; the visit ID cookie will be set on the Piwik server domain as well ; this is useful when you want to do cross websites analysis @@ -421,14 +583,14 @@ use_third_party_id_cookie = 0 ; Once enabled (set to 1) messages will be logged to all loggers defined in "[log] log_writers" config. debug = 0 -; There is a feature in the Tracking API that lets you create new visit at any given time, for example if you know that a different user/customer is using -; the app then you would want to tell Piwik to create a new visit (even though both users are using the same browser/computer). -; To prevent abuse and easy creation of fake visits, this feature requires admin token_auth by default -; If you wish to use this feature using the Javascript tracker, you can set the setting new_visit_api_requires_admin=0, and in Javascript write: -; _paq.push(['appendToTrackingUrl', 'new_visit=1']); -new_visit_api_requires_admin = 1 +; 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 paramater 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 should only be set to 1 in an intranet setting, where most users have the same configuration (browsers, OS) +; This setting is described in this FAQ: http://piwik.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 @@ -436,9 +598,9 @@ trust_visitors_cookies = 0 ; This is used only if use_third_party_id_cookie = 1 cookie_name = _pk_uid -; by default, the Piwik tracking cookie expires in 2 years +; by default, the Piwik tracking cookie expires in 13 months (365 + 28 days) ; This is used only if use_third_party_id_cookie = 1 -cookie_expire = 63072000 +cookie_expire = 33955200; ; The path on the server in which the cookie will be available on. ; Defaults to empty. See spec in http://curl.haxx.se/rfc/cookie_spec.html @@ -466,7 +628,7 @@ default_time_one_page_visit = 0 ; The mapping is defined in core/DataFiles/LanguageToCountry.php, enable_language_to_country_guess = 1 -; When the misc/cron/archive.php cron hasn't been setup, we still need to regularly run some maintenance tasks. +; 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 Piwik 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. @@ -479,13 +641,27 @@ 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_campaign,piwik_campaign,utm_campaign,utm_source,utm_medium" +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,piwik_kwd,pk_keyword,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 PIWIK 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; @@ -494,9 +670,16 @@ page_maximum_length = 1024; ; 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 PIWIK SERVER ; !!! Security risk: if set to 0, it would allow anyone to push data to Piwik 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, +; 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 PIWIK SERVERS tracking_requests_require_authentication = 1 @@ -509,7 +692,7 @@ tracking_requests_require_authentication = 1 ; for which all reports should be Archived during the cron execution ; All segment values MUST be URL encoded. ;Segments[]="visitorType==new" -;Segments[]="visitorType==returning" +;Segments[]="visitorType==returning,visitorType==returningCustomer" ; If you define Custom Variables for your visitor, for example set the visit type ;Segments[]="customVariableName1==VisitType;customVariableValue1==Customer" @@ -554,9 +737,12 @@ 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 Piwik platform Plugins[] = CorePluginsAdmin Plugins[] = CoreAdminHome Plugins[] = CoreHome +Plugins[] = WebsiteMeasurable +Plugins[] = Diagnostics Plugins[] = CoreVisualizations Plugins[] = Proxy Plugins[] = API @@ -568,8 +754,10 @@ Plugins[] = Actions Plugins[] = Dashboard Plugins[] = MultiSites Plugins[] = Referrers -Plugins[] = UserSettings +Plugins[] = UserLanguage +Plugins[] = DevicesDetection Plugins[] = Goals +Plugins[] = Ecommerce Plugins[] = SEO Plugins[] = Events Plugins[] = UserCountry @@ -579,8 +767,8 @@ Plugins[] = VisitTime Plugins[] = VisitorInterest Plugins[] = ExampleAPI Plugins[] = ExampleRssWidget -Plugins[] = Provider Plugins[] = Feedback +Plugins[] = Monolog Plugins[] = Login Plugins[] = UsersManager @@ -599,29 +787,31 @@ Plugins[] = MobileMessaging Plugins[] = Overlay Plugins[] = SegmentEditor Plugins[] = Insights - Plugins[] = Morpheus +Plugins[] = Contents +Plugins[] = BulkTracking +Plugins[] = Resolution +Plugins[] = DevicePlugins +Plugins[] = Heartbeat +Plugins[] = Intl +Plugins[] = PiwikPro [PluginsInstalled] +PluginsInstalled[] = Diagnostics PluginsInstalled[] = Login PluginsInstalled[] = CoreAdminHome PluginsInstalled[] = UsersManager PluginsInstalled[] = SitesManager PluginsInstalled[] = Installation - -[Plugins_Tracker] -Plugins_Tracker[] = Provider -Plugins_Tracker[] = Goals -Plugins_Tracker[] = PrivacyManager -Plugins_Tracker[] = UserCountry -Plugins_Tracker[] = Login +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 +; index.php?module=API&method=API.getSettings ; This can be used to expose values from Piwik, 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 - + diff --git a/www/analytics/config/global.php b/www/analytics/config/global.php new file mode 100644 index 00000000..36e04970 --- /dev/null +++ b/www/analytics/config/global.php @@ -0,0 +1,85 @@ + 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 = ''; + } + + 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) { + 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) . '-'; + }, + + '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(), + + 'Piwik\EventDispatcher' => DI\object()->constructorParameter('observers', DI\get('observers.global')), + + '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')), + +); diff --git a/www/analytics/config/manifest.inc.php b/www/analytics/config/manifest.inc.php index 2a4eada2..f1013575 100644 --- a/www/analytics/config/manifest.inc.php +++ b/www/analytics/config/manifest.inc.php @@ -1,423 +1,1070 @@ array("890", "69246584e6b57bfbc8d39799cd3b9213"), - "composer.lock" => array("10488", "8a98b2fd7cda2b841fe15aa1e72402a5"), - "config/global.ini.php" => array("28859", "5d3e559652bf2ebd390c2a4c0054b02b"), - "console" => array("924", "a4877c66060ee26f1edc476a5ee72776"), - "core/Access.php" => array("12704", "c9839d3aa44e8edc82acfbf1c2e9a4aa"), - "core/API/DataTableGenericFilter.php" => array("5060", "ab1da3c6e3e965a56f5d1274b4703a69"), - "core/API/DataTableManipulator/Flattener.php" => array("4096", "1ee3893fee29909ea33f6e4b767c99d5"), - "core/API/DataTableManipulator/LabelFilter.php" => array("5127", "92fffb40df331cd852a222cc482ea273"), - "core/API/DataTableManipulator.php" => array("6316", "45d5aabe63f1d50766fb036dfd9e5aa1"), - "core/API/DataTableManipulator/ReportTotalsCalculator.php" => array("7459", "dc3674946b59f691b4da26775ab575a9"), - "core/API/DocumentationGenerator.php" => array("10290", "2a0b1f027ca702048e7af16e238f89fa"), - "core/API/Proxy.php" => array("20309", "b447ae17730f347efd4c8407e2172c85"), - "core/API/Request.php" => array("14637", "3373eb0d7cf984a910a18e0335c1576a"), - "core/API/ResponseBuilder.php" => array("17712", "5b666de76bebe3f4163d106da1ef1610"), - "core/Archive/DataCollection.php" => array("11407", "64bcbd03c02f25cc0548ec9960459830"), - "core/Archive/DataTableFactory.php" => array("13364", "3fc26be6b543d69fcf5a5e24b071c89c"), - "core/Archive/Parameters.php" => array("1333", "4d8b5cee96ed8db3b9accb37e3be80b8"), - "core/Archive.php" => array("33032", "9102dc388920e1ce6962103d3afc5e01"), - "core/ArchiveProcessor/Loader.php" => array("6875", "d6dea12a41b609b1fb1a2cc52dc8ae8a"), - "core/ArchiveProcessor/Parameters.php" => array("4312", "fe51534b7e53509e371481a13295cbcd"), - "core/ArchiveProcessor.php" => array("18191", "e1266497acaac1c9f772965a123d832d"), - "core/ArchiveProcessor/PluginsArchiver.php" => array("5983", "aa8b4c422def253126cec71a6a7f4e26"), - "core/ArchiveProcessor/Rules.php" => array("11465", "6133c6cccad15b54b92e22b46ede06c9"), - "core/AssetManager.php" => array("11423", "97e469ef84fdd26bd050169ab869e8b3"), - "core/AssetManager/UIAssetCacheBuster.php" => array("1508", "4d376f8e099aaea9a2341528bf1db023"), - "core/AssetManager/UIAssetCatalog.php" => array("1340", "a225abb9e87ad2ee661f81f855d44872"), - "core/AssetManager/UIAssetCatalogSorter.php" => array("1512", "c6989c08fb3124904ced569774468488"), - "core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php" => array("2772", "8aa332089d60c0362bfabe121020448c"), - "core/AssetManager/UIAssetFetcher.php" => array("2247", "fb0cd6454f394c71adcbb3c19d897dfd"), - "core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php" => array("729", "736a364eb1c3f2bef8d420364a17c6f4"), - "core/AssetManager/UIAssetFetcher/StylesheetUIAssetFetcher.php" => array("1915", "5476bd9d96ec0effeed0bd4696dadae6"), - "core/AssetManager/UIAsset/InMemoryUIAsset.php" => array("1090", "f26f85ec2046ec9d767f7d69434247a8"), - "core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php" => array("2466", "9098ce95776b28e6d48fab8dab09ea74"), - "core/AssetManager/UIAssetMerger.php" => array("4549", "9ac6a5d8b409076548b0dcc277985e29"), - "core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php" => array("4254", "d85777f9b6a2fffefbe3d329e450cc79"), - "core/AssetManager/UIAssetMinifier.php" => array("1727", "28d43428fd6ae8b1460c3e444e269b29"), - "core/AssetManager/UIAsset/OnDiskUIAsset.php" => array("2540", "f70f89fd35d15983aae8681e1facea6b"), - "core/AssetManager/UIAsset.php" => array("1198", "073b3a89bd5737ca6176b597ef16eea5"), - "core/Auth.php" => array("2915", "aeb757345dec1077cd0ba75774356962"), - "core/CacheFile.php" => array("5665", "985342d2fdae584019a7f4c1e955976a"), - "core/CliMulti/Output.php" => array("1063", "d00891b635aaf4d1dc2120ee39b5c5a6"), - "core/CliMulti.php" => array("8349", "265403a19c082fc4b0b4c74959a9208b"), - "core/CliMulti/Process.php" => array("4681", "3a6faa96f191b8d405f6a4972abd13b3"), - "core/CliMulti/RequestCommand.php" => array("2249", "c224d55ef5d6a3a55295cf35259feb96"), - "core/Common.php" => array("35753", "a8c976da84bc9ff1c54918992725eadc"), - "core/Config.php" => array("22997", "147ab7aa770b1fa5423e46871bc0278a"), - "core/Console.php" => array("4872", "c594ab40478f30fa294ea96484389439"), - "core/Cookie.php" => array("11300", "9f160dcf724d3f0f30ac6c50556ca88d"), - "core/CronArchive/FixedSiteIds.php" => array("1305", "7b0307e871cc50d74bbd00e8e26cccbd"), - "core/CronArchive.php" => array("47570", "18a2222278c77c7f4fba542005fa3f17"), - "core/CronArchive/SharedSiteIds.php" => array("4596", "0bc0bd027d24d9cb11f8cc99f73023bc"), - "core/DataAccess/ArchiveSelector.php" => array("14351", "98eaf72ed205b6261ca663d93a6693fe"), - "core/DataAccess/ArchiveTableCreator.php" => array("3320", "455700705f6f6c784f97dc2c84be1f7e"), - "core/DataAccess/ArchiveWriter.php" => array("9384", "3e3235f3a37fdd1e6a78e5624048dbf3"), - "core/DataAccess/LogAggregator.php" => array("41234", "7f733c60c036d166c31cea37567791e9"), - "core/DataArray.php" => array("16587", "ca482fd7e31e0f3479387c80ae746e88"), - "core/DataFiles/Countries.php" => array("7867", "32e4468ba434ffd83bc07d70ad7f4969"), - "core/DataFiles/Currencies.php" => array("8648", "4edf0cb07ef189624b4d14d682183d7a"), - "core/DataFiles/Languages.php" => array("7317", "7cbf38c6feae3f1dea58bc3d74a6e6a4"), - "core/DataFiles/LanguageToCountry.php" => array("2520", "d2de6f0a9b23560bc52c7b1726f1e5c7"), - "core/DataFiles/Providers.php" => array("1792", "bbb4dec4a616cd73ae6566feadc24154"), - "core/DataFiles/SearchEngines.php" => array("45747", "c4f37ecde5286617e51a445300502034"), - "core/DataFiles/Socials.php" => array("5467", "ff231864e2c740a1cac4c70311c09756"), - "core/DataTable/BaseFilter.php" => array("2042", "fce60bb7fa6dc0fe4f9ac0cc5cb0de69"), - "core/DataTable/Bridges.php" => array("562", "d0d252681214102d0f10a277bbddad57"), - "core/DataTable/DataTableInterface.php" => array("857", "19babd1eb37f5541bdd436b2eadbca5e"), - "core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php" => array("10060", "19245f7364829705ed645129187a26be"), - "core/DataTable/Filter/AddColumnsProcessedMetrics.php" => array("5444", "6cab57109eb9db0400a5d747bf88539e"), - "core/DataTable/Filter/AddSummaryRow.php" => array("1380", "072952c5aebbc41e28a032f3766b3e71"), - "core/DataTable/Filter/BeautifyRangeLabels.php" => array("6074", "ea93fc021a0a3f19c9a83111e63279d3"), - "core/DataTable/Filter/BeautifyTimeRangeLabels.php" => array("4548", "bcc614caf8c17085b9ac4b12e15701cd"), - "core/DataTable/Filter/CalculateEvolutionFilter.php" => array("5891", "fe1b274e742a054dbc701d905a5087f1"), - "core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php" => array("1012", "2132297b4a7073faf44f38eec883eb58"), - "core/DataTable/Filter/ColumnCallbackAddColumn.php" => array("2845", "ceb1d9be8b76d9869585dfe107c4a45a"), - "core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php" => array("5056", "6e5056fd84b5745733e65c628609e184"), - "core/DataTable/Filter/ColumnCallbackAddMetadata.php" => array("3021", "29eea90b9dbb6b5c3ce730bd5eb8ef29"), - "core/DataTable/Filter/ColumnCallbackDeleteRow.php" => array("2460", "8bf70a091da4f4e014ae393fbc999bc2"), - "core/DataTable/Filter/ColumnCallbackReplace.php" => array("4249", "fec351b71143e9298255c6e86a12ca6c"), - "core/DataTable/Filter/ColumnDelete.php" => array("5062", "dc7f5cd71a488bbc996fd38f1bf0bb30"), - "core/DataTable/Filter/ExcludeLowPopulation.php" => array("3379", "9847cc4a48fc0549546b51427f223457"), - "core/DataTable/Filter/GroupBy.php" => array("3472", "551e742f8d0c5e64e89b0660969fa6f5"), - "core/DataTable/Filter/Limit.php" => array("1892", "276f4352a6918be4c549d008d406c7c0"), - "core/DataTable/Filter/MetadataCallbackAddMetadata.php" => array("2651", "e0576cdfa84a99765f6e88960bb8e422"), - "core/DataTable/Filter/MetadataCallbackReplace.php" => array("2346", "be71bc6d2a6aca70a04404b42a1085f0"), - "core/DataTable/Filter/Pattern.php" => array("2933", "08e09a9da40a39a12d262fbc51d2753d"), - "core/DataTable/Filter/PatternRecursive.php" => array("2743", "12abb36c7bde6d4cf981f9d9a0684c9c"), - "core/DataTable/Filter/RangeCheck.php" => array("1607", "091a7675c8217cb03448b129b8daca5e"), - "core/DataTable/Filter/ReplaceColumnNames.php" => array("5749", "ff15fca13dcd7b5ed2f669f583233dc9"), - "core/DataTable/Filter/ReplaceSummaryRowLabel.php" => array("2089", "ada7b250bd2ea1079d3b10387763627f"), - "core/DataTable/Filter/SafeDecodeLabel.php" => array("1832", "4e46025ff3ae6b1fb1b8fae2db421785"), - "core/DataTable/Filter/Sort.php" => array("6432", "424750b4a5d7f94f4896c4705837cc53"), - "core/DataTable/Filter/Truncate.php" => array("4175", "7ea93e0bd0abd847c0e919207d1a1320"), - "core/DataTable/Manager.php" => array("4400", "c95164db72086a7c6479257aeef0d173"), - "core/DataTable/Map.php" => array("13011", "8dfdbc7dfe0f7992a83977005f0e2324"), - "core/DataTable.php" => array("57572", "d509d1932005f8dfb00c3a9f03ea78c9"), - "core/DataTable/Renderer/Console.php" => array("5028", "4b1b777fec6b5ae5ccd9c9ec2d523a0a"), - "core/DataTable/Renderer/Csv.php" => array("12195", "d127b1ac63e24895389b17cb43072752"), - "core/DataTable/Renderer/Html.php" => array("5839", "559f0fc629844984a9adf5edeb0b1507"), - "core/DataTable/Renderer/Json.php" => array("3671", "e4b3138a405fc1e129185150cee5b8ce"), - "core/DataTable/Renderer.php" => array("13396", "cfad13e54038358b5776d4cda01e98ee"), - "core/DataTable/Renderer/Php.php" => array("7732", "c603669a6b9945e711ba68b59c8b737d"), - "core/DataTable/Renderer/Rss.php" => array("6068", "fbb296b297c6d3ad4705196c587f76db"), - "core/DataTable/Renderer/Tsv.php" => array("701", "772cf9af832f601154357535a62a9d32"), - "core/DataTable/Renderer/Xml.php" => array("15715", "37c3a6d8f2b5965c720e2b6abca0c0b0"), - "core/DataTable/Row/DataTableSummaryRow.php" => array("1903", "8a25ba9516a43929261f157023402b49"), - "core/DataTable/Row.php" => array("21822", "91d9896e5123c829035a1011c524dd3a"), - "core/DataTable/Simple.php" => array("967", "5e662c2478307b1a9ca6b69294b99c0d"), - "core/DataTable/TableNotFoundException.php" => array("234", "74c2ecb43569238eb30d3f91a21941d8"), - "core/Date.php" => array("22031", "9a8a5696c6595e2ea04d5e5b2f17a61d"), - "core/Db/AdapterInterface.php" => array("1372", "e61f5273b376e6698f3a0a83e15896a6"), - "core/Db/Adapter/Mysqli.php" => array("4455", "8aef57991aae0aef6210a958569c2415"), - "core/Db/Adapter/Pdo/Mssql.php" => array("7582", "f5bbeeb60469cd98048933056286bcec"), - "core/Db/Adapter/Pdo/Mysql.php" => array("6172", "a1c2f38bf52784583abd6dbc7af639fe"), - "core/Db/Adapter/Pdo/Pgsql.php" => array("4769", "decde138f1566ea834a159b8694433f1"), - "core/Db/Adapter.php" => array("2807", "87a49f2522ddbec917fb1d7ec1456708"), - "core/Db/BatchInsert.php" => array("9296", "33039a060eaff7d7abaff65eaaa1c93b"), - "core/DbHelper.php" => array("3822", "a2b18b7df87431bcc7d9740557150412"), - "core/Db.php" => array("24578", "9f2e812e7a76db407a59a9f7495d1e14"), - "core/Db/SchemaInterface.php" => array("2112", "f55d75b05af2cb7de8133942faef36ca"), - "core/Db/Schema/Mysql.php" => array("19642", "9fe1d3d20a5c3a824fa13d7ae7e8da7c"), - "core/Db/Schema.php" => array("5616", "622f40c2f6a7a49e333f7309bb7e7111"), - "core/dispatch.php" => array("955", "6a9a4c5f99af08ed0048f8b2de1886ec"), - "core/Error.php" => array("7210", "5153b7ae689b464cce8b721ddb361d28"), - "core/EventDispatcher.php" => array("6188", "787ae06fb148af48dacf6fe2ab768586"), - "core/ExceptionHandler.php" => array("2053", "86c9c27bd7868af403d53c8a0b143fff"), - "core/Filechecks.php" => array("8786", "576db7c700047fbe3de33abd3a3e6dd3"), - "core/Filesystem.php" => array("10596", "fefaf74a3736cb39ee144e22a77a30ac"), - "core/FrontController.php" => array("20372", "3acd41cc80fdf61723cf0ec8f94c60b0"), - "core/Http.php" => array("29559", "4319b7bb5789c60dfb67b91f2c09520e"), - "core/IP.php" => array("14032", "c418038633d69c9c6acb2f512b08ae14"), - "core/Loader.php" => array("3251", "8a3541845131b8a209b60057b1a97568"), - "core/Log.php" => array("22487", "ae607b359689ee4539d1acade5dfe0bd"), - "core/Mail.php" => array("3051", "dd77f65726ccb1fb35125bc446fc7e9e"), - "core/Menu/MenuAbstract.php" => array("8516", "2380174f7029dad633a52319be1af913"), - "core/Menu/MenuAdmin.php" => array("3752", "d52a4c7296cb935b74122b42a802d8ab"), - "core/Menu/MenuMain.php" => array("2644", "d74adf1afca5dc8fc72e80b93b99a5c4"), - "core/Menu/MenuTop.php" => array("4237", "62983c9f66e4caf4812af80c9407b348"), - "core/MetricsFormatter.php" => array("8530", "aa60c76868df55960a3235d7e2f4b37f"), - "core/Metrics.php" => array("14842", "b1dbee19d075b83a3080a99909b0b380"), - "core/Nonce.php" => array("5260", "6908ea4c50150171fd6eed821a916a14"), - "core/Notification/Manager.php" => array("3903", "a23c790d484dc3ac76e19947efb6fd54"), - "core/Notification.php" => array("5732", "1cbe56fe6665c7206ca88739aad8832f"), - "core/Option.php" => array("6684", "3b110408402a646d6c7861dfd3eaed4e"), - "core/Period/Day.php" => array("2058", "dd8cc0376ce9235221bbc25c8730bc41"), - "core/Period/Month.php" => array("1630", "974b6b2604e1f9ef2a928ab4eaab58b7"), - "core/Period.php" => array("9748", "def7194a46591d12a1b66eb93a97a9cb"), - "core/Period/Range.php" => array("15936", "30c69c5eacb62cc6c173e58b95ca96dc"), - "core/Period/Week.php" => array("2544", "663151eb94b86e7a30404e49146088c2"), - "core/Period/Year.php" => array("1869", "da04fd59cdd94516ecb842c8ea00e5f2"), - "core/Piwik.php" => array("26583", "e8e87eb48bd0df5b155c9649e55cd457"), - "core/Plugin/API.php" => array("1108", "ee9a5d269cf33cde142c8c438322ace9"), - "core/Plugin/Archiver.php" => array("4319", "bd0c156de5ef58c75c3bb68cc79bc6f5"), - "core/Plugin/ConsoleCommand.php" => array("1805", "f1850b6f0d989d47b369afe18fb261f2"), - "core/Plugin/ControllerAdmin.php" => array("7991", "fe4081f2d6d58c5587b1b462afe20b0a"), - "core/Plugin/Controller.php" => array("38680", "7fcbba7a289fb5b1908046e7ef1a35ff"), - "core/Plugin/Dependency.php" => array("2890", "aaa67fe7929ebd4ca30a19a9635a94b8"), - "core/Plugin/Manager.php" => array("37639", "21ba8ef6db04c3800ea9074dafd4c7fe"), - "core/Plugin/MetadataLoader.php" => array("2553", "22ce1fad1069035d6692129b65cec244"), - "core/Plugin.php" => array("10373", "715f226ecb91745815cac730585df8d5"), - "core/Plugin/Settings.php" => array("12013", "ddb5052a732359804337c4eadee99bb4"), - "core/Plugin/ViewDataTable.php" => array("15746", "d6e134e0ae998383ea20d981106400a8"), - "core/Plugin/Visualization.php" => array("22404", "e0313135a50bbf1cd236a4de20606f81"), - "core/Profiler.php" => array("9578", "fb3a2b8d4abe4e848976ea842560f064"), - "core/ProxyHeaders.php" => array("2329", "2c2040497ebd44a257a7813b873ce8ae"), - "core/ProxyHttp.php" => array("9293", "a66d7477c1715daa43bfaf03848bc2d1"), - "core/QuickForm2.php" => array("3991", "38181ddffae0ddb03dded14f44ae8233"), - "core/RankingQuery.php" => array("12775", "86664cae23f6c9669606d1189d33d4af"), - "core/Registry.php" => array("1104", "5f93fbe06640eef9980ada45677c6ebe"), - "core/ReportRenderer/Csv.php" => array("3977", "5ec7fe33c5a71d6a0044446d2dc823bf"), - "core/ReportRenderer/Html.php" => array("5797", "b46530e59c537058a1cd5bebed7d5d9c"), - "core/ReportRenderer/Pdf.php" => array("20769", "1ace48a18c949c67516c3124db35cad7"), - "core/ReportRenderer.php" => array("7959", "bf5443a70c2f0c83ee4649139b3d25a2"), - "core/ScheduledTask.php" => array("5696", "83c2633ccf1c6d970827bb43bbe59578"), - "core/ScheduledTaskTimetable.php" => array("3398", "166f58a5c50c282c7c29d6f644ccc58a"), - "core/ScheduledTime/Daily.php" => array("1233", "d022648a5088a797a26348dc450fd774"), - "core/ScheduledTime/Hourly.php" => array("1300", "05f17149dbc4a4fbe3763097abc2526a"), - "core/ScheduledTime/Monthly.php" => array("4052", "8badd753f6c06f07ecac69f5e91bf16c"), - "core/ScheduledTime.php" => array("7625", "ab30399dcfca1ba1f3aa2d89c67e3cc8"), - "core/ScheduledTime/Weekly.php" => array("2032", "8de6f993989ee66ea786ad1ddc76baab"), - "core/SegmentExpression.php" => array("12147", "1c989bf36472614b2be51beef8f59eea"), - "core/Segment.php" => array("16496", "4a4795937a2c7726150e5d5d797a1f4c"), - "core/Session.php" => array("5408", "9c0b50a50516a2078ee3e043e5c5cdf8"), - "core/Session/SaveHandler/DbTable.php" => array("3400", "231cf14b800bbfbe688ced3fba0c743c"), - "core/Session/SessionNamespace.php" => array("656", "43fd5d53fdb1a60b41e93be8c8f80b44"), - "core/Settings/Manager.php" => array("4028", "42535abca90e8eb964a46c7289dde797"), - "core/SettingsPiwik.php" => array("12885", "a07fe55e36ea565d9bdb38c384345ce5"), - "core/SettingsServer.php" => array("6313", "2785b33d06035d586e892f2868ff57d0"), - "core/Settings/Setting.php" => array("6833", "82707639f441064423be496ecb229ede"), - "core/Settings/StorageInterface.php" => array("1615", "eacd97cea047d06e137d569e15af1179"), - "core/Settings/SystemSetting.php" => array("984", "821339fb538bc6fe35f6cc2ede502e8b"), - "core/Settings/UserSetting.php" => array("3449", "79cc353b6019a245fef3f0bacc6ec7e6"), - "core/Singleton.php" => array("1353", "ee0398b6ffb82242e498de630f73c091"), - "core/Site.php" => array("15122", "9ce12ed8d49fb31fbabf3380052cd199"), - "core/TaskScheduler.php" => array("6626", "4850a933550e4570430b73512cceab7a"), - "core/TCPDF.php" => array("1971", "a94bb257665eefecceadf7f753d7fc77"), - "core/testMinimumPhpVersion.php" => array("7398", "deada667cb4971e1296074cdc780d07f"), - "core/Theme.php" => array("4541", "d4de1d79c1cd622603ca70e5f0af91ae"), - "core/Timer.php" => array("1808", "39acf463b724b25bcce2583d7292d87f"), - "core/Tracker/ActionClickUrl.php" => array("1742", "61c02b31e21b6f9a401eb657fee4364e"), - "core/Tracker/ActionEvent.php" => array("2265", "336607e4d024a5f08c3be764f33c2460"), - "core/Tracker/ActionPageview.php" => array("2013", "366739709f90aa8d232511834f11414b"), - "core/Tracker/Action.php" => array("9811", "2f4aa70d96316e21d62aa56ce1cac5fb"), - "core/Tracker/ActionSiteSearch.php" => array("9254", "1b0f81d05b783b2bca998cd896e1ab8f"), - "core/Tracker/Cache.php" => array("6289", "94a28a7608f12b3e8acffe67c7acc211"), - "core/Tracker/Db/DbException.php" => array("271", "e705dd83b4a7498aa4f37ba24f7a82ca"), - "core/Tracker/Db/Mysqli.php" => array("7571", "63642f63c2364d44188c65e543d44a34"), - "core/Tracker/Db/Pdo/Mysql.php" => array("6810", "32af933dd0219d7f17234d61b6222f75"), - "core/Tracker/Db/Pdo/Pgsql.php" => array("3204", "d76f5782916317fa5d560baddf554877"), - "core/Tracker/Db.php" => array("6120", "f2fda7c7dfc83c28c5ab585ed47e26f0"), - "core/Tracker/GoalManager.php" => array("35981", "be9605a31afa46221ad36eb0a1dcb249"), - "core/Tracker/IgnoreCookie.php" => array("1762", "9c438161c21ee28ca5fceeb67d0884f8"), - "core/Tracker/PageUrl.php" => array("11076", "3c36bc8fdc8fcaf564f25a97ad1c766f"), - "core/Tracker.php" => array("28846", "9edc0aec5accc5d1f6c3a1bdea7fced1"), - "core/Tracker/Referrer.php" => array("11012", "0697bfd2c20a646fc6de922a7ec6e153"), - "core/Tracker/Request.php" => array("18612", "8796d52a20094467b8f3cb46b24ee4c6"), - "core/Tracker/TableLogAction.php" => array("9173", "0688a6b89a8eef0943bbb051dd5464b8"), - "core/Tracker/VisitExcluded.php" => array("8453", "d9179d7244a794b2a9fc471e53e0a96b"), - "core/Tracker/VisitInterface.php" => array("591", "44e61d7caeb0701b0a0cbd4351b8a4d5"), - "core/Tracker/VisitorNotFoundInDb.php" => array("237", "bb64791a33dfee4d64aff1f8495302ec"), - "core/Tracker/Visit.php" => array("42168", "193887df85ce090bcfb9f50bcf326422"), - "core/Translate/Filter/ByBaseTranslations.php" => array("1711", "0592c1a63f43e15c49b6f9579e80b0e2"), - "core/Translate/Filter/ByParameterCount.php" => array("2399", "1b054c3111be6abbbb393efcd80cdecf"), - "core/Translate/Filter/EmptyTranslations.php" => array("1140", "21a8ee941ba93b652f22043424ef0ca6"), - "core/Translate/Filter/EncodedEntities.php" => array("1028", "0fcf7b3c769742621beef97887b6f968"), - "core/Translate/Filter/FilterAbstract.php" => array("651", "385a5f448f1f38e14570a3e462c477b9"), - "core/Translate/Filter/UnnecassaryWhitespaces.php" => array("2408", "502ac2955cf7be7f655ddd5bc0c867b1"), - "core/Translate.php" => array("6857", "b18b68afe4900f9a9b42df364e4370ae"), - "core/Translate/Validate/CoreTranslations.php" => array("3269", "42f30ed563dbac7855e1558194bb28f6"), - "core/Translate/Validate/NoScripts.php" => array("1023", "d1db00f141ac004f0cb195a44d327468"), - "core/Translate/Validate/ValidateAbstract.php" => array("685", "5af8be0c67032bb6405c3dc6dd36a88f"), - "core/Translate/Writer.php" => array("9383", "2ffc837feb2e9e3c3b27f03a0d0cb931"), - "core/Twig.php" => array("10465", "f2cdbdb455ea6537b221905d9823d7bb"), - "core/Unzip/Gzip.php" => array("1588", "c37fe684918ef45cf5fceba0d90f2db8"), - "core/Unzip/PclZip.php" => array("2203", "a9b66ff795a00b9f9489231c40f00f38"), - "core/Unzip.php" => array("1246", "962c3818d46540dba5d0bdb1ff99d747"), - "core/Unzip/Tar.php" => array("1991", "aca63f78845e3a94b2c7c4afc65e0fa1"), - "core/Unzip/UncompressInterface.php" => array("789", "914626f0c5ff3750f2db9b618146a9fa"), - "core/Unzip/ZipArchive.php" => array("4446", "aa086fbaa27dc9653c49019022e912dc"), - "core/UpdateCheck.php" => array("3554", "78229bc91a7ab4d029e2c1a8f1285f11"), - "core/Updater.php" => array("11899", "1f59e2ee92b7cec575344d2c51fa4eae"), - "core/Updates/0.2.10.php" => array("2651", "876e9fe07b71a4fe9d76fd8fd174f0a6"), - "core/Updates/0.2.12.php" => array("932", "59f8221c661ccf08e94b5bf923d67c51"), - "core/Updates/0.2.13.php" => array("786", "349c538337653cb7c1b61603001ead79"), - "core/Updates/0.2.24.php" => array("978", "db0f1757f204103520d52a369d305406"), - "core/Updates/0.2.27.php" => array("2791", "4947fca28656acd2e1343514c7203ec3"), - "core/Updates/0.2.32.php" => array("1092", "0962b3c0781f2621e15825facc5f5ed8"), - "core/Updates/0.2.33.php" => array("1185", "a9d96ddf9036a5dd819da808ed875d68"), - "core/Updates/0.2.34.php" => array("592", "98ae3b028633eba1791d9b92c794d760"), - "core/Updates/0.2.35.php" => array("587", "a5c358cd52da528bcf9becb54172df8c"), - "core/Updates/0.2.37.php" => array("642", "ec8400f457c7a945713fe252947a9436"), - "core/Updates/0.4.1.php" => array("806", "e223d3c70ea7e3c1d804489a91853c7b"), - "core/Updates/0.4.2.php" => array("1156", "d37d6ddf7068db07377601fa6595ffb7"), - "core/Updates/0.4.4.php" => array("660", "c72894136443d613abb674baa7c11338"), - "core/Updates/0.4.php" => array("1167", "98af006ec25c0c0140e7f5ad9135794d"), - "core/Updates/0.5.4.php" => array("1958", "779d5f3a0d4d326f9e2f74d1c8cd128e"), - "core/Updates/0.5.5.php" => array("1293", "7f0033047ccc97f6d66d30390402070a"), - "core/Updates/0.5.php" => array("2096", "959cd8b36d92f8ef20ebc1ad78b1b28f"), - "core/Updates/0.6.2.php" => array("1126", "ac49a76d42ed5be509d3c7a40276ea6d"), - "core/Updates/0.6.3.php" => array("1279", "d2fb0180705d57144e3652f69ec3b166"), - "core/Updates/0.6-rc1.php" => array("4507", "9634cef986b658716b06e3a62c066adb"), - "core/Updates/0.7.php" => array("594", "6210a8eeb517c83d6d839e6b1864c13f"), - "core/Updates/0.9.1.php" => array("1454", "1c5185e07ca0bb2f08c93a86c65ed4b4"), - "core/Updates/1.10.1.php" => array("515", "9c1f18c6bed150d9088e4ffbd6ae85e0"), - "core/Updates/1.10.2-b1.php" => array("660", "6b8e21cc1f8082b01858940dee50fb92"), - "core/Updates/1.10.2-b2.php" => array("674", "64b57788df79cbc9ea1180b2ca2f4ced"), - "core/Updates/1.10-b4.php" => array("524", "da5f285f2e7de1a17ef8d39df8778fbf"), - "core/Updates/1.11-b1.php" => array("523", "e230d3911722fd98b6cbf349d34c664d"), - "core/Updates/1.12-b15.php" => array("452", "31134ebb6971b1459ca2f876d4892f22"), - "core/Updates/1.12-b16.php" => array("650", "7e4186377a518ddb166b1d55b8d25bac"), - "core/Updates/1.12-b1.php" => array("668", "bb6566428c9bf4382ff58cba3bc90091"), - "core/Updates/1.1.php" => array("971", "270bf13926c63efc04e6157d1e351ef8"), - "core/Updates/1.2.3.php" => array("1077", "f667e11902edc92fd98b335c236e2afe"), - "core/Updates/1.2.5-rc1.php" => array("864", "a36b86d356ce093a63177efcc56a8d3c"), - "core/Updates/1.2.5-rc7.php" => array("604", "8db19752e89143fe991cd9529d8be11d"), - "core/Updates/1.2-rc1.php" => array("6822", "346d520a049c2426fb2442e7f29347c3"), - "core/Updates/1.2-rc2.php" => array("434", "c92ff71c25dfb7eccec537abf745e1e1"), - "core/Updates/1.4-rc1.php" => array("805", "0691f7982446f93ddcc1047291c86001"), - "core/Updates/1.4-rc2.php" => array("1639", "c9f5619ad1c8a92545c73d548e44b58a"), - "core/Updates/1.5-b1.php" => array("2262", "ba785cbad196b247571781cadada94b6"), - "core/Updates/1.5-b2.php" => array("1087", "5f5e77f985c277d714b7281d7f965069"), - "core/Updates/1.5-b3.php" => array("2805", "3b4e6761ecdf28343cb3184223525f75"), - "core/Updates/1.5-b4.php" => array("571", "ad302b8cb925c0c10a417456fb502b07"), - "core/Updates/1.5-b5.php" => array("700", "96c669e08b45e60525fe1bb1fb943c9d"), - "core/Updates/1.5-rc6.php" => array("433", "db7f6f03adbb92928cb439c53d9d9ef9"), - "core/Updates/1.6-b1.php" => array("3151", "e5544d116d44ab8538b92eeeca4f3c58"), - "core/Updates/1.6-rc1.php" => array("429", "abc0632476540e4e8ef7e8a0c5d311f9"), - "core/Updates/1.7.2-rc5.php" => array("675", "c204aae2d7f7689d37f4091c37524268"), - "core/Updates/1.7.2-rc7.php" => array("1310", "111c0d0cb4f19766ff8e7635c387eb48"), - "core/Updates/1.7-b1.php" => array("801", "7d0bf59a66243ae5adb41e01b584a30a"), - "core/Updates/1.8.3-b1.php" => array("4214", "d2dab6e537e059c3f42be3b3fd00c1d2"), - "core/Updates/1.8.4-b1.php" => array("5602", "f6e09c6abb389ebb423530214fcb3732"), - "core/Updates/1.9.1-b2.php" => array("756", "926f80df9c4e1874aefe77d59a6ec56e"), - "core/Updates/1.9.3-b10.php" => array("522", "4b239e1966e6183b52fe8a56887e0480"), - "core/Updates/1.9.3-b3.php" => array("687", "effe8f40e5733e5a0244421735944f63"), - "core/Updates/1.9.3-b8.php" => array("730", "34ccb3d0357dbcd9a0d9df4030fdbf36"), - "core/Updates/1.9-b16.php" => array("1493", "07e70dd30d645aada68bfd71e65e9e49"), - "core/Updates/1.9-b19.php" => array("976", "a1f50808b7f7339bec7bb31c7423b120"), - "core/Updates/1.9-b9.php" => array("1440", "67363fcdc79574bda86993f3cea59405"), - "core/Updates/2.0.3-b7.php" => array("1816", "80902356a3532433940567151aaa5a82"), - "core/Updates/2.0.4-b5.php" => array("2589", "95f1d30f619a50ad99ad21d52713c949"), - "core/Updates/2.0.4-b7.php" => array("1773", "1bb19c5e4277994b1f414647def74cd6"), - "core/Updates/2.0.4-b8.php" => array("2124", "91e5b1108076279e5a3c8b0764887573"), - "core/Updates/2.0-a12.php" => array("1221", "c96c6665f2f05c26527968bb9391b01e"), - "core/Updates/2.0-a13.php" => array("2361", "bcc1c2d24b5e72bace075ac8075918fb"), - "core/Updates/2.0-a17.php" => array("1015", "c5d6321d2bbb8fd436c733a4213e3513"), - "core/Updates/2.0-a7.php" => array("872", "72f65ce2241de0e53e02e8f9a6911709"), - "core/Updates/2.0-b10.php" => array("404", "c10a63eb97efeb774f78b5efdbf8a866"), - "core/Updates/2.0-b13.php" => array("986", "897baf57c6104b512bb81ec186ed173a"), - "core/Updates/2.0-b3.php" => array("1122", "5f4d8995e425682b8e2feb9dc162e35a"), - "core/Updates/2.0-b9.php" => array("653", "8d3bd1b10367fac6d9ac351addd2e4da"), - "core/Updates/2.0-rc1.php" => array("431", "172c8341bd784da35b3b83b2c2f5b1f6"), - "core/Updates/2.1.1-b11.php" => array("5745", "acce7f7e6ff35875809478861353dbcb"), - "core/Updates/2.2.0-b15.php" => array("639", "c89858a73296cda3e66b2ded05ba6f4d"), - "core/Updates.php" => array("2803", "2deac460daaceeee0fa1fe9e479185bb"), - "core/UrlHelper.php" => array("18349", "dbd9d9cd951cfafa14e29bcc382502c5"), - "core/Url.php" => array("17731", "554508c27948cb1b29da65844b73b6aa"), - "core/Version.php" => array("345", "636e0ebdf450757751fa98b6ccca24c4"), - "core/ViewDataTable/Config.php" => array("20852", "41d12a2246ca76006b223105edcf5e4d"), - "core/ViewDataTable/Factory.php" => array("7098", "36920d013e8245b49cbf92ba2c0b500d"), - "core/ViewDataTable/Manager.php" => array("8846", "51e33664ee6ae517ed42648eddd31964"), - "core/ViewDataTable/RequestConfig.php" => array("8907", "343b0baae408f730c167129bf1ba2571"), - "core/ViewDataTable/Request.php" => array("4031", "d7b2c817853f519a8b2fda06e6211038"), - "core/View/OneClickDone.php" => array("2077", "bf64751dc4c8fa73f7bbc49b319308c6"), - "core/View.php" => array("12640", "47020ad4b4284ce91a83b1149a1190e5"), - "core/View/RenderTokenParser.php" => array("2051", "a8b5b4566507c29e1fdd09bfaeedd99e"), - "core/View/ReportsByDimension.php" => array("4192", "d72f400326ec14706f7f08f5386828c3"), - "core/View/UIControl.php" => array("4410", "d16a751a2063cac0b834ab63b448fdef"), - "core/View/ViewInterface.php" => array("403", "4a9732dd334255ba0ead94222d74f87f"), - "core/Visualization/Sparkline.php" => array("4576", "3533412a25076c86b0b2a2474a024e74"), - "core/WidgetsList.php" => array("7060", "dc525a4a4e618d56ae0563d22549bb08"), - "index.php" => array("1546", "15d01f358d90f66a19dcab6946679095"), - "js/index.php" => array("749", "a80b88022a2748952a62401871d025bd"), - "js/LICENSE.txt" => array("1549", "c1d16895887980c4494a95a7f6ee3671"), - "js/piwik.js" => array("125559", "9f58141514a1bb0001636373cbb8e068"), - "js/README.md" => array("2142", "a59319062bafe9657baac94e9fdb7995"), - "lang/am.json" => array("43298", "2e05b5fda32bdb397888910de8165463"), - "lang/ar.json" => array("116774", "108b69246990df2762bfddbe22696501"), - "lang/be.json" => array("133845", "d4791872ccafc77296fdc7f01c18935a"), - "lang/bg.json" => array("236400", "c618daca1fc95e32e57eefdcefcd6642"), - "lang/bn.json" => array("30389", "de30a94afc6c0cfd5e39805821dbcb8a"), - "lang/bs.json" => array("47846", "5f02521870cfe3146efee207e59bca4b"), - "lang/ca.json" => array("148886", "6219033d88bf58c96e5c09029708e38b"), - "lang/cs.json" => array("96631", "95e50796f835a36f0dd4a2f5b45ba362"), - "lang/cy.json" => array("33669", "3ae7125bad892632b4dbadbcde73e0eb"), - "lang/da.json" => array("175497", "3770805fc026e5100fd9ae140849514e"), - "lang/de.json" => array("192559", "021bc6cbdbce3eaae44017ef834d2f09"), - "lang/el.json" => array("293119", "f3a72418fd95b71ca8ec2ba7f833e0f2"), - "lang/en.json" => array("176161", "9c820d429d7a4cc0db23376a3166d9e2"), - "lang/es.json" => array("187985", "003cf7629a086625ba4277b9d1bdbe50"), - "lang/et.json" => array("89740", "7c339aa2dca5c02fcddd31ea37566a98"), - "lang/eu.json" => array("46504", "220e4c85c784d98f04b7915a07070dc0"), - "lang/fa.json" => array("185290", "3b5a3430bd600b2980f13418a2457884"), - "lang/fi.json" => array("171923", "92fabf05e4bc46c407dba69407077b53"), - "lang/fr.json" => array("191935", "169d8019b419009aaeee37c7f5ab2682"), - "lang/gl.json" => array("31239", "6e8e2ee3b00d9edb52d909ddf3765d0f"), - "lang/he.json" => array("66841", "5b3bded179f179a250499886a4dcb141"), - "lang/hi.json" => array("267566", "1243af856ce0f2de652a47777c01bdeb"), - "lang/hr.json" => array("44181", "d4d1f8a8210b743dfc74b2909c93e29d"), - "lang/hu.json" => array("88631", "5a393be54f20621ec373aae7952119b0"), - "lang/id.json" => array("160275", "8fe3288e6ab07886257964d2755af0d0"), - "lang/is.json" => array("44565", "d3e6d051c1bd28ec5677c50582e2249b"), - "lang/it.json" => array("188242", "f7547dcef63cce6d91627d6c001d22c1"), - "lang/ja.json" => array("146362", "f47bd751197cfc3d072f806d8deaaef2"), - "lang/ka.json" => array("133972", "d919209c916cd66daad1e233b97c24a6"), - "lang/ko.json" => array("159126", "7e91ce1f784680466763eb7b5a664c87"), - "lang/lt.json" => array("72812", "bacac0086478ec8d07f63229cd109c04"), - "lang/lv.json" => array("68729", "4073b4637d26b430110508ff59a800e8"), - "lang/nb.json" => array("79362", "b0da604e507b5c621b7e697ea1c6bf84"), - "lang/nl.json" => array("159749", "25663b03d177a31f8a8986dd8edcbde6"), - "lang/nn.json" => array("72987", "61ece0f6d31afbd53f42e840643613a8"), - "lang/pl.json" => array("105283", "96ea726f5a9c5ebf8b85f531069760eb"), - "lang/pt-br.json" => array("178705", "955707f471cbde34c6597fc06199469b"), - "lang/pt.json" => array("103399", "c8db5c35133612b7badeca45967a8cf8"), - "lang/README.md" => array("439", "7f5249109f73ae292bcd01eb23853738"), - "lang/ro.json" => array("107388", "4e16d637081c5f7d3bf887debbadee3d"), - "lang/ru.json" => array("236266", "3601f2fbf822f4e3eade4a99f25f0abb"), - "lang/sk.json" => array("72678", "82b11fbdab21b8bcb55665b48794ff42"), - "lang/sl.json" => array("71473", "8e2836b6c48a39847f029a08cc864768"), - "lang/sq.json" => array("108350", "abde20a3257f155aa6b6e88dd65830c9"), - "lang/sr.json" => array("171239", "b348d6ce2bb4f75a190b0e468e0b1584"), - "lang/sv.json" => array("179173", "ce59c0b2c6dab1f8180c6f87daffc90d"), - "lang/ta.json" => array("89885", "f861e7329aac8c31dc1f634d7bab8625"), - "lang/te.json" => array("45655", "41136b9370c8df0f8264d098e576a984"), - "lang/th.json" => array("161630", "ee834a50d83b3a356b0ee7069b80d6f2"), - "lang/tr.json" => array("67775", "d3059314807802e6e9c308c02c7a2b1f"), - "lang/uk.json" => array("101355", "305b6faaf47f8f52ff96070300759047"), - "lang/vi.json" => array("201534", "7a21f65bd54d7b500a4fbf1e6d4de09c"), - "lang/zh-cn.json" => array("149382", "3a6056925b838c9c17ec72ce97705958"), - "lang/zh-tw.json" => array("69883", "1b3dc84c6da058b41f3faf6f33052302"), - "LEGALNOTICE" => array("7322", "cec47f121512f46d75f04edc3821e0e9"), - "libs/angularjs/angular-animate.js" => array("73299", "f57eec6cfc24a9d7de89e7815c54b9cb"), - "libs/angularjs/angular-animate.min.js" => array("10506", "5927bc7044bf9f88c7fcef982cfc84e0"), - "libs/angularjs/angular-cookies.js" => array("5712", "8f359b0b2ccce92d6d0f27a275178646"), - "libs/angularjs/angular-cookies.min.js" => array("850", "f70eb186d69b8c34b9fc083fd95ad46e"), - "libs/angularjs/angular-csp.css" => array("346", "34f147527516ef16935f3499d13fdadf"), - "libs/angularjs/angular.js" => array("737843", "823656f04f0d28049733ea5472eaf885"), - "libs/angularjs/angular-loader.js" => array("14464", "dbd936b897ff5995f868309b1682973e"), - "libs/angularjs/angular-loader.min.js" => array("1505", "540937ba021f2ba2d65679de81cfc225"), - "libs/angularjs/angular.min.js" => array("101076", "f13ec1ca50778530d05f5e7d55964873"), - "libs/angularjs/angular-mocks.js" => array("67709", "b64cf0b49047bda8058ffebcb4b969ba"), - "libs/angularjs/angular-resource.js" => array("23494", "e567b1825d04f7d3c8c4932bafe45c92"), - "libs/angularjs/angular-resource.min.js" => array("3297", "52f14aa58396ba6f06c873048ac757ad"), - "libs/angularjs/angular-route.js" => array("32505", "8eaf3177d26ff41eadaa0bb325cc84cc"), - "libs/angularjs/angular-route.min.js" => array("3885", "39e7751707e2fb3108f6148c957bb726"), - "libs/angularjs/angular-sanitize.js" => array("20736", "bccd4bf0adf6318da2e84e87eea451ef"), - "libs/angularjs/angular-sanitize.min.js" => array("4246", "57046863351fc02e0058321e20691254"), - "libs/angularjs/angular-scenario.js" => array("1080031", "b4202d55bb0ef488e710d0ec0b718f7b"), - "libs/angularjs/angular-touch.js" => array("20706", "f1c23f96e05f1cf0991be9f0ac5f44d8"), - "libs/angularjs/angular-touch.min.js" => array("3205", "27edc5cdcdb0cc9da0a8e1bc6510cb9b"), - "libs/angularjs/errors.json" => array("5385", "4de0bf0d0e45b08c96275179d1e45445"), - "libs/angularjs/LICENSE" => array("1098", "0547d38c79782d84bb4df3c036a0bcc0"), - "libs/angularjs/version.json" => array("102", "2ca74bf29fbf307a7f626d922bb20696"), - "libs/angularjs/version.txt" => array("6", "88979175ec60695fcd6e3c97243cdecd"), - "libs/Archive_Tar/Tar.php" => array("66956", "501bf628481dce9ded472e4ae24d1c7a"), - "libs/html5shiv/html5shiv.js" => array("2427", "11af8654413ddf8b0fe00c3395787e77"), + "bower.json" => array("932", "a53acf32175be6e1097f43d7caaf19f9"), + "CHANGELOG.md" => array("36412", "0eeec5e48ef475da856d158d85ecb17e"), + "composer.json" => array("3483", "de61be52972a0fe8fe751306c271f4b8"), + "composer.lock" => array("96582", "d31ce9595493a228c06d04744acbbd04"), + "config/environment/dev.php" => array("416", "27239ab7df09b6997d1503a58fd5f0bf"), + "config/environment/test.php" => array("3826", "0574d36a20fdc5d2921b9fdb56801e94"), + "config/environment/ui-test.php" => array("2371", "5558f0df8a360aea4ac2ef7a26507e91"), + "config/global.ini.php" => array("39258", "8b6e0857ded5b3b9409b8360587b16c6"), + "config/global.php" => array("3102", "a9f65997997900a0c2816d5e78c03117"), + "console" => array("689", "b89a89288416184c6fc76e029d746211"), + "CONTRIBUTING.md" => array("700", "4a0126deecb900a4b0c3aae95aa9e515"), + "core/Access.php" => array("14255", "baaba9f6c9e66db2e0d1899cd6bff63a"), + "core/API/ApiRenderer.php" => array("3544", "d95b52974730120b83517a7cfe757ee3"), + "core/API/CORSHandler.php" => array("936", "04ca601b499bc60e0526fb4f6b9683cc"), + "core/API/DataTableGenericFilter.php" => array("6658", "7d2ae6fbf9f9898a3806bec9ea645a2a"), + "core/API/DataTableManipulator/Flattener.php" => array("4615", "596866ee834f78d1f4149966ac2e1e78"), + "core/API/DataTableManipulator/LabelFilter.php" => array("6048", "7a5554ef49ffee709082231f0fccef50"), + "core/API/DataTableManipulator.php" => array("6733", "79f3699d86502c88502574a392f2b973"), + "core/API/DataTableManipulator/ReportTotalsCalculator.php" => array("7422", "c782df8d1b48b3c876566d62d8a76420"), + "core/API/DataTablePostProcessor.php" => array("15122", "a0393bedf70ce89758bb318abee0da36"), + "core/API/DocumentationGenerator.php" => array("16028", "8bf07e2b005ec485dd33d8d58ffbc2ad"), + "core/API/Inconsistencies.php" => array("1267", "2609882ac7aced8574bcf948c9a01ad0"), + "core/API/Proxy.php" => array("21438", "09612a4c058f0dd4b97d7334e1de6d86"), + "core/API/Request.php" => array("19528", "c476d6be682149a836f3c7aabc0570d8"), + "core/API/ResponseBuilder.php" => array("9446", "870270905e45498fdcf302172bfaad99"), + "core/Application/EnvironmentManipulator.php" => array("1649", "6dbdaba74fcc168504ba8ae0ee48e263"), + "core/Application/Environment.php" => array("7282", "6ffb92b5b91a929b13c5e1349cdd6000"), + "core/Application/Kernel/EnvironmentValidator.php" => array("2403", "528525e3a3e7a2b404b193ac54a64cce"), + "core/Application/Kernel/GlobalSettingsProvider.php" => array("2841", "5906966232aa0b97b2d95515e4781ecd"), + "core/Application/Kernel/PluginList.php" => array("3373", "048f4fd44165938e2bb232835eb0bb05"), + "core/Archive/ArchiveInvalidator/InvalidationResult.php" => array("1485", "8eebf5cac5846e865d8ea9b52e6ddabe"), + "core/Archive/ArchiveInvalidator.php" => array("10512", "ed335369b9aad39d660d9a2180be8ef7"), + "core/Archive/ArchivePurger.php" => array("9620", "038dc88337663e38f0d529a85f8371d2"), + "core/Archive/Chunk.php" => array("4497", "0ba051ecf2539b61de253264fcabf5fa"), + "core/Archive/DataCollection.php" => array("12559", "80010277e191ff708368a0cd45fd1ed8"), + "core/Archive/DataTableFactory.php" => array("17860", "567181b7c3d5b779b60938b7b76d9510"), + "core/Archive/Parameters.php" => array("1029", "35b01b1524b487de5898963f0495407b"), + "core/Archive.php" => array("36235", "a86fad6e05d609ece1bfc475ca6078c2"), + "core/ArchiveProcessor/Loader.php" => array("7668", "bb908afd463e2348c5840903ce70842b"), + "core/ArchiveProcessor/Parameters.php" => array("4090", "9acca7a0758dcbd304050df21fbb19fa"), + "core/ArchiveProcessor.php" => array("22393", "6f127ce147f1348e5d1fc47accdfe279"), + "core/ArchiveProcessor/PluginsArchiver.php" => array("7811", "1436f0b97ecc07ab6b407edeedbccc62"), + "core/ArchiveProcessor/Rules.php" => array("9925", "c8495433ac322e836170460e5385a7a9"), + "core/Archiver/Request.php" => array("741", "a959db4ff137ec0ceb694ae0b3b9459f"), + "core/AssetManager.php" => array("11565", "da7731fd80f2eb2426d7c7f42871dd36"), + "core/AssetManager/UIAssetCacheBuster.php" => array("1512", "d3665922dab19d06e1ebf46b7f34f9f9"), + "core/AssetManager/UIAssetCatalog.php" => array("1449", "e78abd58f417d1a5a181147ad732ecb1"), + "core/AssetManager/UIAssetCatalogSorter.php" => array("1523", "ba51945bc4c0f65ab299bd96eda78bd5"), + "core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php" => array("2995", "e4ca818eaca60c892d8a740323ab79bf"), + "core/AssetManager/UIAssetFetcher.php" => array("2270", "92412ef0ca70c078c69567bebf36fadf"), + "core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php" => array("739", "758f8dcefd0ddb6957fa2020c7d926cc"), + "core/AssetManager/UIAssetFetcher/StylesheetUIAssetFetcher.php" => array("2354", "3abc9973624bbe4762b86819954046ed"), + "core/AssetManager/UIAsset/InMemoryUIAsset.php" => array("1093", "09d2f65d9226e7a05d975678d34831bb"), + "core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php" => array("2406", "6f117aec5229fd3200c68fcda8ae3f2f"), + "core/AssetManager/UIAssetMerger.php" => array("4176", "8f66c5adab442805c45ab2252801cbd8"), + "core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php" => array("4994", "4fdf12e5eb24db90936b4e79dfc28f3d"), + "core/AssetManager/UIAssetMinifier.php" => array("1735", "52e1fc18128304f091525209bbf2ddba"), + "core/AssetManager/UIAsset/OnDiskUIAsset.php" => array("2709", "1dc6a4a6e6956366250adbb05b9c1124"), + "core/AssetManager/UIAsset.php" => array("1202", "1aa6935c82b4bf06c7973898adc18311"), + "core/Auth.php" => array("6113", "5f01585abf520f55f1e8c0b76c98ef39"), + "core/BaseFactory.php" => array("1703", "eb8862177d58c70cae4610fcefaf08d3"), + "core/bootstrap.php" => array("1825", "d4f57207e954d0ba5db1d39cbd973b97"), + "core/CacheId.php" => array("678", "c545036c692908982b1c7bb4fce0ba8f"), + "core/Cache.php" => array("3572", "9bd1eff45172d8115f765550c040c8ff"), + "core/CliMulti/CliPhp.php" => array("2415", "28100e5127c4270a433af26cf2588f88"), + "core/CliMulti/Output.php" => array("1365", "3d6f4f6607f87c0d13f316d14665eb87"), + "core/CliMulti.php" => array("10887", "8a53fef217addb2e2b20e1f3b3222380"), + "core/CliMulti/Process.php" => array("6275", "168693d4591a5536fb41f9d47643523d"), + "core/CliMulti/RequestCommand.php" => array("3628", "04cacb08a25b1ded6ad8787db7345aa4"), + "core/Columns/Dimension.php" => array("6686", "f84d24342b22776fc85fe1e862ebe772"), + "core/Columns/Updater.php" => array("15383", "7ec773b6c0ef1dc5d189e78d450f161b"), + "core/Common.php" => array("41168", "1da5e1a95d13639bddd440ca1fcec32d"), + "core/Composer/ScriptHandler.php" => array("1069", "3c232a235313a3952f3be1c8e7adf9a8"), + "core/Concurrency/DistributedList.php" => array("4259", "5b26014730d1e9f6cb2c84eaf7d38b0c"), + "core/Config/ConfigNotFoundException.php" => array("296", "3de0a882212918def5458e2ba8406deb"), + "core/Config/IniFileChain.php" => array("15780", "56a2093783f6e4d7e0771a600291c1db"), + "core/Config.php" => array("11059", "d6ea70b90e058f13ae1abfc33ff04aa0"), + "core/Console.php" => array("6868", "ea38f4aff3b96b3a70230efe84ee6ce8"), + "core/Container/ContainerDoesNotExistException.php" => array("360", "4015d88d190271af36698fc83168e3da"), + "core/Container/ContainerFactory.php" => array("4224", "18e45926c0eaceac552a5088243a0921"), + "core/Container/IniConfigDefinitionSource.php" => array("2206", "c598467a334cb61abb2f1e86a46cabe0"), + "core/Container/StaticContainer.php" => array("1968", "2e00378ebf7530a10dfb4cc1ca93116d"), + "core/Cookie.php" => array("11461", "8c5d82bf265801980d9f6713a63834f7"), + "core/CronArchive/FixedSiteIds.php" => array("1284", "aa4a399d9eb5bf26b082de952052292f"), + "core/CronArchive.php" => array("62627", "25ffa4b168cb92d8bbdfe1df80fc992d"), + "core/CronArchive/SegmentArchivingRequestUrlProvider.php" => array("7977", "7e997c70128bbb29606f2d647dd8c516"), + "core/CronArchive/SharedSiteIds.php" => array("4833", "8e99eb0b10dcd809a4a3d3d39f830d55"), + "core/CronArchive/SitesToReprocessDistributedList.php" => array("1015", "ed72a0aecc63d1a7fadbf689da9eff34"), + "core/DataAccess/Actions.php" => array("730", "6a573f3c38217812f0fa321bd58a033b"), + "core/DataAccess/ArchiveSelector.php" => array("13920", "09043083b1b13d81f27e3099d8af2343"), + "core/DataAccess/ArchiveTableCreator.php" => array("3244", "c4709d1b7084f1e4bb0bd46c81c4f55e"), + "core/DataAccess/ArchiveTableDao.php" => array("3267", "fecb7478d77333675fe9a55b3e2f8c00"), + "core/DataAccess/ArchiveWriter.php" => array("7664", "5a933065334cdcec3b679f30358a8284"), + "core/DataAccess/LogAggregator.php" => array("42646", "cad25cd093d16e6068dc1934a2dad2c7"), + "core/DataAccess/LogQueryBuilder.php" => array("15474", "a3f53f45f774cf29505effddb3b2b286"), + "core/DataAccess/Model.php" => array("12717", "cd4ff4bf5a7a75224ff98812f800254c"), + "core/DataAccess/RawLogDao.php" => array("13481", "4d9b0d3cf667bab473073bedef297565"), + "core/DataAccess/TableMetadata.php" => array("1429", "06530dcea7f12e755507852f68b64e92"), + "core/DataArray.php" => array("18573", "d704495631d966d3a849aca4380bd6a6"), + "core/DataFiles/cacert.pem" => array("258069", "31e74d3b981e5fd89e10f2dd92b7a0bf"), + "core/DataFiles/Providers.php" => array("1796", "b3981a0e46d14a6b6733eb73582824f4"), + "core/DataTable/BaseFilter.php" => array("1962", "8d0895efe663067a088b13d0de1db36b"), + "core/DataTable/Bridges.php" => array("864", "690b9444bef7cb957ccb9aeeab797add"), + "core/DataTable/DataTableInterface.php" => array("861", "7dc36f12bc9ebd52eaf1431e8b1ea8a8"), + "core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php" => array("6839", "c8f30dd8113b3cae3d4788573933af8f"), + "core/DataTable/Filter/AddColumnsProcessedMetrics.php" => array("3108", "ccbf38f0a632728ac48220f91b9bf08b"), + "core/DataTable/Filter/AddSegmentByLabelMapping.php" => array("1573", "66a7f7411ea11b367bcfe67bd8242a03"), + "core/DataTable/Filter/AddSegmentByLabel.php" => array("3250", "4ada3fb332c275a0534b4028cec0a147"), + "core/DataTable/Filter/AddSegmentBySegmentValue.php" => array("1862", "b009b774281803d8c4c6ccfb53b2c431"), + "core/DataTable/Filter/AddSegmentValue.php" => array("815", "c53508cba03306ba825bbf0aff372345"), + "core/DataTable/Filter/AddSummaryRow.php" => array("1381", "2454d091551cc5f41858b16566ebd0d0"), + "core/DataTable/Filter/BeautifyRangeLabels.php" => array("6077", "e58a217369ec08d2cf1f236072e23f33"), + "core/DataTable/Filter/BeautifyTimeRangeLabels.php" => array("4548", "c61c80d570d6db0492b38bf6d4905358"), + "core/DataTable/Filter/CalculateEvolutionFilter.php" => array("6163", "41bdc80b0944bce9e172dd3360e1f385"), + "core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php" => array("1014", "b192f6689787c5db2d424733bab6fd45"), + "core/DataTable/Filter/ColumnCallbackAddColumn.php" => array("3569", "9930e9bcb718422a44d6e52494f37a73"), + "core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php" => array("5048", "cf21c49fd168a36040c95e0fdbbfe9dc"), + "core/DataTable/Filter/ColumnCallbackAddMetadata.php" => array("3063", "c82351905c5bf7d485e5dad69a991c98"), + "core/DataTable/Filter/ColumnCallbackDeleteMetadata.php" => array("1318", "5b7ddf859f130800b70d3f618f41607d"), + "core/DataTable/Filter/ColumnCallbackDeleteRow.php" => array("2432", "e4b9e87361a427ae4a312a9678418ab4"), + "core/DataTable/Filter/ColumnCallbackReplace.php" => array("4421", "e69b3320f7b0cd3033672a7651f6b1e9"), + "core/DataTable/Filter/ColumnDelete.php" => array("5100", "a9dfc6fffafea29454fa24421a79f78d"), + "core/DataTable/Filter/ExcludeLowPopulation.php" => array("4333", "de340d2d8dd98d770103da55698a2f37"), + "core/DataTable/Filter/GroupBy.php" => array("3716", "1a0c35d9c4b567869f4c25f9adba17a3"), + "core/DataTable/Filter/Limit.php" => array("1895", "673c5777380e28a62ff617e45477d2af"), + "core/DataTable/Filter/MetadataCallbackAddMetadata.php" => array("2680", "bc588d2fe74248048e9eee6171a4f7bf"), + "core/DataTable/Filter/MetadataCallbackReplace.php" => array("2347", "0b94bc6a1177a3e3b2faf6552529d864"), + "core/DataTable/Filter/Pattern.php" => array("3608", "741c748630ac92f6869e9e26c86ba89a"), + "core/DataTable/Filter/PatternRecursive.php" => array("2548", "5e4776abe6360abc10fb4399172936b2"), + "core/DataTable/Filter/PivotByDimension.php" => array("20576", "e835242ddf0520612b1ac2f5cefa1643"), + "core/DataTable/Filter/PrependSegment.php" => array("916", "7330235ff32797105ccf9901a815abeb"), + "core/DataTable/Filter/PrependValueToMetadata.php" => array("1839", "4207f49e1b877f9e8c0011b30a9f19b8"), + "core/DataTable/Filter/RangeCheck.php" => array("2176", "688424acb31f69de9550418536b76244"), + "core/DataTable/Filter/ReplaceColumnNames.php" => array("5631", "c059555f586e099a496412899304c964"), + "core/DataTable/Filter/ReplaceSummaryRowLabel.php" => array("2042", "11f3399ce9ab64de1cb117424af459b5"), + "core/DataTable/Filter/SafeDecodeLabel.php" => array("1835", "120603c3cb24fb2d0c2d13629f3b0425"), + "core/DataTable/Filter/Sort.php" => array("7947", "95c443d2f43efb7955cf873dc1611718"), + "core/DataTable/Filter/Truncate.php" => array("4360", "72f09d072570b1c1c20268865b38e0ee"), + "core/DataTable/Manager.php" => array("4307", "c3b5b199fc58a0c4ff39a2b44349b105"), + "core/DataTable/Map.php" => array("14715", "b65cd2c33732e73aaa111623eda04934"), + "core/DataTable.php" => array("65275", "288ef225eab72eb0cd5e3788751c6873"), + "core/DataTable/Renderer/Console.php" => array("4668", "0d9fcfb6611e770e1aab513b2623e9ba"), + "core/DataTable/Renderer/Csv.php" => array("13569", "80150167b3092887d6ed0f9082945a47"), + "core/DataTable/Renderer/Html.php" => array("5425", "782e5917fc41cc233466bddfafd043aa"), + "core/DataTable/Renderer/Json.php" => array("2559", "a6c09118d5df967e770ab0fad8d4a38e"), + "core/DataTable/Renderer.php" => array("12030", "dfa2c0ddeb6a2cf6cf600967704cee49"), + "core/DataTable/Renderer/Php.php" => array("7211", "cce3a60218d09ef600c0eca694fc030d"), + "core/DataTable/Renderer/Rss.php" => array("5527", "20b70c785e000a90dac603a6341238bd"), + "core/DataTable/Renderer/Tsv.php" => array("718", "40d08a020e0dfd1abc7e2e29f5b9e4f9"), + "core/DataTable/Renderer/Xml.php" => array("16599", "ebbf351d287288c8b90c9b757cd6bfb0"), + "core/DataTable/Row/DataTableSummaryRow.php" => array("2035", "029fee4adfad884e57f2af08b34f4def"), + "core/DataTable/Row.php" => array("23215", "2380e7ebb6382d7b982656f056ab55f1"), + "core/DataTable/Simple.php" => array("949", "eed3e8bae119305c45245561dcc26490"), + "core/DataTable/TableNotFoundException.php" => array("236", "d20b1cd593341976965ff239e1d20f07"), + "core/Date.php" => array("30919", "a9fa3cabe6862ccce70f14891fc49566"), + "core/Db/AdapterInterface.php" => array("1376", "b76e32c114c3be1b72da40ac5fdeaa5b"), + "core/Db/Adapter/Mysqli.php" => array("4703", "858b2d9e885c293be5208320e4766387"), + "core/Db/Adapter/Pdo/Mssql.php" => array("7537", "6dbd56dc5ca816e5ed253b2cb217691b"), + "core/Db/Adapter/Pdo/Mysql.php" => array("6807", "f95b8e1659af5284c672e2b15c949bbe"), + "core/Db/Adapter/Pdo/Pgsql.php" => array("4717", "afae9926296e42cb6602f22476b93985"), + "core/Db/Adapter.php" => array("3134", "6659a8c19d0467687fa1fde81e9587d9"), + "core/Db/BatchInsert.php" => array("9659", "933d756325925458c38ae2f5947f9c00"), + "core/DbHelper.php" => array("4595", "df048bbcec43bcedb718799a0900de0d"), + "core/Db.php" => array("27004", "d3bdfc2e22ae67965e72a965fcb7e96b"), + "core/Db/SchemaInterface.php" => array("2201", "1ecdac30e16334d5b777fac696b03d8a"), + "core/Db/Schema/Mysql.php" => array("20904", "d658b2ece43b1bbed49d69418078cbc6"), + "core/Db/Schema.php" => array("4457", "e57fd807adc994ad227b6009ef3a64de"), + "core/Db/Settings.php" => array("803", "2d50ee3e267f89e10f8fe014cdfa8102"), + "core/Development.php" => array("6648", "73aea90822fb126781d480cae708e415"), + "core/DeviceDetectorCache.php" => array("2151", "941f8c7afe0e918ae1abb3c64bc7bd25"), + "core/DeviceDetectorFactory.php" => array("983", "0106a3ebdef29c5515f57cd1a2c7d007"), + "core/dispatch.php" => array("946", "70af16b3b0712420a29e57fee56f1116"), + "core/ErrorHandler.php" => array("4557", "56c7357e740f7045c5a0c071370b2ba5"), + "core/EventDispatcher.php" => array("6693", "7e43dda0304ddcac2119922bbcaf2188"), + "core/Exception/AuthenticationFailedException.php" => array("242", "03569d50c99c2826e38eeb363aaca2be"), + "core/Exception/DatabaseSchemaIsNewerThanCodebaseException.php" => array("255", "1adc44615b8e4aa1aa9338b2a361383a"), + "core/Exception/ErrorException.php" => array("380", "7466dc25931bc56fa2dfcd21d8711057"), + "core/Exception/Exception.php" => array("639", "ddbbfcc7d725528119aaad170dab9275"), + "core/ExceptionHandler.php" => array("3686", "a1821d0fcf963a325c0ec1b3789db208"), + "core/Exception/InvalidRequestParameterException.php" => array("245", "e36bf68c3289ea85a0630d6deaec8f9e"), + "core/Exception/MissingFilePermissionException.php" => array("243", "f0dec73dc0b363cf77c6d78bbb7984fb"), + "core/Exception/NoPrivilegesException.php" => array("234", "fd3f1cd06ac88fbca9f9b98cfe0f664a"), + "core/Exception/NoWebsiteFoundException.php" => array("236", "986b23877e39de9d514257620d86abd4"), + "core/Exception/UnexpectedWebsiteFoundException.php" => array("244", "79ce36dd4c78bb28ad2ddf4951ebc245"), + "core/Filechecks.php" => array("9250", "107dfed3b846efd353a488bf3287caaa"), + "core/Filesystem.php" => array("16057", "a3f5cf6972f6236c1249f6d70c70b866"), + "core/FrontController.php" => array("20149", "4a7e168f6fe0b729a14cbaf083e3f962"), + "core/Http/ControllerResolver.php" => array("3756", "7ecca7ba5206dd979a068a9851684ef3"), + "core/Http.php" => array("34909", "f55dd5741e1aae8e16b50348820f1734"), + "core/Http/Router.php" => array("906", "d5c83a5ef470e70763e7fa1a7783827e"), + "core/Intl/Data/Provider/CurrencyDataProvider.php" => array("777", "551a10021fe039419af3d570587d9023"), + "core/Intl/Data/Provider/DateTimeFormatProvider.php" => array("2133", "b1df0224a07886edca7f157eab32c7f8"), + "core/Intl/Data/Provider/LanguageDataProvider.php" => array("1316", "0e0fb5afee1e58232f19a059f8b6a511"), + "core/Intl/Data/Provider/RegionDataProvider.php" => array("1480", "cd7883f3e703d99fbde35929106c1416"), + "core/Intl/Data/Resources/continents.php" => array("462", "4b09814881cec1ed513058566e3c003c"), + "core/Intl/Data/Resources/countries-extra.php" => array("1377", "923b0a830ecabd680a3ca8ac83a369ec"), + "core/Intl/Data/Resources/countries.php" => array("5468", "0fa69a37b7801fcfe287009f95ecbdd9"), + "core/Intl/Data/Resources/currencies.php" => array("7919", "fb256d28473ef1e6911693e2c7e4a649"), + "core/Intl/Data/Resources/languages.php" => array("6500", "5010596e39fbec3975afe6240395c69a"), + "core/Intl/Data/Resources/languages-to-countries.php" => array("2269", "01890c31b02f8bd55bbd10c41a9bd648"), + "core/Intl/Locale.php" => array("1092", "d69a28471ab6cc188f5b68e15d13587a"), + "core/IP.php" => array("4263", "87869784c745552835a9608a5b844fee"), + "core/LogDeleter.php" => array("3769", "4c307c949b50159db7192ddd69e3f1e7"), + "core/Log.php" => array("7772", "5640acd6c1345cc951501822ad7a2d3e"), + "core/Mail.php" => array("4992", "4f79fb8e7f1fd4452fa6e62130fb632c"), + "core/Measurable/Measurable.php" => array("720", "4165c06a37f452ceda09447d599664a8"), + "core/Measurable/MeasurableSetting.php" => array("1807", "9b4c4e04e1ca69b1e83cc5b22b9e2f09"), + "core/Measurable/MeasurableSettings.php" => array("3095", "53bb4dbafe9c98e9ffb23d409e79ee3c"), + "core/Measurable/Settings/Storage.php" => array("2650", "c523559ef17c6a40e4d150bcc18827d2"), + "core/Measurable/Type.php" => array("1409", "d39d87deb72a7e665d5c058bd1e8c8b9"), + "core/Measurable/Type/TypeManager.php" => array("764", "dc16f3ac253c9717a9cb7c83fdf171ca"), + "core/Menu/Group.php" => array("531", "1d168fb585879fe2067f42948658700b"), + "core/Menu/MenuAbstract.php" => array("12037", "5c4e6caeaeeedb22eba95a2c7db742ff"), + "core/Menu/MenuAdmin.php" => array("3663", "6be47baaac3221ccbaafb17f05c3e5c2"), + "core/Menu/MenuMain.php" => array("329", "0681af8c36bc5518ad0b4e678d990f6e"), + "core/Menu/MenuReporting.php" => array("3945", "c79532032efc43bf3f4ec4f5c1cdaf00"), + "core/Menu/MenuTop.php" => array("2244", "abc2f1bfd3d7aa3dad1883fe4d8f1a8c"), + "core/Menu/MenuUser.php" => array("2604", "bbc9c9f5d07155134121694cf549af34"), + "core/Metrics/Formatter/Html.php" => array("1320", "e24b8efb23561b391dbfea737180ab69"), + "core/Metrics/Formatter.php" => array("10172", "dd36cd68936019827eddebc51e1505ec"), + "core/MetricsFormatter.php" => array("2044", "544d2045542a6400218ab413332d7ee0"), + "core/Metrics.php" => array("17776", "16876a085528f1809dd9ef4ad593a8f3"), + "core/Nonce.php" => array("5278", "8629d0efe09f512e156e9c2fa8129ec0"), + "core/Notification/Manager.php" => array("4639", "4aaa21a3cf855bbe005fcd8e5060c4be"), + "core/Notification.php" => array("5711", "6b640f97f0108bc407fa07cfc5cf0d84"), + "core/NumberFormatter.php" => array("11211", "dbb836c9db476f29b9d4a19b1db8d68c"), + "core/Option.php" => array("7006", "ca958d52506f8437c2990c876cb06e20"), + "core/Period/Day.php" => array("2194", "ad3c9ebff44b941b4d0ee9e9a3b319d0"), + "core/Period/Factory.php" => array("4101", "b1bc16d65ab0676e1c4c9eb87ffdf1df"), + "core/Period/Month.php" => array("2989", "130dae73ac24f8f3ca9683f860d00c25"), + "core/Period/PeriodValidator.php" => array("1105", "4ecff97d7f6011041109751a1abdbd9d"), + "core/Period.php" => array("11738", "5ffcbc06fa02f336039fe1d0c8a16750"), + "core/Period/Range.php" => array("17339", "70dede775fcf2c4351cfcbeecc733c6d"), + "core/Period/Week.php" => array("2001", "dbbf6ed1e0b769d36f3d9275fd326478"), + "core/Period/Year.php" => array("2084", "cab9627d3a1401c40bac4faaff1e22e7"), + "core/Piwik.php" => array("21796", "028eeffec0e7c3dc6162154b09c7517e"), + "core/PiwikPro/Advertising.php" => array("3885", "bf081b4a20a9a2fa0b2c4b36fbdd6124"), + "core/Plugin/AggregatedMetric.php" => array("609", "6c6904e8f148dbf93e4ed973f108b61b"), + "core/Plugin/API.php" => array("2726", "743de5e429f5b95b3987400e360b6068"), + "core/Plugin/Archiver.php" => array("4274", "9211abae3264fcbed831c6f9dec3f8b6"), + "core/Plugin/ComponentFactory.php" => array("5057", "5666dc9a7a366684967fe778e38ca9ca"), + "core/Plugin/ConsoleCommand.php" => array("1450", "0fcee729503052cfd4dda3faefe64dd3"), + "core/Plugin/ControllerAdmin.php" => array("10701", "7bf1d8d48e5568e47abb0f299ba01da3"), + "core/Plugin/Controller.php" => array("41653", "77870887696277ce4ff397396058a9a4"), + "core/PluginDeactivatedException.php" => array("503", "e588dcac865d328aa7e80a56287b73ce"), + "core/Plugin/Dependency.php" => array("2910", "8cf99f6f904a90adb57bd983b0c5dc69"), + "core/Plugin/Dimension/ActionDimension.php" => array("9343", "6d879b12704d7bced2da4b9eb2913ea2"), + "core/Plugin/Dimension/ConversionDimension.php" => array("7999", "2e0dfb4aff81e444c45c4697dc8288c3"), + "core/Plugin/Dimension/DimensionMetadataProvider.php" => array("3375", "33c6b3c8da4ac7440f78cbf279aaf0fb"), + "core/Plugin/Dimension/VisitDimension.php" => array("14094", "204632db2ff50deea3678c541aebf356"), + "core/Plugin/Manager.php" => array("40494", "89a3c7cf068a8a906f78cce4fa649422"), + "core/Plugin/Menu.php" => array("10587", "ca9317c58d44817e5dc59a2be0465cb0"), + "core/Plugin/MetadataLoader.php" => array("2773", "ea985b736f03f57d84935aa35da6ef5e"), + "core/Plugin/Metric.php" => array("6286", "43a030e4f61dab13526c21c3329ce0c7"), + "core/Plugin.php" => array("17392", "d8287cf6729e89560e40b9d9ac484708"), + "core/Plugin/PluginException.php" => array("1135", "62c32d13fab8e734c958ede2f6088bfd"), + "core/Plugin/ProcessedMetric.php" => array("2055", "29f9c4e9b2af9498dc4e1ad7613cd14b"), + "core/Plugin/ReleaseChannels.php" => array("2578", "3e005c5e6c0f33ca8239465c4a8dbaa0"), + "core/Plugin/Report.php" => array("31950", "0977a62ebc4abcc1edf6ed4e874f4e66"), + "core/Plugin/RequestProcessors.php" => array("627", "82b34eaf6f8ba947d3fd8496c7a022a3"), + "core/Plugin/Segment.php" => array("10325", "6a75c70d54e9958666a1dbc24e828c7d"), + "core/Plugin/Settings.php" => array("9275", "b3448ca05c1e42e4628f91f17f80380e"), + "core/Plugin/Tasks.php" => array("5494", "2b7819ed6ef29a8b9ca975d6682acba9"), + "core/Plugin/ViewDataTable.php" => array("19359", "b0257aa89e9d00c83cbe7ccb95070fc5"), + "core/Plugin/Visualization.php" => array("28130", "d05aba629c7f23c85ba4c6324f775587"), + "core/Plugin/Widgets.php" => array("6080", "e40f161da230b167af08bc151389362b"), + "core/Profiler.php" => array("11605", "0f7eda0e1fb580514e8b3202850a618d"), + "core/ProxyHeaders.php" => array("2209", "3b4911643f8b324a9306478fabf9c7d1"), + "core/ProxyHttp.php" => array("10512", "a7f9e74ff5d118f0c56766aad9e437ee"), + "core/QuickForm2.php" => array("4030", "67a7f96d3361e949f9af570cde78b443"), + "core/RankingQuery.php" => array("12792", "3010f308493eabd03163a0daca433d83"), + "core/Registry.php" => array("1246", "90d8f95d29909388b0bc4dcc9448a242"), + "core/ReportRenderer/Csv.php" => array("4444", "da1dd74f9fe897038033cb4171edcc02"), + "core/ReportRenderer/Html.php" => array("7754", "8bd7ff2f39a98aae21c74a222738e000"), + "core/ReportRenderer/Pdf.php" => array("21790", "87708551539360cb617ff8a2acbe2dcb"), + "core/ReportRenderer.php" => array("8337", "fd7a298df1aedc425c81b60799b11be2"), + "core/ScheduledTask.php" => array("508", "6959610b9389a000e67fbc01b86cfcc4"), + "core/Scheduler/Schedule/Daily.php" => array("1220", "d289f15335f21201d604654a0c2ea67f"), + "core/Scheduler/Schedule/Hourly.php" => array("1283", "c032998f3c1eba2712466bfe81f06f74"), + "core/Scheduler/Schedule/Monthly.php" => array("4013", "c0377aac646fe4e1d0895073ff1d30c6"), + "core/Scheduler/Scheduler.php" => array("7023", "8bf77c2773f9e161355c1cbcaa8b620a"), + "core/Scheduler/Schedule/Schedule.php" => array("7548", "7daca08fa20f94788697670c65547639"), + "core/Scheduler/Schedule/Weekly.php" => array("2015", "ef8bf8b0fb5d7995b1004cac5b7f2b81"), + "core/Scheduler/TaskLoader.php" => array("790", "4c2ae49c4512f6d6e0e44f3fd6af4e67"), + "core/Scheduler/Task.php" => array("5624", "0296de70d32ac414e447a393b3c24c9f"), + "core/Scheduler/Timetable.php" => array("3518", "4350f80ceaf697d0ef3c0c3cb08e416f"), + "core/Segment.php" => array("11081", "7fc74f06701c5a9d3b62fe696a6a22eb"), + "core/Segment/SegmentExpression.php" => array("15314", "729347d96ce1b8655c850e0766b2b7bd"), + "core/Sequence.php" => array("3182", "f19d6cd7bde41febdc85db3a77b24b73"), + "core/Session.php" => array("5529", "279c40a43a729cedfa97fac01b15e787"), + "core/Session/SaveHandler/DbTable.php" => array("3411", "5ecddc4b18a768d3d676fda9b76dc97f"), + "core/Session/SessionNamespace.php" => array("706", "97c6aecfc5fef6ab7e8719806806f538"), + "core/Settings/Manager.php" => array("4723", "58a0b18b16d3e673b8a54be2a2d6b30c"), + "core/SettingsPiwik.php" => array("15551", "d47fc3b07e467e460071ae5d8601aa5c"), + "core/SettingsServer.php" => array("6751", "4e766c0eb2d25cfe612c96ffea2d47b9"), + "core/Settings/Setting.php" => array("9482", "486c9f13b94a5779ea07cdc4adf03928"), + "core/Settings/Storage/Factory.php" => array("569", "e455c550b211c666450188966825eba7"), + "core/Settings/StorageInterface.php" => array("1819", "e87f20aa72227ba837a3bace502f805f"), + "core/Settings/Storage.php" => array("3862", "22d8763e894638a53eae37c4e54ae1d0"), + "core/Settings/Storage/StaticStorage.php" => array("653", "04369019ba7b72e99e957070ab3febec"), + "core/Settings/SystemSetting.php" => array("2800", "68e4e4cf01b7c420c8382e5ee99152b0"), + "core/Settings/UserSetting.php" => array("4185", "31fe1183624232c9ed0094d83fe97788"), + "core/Singleton.php" => array("1491", "b8e1d3cee216a7b15027291d8fc2045c"), + "core/Site.php" => array("17275", "2ee1d8187207e92eda075f86a127b2fc"), + "core/TaskScheduler.php" => array("3635", "65a96edfb794c8e516912e845e3db2fe"), + "core/TCPDF.php" => array("1913", "f57e46513a5e5d330ceb9b16e573e975"), + "core/testMinimumPhpVersion.php" => array("9467", "431105b69f4d9b3c37c8632b13ffa8fe"), + "core/Theme.php" => array("4551", "a7d4831a5fb3146850e1926912f43148"), + "core/Timer.php" => array("1911", "2859f53d6bb63e55ff351df2a321bae5"), + "core/Tracker/ActionPageview.php" => array("2515", "e8d2c459f51e484583ef4b65db40663b"), + "core/Tracker/Action.php" => array("12588", "8f4db3db70a4db9a08964759fd8e870c"), + "core/Tracker/Cache.php" => array("6406", "4bafd6e9e123f3eb5138fff18dac19f5"), + "core/Tracker/Db/DbException.php" => array("275", "bc34a06d73b8485a34dba83a0995047e"), + "core/Tracker/Db/Mysqli.php" => array("9484", "b431e382369d8117144e2478ccf42f48"), + "core/Tracker/Db/Pdo/Mysql.php" => array("8626", "aa0a526866232979826f199289ba6f7a"), + "core/Tracker/Db/Pdo/Pgsql.php" => array("3208", "d33fa635427c3ef2b8b62b10af735c8c"), + "core/Tracker/Db.php" => array("8682", "26fba1411555e123f4f1b1c954f0d6d2"), + "core/Tracker/GoalManager.php" => array("32798", "e32aa7e80004392ee76a0ac95968605a"), + "core/Tracker/Handler/Factory.php" => array("1340", "0bed98e2f1edebadedf6f11e8c83aaac"), + "core/Tracker/Handler.php" => array("2767", "98cf921a6b3f3cb7d17816a10473785b"), + "core/Tracker/IgnoreCookie.php" => array("1766", "1b1d10bc1e94d6bbbc0ad04879d64174"), + "core/Tracker/Model.php" => array("14979", "01264120211ed47d1b0d77a9401d01dd"), + "core/Tracker/PageUrl.php" => array("12410", "48e37aafe11da255d1c3b65f39b5f5c3"), + "core/Tracker.php" => array("10524", "37f63bf6a8edb927646cbadf0937bf91"), + "core/Tracker/Request.php" => array("25498", "38d49a827293e920faa60db2af1aef24"), + "core/Tracker/RequestProcessor.php" => array("7384", "bc43cdc2d74865c3e366f9fd974fad7d"), + "core/Tracker/RequestSet.php" => array("6189", "ef3406d0add4c63cf84489441164d42f"), + "core/Tracker/Response.php" => array("5676", "f1b1720bab602f60bad4f95b9511fbf1"), + "core/Tracker/ScheduledTasksRunner.php" => array("2925", "9b51330fce9644c85e6e71b2d8b602b1"), + "core/Tracker/Settings.php" => array("4326", "d89ea776e9c610345592ac0a49414ffd"), + "core/Tracker/SettingsStorage.php" => array("1212", "1eb1f2939f515a0ec38e8bd733be7078"), + "core/Tracker/TableLogAction/Cache.php" => array("4075", "2a08dee9d8d8843c0103bf47298b3004"), + "core/Tracker/TableLogAction.php" => array("10251", "695dd0965e9514fae2b5e71d171e3ae6"), + "core/Tracker/TrackerCodeGenerator.php" => array("8432", "83707b2ebb9ece6a4b306e0d094e6dde"), + "core/Tracker/TrackerConfig.php" => array("791", "60712345d7cc78af362d3f114ced2bfa"), + "core/Tracker/VisitExcluded.php" => array("10355", "e69da7c1a7a35955cdcb299e26293832"), + "core/Tracker/Visit/Factory.php" => array("1441", "d26bc067d88ecf846cc6c159d3a14cb7"), + "core/Tracker/VisitInterface.php" => array("592", "b171ec552c3f469bccd33ab6362737ea"), + "core/Tracker/VisitorNotFoundInDb.php" => array("240", "3f1ce6ea862903ea111db7bc74068ba2"), + "core/Tracker/Visitor.php" => array("1465", "0fdadbff85b57b51a81624ad801a825c"), + "core/Tracker/VisitorRecognizer.php" => array("9807", "d42f48ecd9cac2095c2b03469765fa69"), + "core/Tracker/Visit.php" => array("20816", "9ddc3a0178637bec447b9257fc7d5d02"), + "core/Tracker/Visit/ReferrerSpamFilter.php" => array("2239", "c68ffcdf46aef2567b3408fa6fd14206"), + "core/Tracker/Visit/VisitProperties.php" => array("1584", "ff6899b2d0678289cb183b5c6b9ad16a"), + "core/Translate.php" => array("3125", "c4190ed894e9afb6952365264bebbebc"), + "core/Translation/Loader/DevelopmentLoader.php" => array("1885", "9eac2043ffac54cfa6d7cc03f740b0b6"), + "core/Translation/Loader/JsonFileLoader.php" => array("1430", "3fce4290ef7b3c1c06e70e17e91766bc"), + "core/Translation/Loader/LoaderCache.php" => array("1444", "3b1fed6f5683d016ee045b06b8f5551c"), + "core/Translation/Loader/LoaderInterface.php" => array("532", "0dd2b5f00463fa4d3dc9e8dff3cffc93"), + "core/Translation/Transifex/API.php" => array("4383", "3500e306909cbe7d7035869ca29c5c01"), + "core/Translation/Translator.php" => array("7633", "e64106500cd9dbb11f4ee9725020e2f4"), + "core/Twig.php" => array("17006", "9514b91e817a9ada67084961e8ef2eb3"), + "core/Unzip.php" => array("1299", "978c4f95e999916f9961c5006cae1f54"), + "core/UpdateCheck.php" => array("3501", "2c03ef6b19921b7347642e7d26c1065e"), + "core/UpdateCheck/ReleaseChannel.php" => array("2521", "30d7ecfe5b45862cad2bd73ec937925a"), + "core/Updater.php" => array("24501", "46c9be4ce7893b0924c030212a625ad9"), + "core/Updater/UpdateObserver.php" => array("4101", "f6bbc3083ffbe3a77f5e5fd387df8042"), + "core/Updates/0.2.10.php" => array("2477", "3a305a654a6f387ceb3e62a71249643c"), + "core/Updates/0.2.12.php" => array("1077", "3fc4ee2d96cedc042b16b05325f5688f"), + "core/Updates/0.2.13.php" => array("868", "7cc89eb32960b96182bb62b894295349"), + "core/Updates/0.2.24.php" => array("1069", "6e01fa473bfc708a6f7d6f82c5d3f8b3"), + "core/Updates/0.2.27.php" => array("2771", "c18926a783b1e60f701b897526dda8d0"), + "core/Updates/0.2.32.php" => array("1175", "3b16a01a4ea5fde2055bbd202f0f46be"), + "core/Updates/0.2.33.php" => array("1268", "aaa1c83b60503edc36190dca810fa895"), + "core/Updates/0.2.35.php" => array("670", "2bbaaaba65dea301219bc89bf9d5fe49"), + "core/Updates/0.2.37.php" => array("725", "c3e1353a09e4b88d0c9c4a191e929a7d"), + "core/Updates/0.4.1.php" => array("889", "262f1f2e698eba2b038687013b2de2be"), + "core/Updates/0.4.2.php" => array("1169", "f4f98e5c8c5fb169263e725935593692"), + "core/Updates/0.4.4.php" => array("701", "29d6e74c9a2899d657dffd85f855dd8e"), + "core/Updates/0.4.php" => array("1221", "60542535c4374ba4f0853b07d27282d8"), + "core/Updates/0.5.4.php" => array("2041", "ea6f0b3156fb67e17ceef9315bc51a09"), + "core/Updates/0.5.5.php" => array("1379", "f30d016e2d17fe730832cc5f275fa6b0"), + "core/Updates/0.5.php" => array("2161", "d4156e3a54951246f10c60ffc84ee61b"), + "core/Updates/0.6.3.php" => array("1364", "21c6aeb624762cd1e822964645950b8f"), + "core/Updates/0.6-rc1.php" => array("4626", "a4118c4c24113c62e428d07ba5ea8c28"), + "core/Updates/0.7.php" => array("677", "7b00ed002817e4bc98a86c2380fbbfab"), + "core/Updates/0.9.1.php" => array("1534", "51b98c724f70887bc32745522330d6f5"), + "core/Updates/1.10.1.php" => array("563", "9dc4c3b8fa7f81452578f87d5a1eed45"), + "core/Updates/1.10.2-b1.php" => array("743", "d00be0f12a51d95f5a4b662708f06e5c"), + "core/Updates/1.10.2-b2.php" => array("757", "efe0ab2d44c51fce419f965c3c1a10d5"), + "core/Updates/1.10-b4.php" => array("572", "a6025f37edf0d8abd2d743f494562bad"), + "core/Updates/1.11-b1.php" => array("571", "0973849010e0b884602008365d099947"), + "core/Updates/1.12-b15.php" => array("493", "f06549e234a7db7e9fb552a3a457d8f6"), + "core/Updates/1.12-b16.php" => array("733", "2d9b656afde21317adf03956072ca6bb"), + "core/Updates/1.12-b1.php" => array("757", "28335b9d3acece7f584d7d89c305dfb4"), + "core/Updates/1.1.php" => array("1012", "81dafb5e782e20fe496217e4b0565a89"), + "core/Updates/1.2.3.php" => array("1132", "985a4b05f43a651e3bc94d1fe0147ae9"), + "core/Updates/1.2.5-rc1.php" => array("894", "f86d83f7d4ec917329a1db2503fe618d"), + "core/Updates/1.2.5-rc7.php" => array("684", "fd27b3e076f452e86b0f561d03c1f793"), + "core/Updates/1.2-rc1.php" => array("7092", "85c37f26ecbfff9e17ecb925725a4650"), + "core/Updates/1.2-rc2.php" => array("474", "47b9069ce3ee5a6f303e4686cdeb9f54"), + "core/Updates/1.4-rc1.php" => array("829", "2342e59e3717d805e0c63ddd32adfb92"), + "core/Updates/1.4-rc2.php" => array("1662", "67d3b8144c608882e1d0e0ce1ccb5b6f"), + "core/Updates/1.5-b1.php" => array("2430", "c3852518456f2e944887a2b509c22eaa"), + "core/Updates/1.5-b2.php" => array("1170", "261ec2c25bdb8ccceece65b21ad838ca"), + "core/Updates/1.5-b3.php" => array("2888", "10fdbaeb429a338d9df454b7e05def7e"), + "core/Updates/1.5-b4.php" => array("653", "80a7424a6fecdfd3bd0c9ff6001adf6f"), + "core/Updates/1.5-b5.php" => array("782", "866864d5ce6e68fd9e7005a8542a1f77"), + "core/Updates/1.5-rc6.php" => array("473", "1cd674d67f706537ef9cefe3af1f2f6d"), + "core/Updates/1.6-b1.php" => array("3156", "0b65aef2b19f4005afd0fe1dd45a33ca"), + "core/Updates/1.6-rc1.php" => array("469", "0b41483c93eb2ca5d664d24adff91412"), + "core/Updates/1.7.2-rc5.php" => array("758", "ceda0cff1323e822a7ec3d776dc56ef7"), + "core/Updates/1.7.2-rc7.php" => array("1392", "57026c93418ffec3e3f2d519d7164633"), + "core/Updates/1.7-b1.php" => array("883", "1ecc9fe3bf66b68bd3f62e664a423dff"), + "core/Updates/1.8.3-b1.php" => array("4279", "449699e4cbdb20695790dd4c4f26f686"), + "core/Updates/1.8.4-b1.php" => array("5691", "e5ed273cf444a4f9854b97ae9bb14778"), + "core/Updates/1.9.1-b2.php" => array("839", "5fca436482aacea98f4880e72c84f58f"), + "core/Updates/1.9.3-b10.php" => array("570", "e5fd5fe6c86d46f0928a2bdd95193bf1"), + "core/Updates/1.9.3-b3.php" => array("728", "87370c33291bf38823f9969cce874544"), + "core/Updates/1.9.3-b8.php" => array("813", "ed75af638541a1a8ae066e9370c0c358"), + "core/Updates/1.9-b16.php" => array("1581", "0fa923796f1328ea2613e641b5b22797"), + "core/Updates/1.9-b19.php" => array("1057", "49be2a481119b28fbe48e06a998ea14d"), + "core/Updates/1.9-b9.php" => array("1677", "6e72fb7dbb17aa86ce0bc3d3f69f711d"), + "core/Updates/2.0.3-b7.php" => array("1843", "b9ddbc0d38ad0459c7dadbf36a34d5bf"), + "core/Updates/2.0.4-b5.php" => array("2736", "b573443eea5ff7538b6fb923e5d196e6"), + "core/Updates/2.0.4-b7.php" => array("1758", "6cec5e6645177efcd6b67732905e97e1"), + "core/Updates/2.0.4-b8.php" => array("2179", "150d877dd6c1da69e91cff16f217c9a1"), + "core/Updates/2.0-a12.php" => array("1290", "a955080340d2d8a00a7d869950e779af"), + "core/Updates/2.0-a13.php" => array("2415", "c26723a290ecd3134298b186bc8bdfb8"), + "core/Updates/2.0-a17.php" => array("1036", "c462e1a6bd3e18a3bb2eb662e4565ee0"), + "core/Updates/2.0-a7.php" => array("955", "1f3938b8666f253b4c4ca117455779ef"), + "core/Updates/2.0-b10.php" => array("445", "5e17edc9cc8d1c6eb20a390d1c7b34cd"), + "core/Updates/2.0-b13.php" => array("1021", "e9de9e9533321a8fbfb0978ea1343b72"), + "core/Updates/2.0-b3.php" => array("1212", "c7ae0aba92a6986b9ee54108ea2097e9"), + "core/Updates/2.0-b9.php" => array("736", "a2ca1e88c9cf57bab28c182fafdc5a70"), + "core/Updates/2.0-rc1.php" => array("466", "057dcb8965b5d1175e80fae9602ca91d"), + "core/Updates/2.10.0-b10.php" => array("1215", "fff11dca4ecd3c1d78311518652bf98f"), + "core/Updates/2.10.0-b4.php" => array("549", "7385e456b6646ac81985582153e6cd33"), + "core/Updates/2.10.0-b5.php" => array("10242", "54fc7622b884789d0e9c9840424ae22f"), + "core/Updates/2.10.0-b7.php" => array("1167", "414dc989b0f8424e813b4415f9b65300"), + "core/Updates/2.10.0-b8.php" => array("505", "72ce572b0b9d6163b45bca1dd587ce18"), + "core/Updates/2.11.0-b2.php" => array("2185", "596d232300284495c87442f2e752f81c"), + "core/Updates/2.11.0-b4.php" => array("1216", "dcd526c41c7551ee75a660a0240f6774"), + "core/Updates/2.11.0-b5.php" => array("469", "c68872040f9cd3fb8f720e06c8c0813b"), + "core/Updates/2.11.1-b4.php" => array("897", "f0131c7824022c39876cad6b4944c0e6"), + "core/Updates/2.1.1-b11.php" => array("5727", "ba49bdbacbe8902de01d05e798e7d31c"), + "core/Updates/2.13.0-b3.php" => array("506", "851f79ff947535ed4e41f6e5f56b283a"), + "core/Updates/2.13.1.php" => array("1138", "63d681aea66e936bb2b4eeeb8eea149e"), + "core/Updates/2.14.0-b1.php" => array("1036", "cf2e76416fb319011e4d4f3ec3ca79e6"), + "core/Updates/2.14.0-b2.php" => array("1120", "053dfa2b35a8a8500d6af640d11a0d63"), + "core/Updates/2.14.2.php" => array("4205", "fd64bd2ebeeb7fd7c051bdca1b382cd9"), + "core/Updates/2.15.0-b12.php" => array("1127", "9b85c6fd9582b421b02a1ac776733c9f"), + "core/Updates/2.15.0-b16.php" => array("1077", "775cb8dd98ed67001479c29a7e209520"), + "core/Updates/2.15.0-b17.php" => array("1059", "d952d26e2a3f1552b5321e2d7e4ef27c"), + "core/Updates/2.15.0-b20.php" => array("1013", "882b41ac5b5de2ab15f9232a52da3df7"), + "core/Updates/2.15.0-b3.php" => array("739", "e6db95ffae19dacb73d75b48221afd10"), + "core/Updates/2.15.0-b4.php" => array("551", "c7675e9e5ee17f43ef6761448fad804c"), + "core/Updates/2.15.0.php" => array("497", "cd36a0901f4c47742992e9eea69b172d"), + "core/Updates/2.16.0-rc2.php" => array("620", "baec837efa66ab7ef5a6078fb71fdc8a"), + "core/Updates/2.2.0-b15.php" => array("644", "fac6fbd6084542970778c1e9fabf6c2b"), + "core/Updates/2.2.3-b6.php" => array("466", "36ca6560082c5b599d82366c08388aae"), + "core/Updates/2.3.0-rc2.php" => array("492", "2858fd746e1940b5f4f2a778419708d6"), + "core/Updates/2.4.0-b1.php" => array("658", "0c7e7bc601300a8207ce17bc3d624a1f"), + "core/Updates/2.4.0-b2.php" => array("599", "1195001194309c1b1f29ce3aabc22533"), + "core/Updates/2.4.0-b3.php" => array("736", "5e2db22ad86414c8f57af710bedb2bc3"), + "core/Updates/2.4.0-b4.php" => array("767", "3cc1e08e2bae9e4ce01715ed2e2fc169"), + "core/Updates/2.4.0-b6.php" => array("509", "101711f41bb4543ed776314d301b1c53"), + "core/Updates/2.4.0-b8.php" => array("667", "e3b27e5255b57bf4b2d66f5b41226e76"), + "core/Updates/2.5.0-b1.php" => array("891", "c4d92f5cde5a74ceb338c729c11de28e"), + "core/Updates/2.5.0-rc2.php" => array("2027", "758eab9dd20862594ee9ee85786e163a"), + "core/Updates/2.5.0-rc4.php" => array("489", "6f23e25ac90ff1fc173d4d4e7db1d8b5"), + "core/Updates/2.6.0-b1.php" => array("687", "f5a9023a8753803876a6e87a490cbeeb"), + "core/Updates/2.7.0-b2.php" => array("510", "8d712b7f4f29a8ce1b0f036a7d26ac5f"), + "core/Updates/2.7.0-b4.php" => array("587", "0209139d7bde416cf5b31b9fba6b6f2f"), + "core/Updates/2.9.0-b1.php" => array("2890", "1d4fdf521b28b4c087729cf5eab93a3c"), + "core/Updates/2.9.0-b7.php" => array("2487", "205527c848fa8df742cd1ed48e59bc59"), + "core/Updates.php" => array("4875", "0968bbf770aea2cee040155397bb19e7"), + "core/UrlHelper.php" => array("10108", "09e30ea3a4e5a288fd58e7aa112746ea"), + "core/Url.php" => array("22690", "7b2f6e9c8569c71ccde556a00edd212b"), + "core/Version.php" => array("770", "a12dc206410108f611ecb05f1d31802c"), + "core/ViewDataTable/Config.php" => array("24035", "672de85be723731ad2e4805eaca35be8"), + "core/ViewDataTable/Factory.php" => array("9731", "bba600c7fa55cc8a28b9e9dd1bd0327e"), + "core/ViewDataTable/Manager.php" => array("13927", "f66c1256f3ca149516695788cb3bd1fb"), + "core/ViewDataTable/RequestConfig.php" => array("9711", "5399c3a73f461202fc90a9ff33762d46"), + "core/ViewDataTable/Request.php" => array("4255", "0da3ccc7845bb59fb65364f47048a571"), + "core/View/OneClickDone.php" => array("2344", "5ada6f6f8cdddbaba7abbc859dda7688"), + "core/View.php" => array("14062", "b100985df09dcc63fcad6de350ebfb33"), + "core/View/RenderTokenParser.php" => array("2029", "b5501cae199ae4434b9b304cdf2bcb04"), + "core/View/ReportsByDimension.php" => array("4198", "8bb5dd7ad80b63057c6586169e851eaa"), + "core/View/UIControl.php" => array("4545", "74127c5ca7208bd83810226c785ab825"), + "core/View/ViewInterface.php" => array("414", "27f1fe14f80dcdd4f525a1c383e90472"), + "core/Visualization/Sparkline.php" => array("4644", "547d13219346c9ccbfbb27ad74754613"), + "core/WidgetsList.php" => array("9079", "b0e3b6414e230b8a06d6928e8251a136"), + "index.php" => array("730", "a401ca678920558c7b9a3a9530d765b6"), + "js/index.php" => array("236", "43ac89baca7c8fbfa63c476653222cff"), + "js/LICENSE.txt" => array("1526", "e8eb3ba0e95713ae6468edbae20c1aa9"), + "js/piwik.js" => array("286459", "25de8861a9fd063cc515ac99cac8e5fb"), + "js/README.md" => array("2348", "4d29433c3712818b797a4ff2793d0c63"), + "js/tracker.php" => array("1324", "2f2614547b3d7c806c9f853dc7355c2b"), + "lang/am.json" => array("3645", "063c791f2acfa861cbccc593db6897ed"), + "lang/ar.json" => array("18975", "1d33ca158bee52a3cd2f823479a39208"), + "lang/be.json" => array("25876", "caf0de80b777b6cef0922091471a0dc2"), + "lang/bg.json" => array("43386", "23567ff51a7807a13d157d561d40e9ba"), + "lang/bn.json" => array("2937", "c6de3aef15ba262e1d73908e24407461"), + "lang/bs.json" => array("19000", "4ae4d2c9a62e1a503f37b10681672fe7"), + "lang/ca.json" => array("26384", "3f02eca1e9e49993b3e76788d45d6220"), + "lang/cs.json" => array("35074", "609d4eea43f227fc353d5119e0528253"), + "lang/cy.json" => array("16511", "06373470eee9f849886394e01abfdfab"), + "lang/da.json" => array("31775", "a1e2d857099e30076d49bcdf381b098e"), + "lang/de.json" => array("36861", "a5fb479ae2a40ab4cc630e9f366ce4bf"), + "lang/dev.json" => array("78", "dc26f1ebd9eefbd146e1506c2ba0affa"), + "lang/el.json" => array("54689", "fec64da3a4cfd062d1edd4dbc8bb7b3a"), + "lang/en.json" => array("33208", "0f873bd2698e69052eb95b51255daed9"), + "lang/es.json" => array("35362", "2b4a5aff766002ebfb486a8b0e0eb30a"), + "lang/et.json" => array("16106", "78bbe57f853117bbf126a9f50a1c534c"), + "lang/eu.json" => array("6935", "c0040785ea654a0138f747a167d2846b"), + "lang/fa.json" => array("36353", "4f332125a8fe19c0deca956550a64685"), + "lang/fi.json" => array("31769", "d7d76299cdf4f1f0c359a45c33595b81"), + "lang/fr.json" => array("37224", "eabfd0792dce3f7400e1208f00018e09"), + "lang/gl.json" => array("5938", "41fb2172ccdbcda419ee33651304c1a1"), + "lang/he.json" => array("16691", "566ef7dc0a8749e158f74f1d2acec209"), + "lang/hi.json" => array("51348", "4a233e823a835097cc65cabe4b947aea"), + "lang/hr.json" => array("20505", "e6b1844136eca09f5c3cee3279df0f70"), + "lang/hu.json" => array("21822", "d2d0488b7bcf67004523605f50a53500"), + "lang/id.json" => array("30629", "48e873feff9395d24ee75649de9c0444"), + "lang/is.json" => array("8843", "0ecf1e3aac19c9adf010dfc30a215fa7"), + "lang/it.json" => array("35589", "67a2fb97cc6046792b4d069056edb8f9"), + "lang/ja.json" => array("40566", "7ef5cd2517a42393b27a811faa14e174"), + "lang/ka.json" => array("21654", "0532a87e1710f73429d0916b89272a99"), + "lang/ko.json" => array("31547", "cf5058c706d2b74250726ed2b2692d57"), + "lang/lt.json" => array("16145", "aa54dd2466aa978ba78d11251878a427"), + "lang/lv.json" => array("16100", "c0f8391417393fafa24433c84b7b2d71"), + "lang/nb.json" => array("34210", "658f42c7d2f928833651b5d22165d352"), + "lang/nl.json" => array("34457", "e6f1444ab0d1feb0862063da08b7d16c"), + "lang/nn.json" => array("16602", "78a2d48580ec85488c5f315a39456edd"), + "lang/pl.json" => array("28339", "76c4b409468f034607efc7e418af3338"), + "lang/pt-br.json" => array("36066", "7e61e00a3bfc7bafd827d67fb0d012ea"), + "lang/pt.json" => array("20578", "e7868b16d89e8e0f9f7c95ee37540555"), + "lang/README.md" => array("456", "ec21de9a7f2c2afd61edac08a28a3907"), + "lang/ro.json" => array("30511", "8b370e1e1a6fd3c099e81a3ba93d8d6d"), + "lang/ru.json" => array("49181", "58c2857da8c1a4fa2e3b84ebd0a2051b"), + "lang/sk.json" => array("35495", "fd91537bbeac9382c71cefad7c6261be"), + "lang/sl.json" => array("32010", "e0b507eb93208f616785d7b5f2e33dac"), + "lang/sq.json" => array("36133", "83ef99905444c61fdec64dd5767a250d"), + "lang/sr.json" => array("33553", "3e1e1a0a7385c98d29b2973bcb52815c"), + "lang/sv.json" => array("34635", "e022d9c203ef4e97575d93b35614bfb4"), + "lang/ta.json" => array("16576", "97e1b5e6ca86235e6db944157c0ffc2f"), + "lang/te.json" => array("6518", "d147a8a90089ab890341b82b2afcbf4c"), + "lang/th.json" => array("34896", "a2421bef979fc96ca5af30c493f8f6b5"), + "lang/tl.json" => array("28375", "35a321ffc37880cdc58be6c4c988f162"), + "lang/tr.json" => array("18066", "2975614f7a4e2a54b29247a4e07ffb32"), + "lang/uk.json" => array("15983", "f9fed854365adf35fd88e17957aeb30d"), + "lang/vi.json" => array("34464", "bf499a6e773b736a47daa03c1572b06a"), + "lang/zh-cn.json" => array("25792", "c4a0849f2734a3e8a4e05fbda2f466cf"), + "lang/zh-tw.json" => array("12671", "378662dfef7d6ea15a79760a3703c4dc"), + "LEGALNOTICE" => array("7486", "c23b7e200217d393650f6149356e0b07"), + "libs/bower_components/angular/angular-csp.css" => array("535", "68c905585cc349c10329b000eacc02ba"), + "libs/bower_components/angular/angular.js" => array("789267", "4e41f4d21f26771e9454f51f5d36e8ed"), + "libs/bower_components/angular/angular.min.js" => array("108028", "c5d22c0a6f50fd66ac9ee980a2b7ac61"), + "libs/bower_components/angular/angular.min.js.gzip" => array("40123", "f15fa37289b1c56fae579c6f09446776"), + "libs/bower_components/angular/angular.min.js.map" => array("290859", "c4277d7fb13c26304b6b0f73679bcacf"), + "libs/bower_components/angular-animate/angular-animate.js" => array("77784", "d27a63f0f64cb90682403cb8b982e51e"), + "libs/bower_components/angular-animate/angular-animate.min.js" => array("11295", "869301b9f7f1828f27b18acc262d166a"), + "libs/bower_components/angular-animate/angular-animate.min.js.map" => array("32715", "26f1bd8d743cea56fa3f4f78b1a01d54"), + "libs/bower_components/angular-animate/bower.json" => array("154", "467d2cba00f80d92f104ea621ae7a952"), + "libs/bower_components/angular-animate/package.json" => array("613", "a51d16e36926e8e99c4b3caca32084b8"), + "libs/bower_components/angular-animate/README.md" => array("2270", "a2c8288dde71cde3c94e2e4e3576b622"), + "libs/bower_components/angular/bower.json" => array("114", "a9e0387f4fdfe0ddd7c59a81c7db3f18"), + "libs/bower_components/angular-cookies/angular-cookies.js" => array("5824", "80acc564cbad7d3a0d1586b588ea004a"), + "libs/bower_components/angular-cookies/angular-cookies.min.js" => array("825", "13a33148e7124f6c200e05ac83c505ac"), + "libs/bower_components/angular-cookies/angular-cookies.min.js.map" => array("2253", "4cd4cef713599958917fec5cf0c983f7"), + "libs/bower_components/angular-cookies/bower.json" => array("154", "a0cb0981bc1b084d8416b6f2b1908419"), + "libs/bower_components/angular-cookies/package.json" => array("608", "92763f49ceefac127fa420d2674c46f6"), + "libs/bower_components/angular-cookies/README.md" => array("2265", "b142b6f8c430bdd4abd3349d04c9cc2a"), + "libs/bower_components/angular-mocks/angular-mocks.js" => array("68823", "a2b7d8ffbfdddc8b526298faaaf589de"), + "libs/bower_components/angular-mocks/bower.json" => array("150", "d29ede7cea19ba5df25cdc4985f69883"), + "libs/bower_components/angular-mocks/package.json" => array("616", "847d5d874fa6ad8ab52c99a02ce7cbcb"), + "libs/bower_components/angular-mocks/README.md" => array("1946", "44e1feae097f7459e6a769e9a20a3124"), + "libs/bower_components/angular/package.json" => array("575", "b4ed7afa49335c7d4f2556151501f36a"), + "libs/bower_components/angular/README.md" => array("2144", "a4bc333832e474821a91ee06c22bced8"), + "libs/bower_components/angular-sanitize/angular-sanitize.js" => array("21815", "9979030c3ed2a947f3fdef2287a253ba"), + "libs/bower_components/angular-sanitize/angular-sanitize.min.js" => array("4566", "e352a441354986f151172a16b0a80a5a"), + "libs/bower_components/angular-sanitize/angular-sanitize.min.js.map" => array("10670", "0b3eceabfc5f7ee474d2f91b4c0b48f1"), + "libs/bower_components/angular-sanitize/bower.json" => array("156", "64bc4f9113d812c96b357a9f390cd85e"), + "libs/bower_components/angular-sanitize/package.json" => array("615", "992bf854fb7cd008ecd4de313ce3c234"), + "libs/bower_components/angular-sanitize/README.md" => array("2279", "b6dca0580c4087849e406a7eebe6a428"), + "libs/bower_components/chroma-js/bower.json" => array("536", "dc8295dbf13eaafe6421b98bbb097c12"), + "libs/bower_components/chroma-js/chroma.js" => array("56192", "0c55632f0d43674b4041c0d053abe79a"), + "libs/bower_components/chroma-js/chroma.min.js" => array("32010", "79d1574b5c9585f7c8e2d0595cf1750f"), + "libs/bower_components/chroma-js/LICENSE" => array("1497", "3cd2b215a2f79da827342dbbf3a1733f"), + "libs/bower_components/chroma-js/LICENSE-colors" => array("806", "a483ff87f333ccabc3ce541ae8c169bc"), + "libs/bower_components/chroma-js/Makefile" => array("623", "9db21bf228f259284a68e923e13992cf"), + "libs/bower_components/chroma-js/package.json" => array("833", "03558c034a09100407e7362e09e167fe"), + "libs/bower_components/chroma-js/readme.md" => array("2038", "90a323713ba39a95abd553084fbb4311"), + "libs/bower_components/html5shiv/bower.json" => array("193", "271d2b708f1e2fdf3943fd4af13d957f"), + "libs/bower_components/html5shiv/dist/html5shiv.js" => array("10189", "ee68da404bd6cbdab3adb3bf9219c207"), + "libs/bower_components/html5shiv/dist/html5shiv.min.js" => array("2636", "3044234175ac91f49b03ff999c592b85"), + "libs/bower_components/html5shiv/dist/html5shiv-printshiv.js" => array("16143", "d0d9a764f9d376be88401200ad930100"), + "libs/bower_components/html5shiv/dist/html5shiv-printshiv.min.js" => array("4272", "8c3c50c95caa7cef54d2c8720de4db37"), + "libs/bower_components/html5shiv/Gruntfile.js" => array("1177", "a6418754499f873e31aff73c8d2dc8e3"), + "libs/bower_components/html5shiv/package.json" => array("340", "681176cfbf9782e1da3a8ce7f4460df8"), + "libs/bower_components/html5shiv/readme.md" => array("8579", "c6be7162b534841b39e49fcee33e257d"), + "libs/bower_components/jquery/bower.json" => array("425", "216eec46a750884fc581801b695b7ac1"), + "libs/bower_components/jquery/dist/jquery.js" => array("282766", "3d93b072d14f2bd1ede58f4847f537fd"), + "libs/bower_components/jquery/dist/jquery.min.js" => array("95821", "d4a20d75db01a33e2d65e303ce5c34f3"), + "libs/bower_components/jquery/dist/jquery.min.map" => array("141666", "d5771a2316a6a8fa000ad94aaae84d60"), + "libs/bower_components/jQuery.dotdotdot/bower.json" => array("582", "17c14fbdc64e32ced55f1f53e7cace83"), + "libs/bower_components/jQuery.dotdotdot/src/js/jquery.dotdotdot.js" => array("12463", "ac3e09c806432a6db99bc8b089c0374c"), + "libs/bower_components/jQuery.dotdotdot/src/js/jquery.dotdotdot.min.js" => array("6117", "a2fe486543d8a20e2bd5056b1126c3c8"), + "libs/bower_components/jquery/MIT-LICENSE.txt" => array("1099", "14f9e86644baad8332932dc6fe2af261"), + "libs/bower_components/jquery-mousewheel/bower.json" => array("267", "ef2a804f62fcf20dcf1694f80609f699"), + "libs/bower_components/jquery-mousewheel/ChangeLog.md" => array("3328", "2c62f63033999287a6d2db448515eeab"), + "libs/bower_components/jquery-mousewheel/jquery.mousewheel.js" => array("8279", "426ff44fdde60c9e548a11806e5e9681"), + "libs/bower_components/jquery-mousewheel/jquery.mousewheel.min.js" => array("2777", "639d1c35a685d111aa4a509a2dbf660c"), + "libs/bower_components/jquery-mousewheel/LICENSE.txt" => array("1084", "3029474551341261688c4e3433afadd7"), + "libs/bower_components/jquery-mousewheel/README.md" => array("2906", "708cee278dd1a949b7152115452c50c9"), + "libs/bower_components/jquery-placeholder/bower.json" => array("91", "537585672ca2d90f3317ef26607427cc"), + "libs/bower_components/jquery-placeholder/demo.html" => array("3232", "8145dd812172b8310b1ab0c509eacee4"), + "libs/bower_components/jquery-placeholder/jquery.placeholder.js" => array("5297", "d7098f9b5df7c2fdf5119c7428a19441"), + "libs/bower_components/jquery-placeholder/LICENSE-MIT.txt" => array("1075", "0146cb31436f780624be47ce08eec616"), + "libs/bower_components/jquery-placeholder/README.md" => array("3918", "21040d5d644d4661824c5c8aa8fec6af"), + "libs/bower_components/jquery.scrollTo/bower.json" => array("557", "3cb24b035d81c24ba8b6cb024f0cd4b7"), + "libs/bower_components/jquery.scrollTo/jquery.scrollTo.js" => array("5606", "206af780ad37f3fdc994527beeabaa3a"), + "libs/bower_components/jquery.scrollTo/jquery.scrollTo.min.js" => array("2712", "a2079fc42e9afdfcceb62088df47d8a0"), + "libs/bower_components/jquery.scrollTo/LICENSE" => array("1101", "f5f6a434ca35e83565e47923006fb721"), + "libs/bower_components/jquery.scrollTo/package.json" => array("564", "bf772f566cd1e23c4e25e4e596b49256"), + "libs/bower_components/jquery.scrollTo/README.md" => array("3185", "953dc68cc4aa0f61ef869efb9f3796ac"), + "libs/bower_components/jquery.scrollTo/scrollTo.jquery.json" => array("972", "5d9d521674f53ed5b53a1b5aea220e9f"), + "libs/bower_components/jquery/src/ajax.js" => array("21812", "adcd45ad7a0a90cf7416c6b3c40c9272"), + "libs/bower_components/jquery/src/ajax/jsonp.js" => array("2516", "c09be897ccc55b18fd553df2e63b0ace"), + "libs/bower_components/jquery/src/ajax/load.js" => array("1681", "370b63e7003b80224c02116059d4adf9"), + "libs/bower_components/jquery/src/ajax/parseJSON.js" => array("1476", "6a6188a3844858d60c3e2c62db3a1bc6"), + "libs/bower_components/jquery/src/ajax/parseXML.js" => array("650", "7d6ed14794f48610ae5418e3194b85f7"), + "libs/bower_components/jquery/src/ajax/script.js" => array("1964", "0e450053763ce6335cc2d4b853c88496"), + "libs/bower_components/jquery/src/ajax/var/nonce.js" => array("73", "c0fee61b182d6c03a455585f74b5e1bd"), + "libs/bower_components/jquery/src/ajax/var/rquery.js" => array("40", "86cc4813fe7ee092d0f25de3403c1811"), + "libs/bower_components/jquery/src/ajax/xhr.js" => array("5786", "6640ee85cecf8fddfb2cf74998f8acb1"), + "libs/bower_components/jquery/src/attributes/attr.js" => array("7263", "7e197ba681b707843e59db99ca8e8ebb"), + "libs/bower_components/jquery/src/attributes/classes.js" => array("4116", "30f07e5fa34ea0661516ccb75476fb7a"), + "libs/bower_components/jquery/src/attributes.js" => array("200", "29b8b42452bec1b7049873dd2c0ba91d"), + "libs/bower_components/jquery/src/attributes/prop.js" => array("3138", "1d7d0d70f78dfc5158c22b5d40043720"), + "libs/bower_components/jquery/src/attributes/support.js" => array("1994", "5075ca7ab8b92f86a75193ab59ede719"), + "libs/bower_components/jquery/src/attributes/val.js" => array("4303", "d89462be24c01fc8c724e63f99ac668e"), + "libs/bower_components/jquery/src/callbacks.js" => array("5506", "de1c5e8d7bc16f7d104b37b13a99681f"), + "libs/bower_components/jquery/src/core/access.js" => array("1219", "5634def0482ba0d43ed3b43bfe8eba7b"), + "libs/bower_components/jquery/src/core/init.js" => array("3716", "d958cd58bcb263413dacdcb8091ccb5f"), + "libs/bower_components/jquery/src/core.js" => array("12500", "444edfd62c1d0cb77af387d1e3aadddf"), + "libs/bower_components/jquery/src/core/parseHTML.js" => array("938", "431b6e91175f25ab58dedad61f1bf849"), + "libs/bower_components/jquery/src/core/ready.js" => array("3896", "a108ade83202e0e549385c12669d5f18"), + "libs/bower_components/jquery/src/core/var/rsingleTag.js" => array("91", "0f07e690c168f77a90f4e0886c76e74b"), + "libs/bower_components/jquery/src/css/addGetHookIf.js" => array("785", "072c24e016cdc8d3f50acd34e66f24e0"), + "libs/bower_components/jquery/src/css/curCSS.js" => array("3307", "8eb595b2d35a5ae0d66154060759d875"), + "libs/bower_components/jquery/src/css/defaultDisplay.js" => array("1907", "a903e2da9c918dacabc40908ee01db2a"), + "libs/bower_components/jquery/src/css/hiddenVisibleSelectors.js" => array("542", "b605ad8f6c7ee57370a5095a940a090c"), + "libs/bower_components/jquery/src/css.js" => array("14612", "3284292cf73000b5107953986b30ddfb"), + "libs/bower_components/jquery/src/css/support.js" => array("4823", "26b9246b5b1b0f94bb9318542a077734"), + "libs/bower_components/jquery/src/css/swap.js" => array("555", "aef668d22cdb39ba3e691caa32294123"), + "libs/bower_components/jquery/src/css/var/cssExpand.js" => array("70", "2d8ac6725d0ff4faaa415eb25708a9ed"), + "libs/bower_components/jquery/src/css/var/isHidden.js" => array("355", "13a6f243bea6c82a6311ff011d3693fa"), + "libs/bower_components/jquery/src/css/var/rmargin.js" => array("45", "028581c37c9dda64dc81ce7295ea5f34"), + "libs/bower_components/jquery/src/css/var/rnumnonpx.js" => array("113", "2a34d3f4def5ae34a4cfc90766f92085"), + "libs/bower_components/jquery/src/data/accepts.js" => array("567", "0e7dfc3a20102946046bf6a6de96e7fb"), + "libs/bower_components/jquery/src/data.js" => array("8509", "515aaba78a521c12763e4ea1829ec013"), + "libs/bower_components/jquery/src/data/support.js" => array("438", "4949c78014e8b038652a8d8096e5fb80"), + "libs/bower_components/jquery/src/deferred.js" => array("4413", "36d5f0378ff200400844238ee0a6ee28"), + "libs/bower_components/jquery/src/deprecated.js" => array("223", "55ef50aae575deea2e12776b6e95ea19"), + "libs/bower_components/jquery/src/dimensions.js" => array("1881", "5966f07a9d63cd9b4058887c5f9024aa"), + "libs/bower_components/jquery/src/effects/animatedSelector.js" => array("225", "293275acc2e12d1a64ed0f9214f85388"), + "libs/bower_components/jquery/src/effects.js" => array("17243", "e899c616eab61a7ee38e9c01bb20c9d0"), + "libs/bower_components/jquery/src/effects/support.js" => array("1459", "54a9fa0f41a0a0c06a4a22e9922566e6"), + "libs/bower_components/jquery/src/effects/Tween.js" => array("3034", "fc737cbaa07e2c89aa92b452a37c34ff"), + "libs/bower_components/jquery/src/event/alias.js" => array("1094", "ba4e3a937d720dccbc0eb99a9e8f72ef"), + "libs/bower_components/jquery/src/event.js" => array("30072", "8d112219ff954b82406a0906b8e1266d"), + "libs/bower_components/jquery/src/event/support.js" => array("641", "57578123c95312b20806ee463a53fcf0"), + "libs/bower_components/jquery/src/exports/amd.js" => array("1006", "0e2411cca15d802f6a8da3aed34d9369"), + "libs/bower_components/jquery/src/exports/global.js" => array("641", "686c97ddbf5a04083351686d06ba6fde"), + "libs/bower_components/jquery/src/intro.js" => array("1405", "501013e055d7a4b09a2457e911bbfe1a"), + "libs/bower_components/jquery/src/jquery.js" => array("571", "97dda5c52a639f4dc0256db2ed7047eb"), + "libs/bower_components/jquery/src/manipulation/_evalUrl.js" => array("240", "d3bbdfa4e6d906378d987a733b87e420"), + "libs/bower_components/jquery/src/manipulation.js" => array("20616", "14e30c7a3cc163b3117bcc7e740e952e"), + "libs/bower_components/jquery/src/manipulation/support.js" => array("2476", "1be243801041a3bda86c0547f49dc622"), + "libs/bower_components/jquery/src/manipulation/var/rcheckableType.js" => array("59", "19f6af061c62c4a89d82c0972a992c61"), + "libs/bower_components/jquery/src/offset.js" => array("5856", "4a5d618f2c76f6f75a8735a920c67c86"), + "libs/bower_components/jquery/src/outro.js" => array("5", "0b9c0e7d4b72a5f95b3ce20f4508a84d"), + "libs/bower_components/jquery/src/queue/delay.js" => array("561", "6e52fac4cd26e9e74694d8c3f9e85294"), + "libs/bower_components/jquery/src/queue.js" => array("3071", "762403c095fa4b03f6f1b57915f211ed"), + "libs/bower_components/jquery/src/selector.js" => array("33", "cdff25b189c9501fbe0b0c540d19074c"), + "libs/bower_components/jquery/src/selector-sizzle.js" => array("294", "08ef80e78fb184932eae6a1d541d2de4"), + "libs/bower_components/jquery/src/serialize.js" => array("3211", "7b20f1d4fa67f16749efb0f81215b02f"), + "libs/bower_components/jquery/src/sizzle/dist/sizzle.js" => array("58579", "8c27c9f5aa4024663effbc88d441e6cf"), + "libs/bower_components/jquery/src/sizzle/dist/sizzle.min.js" => array("18574", "1f6c920ee2c6d21942d507dd24b4c7e5"), + "libs/bower_components/jquery/src/sizzle/dist/sizzle.min.map" => array("28986", "176774d8e1902a2b32f3b98cdbf716e6"), + "libs/bower_components/jquery/src/support.js" => array("1697", "d9283e88af6c6e2813267f2e5d03afbb"), + "libs/bower_components/jquery/src/traversing/findFilter.js" => array("2466", "6af2a4ec1d78ab4d03f58048ebc34f07"), + "libs/bower_components/jquery/src/traversing.js" => array("4575", "813d071c13be81cecc2339d046dce7d0"), + "libs/bower_components/jquery/src/traversing/var/rneedsContext.js" => array("110", "b5676c00977e2e54de53af1228e39020"), + "libs/bower_components/jquery/src/var/class2type.js" => array("64", "8beeb098fb5eca5a728bd7bf2de1ceed"), + "libs/bower_components/jquery/src/var/concat.js" => array("84", "246dde37b4e3635d98eea3e6b32b7493"), + "libs/bower_components/jquery/src/var/deletedIds.js" => array("36", "73fe8cc31324cb3022fce1b1be3c9e92"), + "libs/bower_components/jquery/src/var/hasOwn.js" => array("92", "068dbad1531e9fac5d842e57914a122a"), + "libs/bower_components/jquery/src/var/indexOf.js" => array("85", "62250190fef0835c88effcc7c575f7ad"), + "libs/bower_components/jquery/src/var/pnum.js" => array("80", "04912d16c7442b5c4c7c946235174395"), + "libs/bower_components/jquery/src/var/push.js" => array("82", "6d7ea35e1edf414310d2a3d529edb854"), + "libs/bower_components/jquery/src/var/rnotwhite.js" => array("42", "b1e91d1278805eff8bf9e85c34a41c26"), + "libs/bower_components/jquery/src/var/slice.js" => array("83", "6c0b9cd2ebca15a8ecf9dbe6b44e27ed"), + "libs/bower_components/jquery/src/var/strundefined.js" => array("50", "9e452cb4a55337a4fe5c8351f1d4c54b"), + "libs/bower_components/jquery/src/var/support.js" => array("99", "135b80391f6ec64fa10d1d702a8bee17"), + "libs/bower_components/jquery/src/var/toString.js" => array("86", "6fc5af5803e4128c9225f8606d9f3c35"), + "libs/bower_components/jquery/src/wrap.js" => array("1456", "399c541599cddb95d2e7155277053da8"), + "libs/bower_components/jquery-ui/AUTHORS.txt" => array("9844", "245c8b0f9f894ccb76d70826de495688"), + "libs/bower_components/jquery-ui/bower.json" => array("135", "ed3d29afab772544be4fe7fa5ef47a45"), + "libs/bower_components/jquery-ui/component.json" => array("227", "262877636ab79d8c250382953d701a07"), + "libs/bower_components/jquery-ui/composer.json" => array("1898", "a0088cd9b6910a217b999babe42447bd"), + "libs/bower_components/jquery-ui/MIT-LICENSE.txt" => array("1336", "54ab1578b1fe12a7dcae71b4c14ee4a8"), + "libs/bower_components/jquery-ui/package.json" => array("1575", "69680490bdf978290e2331239ecdb334"), + "libs/bower_components/jquery-ui/README.md" => array("758", "bb910c57516d6e86bcf3d176ac9c1f7d"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-af.js" => array("896", "3f6dc7167ebfdab2e4c06ca1f7ecbf55"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-ar-DZ.js" => array("1207", "f9c86467366d98200c97ac9c8b843fbb"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-ar.js" => array("1297", "9924612cef93d8722863287157768180"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-az.js" => array("921", "f9f7fb74b273da0307a8bd4ec7acb6b5"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-be.js" => array("1146", "7b1c87006e19cac0f3590845efd008fb"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-bg.js" => array("1124", "d965b3639b678aaf5819db10189d3472"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-bs.js" => array("844", "eeccd3d7df38ab2c37ae290e46b3cd93"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-ca.js" => array("874", "a857021fa5601842d94a41f20a5cab9d"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-cs.js" => array("923", "54791b35c9819515ce0cad20b7537277"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-cy-GB.js" => array("904", "fb40b70ba78ef9f4251a86355c5f65f7"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-da.js" => array("890", "656d173c027d5a08186e39c156ba5597"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-de.js" => array("881", "bf12bcbfcb995b003e6cb3c257904be6"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-el.js" => array("1199", "c3150764daf20b6fa2142581180be1ad"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-en-AU.js" => array("897", "4a38655904f6c55da227cea464b55a2b"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-en-GB.js" => array("874", "24a226a281a11799c495abc21f696c23"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-en-NZ.js" => array("899", "af985e8d034123f14696aa116027760d"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-eo.js" => array("893", "e1f5d8ed4599ca392aeb284fe637df33"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-es.js" => array("888", "f576dede2a5e0e2dc999057b1b2bd3b0"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-et.js" => array("951", "1a7c15ddc89179a0e309d9e7d2b97ad4"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-eu.js" => array("913", "24751dd4dcabb58b82ee0817fea84fd3"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-fa.js" => array("1164", "9687cad817acecd88a808d7ef8c58fcf"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-fi.js" => array("945", "60c06554626335687497e570d49f2e5e"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-fo.js" => array("918", "85444da9fc4c900eba95f8ff4704688f"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-fr-CA.js" => array("913", "da166bae7a0b6dfd7d9e2f6bab4576dc"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-fr-CH.js" => array("957", "b23437da5946bba5bf560ec26a0c6d83"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-fr.js" => array("1042", "1957028ad184b40a7cca3448dbb647b9"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-gl.js" => array("890", "0f4dee4528f5f8fb8eb20a14496b7e37"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-he.js" => array("1016", "b2ad344bf1df226aa1a760f1d3653da7"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-hi.js" => array("1313", "1e2602a3c232f31242c47a9cadccf9dd"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-hr.js" => array("869", "7582ea79c7fd35b2b7758ff103b11b4b"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-hu.js" => array("948", "dee235f99823541ec88be57dec431230"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-hy.js" => array("1174", "2f3828a4c02a475b1b8966609721b9c3"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-id.js" => array("882", "fb0ad98a3ad212b1986fcac5015b0435"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-is.js" => array("946", "0cc09cc96c4d279ebaba585507cc6abd"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-it.js" => array("912", "7e651d93d0219066bf596faa06db4a81"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-ja.js" => array("902", "bf1cf98e79f2d6792c7c7a193b4c7497"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-ka.js" => array("1414", "fd0b08bdc63b1d969fa2df907083062a"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-kk.js" => array("1117", "57a792b4c55dc23b2095cc190180c440"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-km.js" => array("1330", "f5c6ed9f64ff97adfd29cb149176021f"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-ko.js" => array("911", "5fb849693b65beed7146624ba498b517"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-ky.js" => array("1109", "a5db310345d66c395b592fd2f6136bf0"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-lb.js" => array("934", "642aa75625a4ab2c324fb5df74063509"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-lt.js" => array("961", "da32786e14f5d1bd858951d9d8fd2796"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-lv.js" => array("942", "ea864f875aa32ef7b5ada2bf3d44876b"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-mk.js" => array("1057", "cdfdd4b3a2e181c9ed297fa55c739d5e"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-ml.js" => array("1436", "17fe3b0548bf5a2c9f4e0b081efaeb04"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-ms.js" => array("891", "51efc50e21ae012a17f4f3cd0f2ac93d"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-nb.js" => array("896", "693af0abc258aaf903c4d4b23a882676"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-nl-BE.js" => array("918", "60b63d90f6eb6ea3334ec75d6a0831ec"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-nl.js" => array("922", "8c765466b1bb2709f8c9db056029ec89"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-nn.js" => array("893", "690553270244b0de96ade29a9e04b02a"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-no.js" => array("888", "5f531f078d367d5f10c287479533b0c8"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-pl.js" => array("917", "2d7dd09c586d4275b402d627778123ca"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-pt-BR.js" => array("945", "2d3c1dc7191cf5081b4f982c8cf78c98"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-pt.js" => array("868", "3b4e1fe50589bc5fadc80a5380ec37c4"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-rm.js" => array("905", "0601228208954434efea2ccf265f5b94"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-ro.js" => array("989", "3e888ad522a6581f99b47ad987292c20"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-ru.js" => array("1117", "813acc83f4f77a0d874426207da0208a"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-sk.js" => array("905", "377b3c5fa2285a8fa665206957c95ceb"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-sl.js" => array("943", "7b87e98ac2241fffb8f3e5bb6415ec07"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-sq.js" => array("887", "47ea965b616f6afeab8d860d75787847"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-sr.js" => array("1037", "7083f39fcb737210e0a13c6196f3feb4"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-sr-SR.js" => array("848", "3d23308dfb3943acdf90bdd46b25f9e2"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-sv.js" => array("890", "88fbc9581e8abeac0fe083d572428c45"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-ta.js" => array("1496", "da7607dd5df15b0bcd4da344c33447a3"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-th.js" => array("1274", "0f1be4ae65e24fc7d6a37dce828a9cee"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-tj.js" => array("1074", "f868a410d5438feee15a20e24e4caf5b"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-tr.js" => array("883", "6d11aae285bdd88294e66353feb284da"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-uk.js" => array("1183", "e0b56bc48d64fa8ffef2b8c39f1db725"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-vi.js" => array("1094", "7d54cb0edfbc31232d4ac12f94cec562"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-zh-CN.js" => array("983", "46cc885a69ff490c660e99173dc05ea3"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-zh-HK.js" => array("981", "ab64f179cc7f62ba45d7708e1dee8cae"), + "libs/bower_components/jquery-ui/ui/i18n/jquery.ui.datepicker-zh-TW.js" => array("977", "411a70a31fe6420be6e5990ea5122e18"), + "libs/bower_components/jquery-ui/ui/i18n/jquery-ui-i18n.js" => array("70069", "5e42ed24fa979312d8711f52eb3a49a0"), + "libs/bower_components/jquery-ui/ui/jquery.ui.accordion.js" => array("14937", "cec7b2afde9634119a7b30c711480c64"), + "libs/bower_components/jquery-ui/ui/jquery.ui.autocomplete.js" => array("15896", "b9924a6e3b305145f156fd027c25d902"), + "libs/bower_components/jquery-ui/ui/jquery.ui.button.js" => array("10972", "e50be67b4e07e1cd61df0806b74566bc"), + "libs/bower_components/jquery-ui/ui/jquery.ui.core.js" => array("8198", "495fc824644e9f56c716f4f19f797394"), + "libs/bower_components/jquery-ui/ui/jquery-ui.custom.js" => array("436715", "2f89b9b0bdd026d1c6c3845fd358183f"), + "libs/bower_components/jquery-ui/ui/jquery.ui.datepicker.js" => array("76324", "fc1180f7327a8534805e8035020da251"), + "libs/bower_components/jquery-ui/ui/jquery.ui.dialog.js" => array("20476", "310f7977156e1caeae4d6c60a1aa9794"), + "libs/bower_components/jquery-ui/ui/jquery.ui.draggable.js" => array("31352", "bea5eccf905d1a97cb97c13d590f1053"), + "libs/bower_components/jquery-ui/ui/jquery.ui.droppable.js" => array("11056", "dbf93a484952fb7d4ef5722b9ca6598d"), + "libs/bower_components/jquery-ui/ui/jquery.ui.effect-blind.js" => array("1928", "a514127e65fdf55c30cb669174b2c788"), + "libs/bower_components/jquery-ui/ui/jquery.ui.effect-bounce.js" => array("2789", "33bbcc23d7d93ccc6b6c1ae57a41a831"), + "libs/bower_components/jquery-ui/ui/jquery.ui.effect-clip.js" => array("1468", "79b4c36bc5dedad582f1db1d39d86e87"), + "libs/bower_components/jquery-ui/ui/jquery.ui.effect-drop.js" => array("1473", "5836525420c6b441c5ccb74d80c1877b"), + "libs/bower_components/jquery-ui/ui/jquery.ui.effect-explode.js" => array("2402", "cc0752b84f66ea9ec6fdd80479d4a98b"), + "libs/bower_components/jquery-ui/ui/jquery.ui.effect-fade.js" => array("558", "db69be03386c29822fbe39f3f0152cc2"), + "libs/bower_components/jquery-ui/ui/jquery.ui.effect-fold.js" => array("1698", "7f51467d5c2d39df62090acf5bf49802"), + "libs/bower_components/jquery-ui/ui/jquery.ui.effect-highlight.js" => array("1002", "384ba1f7dac1e5fb14d83b5c548084d2"), + "libs/bower_components/jquery-ui/ui/jquery.ui.effect.js" => array("31918", "09855fe0bc8af60aa9db954e6124ddc6"), + "libs/bower_components/jquery-ui/ui/jquery.ui.effect-pulsate.js" => array("1374", "b4c0f9e9a0bd8360c744b0e221af99d7"), + "libs/bower_components/jquery-ui/ui/jquery.ui.effect-scale.js" => array("8274", "bbb2b31cf99419c96ea93e6137d8657c"), + "libs/bower_components/jquery-ui/ui/jquery.ui.effect-shake.js" => array("1948", "b70a671758326cdbc1439e9f22645278"), + "libs/bower_components/jquery-ui/ui/jquery.ui.effect-slide.js" => array("1472", "261491a676a7b1f25042e4e421ac0aa1"), + "libs/bower_components/jquery-ui/ui/jquery.ui.effect-transfer.js" => array("1237", "206b754fca95f138508708f7498ed0d7"), + "libs/bower_components/jquery-ui/ui/jquery-ui.js" => array("436715", "27c35e4ff8c3538f6d74aad67c494113"), + "libs/bower_components/jquery-ui/ui/jquery.ui.menu.js" => array("16834", "7ed11ba837b32589f672e2b8b464b042"), + "libs/bower_components/jquery-ui/ui/jquery.ui.mouse.js" => array("4561", "1088fc344eb2a5f2e8ed23d396873332"), + "libs/bower_components/jquery-ui/ui/jquery.ui.position.js" => array("15845", "781f11b2ff2f551a965346c64dd2c752"), + "libs/bower_components/jquery-ui/ui/jquery.ui.progressbar.js" => array("3368", "8ab2415c18a1a87855ebf42c0bf05428"), + "libs/bower_components/jquery-ui/ui/jquery.ui.resizable.js" => array("27692", "5c5cbeb41c39f661fa7cc7e279cdfc33"), + "libs/bower_components/jquery-ui/ui/jquery.ui.selectable.js" => array("6967", "3f00c36bd6c3a57f2af9e132fe6cad09"), + "libs/bower_components/jquery-ui/ui/jquery.ui.slider.js" => array("18177", "90eadaecb851adf9f493d81208b48042"), + "libs/bower_components/jquery-ui/ui/jquery.ui.sortable.js" => array("42714", "288c75389c0f45ecd974b7e05b6cfcde"), + "libs/bower_components/jquery-ui/ui/jquery.ui.spinner.js" => array("12529", "1926c0d5fa6d453cbddd2adace7a0afd"), + "libs/bower_components/jquery-ui/ui/jquery.ui.tabs.js" => array("22081", "8253514521d5ccd4195e29efcdb006a4"), + "libs/bower_components/jquery-ui/ui/jquery.ui.tooltip.js" => array("10822", "719ee6c85bc2a220fb129f570807dc8a"), + "libs/bower_components/jquery-ui/ui/jquery.ui.widget.js" => array("15085", "d1d42e7b1fa13faeeb3f1d9487107135"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-af.min.js" => array("818", "95dd1e3ff70e29ac714e88a7fee23f2d"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-ar-DZ.min.js" => array("1066", "7f388a3fe5d539ccb8e743f017046dcb"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-ar.min.js" => array("1016", "0abf55fb78e61291703d21513e182ad1"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-az.min.js" => array("835", "b209479bc518183890a63b8f4461c939"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-be.min.js" => array("1067", "7d875d926506d1c48327c267d435de3d"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-bg.min.js" => array("1048", "dc9168f71eeddc07c473cebf9debfef9"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-bs.min.js" => array("805", "5310e3fb7f2795c71bf3704797e3ab87"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-ca.min.js" => array("804", "7bfaf9c32510cb57e4857f4011cf3853"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-cs.min.js" => array("838", "caaa7f653067c29313d662a2f4c34954"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-cy-GB.min.js" => array("832", "fe39c5be2d1b2795a57c28b4ca2573da"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-da.min.js" => array("813", "985e2a264f3df994d46f1ac2c3dde06a"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-de.min.js" => array("814", "3a1d2248e816af1535ad907934886402"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-el.min.js" => array("1118", "ddd39f9f9c3bfe65ba92d0667b30bddb"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-en-AU.min.js" => array("811", "7c1bcd90c08cc22bacc93bbc72d5e06e"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-en-GB.min.js" => array("811", "95c8dd4c74fc551730deca102d3b7c5d"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-en-NZ.min.js" => array("811", "36fa506b7bb291b212afdfc2db1f6cb2"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-eo.min.js" => array("818", "60f7812b48343991fc2419dcd43ba3e2"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-es.min.js" => array("805", "81680e3e6cc802b3b2357702c533ecf6"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-et.min.js" => array("850", "3b810dc582cfd63e26bce8155f4f2c34"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-eu.min.js" => array("839", "729ab24b14fca434b2cfd5d9a5b4779c"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-fa.min.js" => array("873", "cfc9031c8b1f60bcffb37a1273d1a59d"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-fi.min.js" => array("870", "9474a1fd76430f607a1edc3c7af81429"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-fo.min.js" => array("845", "d7334e577a2ee2bc20361921096919f6"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-fr-CA.min.js" => array("847", "bb39bf8d07f8017af829d9fb52e85173"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-fr-CH.min.js" => array("844", "10c54f9640b4e909d23bae7cb89d09dd"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-fr.min.js" => array("835", "c40aa3d1ff446ee489bff0326ec1371d"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-gl.min.js" => array("811", "fd82165aa3a825c9273e14f9f68e98b9"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-he.min.js" => array("935", "ca91c5ce4f69dc19dc996777cd1bf978"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-hi.min.js" => array("1235", "0319ff15589798907b1c288dcaf09071"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-hr.min.js" => array("825", "38ad296e324f4b1677846deead50ff1e"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-hu.min.js" => array("834", "b048ce59a1cb6bca2c1f78c441682b77"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-hy.min.js" => array("1089", "68613abd5cc8ad7f9769a59ba3fce29c"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-id.min.js" => array("805", "b13647a4ad6222080aca4cf6cc71a43a"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-is.min.js" => array("869", "cd878178ca371d75ad0135d788067a92"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-it.min.js" => array("825", "e71d3804ddb5874aae49584dea4c008c"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-ja.min.js" => array("831", "3d42e25ebe6eb800845a2104134324ca"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-ka.min.js" => array("1334", "b82289725d5f81c57592bfe01f993c5f"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-kk.min.js" => array("1027", "5831fe16fe18cb8e0afb65173e1f1a48"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-km.min.js" => array("1239", "1c736cd489e4661f925439bf3e387283"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-ko.min.js" => array("827", "e9e327ea651b28c8d96a22eddff8c268"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-ky.min.js" => array("1013", "b0d670eed1ee7d22021c5e5305b398fc"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-lb.min.js" => array("824", "a821f60a3e99d934b2c32b368f7f7d0d"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-lt.min.js" => array("879", "2aea79541c11a3d4e6a3887af1525d85"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-lv.min.js" => array("848", "286e505793926fa29b9f128db252f052"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-mk.min.js" => array("1009", "762b19cf6aba4d89c4a52b29f77803c2"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-ml.min.js" => array("1330", "dccc7bf4963ba028c2ab77bee461bee7"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-ms.min.js" => array("803", "2ca7efa65446b5b94ba15edf083be79f"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-nb.min.js" => array("813", "ac119f93089e005957570bc0a061f8ae"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-nl-BE.min.js" => array("818", "ab1393b432d36410b44042ae96ac2e8c"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-nl.min.js" => array("806", "51bbdcba389c2bba9901d856dc3d2f4f"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-nn.min.js" => array("810", "4fc51fff289245c001aa527b3eb10ca9"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-no.min.js" => array("813", "af1547c827feeb2dbe17949e3ad83f29"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-pl.min.js" => array("841", "2ac0374b67e30860d9cb8291119e4b77"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-pt-BR.min.js" => array("869", "e4505ef936512fdc3d15dd2ef7dd5031"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-pt.min.js" => array("846", "98be5be90fcd05600f7496e6b1f43c86"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-rm.min.js" => array("833", "54479a6b44a450ebb767bfd590a9d8ac"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-ro.min.js" => array("851", "9a94b4552a43189d325492c5235493a0"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-ru.min.js" => array("1035", "8c88c42627db5a3c11476e5efbe16d18"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-sk.min.js" => array("840", "d82f4ab3ab4568aa24f001baed6af597"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-sl.min.js" => array("826", "827adcd038af02f7c97cf5faca6c10c3"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-sq.min.js" => array("814", "f6629a414e8c4611924dd104a83c9e9b"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-sr.min.js" => array("997", "521b0e16d8e878da601f7e48975c8abd"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-sr-SR.min.js" => array("814", "13f0ad99e0b646e5e698e83450b0de4f"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-sv.min.js" => array("818", "1c36624e7d3ffd465c719342aa0df1c9"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-ta.min.js" => array("1417", "20e2ca602a5f72f48ce222686ea2dc3b"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-th.min.js" => array("1216", "7d95bf362917429854c0f3fd813f51de"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-tj.min.js" => array("991", "95d40ad3c35f157524536bb238d6f57f"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-tr.min.js" => array("806", "0b9646228e7eabee7854012a8ebbaafe"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-uk.min.js" => array("1040", "4b33acff7e9ef7de642984987a1ba9ec"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-vi.min.js" => array("973", "e0b50d9e810097c4cdffa6edae9a255d"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-zh-CN.min.js" => array("921", "751c0c3690cca6e18e0bf1d273ac66a0"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-zh-HK.min.js" => array("921", "75d33aa6b761bcd9b8613530b12abb79"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery.ui.datepicker-zh-TW.min.js" => array("921", "37c6107e860ca8d2d85992e4204b0790"), + "libs/bower_components/jquery-ui/ui/minified/i18n/jquery-ui-i18n.min.js" => array("58950", "4619143190adccef201ab6dc7bcf28ec"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.accordion.min.js" => array("8367", "8bde422d37ea79520da13b9e8e3c9dea"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.autocomplete.min.js" => array("7787", "01cc67ccc5744bbb131707e131906bc8"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.button.min.js" => array("6874", "5d05441442f31a37834bf7aeb3389f48"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.core.min.js" => array("4290", "940f7fb38a963e3ecef6a011ab78ad11"), + "libs/bower_components/jquery-ui/ui/minified/jquery-ui.custom.min.js" => array("228539", "0e3eecf578b68a7ba6b8008b03a6f9be"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.datepicker.min.js" => array("35807", "9c2c6c9d53ed3df9d074d16a14632e3d"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.dialog.min.js" => array("11263", "17de9be551fa5a26b1853fb7895c36b0"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.draggable.min.js" => array("18560", "8670376fb0ef0356ad4d28dd4540a05a"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.droppable.min.js" => array("5985", "0d6ea5daaacde6a8fceedd4c7fa3d99b"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.effect-blind.min.js" => array("956", "a380f75e908a8d1f5b49431821e65fae"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.effect-bounce.min.js" => array("1058", "9b7767b75ace68d88ff55b0ea1c5247a"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.effect-clip.min.js" => array("733", "7b32f3f4013f500ebd32d552e2ba148d"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.effect-drop.min.js" => array("812", "1b475a12106b3fac915d6cfde4b864e1"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.effect-explode.min.js" => array("985", "0c3ab3a5bc731c78077386aa512270ae"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.effect-fade.min.js" => array("330", "5698d995935764f500450ab94c9e58b4"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.effect-fold.min.js" => array("845", "a9fb1583821e1db2e7bda45e9b0aceff"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.effect-highlight.min.js" => array("594", "b90ce04b7d12b17acca1963ec743bf49"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.effect.min.js" => array("12973", "b45e8e071bebe3a23b8841ed2b394673"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.effect-pulsate.min.js" => array("609", "4b491d7c9d9a578c94f99837d7c1950a"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.effect-scale.min.js" => array("4356", "abf1870462ffc6ad3a7bf64178caefd5"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.effect-shake.min.js" => array("914", "9ad5301b69c5792620e04df6f77982ec"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.effect-slide.min.js" => array("775", "d7f15e22c25ef798f02d02755ed8701e"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.effect-transfer.min.js" => array("664", "3a0a3c44fce6272241ddd6df463968f0"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.menu.min.js" => array("9584", "393ffb17e43535f7d8387b16907ef693"), + "libs/bower_components/jquery-ui/ui/minified/jquery-ui.min.js" => array("228539", "fb4770e78488812ef9f99b7c7484688d"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.mouse.min.js" => array("2842", "8609e8e9044082f7f9c8826d11a3ee1f"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.position.min.js" => array("6361", "b9c5a4f9ba4b40d8cfb67a7f1766029a"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.progressbar.min.js" => array("2179", "4955d934978b2770fd6d8dd42c8d8efa"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.resizable.min.js" => array("17410", "e92766a5a1af65fc51fc30e71ec2c6bd"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.selectable.min.js" => array("4066", "dc2491c9c1f6d7c679a104f6b4c048ec"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.slider.min.js" => array("10245", "ff4d6dc74d90401632f0e7c5968efa0d"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.sortable.min.js" => array("24111", "8f18c8fbafb73d566a399b978f9169fa"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.spinner.min.js" => array("6890", "271c0eebb1c42fb201e681327a26f5e3"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.tabs.min.js" => array("11624", "4ac127fe79fe4068c60649e2089c766e"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.tooltip.min.js" => array("4782", "0b7c86ef7b1c66719c3cecac0001b48f"), + "libs/bower_components/jquery-ui/ui/minified/jquery.ui.widget.min.js" => array("6522", "63ddc580856fe7821bf537f6e214de07"), + "libs/bower_components/jScrollPane/ajax_content.html" => array("5592", "102fd3f0de3dd102cc44c131a28cd3a9"), + "libs/bower_components/jScrollPane/ajax.html" => array("7570", "63e97faba95b27c09d35951fbe983068"), + "libs/bower_components/jScrollPane/anchors.html" => array("7784", "f48e17a50dda08d773f50087da2644c0"), + "libs/bower_components/jScrollPane/api.html" => array("9105", "742a5b74d67594a6194062b48d39c0db"), + "libs/bower_components/jScrollPane/arrow_hover.html" => array("12410", "cb56370ed3b94f10c280ff5af62cd4c6"), + "libs/bower_components/jScrollPane/arrow_positions.html" => array("43068", "69d58be759b22da688b11989b3963544"), + "libs/bower_components/jScrollPane/arrows.html" => array("12321", "a1310c80e04db681de3fe164cd95743d"), + "libs/bower_components/jScrollPane/auto_reinitialise.html" => array("3566", "84515480d57ad33e8264c5e3e9958320"), + "libs/bower_components/jScrollPane/basic.html" => array("12344", "cde118345bbe03363d02103498b1a227"), + "libs/bower_components/jScrollPane/caps.html" => array("14214", "02f55ac7582d162e1f04d67af136f8c1"), + "libs/bower_components/jScrollPane/changelog.html" => array("4401", "34d5c1d74457686e913fd880ecd6b025"), + "libs/bower_components/jScrollPane/destroy.html" => array("12912", "5545f0b0899e9a2674c92efd180379b4"), + "libs/bower_components/jScrollPane/drag_size.html" => array("12663", "0f230c14887df09460f9721f682ab808"), + "libs/bower_components/jScrollPane/dynamic_content.html" => array("3365", "6ac6060980497c9dfe71fdccc73da2ab"), + "libs/bower_components/jScrollPane/dynamic_height.html" => array("10699", "fb5dbd84c24cfcbf126205bdf91697be"), + "libs/bower_components/jScrollPane/dynamic_width.html" => array("23060", "f61abbaebe7d9e40904cfcd0cb117868"), + "libs/bower_components/jScrollPane/events.html" => array("14242", "da2a2a5cfd4dde4723cd4c94f8775569"), + "libs/bower_components/jScrollPane/faqs.html" => array("2446", "ac72e3d8f138e336ee81e35dd39df7f7"), + "libs/bower_components/jScrollPane/fixed_width.html" => array("13020", "1ee207ce45b9224803d13daf15bf03ef"), + "libs/bower_components/jScrollPane/focus.html" => array("12254", "b12e6df7262a1eb553335368cd1f17c6"), + "libs/bower_components/jScrollPane/fullpage_scroll.html" => array("13791", "0aceebf5176d06e8bea327cd136b0565"), + "libs/bower_components/jScrollPane/GPL-LICENSE.txt" => array("15099", "2c1778696d3ba68569a0352e709ae6b7"), + "libs/bower_components/jScrollPane/iframe2.html" => array("4113", "82f5fb0621df570c6e67b076797608f9"), + "libs/bower_components/jScrollPane/iframe_content1.html" => array("6702", "df5484eec27a4e2d59bb997cb68bf157"), + "libs/bower_components/jScrollPane/iframe_content2.html" => array("6735", "c285b02ebd24dec1b1616ece083f39fb"), + "libs/bower_components/jScrollPane/iframe_content3.html" => array("4626", "1802c5f3d7ab8a1dd00523c6164d948d"), + "libs/bower_components/jScrollPane/iframe_content4.html" => array("4647", "445cc6d09fcc217b8d0877002a3642ad"), + "libs/bower_components/jScrollPane/iframe.html" => array("3195", "e095b1982e2897da61740fdc59c020e2"), + "libs/bower_components/jScrollPane/image2.html" => array("3411", "1f2f8b074ba184256bd660ff5cb8cfc7"), + "libs/bower_components/jScrollPane/image.html" => array("3613", "b1683a7b0449e70a210c5e9b0bb0ea5a"), + "libs/bower_components/jScrollPane/image/logo.png" => array("1838", "3359cf416860883290483674e17eb5bf"), + "libs/bower_components/jScrollPane/index.html" => array("15038", "00441568b7b288b69a8a3fac9512dd13"), + "libs/bower_components/jScrollPane/invisibles.html" => array("33431", "5268959b3e104a509b0b66c2c589e500"), + "libs/bower_components/jScrollPane/issues/11/after.html" => array("2160", "817c9bc671c3ffc1946738c48eeacdff"), + "libs/bower_components/jScrollPane/issues/11/before.html" => array("2116", "a461a3342e08e7911b28ddfaaf950f80"), + "libs/bower_components/jScrollPane/issues/11/brs_main.css" => array("5093", "f063a1ceaa272c398ce79eabb90d7039"), + "libs/bower_components/jScrollPane/issues/11/index.html" => array("2061", "6903d939c464a27bbc6607753ed780e0"), + "libs/bower_components/jScrollPane/issues/11/jquery.mousewheel.js" => array("2200", "b9f3122f01f7b9d0b9ea8d6bb09edf31"), + "libs/bower_components/jScrollPane/issues/11/jscrollpane-2b3.css" => array("1864", "51c400fc8b2d2a23ff5a06f117266c46"), + "libs/bower_components/jScrollPane/issues/11/jscrollpane-2b3.js" => array("33568", "68a64c6549b6317008cba70a74380685"), + "libs/bower_components/jScrollPane/issues/11/native.html" => array("2107", "23fa665cee7ea18ae72fdf948e2f1ec1"), + "libs/bower_components/jScrollPane/issues/12/after.html" => array("14454", "eef1f42bc797ad609a638b26eaa5d6c5"), + "libs/bower_components/jScrollPane/issues/12/after_reinit.html" => array("14362", "a877214ba9cd2b84e09d13ce4101dd12"), + "libs/bower_components/jScrollPane/issues/12/before.html" => array("14292", "0470876a7fd4219f265b9a15940ce505"), + "libs/bower_components/jScrollPane/issues/12/before_reinit.html" => array("14318", "c37b7404817ad9a97ce7e6480423da0c"), + "libs/bower_components/jScrollPane/issues/12/brs_main.css" => array("5093", "f063a1ceaa272c398ce79eabb90d7039"), + "libs/bower_components/jScrollPane/issues/12/index.html" => array("2061", "58769a0ebff71ccf2c10d9dd42a2ea74"), + "libs/bower_components/jScrollPane/issues/12/jquery.mousewheel.js" => array("2200", "b9f3122f01f7b9d0b9ea8d6bb09edf31"), + "libs/bower_components/jScrollPane/issues/12/jscrollpane-2b3.css" => array("1864", "51c400fc8b2d2a23ff5a06f117266c46"), + "libs/bower_components/jScrollPane/issues/12/jscrollpane-2b3.js" => array("33568", "68a64c6549b6317008cba70a74380685"), + "libs/bower_components/jScrollPane/issues/12/native.html" => array("14283", "4529d89c5cfc578bb67343e4b0d764a8"), + "libs/bower_components/jScrollPane/issues/12/wrapped.html" => array("14430", "a8adf06da0a1b5db7dde64a0efb846ea"), + "libs/bower_components/jScrollPane/issues/7/after.html" => array("4910", "d544b806ed495bc104c320d3d2e188a5"), + "libs/bower_components/jScrollPane/issues/7/before.html" => array("4881", "8dbd05c085081ba46885e32d0ed67174"), + "libs/bower_components/jScrollPane/issues/7/index.html" => array("2003", "a054b31dd7e0f8be5615e41bb87e85a2"), + "libs/bower_components/jScrollPane/issues/7/jscrollpane-2b1.css" => array("1423", "65b3d741ebfbc939998a3bd8c905d8e0"), + "libs/bower_components/jScrollPane/issues/7/jscrollpane-2b2.js" => array("30109", "4a38c9839d7c67a4b94274700003bd17"), + "libs/bower_components/jScrollPane/issues/7/native.html" => array("4466", "ad344a030dcd000bf122d00d7aad1a40"), + "libs/bower_components/jScrollPane/known_issues.html" => array("3140", "b2cf2458244a914393e7daf3b829fb52"), + "libs/bower_components/jScrollPane/less_basic.html" => array("9468", "82c854318de452f35a9a56b59d7edc9f"), + "libs/bower_components/jScrollPane/MIT-LICENSE.txt" => array("1069", "adafb28f6f446ffd3d347599818c5b55"), + "libs/bower_components/jScrollPane/mwheel_intent.html" => array("12540", "82421440aaddae1bde635b8b187942d8"), + "libs/bower_components/jScrollPane/override_animate.html" => array("9130", "3d58564cca9c460fd39c13b2492b8c0a"), + "libs/bower_components/jScrollPane/README.md" => array("552", "66c2c0bd6e8f2a6bc919d2374921f261"), + "libs/bower_components/jScrollPane/runeimp2.html" => array("20735", "d0427278e02f7723a671aafe34215d7e"), + "libs/bower_components/jScrollPane/runeimp.html" => array("20550", "373a2545ee6fe87211a9f72871d835c4"), + "libs/bower_components/jScrollPane/script/demo.js" => array("1619", "ce61d8ee3ee746c708ab42e0f025243b"), + "libs/bower_components/jScrollPane/script/jquery.jscrollpane.js" => array("46340", "b44fef9c7881542e1f28e057d0de9d3a"), + "libs/bower_components/jScrollPane/script/jquery.jscrollpane.min.js" => array("15114", "390d645ad63cfabaa347286a1023a957"), + "libs/bower_components/jScrollPane/script/jquery.mousewheel.js" => array("3846", "f77bd9ca0396c7a8672f536884b1e1aa"), + "libs/bower_components/jScrollPane/script/mwheelIntent.js" => array("1748", "3d22ec7b158eb1a1518a11f4124f5ff4"), + "libs/bower_components/jScrollPane/scroll_on_left.html" => array("6894", "ca1ed4de9866a6b109e4fe4ef2606676"), + "libs/bower_components/jScrollPane/scroll_to_animate.html" => array("9044", "305f3e06ef2c56f4c144ae7bc13f2832"), + "libs/bower_components/jScrollPane/scroll_to.html" => array("8404", "bb5dc6a9ecd533d401507310e0a56424"), + "libs/bower_components/jScrollPane/settings.html" => array("10422", "2acc1e1b2923c8065ceeffaac05fbb6c"), + "libs/bower_components/jScrollPane/short.html" => array("3289", "9be69211f6b3448c54730d8ab6699dc9"), + "libs/bower_components/jScrollPane/style/demo.css" => array("2578", "322bdee8d36094459294f97af449d5cc"), + "libs/bower_components/jScrollPane/style/jquery.jscrollpane.css" => array("1423", "65b3d741ebfbc939998a3bd8c905d8e0"), + "libs/bower_components/jScrollPane/themes/lozenge/image/ui-icons_222222_256x240.png" => array("4369", "ebe6b6902a408fbf9cac6379a1477525"), + "libs/bower_components/jScrollPane/themes/lozenge/image/ui-icons_888888_256x240.png" => array("4369", "9c46d7cab43e22a14bad26d2d4806d80"), + "libs/bower_components/jScrollPane/themes/lozenge/image/ui-icons_cd0a0a_256x240.png" => array("4369", "3e450c2a2c66328d9498e7001ad7197c"), + "libs/bower_components/jScrollPane/themes/lozenge/index.html" => array("22285", "b295c70a2366356ab3836dff06b13d78"), + "libs/bower_components/jScrollPane/themes/lozenge/style/jquery.jscrollpane.lozenge.css" => array("1105", "70c1450ad9476106f7ef415aad683ea8"), + "libs/bower_components/jScrollPane/v1.html" => array("2122", "8c514af4e1f780b7ccd43cbd0bfd66da"), + "libs/bower_components/mousetrap/Gruntfile.js" => array("1061", "d00d437139b0c76968d13ba0abca0c81"), + "libs/bower_components/mousetrap/mousetrap.js" => array("29576", "0faa7fc59989c1db78acfa7d62c64de4"), + "libs/bower_components/mousetrap/mousetrap.min.js" => array("3850", "5543a5480413b59a5f50a8ec189c5214"), + "libs/bower_components/mousetrap/package.json" => array("654", "c674554370b801aa019fb9c13bcbbbae"), + "libs/bower_components/mousetrap/plugins/bind-dictionary/mousetrap-bind-dictionary.js" => array("956", "45d1531622eac12812a7e88f84519d06"), + "libs/bower_components/mousetrap/plugins/bind-dictionary/mousetrap-bind-dictionary.min.js" => array("222", "73ea6cfb494e9969674b55d6f59eee9e"), + "libs/bower_components/mousetrap/plugins/bind-dictionary/README.md" => array("431", "df9f4621884e550fcf9a86fdb6d071b2"), + "libs/bower_components/mousetrap/plugins/global-bind/mousetrap-global-bind.js" => array("987", "8602f9fa8f531430d3d9866b6d6063a2"), + "libs/bower_components/mousetrap/plugins/global-bind/mousetrap-global-bind.min.js" => array("239", "f0a67ff6d71bcafb29c0cc40749e595d"), + "libs/bower_components/mousetrap/plugins/global-bind/README.md" => array("552", "615d515d6c01717270a4d597363e3ff6"), + "libs/bower_components/mousetrap/plugins/pause/mousetrap-pause.js" => array("678", "7b693460cbe9dbb34db5c5eab1434129"), + "libs/bower_components/mousetrap/plugins/pause/mousetrap-pause.min.js" => array("175", "0494f81db4e6bea30cbebec43705a3af"), + "libs/bower_components/mousetrap/plugins/pause/README.md" => array("291", "40c5516e147bc30e16823ece328a13e0"), + "libs/bower_components/mousetrap/plugins/README.md" => array("563", "8501ed3c4f3fcac11d06a3ab942c8bc4"), + "libs/bower_components/mousetrap/plugins/record/mousetrap-record.js" => array("5146", "8d9ea4fe7b076ad376124bde92b33308"), + "libs/bower_components/mousetrap/plugins/record/mousetrap-record.min.js" => array("630", "16c15797fa7af825ab161da1c978b08c"), + "libs/bower_components/mousetrap/plugins/record/README.md" => array("391", "5415944b891db38e56dc31d4baaab211"), + "libs/bower_components/mousetrap/README.md" => array("3121", "9b6c10a7afb8e136f459aa095314112e"), + "libs/bower_components/ngDialog/bower.json" => array("691", "d81f9ae975b126960b0cb07c73faa8d7"), + "libs/bower_components/ngDialog/css/ngDialog.css" => array("1579", "db5cb6ed0da790a4dc59305729b41797"), + "libs/bower_components/ngDialog/css/ngDialog.min.css" => array("1320", "24cf82c389768be5a05a60ca5872f6eb"), + "libs/bower_components/ngDialog/css/ngDialog-theme-default.css" => array("4471", "09dfaa02adaa2d2083d7d15ab9fb4c03"), + "libs/bower_components/ngDialog/css/ngDialog-theme-default.min.css" => array("3852", "92346177a86da9200d8debf5dd52914b"), + "libs/bower_components/ngDialog/css/ngDialog-theme-plain.css" => array("3451", "5cc4d9ad1b2c21dba85c9609a70372d8"), + "libs/bower_components/ngDialog/css/ngDialog-theme-plain.min.css" => array("3117", "9b3aca326ddb24049f0342c2c065330b"), + "libs/bower_components/ngDialog/js/ngDialog.js" => array("10788", "ce9d17ce99239bfa8a7bfb9dfebd9acd"), + "libs/bower_components/ngDialog/js/ngDialog.min.js" => array("4722", "6e43eca51d89f4bc0f75305ec4d89e17"), + "libs/bower_components/ngDialog/package.json" => array("995", "7eb269f40ac2098bdba2a18faca9c241"), + "libs/bower_components/ngDialog/README.md" => array("11280", "e075efd034400283ddbd792963ce22c4"), + "libs/bower_components/sprintf/bower.json" => array("439", "49b6a39e763755263ab6d758690b1742"), + "libs/bower_components/sprintf/demo/angular.html" => array("690", "61276ccc42eb16f69df6f9dc82527ff2"), + "libs/bower_components/sprintf/dist/angular-sprintf.min.js" => array("449", "80b1dd478d4cf875a1118c1c2ad8c3c1"), + "libs/bower_components/sprintf/dist/angular-sprintf.min.map" => array("429", "88110c6656f210b1a33dfbacaca20bc8"), + "libs/bower_components/sprintf/dist/sprintf.min.js" => array("2955", "9d4d5248855a0a744e4354e516ace711"), + "libs/bower_components/sprintf/dist/sprintf.min.map" => array("4160", "124b5979ca1eeb818c6668f5cf9e0e06"), + "libs/bower_components/sprintf/gruntfile.js" => array("970", "4758263aff4cfbc3c1680d34c4d763e5"), + "libs/bower_components/sprintf/LICENSE" => array("1518", "1168e69d403e6f51cbbb01752ae99667"), + "libs/bower_components/sprintf/package.json" => array("598", "f5001fa43a0a710f86595a0397e6d262"), + "libs/bower_components/sprintf/README.md" => array("4405", "cb1413160aa7132ea34f625c98e4e3b2"), + "libs/bower_components/sprintf/src/angular-sprintf.js" => array("490", "7955cc90728c050c63177dd9c53f6b5e"), + "libs/bower_components/sprintf/src/sprintf.js" => array("7536", "d7c902d0301e4a3aad06bc3d581b4f4c"), + "libs/bower_components/sprintf/test/test.js" => array("3212", "4014279da9e8b364ab8f510a622fa3cd"), + "libs/bower_components/visibilityjs/bower.json" => array("418", "a7c00e98663476c18f30f7cef20cf834"), + "libs/bower_components/visibilityjs/ChangeLog.md" => array("2260", "197d392d11975152af573b9223dc84fb"), + "libs/bower_components/visibilityjs/index.js" => array("55", "8d0169177fdb0221daf60c6ec19e2a84"), + "libs/bower_components/visibilityjs/lib/visibility.core.js" => array("6458", "0f3d3058659304f2cdacbc12c0b8719b"), + "libs/bower_components/visibilityjs/lib/visibility.fallback.js" => array("1743", "1d9595749bb960ce6b15a896f3d93f6e"), + "libs/bower_components/visibilityjs/lib/visibility.js" => array("93", "a95bfd47da3a0a8e041a2be58e3d2db4"), + "libs/bower_components/visibilityjs/lib/visibility.timers.js" => array("5244", "8ac6ab268bfbe3855e2526c6c0516fe5"), + "libs/bower_components/visibilityjs/LICENSE" => array("1095", "4a9937d1d46c5d507f091ca83314620c"), + "libs/bower_components/visibilityjs/logo.svg" => array("3068", "9b5651c6cda2be8ac1152fcc65a3c471"), + "libs/bower_components/visibilityjs/README.md" => array("9574", "f5eeb1e85219c50191ee987b5f863d5c"), "libs/HTML/Common2.php" => array("15005", "a9c9b708cd3f2691192171bb78177db3"), "libs/HTML/QuickForm2/Container/Fieldset.php" => array("3009", "c01d23505028494f202da1e7c092c285"), "libs/HTML/QuickForm2/Container/Group.php" => array("10690", "af6598a96ab2505936156cf3c82eb231"), @@ -482,15 +1129,14 @@ class Manifest { "libs/HTML/QuickForm2/Rule/Regex.php" => array("5158", "842b30f0687b7d65c2c940bbfc44008e"), "libs/HTML/QuickForm2/Rule/Required.php" => array("3555", "4c440ff4e187faf10e307b04e774011b"), "libs/javascript/json2.js" => array("3377", "ba3293970e13b03a2ea92f5b6b5bf544"), - "libs/javascript/sprintf.js" => array("3795", "f8659e7549fb9e482d4f8145399f421c"), - "libs/jqplot/build_minified_script.sh" => array("1434", "b5fe6cddbb5e8bf8f440a380f3e7e5a9"), + "libs/jqplot/build_minified_script.sh" => array("1434", "9847924971c2ac2efb45b990fddb1867"), "libs/jqplot/excanvas.min.js" => array("19351", "5e2fefd5c782233c12383cca3b19e935"), "libs/jqplot/gpl-2.0.txt" => array("15112", "8ef64b86db8e0e63606284cf36b643be"), "libs/jqplot/jqplot.axisLabelRenderer.js" => array("3275", "61248e4baf0752ee2e89cf303c52a95e"), "libs/jqplot/jqplot.axisTickRenderer.js" => array("6570", "f7bdc4053619d39dd6ad9564de13b779"), "libs/jqplot/jqplot.canvasGridRenderer.js" => array("19588", "5d6c6588a60d6a298747d095fa11c662"), "libs/jqplot/jqplot.core.js" => array("175127", "57d2753e6e1a1aedb78e4c1ed7e2f48b"), - "libs/jqplot/jqplot-custom.min.js" => array("191785", "ad2a8daa551040c7c6061ade311f91b0"), + "libs/jqplot/jqplot-custom.min.js" => array("191818", "bb742f360c7af1d5f3b656a2bd56888d"), "libs/jqplot/jqplot.divTitleRenderer.js" => array("4174", "1ae09b12907cc772b2137ce60364b8a0"), "libs/jqplot/jqplot.linearAxisRenderer.js" => array("45487", "187b69d695443da0b2135a5e46d7b292"), "libs/jqplot/jqplot.linePattern.js" => array("4772", "1091be3885aaa2a2d9ed8171b85aed80"), @@ -506,7 +1152,7 @@ class Manifest { "libs/jqplot/plugins/jqplot.canvasAxisTickRenderer.js" => array("9843", "3f8a0bbc13e1793f6fc9a123c9bf3b98"), "libs/jqplot/plugins/jqplot.canvasTextRenderer.js" => array("24372", "58a963d919b36061685c46a75f6a4a50"), "libs/jqplot/plugins/jqplot.categoryAxisRenderer.js" => array("28571", "77d0a561f20e0b889e88758449ba7180"), - "libs/jqplot/plugins/jqplot.pieRenderer.js" => array("35551", "f78c034a0bae5eb0310392b5632907ec"), + "libs/jqplot/plugins/jqplot.pieRenderer.js" => array("35584", "bd7f7899e4a7414ab30c4034ac23ebe1"), "libs/jquery/gpl-2.0.txt" => array("15099", "2c1778696d3ba68569a0352e709ae6b7"), "libs/jquery/gpl-3.0.txt" => array("35147", "d32239bcb673463ab874e80d47fae504"), "libs/jquery/images/down_arrow.png" => array("2898", "6a5d9f1bd953608817ebf4904d4591bc"), @@ -514,25 +1160,14 @@ class Manifest { "libs/jquery/images/slide.png" => array("2831", "ef737b4f1d6b594656bca9e99b7f5968"), "libs/jquery/images/up_arrow.png" => array("2881", "bf4bbd999b2027d836e00fde27a46b39"), "libs/jquery/jquery.browser.js" => array("619", "236a3b854b2eeae01c8640ad82bb4178"), - "libs/jquery/jquery.history.js" => array("5592", "acd0667f090e0bf6e759bb86db1d674f"), - "libs/jquery/jquery.js" => array("96380", "52d16e147b5346147d0f3269cd4d0f80"), - "libs/jquery/jquery.jscrollpane.js" => array("63508", "08450c17ce2a83ab43176f80253d6f99"), - "libs/jquery/jquery.mousewheel.js" => array("3846", "f77bd9ca0396c7a8672f536884b1e1aa"), - "libs/jquery/jquery.placeholder.js" => array("4515", "6e5b889042b348bdee267ceecbe4159d"), - "libs/jquery/jquery.scrollTo.js" => array("2434", "c4dff68594e0fdb05b48aac9a90c0a19"), - "libs/jquery/jquery.smartbanner.js" => array("9921", "4e24cb04b88e2e0a2c013ab589ef5e2f"), + "libs/jquery/jquery.smartbanner.js" => array("14851", "82cfe5a1bb8e42ae3d168b04e482621e"), "libs/jquery/jquery.truncate.js" => array("2452", "cc3fcfc30d8a4496e891469ee20fbd77"), - "libs/jquery/jquery-ui.js" => array("228546", "ea464a34403c8772f3bc6ff2f5355f4b"), "libs/jquery/LICENSE-sizzle.txt" => array("17635", "418d0239a1435dae6b5c2e919a75f8c9"), "libs/jquery/MIT-LICENSE-history.txt" => array("1109", "242685841d67aa0bbc837735b177709f"), - "libs/jquery/MIT-LICENSE-jquery.txt" => array("1074", "13a84fe33922678518c49596de032d92"), - "libs/jquery/MIT-LICENSE-jqueryui.txt" => array("1311", "bac9338b4387621f0cea7720ace0450c"), - "libs/jquery/MIT-LICENSE-placeholder.txt" => array("1075", "0146cb31436f780624be47ce08eec616"), - "libs/jquery/MIT-LICENSE-scrollto.txt" => array("1120", "4a5d0d8578e331f5172fe5d0d1470fc4"), "libs/jquery/MIT-LICENSE-smartbanner.txt" => array("1071", "a23932b5f367ad0272456270829b52a7"), "libs/jquery/mwheelIntent.js" => array("2249", "71fe6a97b5e149ae08829a586fe7e7ad"), "libs/jquery/stylesheets/jquery.jscrollpane.css" => array("1400", "ead9005a6e67449768db45b42e3f9b89"), - "libs/jquery/stylesheets/jquery.smartbanner.css" => array("3901", "94fb09ced6636405d47b5ec6c1ed23c1"), + "libs/jquery/stylesheets/jquery.smartbanner.css" => array("4004", "d9e2cba33edaf7e2d21076924184b805"), "libs/jquery/stylesheets/scroll.less" => array("2066", "ab33e88a6d4360052d87685173dbf07a"), "libs/jquery/themes/base/images/ui-anim_basic_16x16.gif" => array("1553", "03ce3dcc84af110e9da8699a841e5200"), "libs/jquery/themes/base/images/ui-bg_flat_0_aaaaaa_40x100.png" => array("180", "2a44fbdb7360c60122bcf6dcef0387d8"), @@ -548,90 +1183,30 @@ class Manifest { "libs/jquery/themes/base/images/ui-icons_454545_256x240.png" => array("4369", "771099482bdc1571ece41073b1752596"), "libs/jquery/themes/base/images/ui-icons_888888_256x240.png" => array("4369", "faf6f5dc44e713178784c1fb053990aa"), "libs/jquery/themes/base/images/ui-icons_cd0a0a_256x240.png" => array("4369", "5d8808d43cefca6f6781a5316d176632"), - "libs/jquery/themes/base/jquery-ui.css" => array("25699", "76b945b2f5246c4cbe31857afd481ceb"), - "libs/MaxMindGeoIP/geoipcity.inc" => array("6973", "d191ea9f911a9cf3bc98a07faadf511d"), - "libs/MaxMindGeoIP/geoip.inc" => array("31447", "26f4d94b6c394dcbb6abf57df5acbccf"), - "libs/MaxMindGeoIP/geoipregionvars.php" => array("95763", "1635d7024c6a8b2502ed6f60659c9a1f"), - "libs/pChart2.1.3/change.log" => array("11838", "47d20227c6b22d5425e0bc60475084ad"), - "libs/pChart2.1.3/class/pData.class.php" => array("30575", "3e35fc351f143b70e8959fb65f6bb522"), - "libs/pChart2.1.3/class/pDraw.class.php" => array("319989", "f27d42bc9fa34966f4483546254c312c"), - "libs/pChart2.1.3/class/pImage.class.php" => array("19960", "abbaf39d5fb456ccec682c0f0814d8a9"), - "libs/pChart2.1.3/class/pPie.class.php" => array("65564", "4f2ed8e8c89bd3646c4289e5f2547b7b"), - "libs/pChart2.1.3/GPLv3.txt" => array("35148", "8f0e2cd40e05189ec81232da84bd6e1a"), - "libs/pChart2.1.3/readme.txt" => array("5971", "8e04cbe332d419f3177b2d9300b15e1a"), - "libs/PclZip/lgpl-2.1.txt" => array("26530", "4fbd65380cdd255951079008b364516c"), - "libs/PclZip/pclzip.lib.php" => array("196363", "968cb96854866df0370e6fd5523fa05a"), - "libs/PEAR5.php" => array("1087", "1a8f67d58009372a6cbcddd638b128cf"), - "libs/PEAR/Exception.php" => array("14006", "424a61a67dbd5f9f3ed5fc3be2b9ac54"), - "libs/PEAR/FixPHP5PEARWarnings.php" => array("152", "ff5f4e5d365b916ea63225840bc0b71a"), - "libs/PEAR/LICENSE" => array("1477", "45b44486d8090de17b2a8b4211fab247"), - "libs/PEAR.php" => array("33932", "f9f83fb6efef354ec16765ffe17d2ae4"), - "libs/PiwikTracker/LICENSE.txt" => array("1505", "7bbcab51f5db7fee7e6864a639ff56ab"), - "libs/PiwikTracker/PiwikTracker.php" => array("56782", "2a84b2de3492460089a52baad3b1fcd6"), - "libs/README.md" => array("1713", "fad15836001bfea82cdc4f362febe4fb"), + "libs/jquery/themes/base/jquery-ui.min.css" => array("25699", "76b945b2f5246c4cbe31857afd481ceb"), + "libs/MaxMindGeoIP/geoipcity.inc" => array("7446", "c5ac78777d5c430ed3236e466040a67f"), + "libs/MaxMindGeoIP/geoip.inc" => array("44492", "17d480b5501dc59dffa4d4ee6dc2448a"), + "libs/MaxMindGeoIP/geoipregionvars.php" => array("125067", "3999586d260df20e06129cf10c187301"), + "libs/pChart/change.log" => array("25310", "c90748e01e5617278081a5a0b4ac845b"), + "libs/pChart/class/pData.class.php" => array("30667", "f45c4fba3489b5b1a940acaa7a59feef"), + "libs/pChart/class/pDraw.class.php" => array("327671", "963177a8bfd387c7a6048a3681a11241"), + "libs/pChart/class/pImage.class.php" => array("20001", "26fcbe95d274656a7a11ba6583f668d4"), + "libs/pChart/class/pPie.class.php" => array("65577", "30b65724de538a03aca427be199ad58a"), + "libs/pChart/GPLv3.txt" => array("35823", "664aa96239b59b044722945d56f70200"), + "libs/pChart/readme.txt" => array("12528", "2417b0df8a049d780f8866db6a6f946f"), + "libs/PiwikTracker/PiwikTracker.php" => array("631", "f4d49c84c2212987a26ce7c0b822eba9"), + "libs/README.md" => array("1712", "04e9ad7e47b792e5c4d0ebdc3b6cfe8f"), "libs/sparkline/CHANGES" => array("648", "b6d213a7ad5d1f2c6e3eac38d48c1f8a"), "libs/sparkline/DESIGN" => array("648", "a9e2a29ce386fb408ab2eaa02a86c2ed"), "libs/sparkline/gpl-2.0.txt" => array("18092", "b234ee4d69f5fce4486a80fdaf4a4263"), - "libs/sparkline/lib/Object.php" => array("3951", "095eae57154bed92a78e3d951e0135ef"), - "libs/sparkline/lib/Sparkline_Bar.php" => array("6834", "c5aa452cf5698ca7baca128aa02ab2f8"), - "libs/sparkline/lib/Sparkline_Line.php" => array("11026", "9cc756ac498e10ae7291d0a252ef3bf6"), - "libs/sparkline/lib/Sparkline.php" => array("16623", "31777f846d96077e1b32cba2f1874138"), + "libs/sparkline/lib/Object.php" => array("3956", "422308842217470ec8ee823019460cd1"), + "libs/sparkline/lib/Sparkline_Bar.php" => array("6834", "43d43b34cd21761fee83094ef402d9df"), + "libs/sparkline/lib/Sparkline_Line.php" => array("11025", "8b7ad1aadf105b470112ebfd4869eb50"), + "libs/sparkline/lib/Sparkline.php" => array("16630", "7ea0fc0be7625a2418e4f4b635856e49"), "libs/sparkline/LICENSE-BSD.txt" => array("1505", "51639a73ddb4999a16fc9249eb445acc"), "libs/sparkline/README" => array("1043", "aa954952640a7c645151f63905e9692a"), - "libs/tcpdf/2dbarcodes.php" => array("7785", "ab7d01f44bbea0b839c6b2c2f362f4cb"), - "libs/tcpdf/barcodes.php" => array("59791", "9f0f79a92e025acfd5aef0c09b0ea736"), - "libs/tcpdf/composer.json" => array("933", "8681cdc1c3eea06625bf0c827f4a353e"), - "libs/tcpdf/config/lang/eng.php" => array("1200", "cc64ed556dacc69a43777204d08c952b"), - "libs/tcpdf/config/tcpdf_config_alt.php" => array("5320", "43c5e852ab49252b52871512ef6bd3db"), - "libs/tcpdf/config/tcpdf_config.php" => array("5202", "f2ca50b70b473fa4f0fd002e34970761"), - "libs/tcpdf/fonts/almohanad.ctg.z" => array("2780", "f2a06979cd7c5262b773f1c5257f93f8"), - "libs/tcpdf/fonts/almohanad.php" => array("14074", "3cf9d431468593f9d4eba1795631943b"), - "libs/tcpdf/fonts/almohanad.z" => array("121292", "80a4fbacf654e77c487bbcefad390dfb"), - "libs/tcpdf/fonts/dejavusans.ctg.z" => array("10120", "b693c24b2880b5d2e5f7c7ecd31213dc"), - "libs/tcpdf/fonts/dejavusans.php" => array("52352", "f38c6f5c629707b6b1197c60a68a77ed"), - "libs/tcpdf/fonts/dejavusans.z" => array("361229", "df214f9763ae5fcab34b85aab01d49d9"), - "libs/tcpdf/fonts/helveticabi.php" => array("2589", "c22fdc8941f2956e0930b20105870468"), - "libs/tcpdf/fonts/helveticab.php" => array("2580", "3daad3713df02c15beebd09ceecacacd"), - "libs/tcpdf/fonts/helveticai.php" => array("2584", "e0a7f23376f50de631db93814aff2e35"), - "libs/tcpdf/fonts/helvetica.php" => array("2575", "2a315fa2593161154c319788f0ef2127"), - "libs/tcpdf/fonts/hysmyeongjostdmedium.php" => array("1829", "51f6fe162641de3714866950d5eff4e8"), - "libs/tcpdf/fonts/kozgopromedium.php" => array("3577", "2c5e8a67d1a805aae9842bbad59a873f"), - "libs/tcpdf/fonts/kozminproregular.php" => array("3454", "78fdf805f1cea6cd01912192821ec734"), - "libs/tcpdf/fonts/msungstdlight.php" => array("1550", "c940b153fb6c5b3498efa181881b5b6c"), - "libs/tcpdf/fonts/stsongstdlight.php" => array("1627", "eb85dc872664c0769e9fab1b7540b4d5"), - "libs/tcpdf/fonts/symbol.php" => array("2555", "20e28c8b386ddbb38ead777f717d7c44"), - "libs/tcpdf/fonts/timesbi.php" => array("2580", "a5f3fbbef1831fe0bcd060edb6e5010b"), - "libs/tcpdf/fonts/timesb.php" => array("2577", "ad485022027867116de0bf6c25b1854a"), - "libs/tcpdf/fonts/timesi.php" => array("2575", "8fd8e9a11cca513a4da0f25ff1a24149"), - "libs/tcpdf/fonts/times.php" => array("2572", "a75033315ee90464410b47cc27ce9ff0"), - "libs/tcpdf/gpl.txt" => array("35147", "d32239bcb673463ab874e80d47fae504"), - "libs/tcpdf/htmlcolors.php" => array("5499", "fe132ea6a41cd787b4c2ff18716625b5"), - "libs/tcpdf/include/sRGB.icc" => array("3048", "060e79448f1454582be37b3de490da2f"), - "libs/tcpdf/include/tcpdf_colors.php" => array("14661", "f782f4cb468e8a5e956ca0759530e132"), - "libs/tcpdf/include/tcpdf_filters.php" => array("14677", "e175abe0e5c8661e08834014abead451"), - "libs/tcpdf/include/tcpdf_font_data.php" => array("313432", "8f83bbc144d70505672f82679546c72d"), - "libs/tcpdf/include/tcpdf_fonts.php" => array("95129", "70425e7872fbabb524b934ec474b0880"), - "libs/tcpdf/include/tcpdf_images.php" => array("11170", "b1c6baba7c286f48d813df899350c8db"), - "libs/tcpdf/include/tcpdf_static.php" => array("106691", "fab9356ece2e9d51e831dd00da64324b"), - "libs/tcpdf/lgpl-3.0.txt" => array("7651", "e6a600fd5e1d9cbde2d983680233ad02"), - "libs/tcpdf/LICENSE.TXT" => array("43636", "5c87b66a5358ebcc495b03e0afcd342c"), - "libs/tcpdf/pdf417.php" => array("53738", "130560d35265c9666440dbf5086f7846"), - "libs/tcpdf/qrcode.php" => array("80058", "59adfba524c4445f850a92380eb2a17f"), - "libs/tcpdf/README.TXT" => array("5396", "46bcf50a79dc0d5f82b4c5c3df95f504"), - "libs/tcpdf/spotcolors.php" => array("2153", "4b3c09efafad1e9c28021fca16f52147"), - "libs/tcpdf/tcpdf_autoconfig.php" => array("6918", "1df4c5b1b14c31160da910ed8e390e91"), - "libs/tcpdf/tcpdf.crt" => array("2290", "c137aab97f7e06a6038589968e10d976"), - "libs/tcpdf/tcpdf.fdf" => array("1286", "96f873c30a6f6a0f884d713d185d5bd8"), - "libs/tcpdf/tcpdf_import.php" => array("3327", "6bb88a8a3d69511d1bf9e7af12ab5f47"), - "libs/tcpdf/tcpdf.p12" => array("1749", "7e078148a1ab66ca05041915f7013204"), - "libs/tcpdf/tcpdf_parser.php" => array("26986", "d7fcc07fbea917f9ae4e07aad9b86fd3"), - "libs/tcpdf/tcpdf.php" => array("883754", "5c02ba6d7a3dba39c140ca7aa25d22f0"), - "libs/tcpdf/unicode_data.php" => array("227828", "2ec28c23ec6ad9f71f8b822a0ce0a1bf"), "libs/upgradephp/README" => array("373", "f0a0f26fd38b1e01be19766c11654564"), - "libs/upgradephp/upgrade.php" => array("16164", "4cc4006f9ae5e23b73e69c51b64cd99c"), - "libs/UserAgentParser/README.md" => array("972", "34ff27b5748bcd32dd255595cb386ece"), - "libs/UserAgentParser/UserAgentParser.php" => array("28445", "974d5ff712e0331175d543d797e821fd"), - "libs/UserAgentParser/UserAgentParser.test.php" => array("4690", "7ec960da6d502b46706f6fa3602a72f4"), + "libs/upgradephp/upgrade.php" => array("16857", "8017764ef397aedbd9c4e96753aa5142"), "libs/Zend/Cache/Backend/Apc.php" => array("11108", "db51f9156bd866fa2ffcb7ee3f8bfd77"), "libs/Zend/Cache/Backend/BlackHole.php" => array("7461", "0058fa2c88b65f6c3aa022534aa1f86e"), "libs/Zend/Cache/Backend/ExtendedInterface.php" => array("4035", "ac5c1f66c2eb9a542dd144a9c1ea8b53"), @@ -770,9 +1345,9 @@ class Manifest { "libs/Zend/Mime.php" => array("12927", "0059c7eef064f1225af33d1ddc0e8cef"), "libs/Zend/Registry.php" => array("6155", "cebfa4d6d7658af3167d521ffdc09153"), "libs/Zend/Session/Abstract.php" => array("6051", "f46f1047a6f5b37b2180cfe18f9f37a2"), - "libs/Zend/Session/Exception.php" => array("2313", "5c87f32e2d6c61f0937481ff6da38d05"), + "libs/Zend/Session/Exception.php" => array("2273", "fb3218b5361f28fa86cfcef6e1eef62f"), "libs/Zend/Session/Namespace.php" => array("16860", "1351d7294b06b262d8fa63f88fed5503"), - "libs/Zend/Session.php" => array("27322", "02d23cef6fbabae08268d017defdb0bc"), + "libs/Zend/Session.php" => array("27324", "cf609c7d0e9b3d327f1436920c9e2ab7"), "libs/Zend/Session/SaveHandler/DbTable.php" => array("17926", "97ed5c81d24270470450211991de3c12"), "libs/Zend/Session/SaveHandler/Exception.php" => array("1174", "b49e92bc8cd9c7e29afb7a5182da8325"), "libs/Zend/Session/SaveHandler/Interface.php" => array("2056", "f27f2b6f050869a7f7aef5a5385a8e05"), @@ -815,7 +1390,7 @@ class Manifest { "libs/Zend/Validate/Hostname/Cn.php" => array("168071", "55fa16bdeecbebf10df472ede8681606"), "libs/Zend/Validate/Hostname/Com.php" => array("13661", "1b25658d4741fdcb5702e0fdc64f7e0e"), "libs/Zend/Validate/Hostname/Jp.php" => array("55551", "0fe384871528b21e2a981bbf1d0de767"), - "libs/Zend/Validate/Hostname.php" => array("35735", "cb6fff7c0769efb452f7e04ba185e54a"), + "libs/Zend/Validate/Hostname.php" => array("45931", "2eafdf34459c8abfa4a9174b08d0f5a8"), "libs/Zend/Validate/Iban.php" => array("7303", "2725d29f25df052fe2a0b5b28464960b"), "libs/Zend/Validate/Identical.php" => array("4094", "3c325081db3380872533a48ffb4e54f3"), "libs/Zend/Validate/InArray.php" => array("5291", "2a1f8aab4b1d7e39da319972c0fcbba5"), @@ -828,368 +1403,1149 @@ class Manifest { "libs/Zend/Validate.php" => array("8509", "88ef476e71d54440da1123a10f59ceea"), "libs/Zend/Validate/PostCode.php" => array("6018", "1cffbba8fc10e1b3780bb4c6e5550956"), "libs/Zend/Validate/Regex.php" => array("4021", "eb2c6b2377b924a3ebcd1c708802caf7"), - "libs/Zend/Validate/StringLength.php" => array("6633", "9c0f8e241452e13eb5a19c3994e74d11"), + "libs/Zend/Validate/StringLength.php" => array("7010", "5855c9a142984f5c8c1a1e82f5521954"), "libs/Zend/Version.php" => array("2558", "5d840e09ef8ece6d15e8bfb786af0faf"), - "misc/cron/archive.php" => array("1427", "17d3d028e0063eddbd2171a971937e96"), - "misc/cron/archive.sh" => array("3411", "310b24292e5968a2b27fc81cc2d0133b"), - "misc/cron/updatetoken.php" => array("1227", "40be39dd7be96bf3cf07a25321646a53"), + "misc/composer/build-xhprof.sh" => array("1487", "21e0b5dceefc2c7b080a6b1a5bfaae4d"), + "misc/composer/clean-xhprof.sh" => array("325", "139953c79fab328c3de2e2c2fee20e5c"), + "misc/cron/archive.php" => array("2630", "cffebf416f25bb2f125dffc61c50b092"), + "misc/cron/archive.sh" => array("1228", "e86b8120a36fe555907de4bd617d6d49"), + "misc/cron/.htaccess" => array("103", "9430d4c88b1d229030349569fefc7b35"), + "misc/cron/updatetoken.php" => array("1833", "40b6df17908ed1ee013fdf971769e1bf"), "misc/gpl-3.0.txt" => array("35147", "d32239bcb673463ab874e80d47fae504"), - "misc/How to install Piwik.html" => array("281", "f4a48dcdad08996699fa52876fe2e59f"), - "misc/log-analytics/import_logs.py" => array("63576", "d27a2bb0e1aeb6c8ef28ee2140eac2f6"), - "misc/log-analytics/README.md" => array("12850", "81e24593a103725475033d73a7911308"), - "misc/others/api_internal_call.php" => array("925", "f0cadfd1d505c9190c5b799116329c27"), - "misc/others/api_rest_call.php" => array("926", "ee4a436279d3588350998d23a4457331"), - "misc/others/cli-script-bootstrap.php" => array("1197", "c9f9cc36be2fa58324405d675f6cdb21"), - "misc/others/download-count.txt" => array("563", "fa8ababaa1c23873c85b576807012997"), - "misc/others/ExamplePiwikTracker.php" => array("650", "fef8fb6167d19edfe9050da691d3e80d"), - "misc/others/geoipUpdateRows.php" => array("8375", "f35ef4770cebf00f9cf6b733aef3b14a"), + "misc/How to install Piwik.html" => array("336", "9e59e7b4b64998114c992b77d2eee716"), + "misc/internal-docs/content-tracking.md" => array("38548", "fc130e30f124b054b646b3641e0b73bb"), + "misc/log-analytics/CONTRIBUTING.md" => array("347", "154beae0480501ee765deb75564e8e7b"), + "misc/log-analytics/import_logs.py" => array("91919", "89b90a5b31b0c5057ade2f451615695e"), + "misc/log-analytics/README.md" => array("16859", "cf215092ac50f23407611bbde12ead0d"), + "misc/others/api_internal_call.php" => array("845", "377eb304bdc41f91da3dc6abf19829ea"), + "misc/others/api_rest_call.php" => array("929", "9dcb1bf366ba9b34ad00bc827058b3de"), + "misc/others/download-count.txt" => array("561", "4ce0d7021c01111dee281cc7d777ac66"), + "misc/others/ExamplePiwikTracker.php" => array("648", "24972c98a5c46a311b77dd17333ab1b4"), + "misc/others/geoipUpdateRows.php" => array("304", "a614a5b93499a7a5e21c2e0a5aa56655"), "misc/others/iframeWidget.htm" => array("613", "3c0f965d8cbc05e01e431f2a060fa3e5"), "misc/others/iframeWidget_localhost.php" => array("2156", "d0ab5ceb17e0010599d67442a6e113f7"), - "misc/others/phpstorm-codestyles/Piwik_codestyle.xml" => array("3585", "18b7d31e7283b1db8e778fdf7ab18721"), - "misc/others/phpstorm-codestyles/README.md" => array("984", "948c9dfd9918312ffe9dab44eb454054"), "misc/others/stress.sh" => array("293", "d1964246f8080b3e2101610fad483c8c"), - "misc/others/test_cookies_GenerateHundredsWebsitesAndVisits.php" => array("1055", "0dee3fddccfe0ab8aa440f1a9f4d7827"), - "misc/others/test_generateLotsVisitsWebsites.php" => array("10290", "2e61928d6e310ddf6ab7a61964850fdb"), - "misc/others/tracker_simpleImageTracker.php" => array("1060", "a9436b017e7ad30d59cff5b87e715308"), - "misc/others/uninstall-delete-piwik-directory.php" => array("1140", "8bdcdad35ced4e49b82cb9b809eebabe"), + "misc/others/tracker_simpleImageTracker.php" => array("992", "079b19e20d6813c3ce394c3091c50e81"), + "misc/others/uninstall-delete-piwik-directory.php" => array("1230", "e2b52ad258ad2d6934e85f360bbb2ece"), "misc/others/widget_example_lastvisits.html" => array("456", "faacc1f323fede8966eabd56cbacc362"), - "misc/proxy-hide-piwik-url/piwik.php" => array("2763", "742ba5f11208ea5e08dc90d8055bc6c8"), - "misc/proxy-hide-piwik-url/README.md" => array("3234", "dd49db51608aa49d3b490053a98f0d68"), - "misc/user/.gitkeep" => array("0", "d41d8cd98f00b204e9800998ecf8427e"), + "misc/proxy-hide-piwik-url/README.md" => array("122", "41d686494ba5be91c02b6a1e9fad5135"), "misc/user/index.html" => array("167", "bcef3676ee6eefa6968695ef954db78f"), - "piwik.js" => array("23183", "6e7bb550f0b87029593c66404f477088"), - "piwik.php" => array("5150", "30817ddf6963d6ef026ec587b8c262c7"), - "plugins/Actions/Actions.php" => array("47067", "8fd3c026d5bda12e6e35d4582fece05e"), - "plugins/Actions/API.php" => array("25131", "eb9695dccffefdb8b0be655caf4c9c7e"), - "plugins/Actions/Archiver.php" => array("22986", "f340a0c2888b7ad69baaa017ae85978f"), - "plugins/Actions/ArchivingHelper.php" => array("23888", "e2a415e828d32a1ba8f07137df8cf60d"), - "plugins/Actions/Controller.php" => array("3508", "d2b0b84b804e880aee1d80a68e8bdd03"), - "plugins/Actions/javascripts/actionsDataTable.js" => array("13340", "00a944a54e01638152a462776094fecf"), + "piwik.js" => array("54851", "7d6acc696e3b30383747e1c03b7ed6e0"), + "piwik.php" => array("2534", "2970c9592a256a7a4230162653780af7"), + "plugins/Actions/Actions/ActionClickUrl.php" => array("1919", "ed36b2d616772e8e1e00a84b4892ded8"), + "plugins/Actions/Actions/ActionDownloadUrl.php" => array("998", "ddecea552c6a2f856336c2d3c00472d1"), + "plugins/Actions/Actions/ActionSiteSearch.php" => array("9777", "7a1982d52f3cec7e8d347ed622851912"), + "plugins/Actions/Actions.php" => array("6214", "5f36a595e02531638efd84aaa89d749b"), + "plugins/Actions/API.php" => array("21178", "01a43ab96e677bab6bb9d3574d7a49ad"), + "plugins/Actions/Archiver.php" => array("21662", "97fc4bfe36d3e9a644b816a28a687019"), + "plugins/Actions/ArchivingHelper.php" => array("24517", "227b79d3691f1b3a728af23f914cd38e"), + "plugins/Actions/Columns/ActionType.php" => array("2250", "f5891d6e4408bedd4d65ad29775a4f81"), + "plugins/Actions/Columns/ActionUrl.php" => array("752", "7e74832ac3d7fddc91e3beb8ca4cc0c1"), + "plugins/Actions/Columns/ClickedUrl.php" => array("741", "569435ad07d5cfc1cc2485964edf14c5"), + "plugins/Actions/Columns/DestinationPage.php" => array("396", "959a4732f630255e15445dced7c8d2db"), + "plugins/Actions/Columns/DownloadUrl.php" => array("745", "4f32fffc5aeec53f04d3f0a7271ac777"), + "plugins/Actions/Columns/EntryPageTitle.php" => array("1288", "d3fdbc2926514818968910c2677c4caf"), + "plugins/Actions/Columns/EntryPageUrl.php" => array("1276", "0555350d3b6ebb11ad5d52d9e1586e55"), + "plugins/Actions/Columns/ExitPageTitle.php" => array("1644", "470abd224b7a526443f8e5ab28893e8a"), + "plugins/Actions/Columns/ExitPageUrl.php" => array("1718", "214186edcb73b85b733172d5efb40038"), + "plugins/Actions/Columns/Keyword.php" => array("380", "3548f9cd71eb77758d5cfa046106902a"), + "plugins/Actions/Columns/KeywordwithNoSearchResult.php" => array("406", "2978f63d8afd8c75c53d7988f8d91fde"), + "plugins/Actions/Columns/Metrics/AveragePageGenerationTime.php" => array("3492", "aeeefc4ee852c8cbee57b24af5e02b3e"), + "plugins/Actions/Columns/Metrics/AverageTimeOnPage.php" => array("1249", "e6667ff89bc97c518e3eaaeba3d6be3b"), + "plugins/Actions/Columns/Metrics/BounceRate.php" => array("1339", "1a6089b086fe2d0053bf8fee849625bf"), + "plugins/Actions/Columns/Metrics/ExitRate.php" => array("1206", "6d50ee5e65c7264f8552415c66d51aaa"), + "plugins/Actions/Columns/PageTitle.php" => array("762", "d7fdc5ca389b02815be1cb14354f98fb"), + "plugins/Actions/Columns/PageUrl.php" => array("911", "8bac626402b01576fbf083f65f4f1875"), + "plugins/Actions/Columns/SearchCategory.php" => array("394", "248bb839017d9d6027961f9019c28cb6"), + "plugins/Actions/Columns/SearchDestinationPage.php" => array("403", "a739fdc7c70e9011018c159a6773a4cd"), + "plugins/Actions/Columns/SearchKeyword.php" => array("749", "c25e2141b475cc2aedab1390344efafa"), + "plugins/Actions/Columns/SearchNoResultKeyword.php" => array("403", "4a565399ecb9870c50a70276f19928a6"), + "plugins/Actions/Columns/TimeSpentRefAction.php" => array("755", "e7d53820c1f0ae2e4c7b698f46d6178f"), + "plugins/Actions/Columns/VisitTotalActions.php" => array("2233", "750e89aa8ddc40d40a746d9475e8fe19"), + "plugins/Actions/Columns/VisitTotalSearches.php" => array("1816", "72e932064fa35eca9e62423a8ee77ff4"), + "plugins/Actions/Controller.php" => array("1215", "5b0d5fa8ef2a35f4404231b0cc6478ad"), + "plugins/Actions/DataTable/Filter/Actions.php" => array("1292", "16c377eccd19f5f0c60b945c0784773d"), + "plugins/Actions/javascripts/actionsDataTable.js" => array("14108", "138b49a1b364dd5b48df26be95762aa4"), + "plugins/Actions/javascripts/rowactions.js" => array("2171", "26baccf5d1b317f3c9fc5213f0662910"), + "plugins/Actions/lang/am.json" => array("376", "5b399d577eb14f72c95fceeaa6c9f137"), + "plugins/Actions/lang/ar.json" => array("8416", "4b728ccea2e3a52b9caa3c8a54f996fb"), + "plugins/Actions/lang/be.json" => array("4066", "93336770aff542bfe5ced0602cbc08fd"), + "plugins/Actions/lang/bg.json" => array("10180", "47b3faef2f99ca3b2438e97c5d4f0ed0"), + "plugins/Actions/lang/bs.json" => array("6320", "158df7218928ea4e808e982cc2a8cac9"), + "plugins/Actions/lang/ca.json" => array("6959", "41e4791112655de05c95d371ab34395a"), + "plugins/Actions/lang/cs.json" => array("6839", "36fc736467b641ae56c9ff7379778801"), + "plugins/Actions/lang/da.json" => array("6240", "f6f417f68a23b23eb92527fa3a0c70fa"), + "plugins/Actions/lang/de.json" => array("6698", "9a749fddf817c5e5def5ee5785a217fd"), + "plugins/Actions/lang/el.json" => array("11578", "1063a46e1d9cad68b71543fe67ceefe3"), + "plugins/Actions/lang/en.json" => array("6307", "833e5c76268d937af82fec8690a36384"), + "plugins/Actions/lang/es.json" => array("7250", "6a7f83856ec803f798b71c995e4ebef3"), + "plugins/Actions/lang/et.json" => array("2418", "793fefd5089a115234cd311d972ee95b"), + "plugins/Actions/lang/eu.json" => array("401", "2e0323e1b050700bbcd649f0d8a6b7c6"), + "plugins/Actions/lang/fa.json" => array("7289", "3694c15bfb66430aaeb4def59561a05b"), + "plugins/Actions/lang/fi.json" => array("5622", "b95bce037cf4a09fc90a64178cd3c28a"), + "plugins/Actions/lang/fr.json" => array("7258", "66c24d7f702e4c525fcad9878c8a362b"), + "plugins/Actions/lang/gl.json" => array("149", "dc6987977eb292a80851170884b71913"), + "plugins/Actions/lang/he.json" => array("6130", "762a07f46348ce2b1dde657293ca48c4"), + "plugins/Actions/lang/hi.json" => array("12782", "a9579f1e31d0525a5950378c7963199c"), + "plugins/Actions/lang/hr.json" => array("4949", "669880aca1d799000da83b7c6820490b"), + "plugins/Actions/lang/hu.json" => array("3774", "52bbfaa80a13f8f0d4b751b0b9ae4696"), + "plugins/Actions/lang/id.json" => array("6429", "9ecee682e447ebda32ca1e597e1d6f70"), + "plugins/Actions/lang/is.json" => array("501", "2eaee8b58c0b9118d736d71a152e65c6"), + "plugins/Actions/lang/it.json" => array("6987", "2b53adaff18d681a2ffae448e9244460"), + "plugins/Actions/lang/ja.json" => array("7429", "b95c7e07037e3b3fc12fb9e1a0065ad8"), + "plugins/Actions/lang/ka.json" => array("820", "054c1b8cf89e6119dc4c7102ae122d79"), + "plugins/Actions/lang/ko.json" => array("6782", "d2c31eb267be8fd20e107299d9fe763e"), + "plugins/Actions/lang/lt.json" => array("620", "1032b329dd62f82fa747d9bda1fc6395"), + "plugins/Actions/lang/lv.json" => array("842", "70862e89e64cad312ec4e2ba795d0e43"), + "plugins/Actions/lang/nb.json" => array("6298", "c44bd08fc530812e22b632b0d63bb7f8"), + "plugins/Actions/lang/nl.json" => array("6534", "761e68260263d4692d120cd9b89cb915"), + "plugins/Actions/lang/nn.json" => array("2852", "8d05622f8bfd4cd16b4e9d8d0358ec78"), + "plugins/Actions/lang/pl.json" => array("6996", "db24a7965fc5554153f55dda79a0f5e0"), + "plugins/Actions/lang/pt-br.json" => array("7121", "f3a16b2d8806f1b32b91a46255a27947"), + "plugins/Actions/lang/pt.json" => array("6086", "79ba7f1f4702b43043c2fbc6a11fb064"), + "plugins/Actions/lang/ro.json" => array("6877", "cc95ca84b8726b30d5787a2631651585"), + "plugins/Actions/lang/ru.json" => array("9889", "09bbed254583204b0bde56b537bc696a"), + "plugins/Actions/lang/sk.json" => array("2376", "f8f75b6767cd43347244bf3677e6b2ee"), + "plugins/Actions/lang/sl.json" => array("3971", "2ec0bf792f37dcc11502c9d9e19a2465"), + "plugins/Actions/lang/sq.json" => array("6907", "c267471d5e400c00a1e4c74d3fda4b59"), + "plugins/Actions/lang/sr.json" => array("6266", "5affec1d5694c6adaff40df76651a6bb"), + "plugins/Actions/lang/sv.json" => array("6381", "b43489b930405c04c3807a027bb605f4"), + "plugins/Actions/lang/ta.json" => array("11774", "aadbc78702afa25b13e1d5fef259c251"), + "plugins/Actions/lang/te.json" => array("1954", "2e66c315d884a6bea6109c0510b3616e"), + "plugins/Actions/lang/th.json" => array("9206", "c2ac213e4689019d18f176b8ec7e2eb9"), + "plugins/Actions/lang/tl.json" => array("7072", "21f36f73c17289033915ad69a1158ceb"), + "plugins/Actions/lang/tr.json" => array("6103", "c36ab78df5e6c8d51fd73450b26fd083"), + "plugins/Actions/lang/uk.json" => array("642", "830df2c8659b0f4cb6f6505b2876f6b0"), + "plugins/Actions/lang/vi.json" => array("7612", "57a05efb7df6bbc7eb79c6a2916dda5b"), + "plugins/Actions/lang/zh-cn.json" => array("5373", "878edf069c32806c2f589341068c178a"), + "plugins/Actions/lang/zh-tw.json" => array("2092", "597e0da9cc0a9e0d7b896ffb772824ea"), + "plugins/Actions/Menu.php" => array("752", "996bcc9fb134543c4982433a1443ae35"), + "plugins/Actions/Metrics.php" => array("3328", "28ac7894cbaec2bd700fccacaedb0b7d"), + "plugins/Actions/Reports/Base.php" => array("4089", "4bc81513de2464ad8a3dc449bc167a53"), + "plugins/Actions/Reports/GetDownloads.php" => array("1698", "1d892a40cf14f77e8a800caec320cfd8"), + "plugins/Actions/Reports/GetEntryPageTitles.php" => array("2815", "4399bdaa9562fe3574b005345bd5df0b"), + "plugins/Actions/Reports/GetEntryPageUrls.php" => array("2784", "c4bdc1218fe29131c63e1a049acdae1c"), + "plugins/Actions/Reports/GetExitPageTitles.php" => array("2834", "da0ad5ca820bf419c8fc6ba006bdf6a9"), + "plugins/Actions/Reports/GetExitPageUrls.php" => array("3163", "f41e1131304cfee4d03470e0063de1a9"), + "plugins/Actions/Reports/GetOutlinks.php" => array("1872", "9e21d00cdeb564e305a7db9a4eb0bd0f"), + "plugins/Actions/Reports/GetPageTitlesFollowingSiteSearch.php" => array("2982", "a4419a52d67cfdbd352837c1c8b64e26"), + "plugins/Actions/Reports/GetPageTitles.php" => array("2817", "e61d82dfb4560e29e683109d5b516bfc"), + "plugins/Actions/Reports/GetPageUrlsFollowingSiteSearch.php" => array("1214", "c90ab0765c4e113efd88929bccde379a"), + "plugins/Actions/Reports/GetPageUrls.php" => array("2341", "87930ff88c6790c33dfc758a63a9a314"), + "plugins/Actions/Reports/Get.php" => array("940", "7cf5da1d4ce4c7e1ae81bbffa36a8dae"), + "plugins/Actions/Reports/GetSiteSearchCategories.php" => array("2368", "ccc6eabe8fd4988cde78a3f366c2b359"), + "plugins/Actions/Reports/GetSiteSearchKeywords.php" => array("2591", "4a31acd8bb77263bd0ab6d41d4f5d84f"), + "plugins/Actions/Reports/GetSiteSearchNoResultKeywords.php" => array("2191", "254b1d8ff89bc88a7bddc516fa975868"), + "plugins/Actions/Reports/SiteSearchBase.php" => array("1840", "4c3af06e5c25d20c05066ed25027695d"), + "plugins/Actions/Segment.php" => array("456", "dd99845b7be42024b0d7696ce125e1e1"), "plugins/Actions/stylesheets/dataTableActions.less" => array("80", "6ea9a28abb3fdf5d59b75af48dbde129"), - "plugins/Actions/templates/indexSiteSearch.twig" => array("598", "f8495db5767bbb21576a5690e9b92435"), - "plugins/Annotations/AnnotationList.php" => array("15708", "0507b807d118f763ff4012e34ffca9ae"), - "plugins/Annotations/Annotations.php" => array("1134", "fa376e6beeda62967ac35bf96f42486f"), - "plugins/Annotations/API.php" => array("14544", "83c95cab8ef9e4862d31cab0dfa21c55"), - "plugins/Annotations/Controller.php" => array("8319", "42bb469975f6bc567f0bc78f3362b08d"), - "plugins/Annotations/javascripts/annotations.js" => array("22584", "b14eadbeac0cfacb80e9858140da762f"), - "plugins/Annotations/stylesheets/annotations.less" => array("3505", "f311fd592023253c5a386753b70eec9f"), - "plugins/Annotations/templates/_annotationList.twig" => array("1281", "6305cb8e31ab433b40e2fde4ae22ecf2"), - "plugins/Annotations/templates/_annotation.twig" => array("2543", "2e4cd965a46c6c3ed241be7ef6253006"), - "plugins/Annotations/templates/getAnnotationManager.twig" => array("1133", "b330fa07ac6c11095ed0c165f3a51446"), - "plugins/Annotations/templates/getEvolutionIcons.twig" => array("859", "fb82d9e0fe92f4e31701a902173f17a1"), + "plugins/Actions/templates/indexSiteSearch.twig" => array("682", "c3b8fe2c3ca8fcdffbb608d21d5c15fe"), + "plugins/Actions/Tracker/ActionsRequestProcessor.php" => array("3574", "3e3572fffed135f5bf2f5a9ffd464fda"), + "plugins/Annotations/AnnotationList.php" => array("15797", "094f4ad14ccb2de71545d6aa32e969cf"), + "plugins/Annotations/Annotations.php" => array("1334", "da2c5837d9a2feea7b450d5a56c3dff8"), + "plugins/Annotations/API.php" => array("14553", "ca5ada6bbf907207518163f98c4c0cad"), + "plugins/Annotations/Controller.php" => array("8501", "45f0d233bd8662fd2b018203ccdd36c6"), + "plugins/Annotations/javascripts/annotations.js" => array("22500", "04bcf977ccfe42c2e3115c9d45da62f8"), + "plugins/Annotations/lang/ar.json" => array("2056", "cfa18772cf457356d04b02e410fde6de"), + "plugins/Annotations/lang/bg.json" => array("2716", "a3b2b49029e20d00e6ef12178c1aa2df"), + "plugins/Annotations/lang/bs.json" => array("1218", "8cabf9ad341afc327135154a707cd821"), + "plugins/Annotations/lang/ca.json" => array("1561", "fa9a9c54b2259e133fa919e3c4a0349f"), + "plugins/Annotations/lang/cs.json" => array("1621", "2be63dce1fa2cb34ce802c3a0752f0f5"), + "plugins/Annotations/lang/da.json" => array("1495", "c6f4ec36258851bf11b79a02c1314752"), + "plugins/Annotations/lang/de.json" => array("1957", "3eb607cc1226ef490c6c647affb4dcbb"), + "plugins/Annotations/lang/el.json" => array("3005", "b6bd308550536f56e6131fd196900aff"), + "plugins/Annotations/lang/en.json" => array("1590", "c4fc006f6a1828222e1ea54a80cde879"), + "plugins/Annotations/lang/es.json" => array("1788", "475baa68587995994b2208a7044a1066"), + "plugins/Annotations/lang/et.json" => array("913", "12f7521cc9cb3e4aa2f2a079b5ac3b83"), + "plugins/Annotations/lang/fa.json" => array("2465", "a7a17289a14a910248aa28f3ace21526"), + "plugins/Annotations/lang/fi.json" => array("1699", "32c6ac22f5222f5273b8ededfd421acb"), + "plugins/Annotations/lang/fr.json" => array("1795", "fef2c8c4ce756918d3427c2371a68360"), + "plugins/Annotations/lang/gl.json" => array("460", "14b5980a4972b6f4bf9399609d69014d"), + "plugins/Annotations/lang/he.json" => array("855", "88977dd5a4d8f6c53a9c0d362c9d9fee"), + "plugins/Annotations/lang/hi.json" => array("3552", "c3e35852bd91c59eed37dd42ab1d94c5"), + "plugins/Annotations/lang/hr.json" => array("444", "bd243877449c1fbf7e51f2e3501b4bd7"), + "plugins/Annotations/lang/id.json" => array("1811", "1aacbbb66973d061e7b44254ee5d161a"), + "plugins/Annotations/lang/it.json" => array("1644", "42d15eee8406b4e8328cbbc8d1abed30"), + "plugins/Annotations/lang/ja.json" => array("1785", "91dea5ad7c0ae86a455520a9e2cfcb3c"), + "plugins/Annotations/lang/ko.json" => array("1855", "1f172e1721e51a7f61ebb517593105e7"), + "plugins/Annotations/lang/nb.json" => array("1649", "ce4e4f4d1479979a34e548ccb64e3ed0"), + "plugins/Annotations/lang/nl.json" => array("1537", "302f2d75bc3fabf3229b2c96a53ced53"), + "plugins/Annotations/lang/pl.json" => array("1752", "cb9d6c799521e0b1c09c89cb0779efc2"), + "plugins/Annotations/lang/pt-br.json" => array("1791", "f4de7db8bb8928f5803ce4f75449cf87"), + "plugins/Annotations/lang/pt.json" => array("1837", "06b7d81bfe3e8e920581841e5fd73072"), + "plugins/Annotations/lang/ro.json" => array("1719", "dd94940c61707cf6f14ef60e0f408003"), + "plugins/Annotations/lang/ru.json" => array("2116", "a490bdb79fe4a473d3bbe8ca529f30a1"), + "plugins/Annotations/lang/sk.json" => array("828", "ec382b9169046d4079f35b0786be030a"), + "plugins/Annotations/lang/sl.json" => array("649", "e294e1b0a44cb8d6aae7d9eb83803ff2"), + "plugins/Annotations/lang/sq.json" => array("1885", "8de93c10dde6ad83f109fd57cd7e3fbe"), + "plugins/Annotations/lang/sr.json" => array("1526", "767a9280bb2310a7cfe3e5e9906765e7"), + "plugins/Annotations/lang/sv.json" => array("1845", "ced9701371e48aee083c257088c00f44"), + "plugins/Annotations/lang/ta.json" => array("2324", "f78b55455e2946df25d39a2ca0b08afe"), + "plugins/Annotations/lang/te.json" => array("114", "b26ba6170dd198efc4e527f6d43ccb3a"), + "plugins/Annotations/lang/th.json" => array("326", "77c4edcc8ed8f20ea5aecb8322dd0193"), + "plugins/Annotations/lang/tl.json" => array("1951", "a1e8694f8dc5407c604e501ee7ff2a8b"), + "plugins/Annotations/lang/tr.json" => array("1303", "9d7c46589c696eace498f5cd6fb24a60"), + "plugins/Annotations/lang/vi.json" => array("2074", "e788d111502408522c9a3251e71a210f"), + "plugins/Annotations/lang/zh-cn.json" => array("1396", "4ce3f41f7622cd97b7adb76e69d04389"), + "plugins/Annotations/lang/zh-tw.json" => array("159", "71019bb0e809bbcfb2090fafd089d6fe"), + "plugins/Annotations/stylesheets/annotations.less" => array("3822", "a906fde0b213d473fdeefc3f8ef259c8"), + "plugins/Annotations/templates/_annotationList.twig" => array("1287", "c96f7400a3e5abfd6ae40c81bdf5dc5c"), + "plugins/Annotations/templates/_annotation.twig" => array("2541", "cf7804d231481ff349031f193a9f88bf"), + "plugins/Annotations/templates/getAnnotationManager.twig" => array("1132", "073ed16924f708080261ea79e45b6c52"), + "plugins/Annotations/templates/getEvolutionIcons.twig" => array("858", "0a28f3417b1a0dbcd17e760ea4606b66"), "plugins/Annotations/templates/saveAnnotation.twig" => array("45", "5ad07d0a082d1cb3b199874b80285ea0"), - "plugins/API/API.php" => array("29238", "2b460cf97b6ab918db29b21d2effae1b"), - "plugins/API/Controller.php" => array("4818", "4d7a6fc1ec88da6888efa79a784387af"), - "plugins/API/ProcessedReport.php" => array("30939", "247cb79b399231721079044292d3686e"), - "plugins/API/RowEvolution.php" => array("19547", "2f282c72293c1b915683ba7e88033bec"), + "plugins/API/API.php" => array("28353", "0a2c33d8193a6735ec06fb1b88f8171b"), + "plugins/API/Controller.php" => array("5463", "f1aabd2a8f2cf22ddc42067b48fd4039"), + "plugins/API/DataTable/MergeDataTables.php" => array("1665", "172b3fd991ad31633409e94ff592515a"), + "plugins/API/Glossary.php" => array("3465", "33542c38e2ac56c19315bd7d0d6de7e6"), + "plugins/API/lang/am.json" => array("100", "c86d0311151a4e58e89a5776455a2d74"), + "plugins/API/lang/ar.json" => array("1721", "7c16f433e9c35aaf62089b1bfefaf9e3"), + "plugins/API/lang/be.json" => array("1394", "1e6cc204b5d32b459a2f569349a5c71a"), + "plugins/API/lang/bg.json" => array("1576", "bce928cf56607ab072dd729405cba3be"), + "plugins/API/lang/bs.json" => array("494", "9b58bc89b4ec2d038b228e4c2cc6715c"), + "plugins/API/lang/ca.json" => array("1061", "46b4a652ff7c6c0e3ca15db6d21e611c"), + "plugins/API/lang/cs.json" => array("1451", "72b85b6c1d2e8d6ddf6f6b0461f7e883"), + "plugins/API/lang/da.json" => array("1113", "894ee84163eb05968e03dbc32c3644d6"), + "plugins/API/lang/de.json" => array("1474", "e6e28462a8cc29062ff0610c12cc8ffc"), + "plugins/API/lang/el.json" => array("2437", "b57a1e8492a19ceaaade3e3c422910b5"), + "plugins/API/lang/en.json" => array("1367", "e36359789ba4f8070f2cbff002b572a2"), + "plugins/API/lang/es.json" => array("1404", "3e9f71d1f86f577467c46863fe845a6c"), + "plugins/API/lang/et.json" => array("257", "15cfccb5af4fd0b6e2a545f149f74897"), + "plugins/API/lang/eu.json" => array("71", "2fd1750a93e5c97728db7600da7defa4"), + "plugins/API/lang/fa.json" => array("554", "34356696095a68ed17b3efe930be7514"), + "plugins/API/lang/fi.json" => array("930", "63498e762db127a59f4b3d20605f7163"), + "plugins/API/lang/fr.json" => array("1562", "4c04029adfca6e6b81cb02f929dda01b"), + "plugins/API/lang/gl.json" => array("138", "46605622a5c30783616b7bfa13dcaa83"), + "plugins/API/lang/he.json" => array("1024", "47ff7e97140ac74ca4312658a2f8aec5"), + "plugins/API/lang/hi.json" => array("2003", "492df06f0a815fe150592da5f75163df"), + "plugins/API/lang/hr.json" => array("286", "45e31ae5f866db5275b07b8cb5e0dd7f"), + "plugins/API/lang/hu.json" => array("1072", "cdd4a2bb0b35a750668eb487b4dcd79c"), + "plugins/API/lang/id.json" => array("988", "47814287f8737c3ac6cca8ec73c2c9f0"), + "plugins/API/lang/is.json" => array("915", "8946fde78eaeb97a80622275a31008bd"), + "plugins/API/lang/it.json" => array("1452", "ce90b7adbb6297aacd40465a3a3779ec"), + "plugins/API/lang/ja.json" => array("1697", "d53598f33eb890b45d58610197bcd023"), + "plugins/API/lang/ka.json" => array("2153", "bb807e4dbc9aa0aeb1948db7c1febcc2"), + "plugins/API/lang/ko.json" => array("1530", "9d908f1e2b14cb59748c8afcc2d9dd8b"), + "plugins/API/lang/lt.json" => array("900", "6c14f4cba95b9ebadb3a2646be2a69d1"), + "plugins/API/lang/lv.json" => array("82", "9f1d4ea8de504f0ae17d893e79800a40"), + "plugins/API/lang/nb.json" => array("1320", "c2082b4d97d52dfef6357e6268930cbf"), + "plugins/API/lang/nl.json" => array("1213", "bfeb54111733c2729bc695f372c093b9"), + "plugins/API/lang/nn.json" => array("70", "1a90a41ba8788bf25b90efe637bf0132"), + "plugins/API/lang/pl.json" => array("1411", "ad6729378d06a4b0e9d9e047c07feeb5"), + "plugins/API/lang/pt-br.json" => array("1496", "69000fc194c00fbce4ba1cda53440551"), + "plugins/API/lang/pt.json" => array("994", "c7277a423b2c94a63bc8e4f8e768b09f"), + "plugins/API/lang/ro.json" => array("958", "665f7d12ff9a578762ff5180b07df148"), + "plugins/API/lang/ru.json" => array("1881", "0efde6aedb0732dc398701531497c7b9"), + "plugins/API/lang/sk.json" => array("130", "7264e4b894b642f053d25627d685f9f3"), + "plugins/API/lang/sl.json" => array("793", "200e842e87fde80256b230a14bcd58ef"), + "plugins/API/lang/sq.json" => array("1080", "bf5d099a851668d25bfbbff03b1add64"), + "plugins/API/lang/sr.json" => array("1189", "5e0f6aa5e32f86b80d1f3042974b2d9c"), + "plugins/API/lang/sv.json" => array("1236", "26b33896d483d409c14ec92977a6bbe4"), + "plugins/API/lang/ta.json" => array("760", "c387cb7e812326eeabfbe53f74ba115a"), + "plugins/API/lang/te.json" => array("95", "1f9dbcd0b8ea2b35694f97b7e55129de"), + "plugins/API/lang/th.json" => array("1653", "74465d1c8584323cd99b078882dac145"), + "plugins/API/lang/tl.json" => array("1071", "47b5c19bc3d3525ca5e0493d81dfbed2"), + "plugins/API/lang/tr.json" => array("952", "635b11c1b9b549322cda07ec054f5898"), + "plugins/API/lang/uk.json" => array("1285", "42c99e62a34c6820da9f51386be28abc"), + "plugins/API/lang/vi.json" => array("1422", "81058d06cb6976056f3f5d1a3611a103"), + "plugins/API/lang/zh-cn.json" => array("876", "6619e2357533180aa4838726a0f3eadf"), + "plugins/API/lang/zh-tw.json" => array("1125", "b53d7875393bbb7e6a39003cca12fb89"), + "plugins/API/Menu.php" => array("1971", "b71f56539d6a41dd0de8654bd4e63bed"), + "plugins/API/ProcessedReport.php" => array("36816", "e95044cbaf12f49223ae08baea1d7448"), + "plugins/API/Renderer/Console.php" => array("596", "af07f2df2f6082b2900f7cc2d900213e"), + "plugins/API/Renderer/Csv.php" => array("1789", "d22ecb074585d10040bab66cbc42ca79"), + "plugins/API/Renderer/Html.php" => array("1395", "d82c4210bc60acda0dc5006144ba4d2c"), + "plugins/API/Renderer/Json2.php" => array("802", "e6ff837766637b8d911831090812306a"), + "plugins/API/Renderer/Json.php" => array("2727", "d7ac763de150d27cdccbef7d1b0e6923"), + "plugins/API/Renderer/Original.php" => array("1590", "ddbcb5ecfe21a9c03ce4d6d536858b1d"), + "plugins/API/Renderer/Php.php" => array("2179", "39e6a05ec9b343193a38890a9cba7335"), + "plugins/API/Renderer/Rss.php" => array("1311", "60eec0ba72d275af0205a2ddd3d4e1b8"), + "plugins/API/Renderer/Tsv.php" => array("485", "3a86bf8a13fab4982e5be0dc93d01093"), + "plugins/API/Renderer/Xml.php" => array("954", "ecf61ddb304c0020164c050d758c5c0a"), + "plugins/API/Reports/Get.php" => array("2763", "fa106ce358229b889cc92302eeb0b484"), + "plugins/API/RowEvolution.php" => array("20719", "226ec756a82192b7e6c2a19a5bd42ec2"), "plugins/API/stylesheets/listAllAPI.less" => array("724", "c18396c6a93e74893483fbdcd7ff3fb6"), - "plugins/API/templates/listAllAPI.twig" => array("1049", "4adf1392c2bd87a6aac4b578dbcd65b6"), - "plugins/CoreAdminHome/API.php" => array("8164", "96c8e23e9390b352a77e4242f79665df"), - "plugins/CoreAdminHome/Controller.php" => array("12138", "5ff36d4263102d2c0e2e576b56354dfe"), - "plugins/CoreAdminHome/CoreAdminHome.php" => array("4902", "36802b98ee443d60935a58497c19306d"), - "plugins/CoreAdminHome/CustomLogo.php" => array("6329", "c8d6e7f8a505489bb4eb6a2e31c2ca05"), - "plugins/CoreAdminHome/javascripts/generalSettings.js" => array("4863", "b219e00ad3f5544e7b5e37b0effb3463"), - "plugins/CoreAdminHome/javascripts/jsTrackingGenerator.js" => array("12282", "291cc9b609d1b3f40ff1b4084043b3a8"), - "plugins/CoreAdminHome/javascripts/pluginSettings.js" => array("2358", "4a5e5984b547f409e8bd3a73542e961c"), - "plugins/CoreAdminHome/stylesheets/generalSettings.less" => array("2793", "fc401910d4ff9fa8b7be809f0468f717"), - "plugins/CoreAdminHome/stylesheets/jsTrackingGenerator.css" => array("1405", "9265daa10ff275ccf75dcfcd2926b9a9"), - "plugins/CoreAdminHome/stylesheets/menu.less" => array("2149", "4f32e2ba236cf2ea0798676b670050d3"), - "plugins/CoreAdminHome/stylesheets/pluginSettings.less" => array("520", "4e1b32c3e7108e45055051cfbdede938"), - "plugins/CoreAdminHome/templates/generalSettings.twig" => array("17731", "16a31e5ccac7ff372d952e492d037d90"), - "plugins/CoreAdminHome/templates/_menu.twig" => array("840", "5ee52c14bfe1e41fa07db26bd0f49e90"), - "plugins/CoreAdminHome/templates/optOut.twig" => array("1122", "4b2985cac29da2d123a4c780d368fa50"), - "plugins/CoreAdminHome/templates/pluginSettings.twig" => array("7608", "52ae5d20cd1ad28f616fe08ecf3047ad"), - "plugins/CoreAdminHome/templates/trackingCodeGenerator.twig" => array("13443", "34381ec263e148ee5991913c51a1bd01"), - "plugins/CoreConsole/Commands/CodeCoverage.php" => array("3188", "675e82de2141b1caadedbf2f91a59529"), - "plugins/CoreConsole/Commands/CoreArchiver.php" => array("4245", "37f3acf3ee8df8d4973f192a56227cb5"), - "plugins/CoreConsole/Commands/GenerateApi.php" => array("1863", "0998c1fcfd9fe56c82bc22812a45bb60"), - "plugins/CoreConsole/Commands/GenerateCommand.php" => array("2898", "d4180aa10d123f70c33ea2385d760cb0"), - "plugins/CoreConsole/Commands/GenerateController.php" => array("1958", "93b4d27330e51e13c497052ca3f5854a"), - "plugins/CoreConsole/Commands/GeneratePluginBase.php" => array("4542", "6f359b866d9df97303139fbf955bd30d"), - "plugins/CoreConsole/Commands/GeneratePlugin.php" => array("7264", "786dcfdc680750902ddea34ee0325aa8"), - "plugins/CoreConsole/Commands/GenerateSettings.php" => array("1935", "d760e64d6b9f41d48431d7cfff2247da"), - "plugins/CoreConsole/Commands/GenerateTest.php" => array("6086", "dab2b156fe2548658562090e686c9b88"), - "plugins/CoreConsole/Commands/GenerateVisualizationPlugin.php" => array("3556", "653ebdfc17dccde2d643134d7c708f1c"), - "plugins/CoreConsole/Commands/GitCommit.php" => array("4456", "dc6ef207850ca10950f694d8cd9b4cc0"), - "plugins/CoreConsole/Commands/GitPull.php" => array("1592", "69f2c84488535f2944a8b3c61619da35"), - "plugins/CoreConsole/Commands/GitPush.php" => array("1186", "774591997e01ba6d6a898e447048b6a7"), - "plugins/CoreConsole/Commands/ManagePlugin.php" => array("2372", "d2926cda27fee604b517b113b2ad83ad"), - "plugins/CoreConsole/Commands/ManageTestFiles.php" => array("1812", "a86ae0b8137c618dcf1af27fa18ba68b"), - "plugins/CoreConsole/Commands/RunTests.php" => array("3057", "b3a65e6e74b23b67d682edbc9e6679b8"), - "plugins/CoreConsole/Commands/RunUITests.php" => array("2647", "538924f987e82a8c4ae48851404ad03f"), - "plugins/CoreConsole/Commands/SetupFixture.php" => array("6080", "2b1aff33dd7ffda5737cb83f9998ac8d"), - "plugins/CoreConsole/Commands/SyncUITestScreenshots.php" => array("3016", "595086d2646fe3e3b15d2e7e904cb948"), - "plugins/CoreConsole/Commands/WatchLog.php" => array("855", "092903712bd6a244a1199eeab7458b50"), - "plugins/CoreHome/angularjs/anchorLinkFix.js" => array("2634", "96f55c02ee23e9e4b2c8f88e9c05e2b8"), - "plugins/CoreHome/angularjs/common/directives/autocomplete-matched.js" => array("1355", "9e42e502d560b4a4d140db85b9ac485e"), - "plugins/CoreHome/angularjs/common/directives/autocomplete-matched_spec.js" => array("1515", "ac1cb173a0b49e64d18f8f7bd2e49c34"), - "plugins/CoreHome/angularjs/common/directives/dialog.js" => array("1197", "343ca648f67762baada217cb848d0564"), - "plugins/CoreHome/angularjs/common/directives/directive.js" => array("174", "35c24d7f8602201a1a86fccd1c2ced63"), - "plugins/CoreHome/angularjs/common/directives/focus-anywhere-but-here.js" => array("1257", "a54b382326b1724ecfe08b75d9bda271"), - "plugins/CoreHome/angularjs/common/directives/focusif.js" => array("727", "3e050dd7229a73fc4c850acba33f215d"), - "plugins/CoreHome/angularjs/common/directives/ignore-click.js" => array("606", "7c44f9411918577878d2786d29149709"), - "plugins/CoreHome/angularjs/common/directives/onenter.js" => array("737", "c4da43d5d72695e6accaa72635e0b619"), - "plugins/CoreHome/angularjs/common/filters/evolution.js" => array("1067", "f03049c52419158e37b3b7485ad9ec97"), - "plugins/CoreHome/angularjs/common/filters/filter.js" => array("170", "e3cb8ba6c47023ea0eb11b00da89d752"), - "plugins/CoreHome/angularjs/common/filters/startfrom.js" => array("319", "b0d892fbbba5a5c59c0ebfe9cccc2ff4"), - "plugins/CoreHome/angularjs/common/filters/startfrom_spec.js" => array("997", "28e65c932ef636402a5436ce9036747b"), - "plugins/CoreHome/angularjs/common/filters/translate.js" => array("518", "4b3608c3d3925c2de1c812ca4c7095b7"), - "plugins/CoreHome/angularjs/common/services/piwik-api.js" => array("4931", "1f26832cd0e4667a9550f140d9b63f2d"), - "plugins/CoreHome/angularjs/common/services/piwik.js" => array("289", "7f8e1209a339cbb3df5c8b1c67608ff5"), - "plugins/CoreHome/angularjs/common/services/piwik_spec.js" => array("958", "918d81d86edb8ac04ea9fd4b2b3233d2"), - "plugins/CoreHome/angularjs/common/services/service.js" => array("172", "7df34c228b662caedc891189287d26bc"), - "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline-directive.js" => array("2117", "f53eb6919e53ee6085b20af35e75a071"), - "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline.html" => array("968", "0a3bb46fea31257f0c3c254e5686a2d4"), - "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline.less" => array("763", "74046d60c1f671857b7583fad14cb2b5"), + "plugins/API/templates/glossary.twig" => array("1107", "231bd30ea68e0837b6219fa716bed02d"), + "plugins/API/templates/listAllAPI.twig" => array("1019", "b6feac2fa9c1a31a4740fa19eeb0b12f"), + "plugins/BulkTracking/BulkTracking.php" => array("1959", "f3346a4202e6f06a479fa9a7fcaa7fa2"), + "plugins/BulkTracking/plugin.json" => array("169", "dfcaeb509de55a67d1490dd7a4b0be3b"), + "plugins/BulkTracking/Tracker/Handler.php" => array("3542", "9570c4f7d5e5d638733373a7190d5dab"), + "plugins/BulkTracking/Tracker/Requests.php" => array("3126", "38c5f0972eb9108a338b9757f64ab9e9"), + "plugins/BulkTracking/Tracker/Response.php" => array("2671", "c24d58752fb6b246879e4199b9b017b2"), + "plugins/Contents/Actions/ActionContent.php" => array("1292", "5f371a333943da6ada48c99f74c8129d"), + "plugins/Contents/API.php" => array("2391", "7ede8a66ff4812eb6c1dfbb1a930f16e"), + "plugins/Contents/Archiver.php" => array("11347", "eabeb434fe17d34fb7bb2f12f810f362"), + "plugins/Contents/Columns/ContentInteraction.php" => array("1423", "6b7e2409b51efcddb1559e5ec0de41ff"), + "plugins/Contents/Columns/ContentName.php" => array("1371", "ea0febf2219494cd8b159736d3249dfb"), + "plugins/Contents/Columns/ContentPiece.php" => array("1389", "8e6dc65714611eac346e42c299aa1faf"), + "plugins/Contents/Columns/ContentTarget.php" => array("1341", "266c87c61a6668d20788959dce02c703"), + "plugins/Contents/Columns/Metrics/InteractionRate.php" => array("1379", "53df551f5d5a80ed6dae0b2270c950f9"), + "plugins/Contents/Contents.php" => array("1547", "fd7f5fe7e87ba4e6d21a032214dca199"), + "plugins/Contents/Controller.php" => array("1382", "f2c2cc1dbfeb5ef089a66fded111e79d"), + "plugins/Contents/DataArray.php" => array("2673", "2416874c5d01e01537996ea90c525b12"), + "plugins/Contents/Dimensions.php" => array("807", "7b4779b739acd14bc219088579b137e8"), + "plugins/Contents/javascripts/contentsDataTable.js" => array("1674", "7b8b8f01e0b2c46c93744ad21e803471"), + "plugins/Contents/lang/ar.json" => array("265", "97d25e52f815e46dc2b8e4fd2c82366c"), + "plugins/Contents/lang/bg.json" => array("428", "1f65f86b45d02103a0b20fd66b10138d"), + "plugins/Contents/lang/ca.json" => array("104", "c52c0aeeb30bb973edc8bcc52bd0a4b2"), + "plugins/Contents/lang/cs.json" => array("864", "4a6c8837a2c18a169f0e4ba00c364fd0"), + "plugins/Contents/lang/da.json" => array("343", "c3f7d7170556f64f9323e19591ab87e4"), + "plugins/Contents/lang/de.json" => array("1000", "c328efae8e09bef2a56d385b438cc56b"), + "plugins/Contents/lang/el.json" => array("1477", "bf0b0ab34b1c135b6ca2eb7e281a854c"), + "plugins/Contents/lang/en.json" => array("896", "07fc464e4b5b63d9e6c1ce2be7d34e47"), + "plugins/Contents/lang/es.json" => array("607", "2327eca3d4f31a145dd08092194991f1"), + "plugins/Contents/lang/et.json" => array("212", "0a1896e43ad9e640cf1c413058e855ff"), + "plugins/Contents/lang/fi.json" => array("355", "c08377ca1b6e3d63007a58f74e304cf8"), + "plugins/Contents/lang/fr.json" => array("991", "91190a312d20676518d1c4010f3a4ea7"), + "plugins/Contents/lang/gl.json" => array("217", "070db1ff75c5f1d9cb4dba6f015f7347"), + "plugins/Contents/lang/hi.json" => array("962", "96c58dcd8cc6845ec5d3f9572d55c99f"), + "plugins/Contents/lang/it.json" => array("1006", "89a5fd657de985b26f23cb23c7f23caf"), + "plugins/Contents/lang/ja.json" => array("1115", "c909aa4b9cd7cd42ceda01a367114930"), + "plugins/Contents/lang/ko.json" => array("326", "1c8f4dad616ab29d6ccaf38cea06113b"), + "plugins/Contents/lang/nb.json" => array("888", "7c27c8e0edcacfd3de93719dba69bd04"), + "plugins/Contents/lang/nl.json" => array("520", "f9b2efd73f7218763c25dee49a62c827"), + "plugins/Contents/lang/pl.json" => array("135", "b24d70c50f2af3a16bebe37fd6d27421"), + "plugins/Contents/lang/pt-br.json" => array("1003", "d8c9944eca5d07260bf8637572741956"), + "plugins/Contents/lang/pt.json" => array("225", "6bb927d09db08e05c79c392c9b350d05"), + "plugins/Contents/lang/ro.json" => array("145", "671fe8ff8d2a3bae4c20c0ef6068602a"), + "plugins/Contents/lang/ru.json" => array("477", "819bea4113f523fde5de4659517ec628"), + "plugins/Contents/lang/sl.json" => array("334", "a738a1d0c556b6e6179795014ea39806"), + "plugins/Contents/lang/sq.json" => array("556", "11f2a10e47d24541db415f24683887cd"), + "plugins/Contents/lang/sr.json" => array("522", "d14a1819817a1c3753b32dce649c2f41"), + "plugins/Contents/lang/sv.json" => array("552", "781dc39d5b3d281007778dd1e9527558"), + "plugins/Contents/lang/ta.json" => array("549", "b7874735f029cc248fcbe1fb56edfbdb"), + "plugins/Contents/lang/tl.json" => array("395", "54758fe5b605f93904a786bededcc967"), + "plugins/Contents/lang/tr.json" => array("357", "9e1987c5811621477a08b9d3ff1a9f19"), + "plugins/Contents/lang/vi.json" => array("593", "439ea2fa8d26cb0e1d1118644b71ae0b"), + "plugins/Contents/Menu.php" => array("743", "6fb2f16bf77a0640c43db44b8177d72f"), + "plugins/Contents/README.md" => array("202", "bdd0c2ddd6acb0a71a9c19c5e79a5c53"), + "plugins/Contents/Reports/Base.php" => array("1797", "7ff68b5d681245a8a533d54d566159c5"), + "plugins/Contents/Reports/GetContentNames.php" => array("1139", "81278332cb0c4c0b76b564b34daaa2a3"), + "plugins/Contents/Reports/GetContentPieces.php" => array("1146", "d235cc6133a0c54c4be6bba1a867d106"), + "plugins/Contents/stylesheets/datatable.less" => array("80", "65d853fbb3cc86d4f83e658b124a99dc"), + "plugins/CoreAdminHome/API.php" => array("5334", "36c1f1da052ffe687dbc95529416cda8"), + "plugins/CoreAdminHome/Commands/DeleteLogsData.php" => array("6759", "2e0dfb015c7e9d36e54d53ee6f5d4b2f"), + "plugins/CoreAdminHome/Commands/FixDuplicateLogActions.php" => array("8041", "69f41e5a31d7ec0e1138f6274b1e4730"), + "plugins/CoreAdminHome/Commands/InvalidateReportData.php" => array("8239", "40ddcdb8b1c5a17730f65972ce5bee09"), + "plugins/CoreAdminHome/Commands/OptimizeArchiveTables.php" => array("4459", "10c12d00a793604486d7b8ace00cb5ed"), + "plugins/CoreAdminHome/Commands/PurgeOldArchiveData.php" => array("8319", "ef82ab26547dbc169dd74e4825673f11"), + "plugins/CoreAdminHome/Commands/RunScheduledTasks.php" => array("2563", "66e9eb6b65454aa1fbe9abf9eda3916f"), + "plugins/CoreAdminHome/Commands/SetConfig/ConfigSettingManipulation.php" => array("4282", "3d14b24d6e4c4426e3774fadef5d2396"), + "plugins/CoreAdminHome/Commands/SetConfig.php" => array("3997", "0d766b8eedfc6dd8d2bc12f0507a09b6"), + "plugins/CoreAdminHome/Controller.php" => array("15193", "b4af385cad369e5456fb37adf634f494"), + "plugins/CoreAdminHome/CoreAdminHome.php" => array("2974", "6cf35367b3dc9d07bbeb2fd95a991806"), + "plugins/CoreAdminHome/CustomLogo.php" => array("6762", "7e9d13b98b1018d913c53cade226759f"), + "plugins/CoreAdminHome/javascripts/generalSettings.js" => array("5441", "4e8ac1080aed868e9a85d3ccf6bde5f2"), + "plugins/CoreAdminHome/javascripts/jsTrackingGenerator.js" => array("12837", "eec10a7366576fcd58a4b70e3c8b10e1"), + "plugins/CoreAdminHome/javascripts/pluginSettings.js" => array("2522", "8a514af58b165d85600300a6f58becbb"), + "plugins/CoreAdminHome/javascripts/protocolCheck.js" => array("1220", "6080df499f95067f1ab049aa7e853135"), + "plugins/CoreAdminHome/lang/ar.json" => array("6447", "30616af68ea7a23b43a9bfd06838f4c8"), + "plugins/CoreAdminHome/lang/be.json" => array("3655", "660940426d406fe06818e95f978c96c0"), + "plugins/CoreAdminHome/lang/bg.json" => array("12221", "cf9784df0edf49b77bae49e45db374fc"), + "plugins/CoreAdminHome/lang/bs.json" => array("259", "7aaac6ef093fe237d25f3ffe01c62768"), + "plugins/CoreAdminHome/lang/ca.json" => array("3743", "d2ae75e8f1e094ea4cd2993cb5cce301"), + "plugins/CoreAdminHome/lang/cs.json" => array("11039", "1c1b3f0d60648033801d4b00367841da"), + "plugins/CoreAdminHome/lang/da.json" => array("9717", "7c4a6c30ba94b06bfc9775a38e454557"), + "plugins/CoreAdminHome/lang/de.json" => array("11661", "80aecf13615ec145f190905346ae8392"), + "plugins/CoreAdminHome/lang/el.json" => array("19513", "4875a4730ee84e295377bd09d00adfff"), + "plugins/CoreAdminHome/lang/en.json" => array("10274", "9d153406739e01e8afc64c9919ade998"), + "plugins/CoreAdminHome/lang/es.json" => array("10883", "0b1cd137adae08cbfebad54e293b8b7a"), + "plugins/CoreAdminHome/lang/et.json" => array("1637", "5d3a3be1703728519da6dea35b28edf3"), + "plugins/CoreAdminHome/lang/fa.json" => array("7720", "06d0907461f5a294cdc1b86f6b3edc08"), + "plugins/CoreAdminHome/lang/fi.json" => array("9000", "6c64802d534579c234c1dbd661def9f1"), + "plugins/CoreAdminHome/lang/fr.json" => array("11831", "049a71e3bcc43a0d0640d8ff77674da1"), + "plugins/CoreAdminHome/lang/he.json" => array("1343", "2e0a7c73007a6b0098b9b62f49b66d6d"), + "plugins/CoreAdminHome/lang/hi.json" => array("15466", "2659cbb14fcda0c2d6b1dba1acceca94"), + "plugins/CoreAdminHome/lang/hr.json" => array("273", "fbd1bb3f0cdc3ac576367a463d981ab8"), + "plugins/CoreAdminHome/lang/hu.json" => array("10709", "1b6344f935b85c729e9f9946ff0113e9"), + "plugins/CoreAdminHome/lang/id.json" => array("8037", "c4d710e7a1dfc143813dfb318fae4c0a"), + "plugins/CoreAdminHome/lang/is.json" => array("806", "cb0dac3505efb91694e324284c7559e2"), + "plugins/CoreAdminHome/lang/it.json" => array("11460", "397c6b7d3175b7b86e9dac863274c2ca"), + "plugins/CoreAdminHome/lang/ja.json" => array("12391", "0e8c6433ff777acae453e42d21a70875"), + "plugins/CoreAdminHome/lang/ka.json" => array("3612", "96fc91c05ee64b1532e970d392d613a5"), + "plugins/CoreAdminHome/lang/ko.json" => array("5932", "7c46ddc03ba9900c03e43a8924fbde17"), + "plugins/CoreAdminHome/lang/lt.json" => array("440", "adbdcce3376f61283deccbac26dfd015"), + "plugins/CoreAdminHome/lang/lv.json" => array("657", "f9b6b26655894da5391f6d2f606a0217"), + "plugins/CoreAdminHome/lang/nb.json" => array("10847", "add2cc083af22d9fa1e6c5dce60e53fb"), + "plugins/CoreAdminHome/lang/nl.json" => array("9974", "f859d0600604141a727c5e9f0268a24b"), + "plugins/CoreAdminHome/lang/nn.json" => array("1857", "7e320976f8a714bc1104016339ecbef1"), + "plugins/CoreAdminHome/lang/pl.json" => array("9526", "b6ec486defc4001ea05cdc4fd5a4a796"), + "plugins/CoreAdminHome/lang/pt-br.json" => array("11699", "11135826f189f961e18ef3ed14566ae3"), + "plugins/CoreAdminHome/lang/pt.json" => array("4988", "c3192f8d8c5a178472f1afa1ba0447c4"), + "plugins/CoreAdminHome/lang/ro.json" => array("9172", "915ef6b04b17529ab7c61503cf27d22d"), + "plugins/CoreAdminHome/lang/ru.json" => array("15081", "617d558db1f983f6f3f331e80f12562d"), + "plugins/CoreAdminHome/lang/sk.json" => array("875", "118598bcb74ee441c95ee2ac72c02406"), + "plugins/CoreAdminHome/lang/sl.json" => array("3115", "2c93e918a108a5b025760f65cc461342"), + "plugins/CoreAdminHome/lang/sq.json" => array("2839", "abf14f13806f6b66bbceaff74bea4b07"), + "plugins/CoreAdminHome/lang/sr.json" => array("9859", "1ca6cbd9eabc589045f6a840f440a644"), + "plugins/CoreAdminHome/lang/sv.json" => array("10556", "102278e9dda7232e1eb8de91cee50e35"), + "plugins/CoreAdminHome/lang/ta.json" => array("6172", "a94a4884b43b16cf182648fd6bec1b37"), + "plugins/CoreAdminHome/lang/te.json" => array("157", "20cf58c9a9f87ed376dd51006742bc12"), + "plugins/CoreAdminHome/lang/th.json" => array("3743", "f51cf365abe5d52c62a3da68a24a6a1a"), + "plugins/CoreAdminHome/lang/tl.json" => array("9925", "bbb2b5a4d5e0e33d2dc2816d20f443a9"), + "plugins/CoreAdminHome/lang/tr.json" => array("7007", "26a6f05a901c65f8cee0554ba77a73b3"), + "plugins/CoreAdminHome/lang/uk.json" => array("2669", "266ed8275eb3d374374218d0773a9295"), + "plugins/CoreAdminHome/lang/vi.json" => array("10030", "cf1b5353891f90af031b111f7e0b1ba5"), + "plugins/CoreAdminHome/lang/zh-cn.json" => array("7148", "19d0f7a0d789dccaa7039153b75204b8"), + "plugins/CoreAdminHome/lang/zh-tw.json" => array("289", "c3333d99acc8ab7186933df7610045fd"), + "plugins/CoreAdminHome/Menu.php" => array("2424", "ea75163e628b76dba829ad7d15874a3a"), + "plugins/CoreAdminHome/Model/DuplicateActionRemover.php" => array("6400", "6af515924590d21369ebbe345de9e311"), + "plugins/CoreAdminHome/OptOutManager.php" => array("5849", "b3d861030f091df9beb3584572aea687"), + "plugins/CoreAdminHome/stylesheets/generalSettings.less" => array("3014", "84865c44cc27ddfc3508ba5594c1e74c"), + "plugins/CoreAdminHome/stylesheets/jsTrackingGenerator.css" => array("435", "a6c7d1871b2ffeafedae152f5d3810fd"), + "plugins/CoreAdminHome/Tasks/ArchivesToPurgeDistributedList.php" => array("2732", "a8d74db1d4856bfcb513ede0e9b485a0"), + "plugins/CoreAdminHome/Tasks.php" => array("5152", "6191b6a3612c6c79b6c27a8c515f35c5"), + "plugins/CoreAdminHome/templates/generalSettings.twig" => array("15119", "35472195341d12136f9e0c6b7341e9ce"), + "plugins/CoreAdminHome/templates/optOut.twig" => array("3380", "e89cf4c609f8c1aec060b8ca8dc5e3cc"), + "plugins/CoreAdminHome/templates/pluginSettings.twig" => array("1636", "e0f77da90b7cde67fd4f58bb08cf5c5d"), + "plugins/CoreAdminHome/templates/trackingCodeGenerator.twig" => array("10816", "ddfa6093d24517c19f8268e5f5752ee3"), + "plugins/CoreConsole/Commands/ClearCaches.php" => array("1116", "b43bf6af0ab5f862e824a9989d389584"), + "plugins/CoreConsole/Commands/CoreArchiver.php" => array("7770", "c74bdd8c7d10604853199a4551e89374"), + "plugins/CoreConsole/Commands/DevelopmentEnable.php" => array("1619", "407b0ca4826cbf533e1b72f8602cf608"), + "plugins/CoreConsole/Commands/DevelopmentManageTestFiles.php" => array("1926", "309b5e9eb08f6de4330f78aa0cda563f"), + "plugins/CoreConsole/Commands/DevelopmentSyncProcessedSystemTests.php" => array("2820", "b26d9b78457853ea2517707eef9b50e9"), + "plugins/CoreConsole/Commands/GenerateAngularDirective.php" => array("5019", "0af6919a4c0c7f4b442738202c5ce9fb"), + "plugins/CoreConsole/Commands/GenerateApi.php" => array("1940", "151c9774dc7ae7893ad00b0de2ab497a"), + "plugins/CoreConsole/Commands/GenerateArchiver.php" => array("2035", "9cfa1f351c7cd479184c4896be84a092"), + "plugins/CoreConsole/Commands/GenerateCommand.php" => array("3624", "16b1663264302d4822bcdd6a2828e017"), + "plugins/CoreConsole/Commands/GenerateController.php" => array("2035", "bcbbc41915ab291d180f454f9db97140"), + "plugins/CoreConsole/Commands/GenerateDimension.php" => array("10430", "b7ce4c30a2ea8df79b30482cc0cd3d15"), + "plugins/CoreConsole/Commands/GenerateMenu.php" => array("1981", "c50510951a7cc7a70b7046e1c63e810a"), + "plugins/CoreConsole/Commands/GeneratePluginBase.php" => array("12095", "da36cb47f5d995c49542eafc947a1a75"), + "plugins/CoreConsole/Commands/GeneratePlugin.php" => array("7165", "a90cce6d5463a496994ccd041b608cf5"), + "plugins/CoreConsole/Commands/GenerateReport.php" => array("10918", "788c12d3ba33aff02c16920a7813e87a"), + "plugins/CoreConsole/Commands/GenerateScheduledTask.php" => array("2052", "60b37f5dc1bc893a87dd37dcdc11447a"), + "plugins/CoreConsole/Commands/GenerateSettings.php" => array("2012", "66c80af3eed723813deb0a0c5870c3e5"), + "plugins/CoreConsole/Commands/GenerateTest.php" => array("6586", "40f41f7d45145c2611be478db1711881"), + "plugins/CoreConsole/Commands/GenerateUpdate.php" => array("3902", "969409dea2ff88f8051934ac352fcc6c"), + "plugins/CoreConsole/Commands/GenerateVisualizationPlugin.php" => array("3672", "52c1bd5425bba2bae10183cf65abe30a"), + "plugins/CoreConsole/Commands/GenerateWidget.php" => array("4100", "ea4193d16cf85c020d2eaa57b337fe30"), + "plugins/CoreConsole/Commands/GitCommit.php" => array("4579", "bcb252fd88f9090b98b9628a629a24d3"), + "plugins/CoreConsole/Commands/GitPull.php" => array("1615", "3efe18272aec58b81c9b14682730767d"), + "plugins/CoreConsole/Commands/GitPush.php" => array("1260", "3245e20d1f7faad56762b75f286292b4"), + "plugins/CoreConsole/Commands/ManagePlugin.php" => array("3827", "9dda766c81f3523550fdc4f9c4bef1a3"), + "plugins/CoreConsole/Commands/WatchLog.php" => array("933", "731015b3f289194da71225ce6435668b"), + "plugins/CoreHome/angularjs/ajax-form/ajax-form.controller.js" => array("2659", "abca56433b192e40e2cfb56ed05e6b74"), + "plugins/CoreHome/angularjs/ajax-form/ajax-form.directive.js" => array("5949", "8ec7bb671f9bb4b01bfbd11c3f8227a4"), + "plugins/CoreHome/angularjs/anchorLinkFix.js" => array("2866", "8e56fdd25f8c036761319f784d35fdca"), + "plugins/CoreHome/angularjs/common/directives/autocomplete-matched.js" => array("2002", "ef1110fa971179ef53181c760ff23473"), + "plugins/CoreHome/angularjs/common/directives/dialog.js" => array("1576", "f3d75742bb632c1f1b9ac71f259aeeab"), + "plugins/CoreHome/angularjs/common/directives/directive.module.js" => array("214", "ac6b77adc1c87808df6f9a251f9cd806"), + "plugins/CoreHome/angularjs/common/directives/focus-anywhere-but-here.js" => array("1499", "69ae4035ad7c899e18723a40024a756f"), + "plugins/CoreHome/angularjs/common/directives/focusif.js" => array("893", "9101239dbcaefeeb8666639bbe2db49d"), + "plugins/CoreHome/angularjs/common/directives/ignore-click.js" => array("710", "f3856cd967f7430a4a7d35c7355e1080"), + "plugins/CoreHome/angularjs/common/directives/onenter.js" => array("853", "fb2635e3675ed8f8f4b94dbb5ef3c9d4"), + "plugins/CoreHome/angularjs/common/directives/translate.js" => array("1226", "25631e79c43116b4bc3d09b0cd1b1888"), + "plugins/CoreHome/angularjs/common/filters/evolution.js" => array("1280", "2d73aaa3bec3eb49a147be5ac1d97a97"), + "plugins/CoreHome/angularjs/common/filters/filter.module.js" => array("211", "437a6034fdb37539d3aed523cfad6313"), + "plugins/CoreHome/angularjs/common/filters/htmldecode.js" => array("686", "ba2d989b0f42815503c418246136ac47"), + "plugins/CoreHome/angularjs/common/filters/length.js" => array("455", "044553b96551318b3631ee0ace4db473"), + "plugins/CoreHome/angularjs/common/filters/pretty-url.js" => array("356", "325e19c11fc5685926d7c0aebd3db9e7"), + "plugins/CoreHome/angularjs/common/filters/startfrom.js" => array("404", "61c73f5f48fe0e59cbd53a8b736de38b"), + "plugins/CoreHome/angularjs/common/filters/startfrom.spec.js" => array("1121", "c349283452bd9333dc923849d796e82d"), + "plugins/CoreHome/angularjs/common/filters/translate.js" => array("623", "506dba3d11f2533d3033f28fbbdaf971"), + "plugins/CoreHome/angularjs/common/filters/trim.js" => array("415", "fa0dcf521dccf4f5e9c3c5fab48a7556"), + "plugins/CoreHome/angularjs/common/filters/ucfirst.js" => array("500", "e6c65f6dde4ccac134a462de0475a594"), + "plugins/CoreHome/angularjs/common/services/piwik-api.js" => array("9853", "aaf46455d03b233014330879ee4dcb3e"), + "plugins/CoreHome/angularjs/common/services/piwik-api.spec.js" => array("9340", "5ad12f7790c88fb3da58af213b3683ea"), + "plugins/CoreHome/angularjs/common/services/piwik.js" => array("374", "ba910785547f2c3596b0eadb22467ec0"), + "plugins/CoreHome/angularjs/common/services/piwik.spec.js" => array("1086", "7bcd31b934b7f60cf85ae84e6ffffb79"), + "plugins/CoreHome/angularjs/common/services/service.module.js" => array("211", "b556271a166215692f022125fa3f5b23"), + "plugins/CoreHome/angularjs/dialogtoggler/dialogtoggler.controller.js" => array("3253", "561ac89dd5a1c3a5f96b8fe2f709d9b4"), + "plugins/CoreHome/angularjs/dialogtoggler/dialogtoggler.directive.js" => array("710", "fc58d980d245e100f8ed2afd2799f58f"), + "plugins/CoreHome/angularjs/dialogtoggler/dialogtoggler-urllistener.service.js" => array("3589", "ddc827dcbb2a6299d775bed94b79e68e"), + "plugins/CoreHome/angularjs/dialogtoggler/ngdialog.less" => array("1363", "c9d1b2a219dde5ccf2e22c2fdbfbe4f7"), + "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline.directive.html" => array("1285", "29ec6b3f32288a79a69e3488fb0889ab"), + "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline.directive.js" => array("2679", "08d2be8497acd8780ea8ddf64b44d866"), + "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline.directive.less" => array("1076", "857cd7a07e8b207bf980a366fa8c0528"), "plugins/CoreHome/angularjs/enrichedheadline/help.png" => array("350", "a2442fd403f530897728f540aa374c70"), - "plugins/CoreHome/angularjs/piwikAppConfig.js" => array("335", "286c144553a0f1389f804a4f2931c27a"), - "plugins/CoreHome/angularjs/piwikApp.js" => array("321", "8b891e5568091f5bef5566fe2a8440f3"), - "plugins/CoreHome/angularjs/siteselector/siteselector-controller.js" => array("1826", "a297e0adb8c651e9c49b975f72290e17"), - "plugins/CoreHome/angularjs/siteselector/siteselector-directive.js" => array("2676", "3c6cb7efaefd524922328842205f15ed"), - "plugins/CoreHome/angularjs/siteselector/siteselector.html" => array("3030", "2a1df7bc671183fb140a04132a0a6a6e"), - "plugins/CoreHome/angularjs/siteselector/siteselector.less" => array("3527", "7741715b7b9ccbdfd9c44f92e9080eff"), - "plugins/CoreHome/angularjs/siteselector/siteselector-model.js" => array("1909", "9ef295ef4cec80096d9c24705411618b"), - "plugins/CoreHome/Controller.php" => array("8189", "5cc9b5304f98cd5dd4fe96c28665a7eb"), - "plugins/CoreHome/CoreHome.php" => array("10068", "d32fe2afae1943efe49db6276a33c164"), - "plugins/CoreHome/DataTableRowAction/MultiRowEvolution.php" => array("2134", "09919d9abed888e1a1ad0149cf241ea9"), - "plugins/CoreHome/DataTableRowAction/RowEvolution.php" => array("11730", "6dbc8fcb21eb457148022097a445d778"), + "plugins/CoreHome/angularjs/history/history.service.js" => array("4428", "f591fbd512bc1774d8b5efcaecdd1c1b"), + "plugins/CoreHome/angularjs/http404check.js" => array("1818", "6fa9ab54fef3905880fde779accc2a19"), + "plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.html" => array("1153", "2df9597c7e1fcd3234a46682d0dd2de8"), + "plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.js" => array("2457", "3183c14e0e9a16899c2f236b0cadf8b5"), + "plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.less" => array("2017", "0a21fa9157b48b56bf06613acba944dd"), + "plugins/CoreHome/angularjs/notification/notification.controller.js" => array("979", "844362df3c2828ea518d4840a2702ff6"), + "plugins/CoreHome/angularjs/notification/notification.directive.html" => array("340", "eaa588a40c0b019ce90daa2b9873a978"), + "plugins/CoreHome/angularjs/notification/notification.directive.js" => array("3534", "ae7c18b571e9bf71706c087f4b5f8f36"), + "plugins/CoreHome/angularjs/notification/notification.directive.less" => array("684", "5a741c01664f038087947bafe89e0f13"), + "plugins/CoreHome/angularjs/piwikApp.config.js" => array("414", "7ff195eda2782e7199367dcc380d3eae"), + "plugins/CoreHome/angularjs/piwikApp.js" => array("434", "db286d351874552917926fa15857c22f"), + "plugins/CoreHome/angularjs/quick-access/quick-access.controller.js" => array("3190", "eda03e846729315b019ee807a76be561"), + "plugins/CoreHome/angularjs/quick-access/quick-access.directive.html" => array("2365", "fc70418bab12871a4c5ff196033fd2e8"), + "plugins/CoreHome/angularjs/quick-access/quick-access.directive.js" => array("10477", "365fd3c6f420cb0472845d74272dc39b"), + "plugins/CoreHome/angularjs/quick-access/quick-access.directive.less" => array("934", "aaee54e0eff2c7d8d47da8031b427754"), + "plugins/CoreHome/angularjs/selector/selector.directive.js" => array("2671", "f390e2b976c77de399f356f4125b3208"), + "plugins/CoreHome/angularjs/selector/selector.directive.less" => array("1006", "226d6c918276cab7bb7264067315ea25"), + "plugins/CoreHome/angularjs/siteselector/siteselector.controller.js" => array("1651", "2f89a00282998b27b94aa74bd1f7f67b"), + "plugins/CoreHome/angularjs/siteselector/siteselector.directive.html" => array("3530", "2aeed0ccddecd57d455bb0c8ba9e827b"), + "plugins/CoreHome/angularjs/siteselector/siteselector.directive.js" => array("3515", "a032c188da7448182a7e012fd4671d51"), + "plugins/CoreHome/angularjs/siteselector/siteselector.directive.less" => array("2746", "7ec054af1c6843acaca5682f54aa0200"), + "plugins/CoreHome/angularjs/siteselector/siteselector-model.service.js" => array("4061", "642a97067d416f851af26584cc2bcc4c"), + "plugins/CoreHome/Columns/IdSite.php" => array("1492", "12ccec9447a69c7bc83f2abc1977f279"), + "plugins/CoreHome/Columns/Metrics/ActionsPerVisit.php" => array("1034", "e537ce58bfe370e7130dcdcfa923ae04"), + "plugins/CoreHome/Columns/Metrics/AverageTimeOnSite.php" => array("1261", "416dc71e93fd63ca0ab3f581bb50ccc3"), + "plugins/CoreHome/Columns/Metrics/BounceRate.php" => array("1231", "04fea9fa33e9a811026575295d776ec4"), + "plugins/CoreHome/Columns/Metrics/CallableProcessedMetric.php" => array("993", "94a21d0e06aee534298764e276d071f0"), + "plugins/CoreHome/Columns/Metrics/ConversionRate.php" => array("1269", "21013305c172cc0af00fcd7ecdada152"), + "plugins/CoreHome/Columns/Metrics/EvolutionMetric.php" => array("3290", "cd1e0addef5107ebfd43d9906837b178"), + "plugins/CoreHome/Columns/Metrics/VisitsPercent.php" => array("1858", "8cd6934fd89b3688100356859126f35d"), + "plugins/CoreHome/Columns/ServerTime.php" => array("935", "c26cec838ae8e40d32625e1975a2a1f6"), + "plugins/CoreHome/Columns/UserId.php" => array("3664", "e7a71268e1a1204f28881c6d72244dfd"), + "plugins/CoreHome/Columns/VisitFirstActionTime.php" => array("817", "7043d4efb0ae2979c9bb0f95d6af5489"), + "plugins/CoreHome/Columns/VisitGoalBuyer.php" => array("4101", "282a73b3f5b8db0b50fd7786b8d88598"), + "plugins/CoreHome/Columns/VisitGoalConverted.php" => array("1276", "27710a0547f2d959c597e6c0e902471d"), + "plugins/CoreHome/Columns/VisitId.php" => array("1030", "53d28c6fad9a785aa5024b346ada98f9"), + "plugins/CoreHome/Columns/VisitIp.php" => array("1190", "48094c2e89ecbace94f2ddcf52b638d0"), + "plugins/CoreHome/Columns/VisitLastActionTime.php" => array("2057", "5d75a82d5081d8e9c3ea3e3bc591e1e0"), + "plugins/CoreHome/Columns/VisitorDaysSinceFirst.php" => array("1383", "e61320b30edcccb4d28b341117f038d2"), + "plugins/CoreHome/Columns/VisitorDaysSinceOrder.php" => array("1544", "99c10b8ed51c65e7f4eb5523725d1483"), + "plugins/CoreHome/Columns/VisitorId.php" => array("1205", "bc54d6c0de8a6c240ac236cd6a30d77a"), + "plugins/CoreHome/Columns/VisitorReturning.php" => array("2296", "0b57df644e458be6d81013de36ad0a98"), + "plugins/CoreHome/Columns/VisitsCount.php" => array("1346", "b3626d13420083b653804d9fe6b40fad"), + "plugins/CoreHome/Columns/VisitTotalTime.php" => array("2970", "0acabfb0da9b3fd8cd765f0f42759f11"), + "plugins/CoreHome/config/config.php" => array("199", "56a0d120c06028ec881f6e32c67a9ef9"), + "plugins/CoreHome/Controller.php" => array("8998", "c986e29cbf4a0ef0146000c6f8f82b48"), + "plugins/CoreHome/CoreHome.php" => array("16077", "7611742a969aec4579fe41468790e33f"), + "plugins/CoreHome/DataTableRowAction/MultiRowEvolution.php" => array("2138", "cc9fc3ed224716fd952d18dce93cd138"), + "plugins/CoreHome/DataTableRowAction/RowEvolution.php" => array("12727", "767f02e506f6a0a6bdf9ceccf48bc478"), "plugins/CoreHome/images/bg_header.jpg" => array("9097", "0af504cfba0e9a1df7024e5e8d37ace9"), "plugins/CoreHome/images/bullet1.gif" => array("52", "4ea97a49fbc122f369c0509a1e312d36"), "plugins/CoreHome/images/bullet2.gif" => array("52", "e105364ac53547afc6453aac69c4e43a"), "plugins/CoreHome/images/favicon.ico" => array("17947", "9f21326e0c543b50bbe7022b44d97615"), + "plugins/CoreHome/images/favicon.png" => array("18085", "b5a8b909b721a7d4e91e5724ce43df1c"), "plugins/CoreHome/images/googleplay.png" => array("16550", "94203701766674b6ea54cce20b5e4a7e"), "plugins/CoreHome/images/more_date.gif" => array("56", "0f97ae90441e7bf074981d7854f7bdb7"), "plugins/CoreHome/images/more_period.gif" => array("53", "0741dc6f4a2e767ac60c619e85142555"), "plugins/CoreHome/images/more.png" => array("1045", "91d6a597dc70d86071f6aa333cce20aa"), + "plugins/CoreHome/images/navigation_collapse.png" => array("484", "71180c660cc8b94acc16c91c100766f6"), + "plugins/CoreHome/images/navigation_expand.png" => array("502", "13d521bb20871a1053d1401ffededf94"), "plugins/CoreHome/images/promo_splash.png" => array("12070", "98b50af9fcfe08214b630aa5c598848e"), "plugins/CoreHome/images/reset_search.png" => array("1021", "7e761f3444bf4edd4cd1779801c963bd"), "plugins/CoreHome/images/search.png" => array("136", "2fae5ccecd05ad37bd228e9490875ad4"), - "plugins/CoreHome/javascripts/broadcast.js" => array("22763", "4c3cd06055cafa714fd2e2efd4ff82ba"), - "plugins/CoreHome/javascripts/calendar.js" => array("22538", "b87e51fbf1cad8c856337656bca22a8b"), - "plugins/CoreHome/javascripts/color_manager.js" => array("10985", "c22a21e71febe410b3e62de68ce31359"), - "plugins/CoreHome/javascripts/corehome.js" => array("5684", "30d487fa1f07c5b3907886359d4b168a"), - "plugins/CoreHome/javascripts/dataTable.js" => array("67710", "03047b604c67c04845a34b7a1ff3074c"), - "plugins/CoreHome/javascripts/dataTable_rowactions.js" => array("12810", "c98799009416f594610fb887bd7414f9"), - "plugins/CoreHome/javascripts/donate.js" => array("5832", "b1519aaa05ab20ff55e6515f73f22840"), - "plugins/CoreHome/javascripts/menu_init.js" => array("445", "d11ca5e76d64fcb588095037bb3a9e4f"), - "plugins/CoreHome/javascripts/menu.js" => array("3808", "0c2b7400db1cd4144b9820d8a71d6dbf"), - "plugins/CoreHome/javascripts/notification.js" => array("6438", "409154616a282cc31d394f15b1207ee9"), - "plugins/CoreHome/javascripts/notification_parser.js" => array("824", "4860da380ee3224ae5628967055c7285"), - "plugins/CoreHome/javascripts/popover.js" => array("8640", "805e980248f8043a1df32b011d677c13"), - "plugins/CoreHome/javascripts/promo.js" => array("541", "d55cfc743448fc782c3a084ca5205619"), - "plugins/CoreHome/javascripts/require.js" => array("1297", "7f170c5276e38599d334b6e4c167ceb7"), - "plugins/CoreHome/javascripts/sparkline.js" => array("3304", "47ab94274d11f6bdbf840aad41129f0e"), - "plugins/CoreHome/javascripts/top_controls.js" => array("704", "5a27c5eeaa6e8e0c0682c170128be44d"), - "plugins/CoreHome/javascripts/uiControl.js" => array("3663", "b0d3706d547d7f102d39c17b000ea894"), - "plugins/CoreHome/stylesheets/cloud.less" => array("874", "7f28097a511022a86beb4735cb3eb877"), + "plugins/CoreHome/javascripts/broadcast.js" => array("25257", "306d383d96a336516550b0cfa48c0762"), + "plugins/CoreHome/javascripts/calendar.js" => array("22845", "3877fd4d6477d2d6ccf9e8c2ccf8c3d3"), + "plugins/CoreHome/javascripts/color_manager.js" => array("10967", "b353d58b5b4e15661aee335885ee410b"), + "plugins/CoreHome/javascripts/corehome.js" => array("6948", "d20870e6505fb3c11a483504825bd03c"), + "plugins/CoreHome/javascripts/dataTable.js" => array("75119", "813db2a07782ba83ebbfce390ef9e721"), + "plugins/CoreHome/javascripts/dataTable_rowactions.js" => array("13810", "1509d2f6fc2118c27abea1e7c96cc315"), + "plugins/CoreHome/javascripts/donate.js" => array("5845", "68b91fa072dceb7f26d298136e063557"), + "plugins/CoreHome/javascripts/menu_init.js" => array("1108", "d3495850c0c21bea0fbb518349867375"), + "plugins/CoreHome/javascripts/menu.js" => array("5223", "6fa924e1bdb0e49439777d15e40b36f9"), + "plugins/CoreHome/javascripts/notification.js" => array("4640", "0f3c5c133a46014bb58b8052d0fd8903"), + "plugins/CoreHome/javascripts/notification_parser.js" => array("840", "0355511f49d021a3b3c0ad7671c3f556"), + "plugins/CoreHome/javascripts/numberFormatter.js" => array("4905", "6b3346bc8bcd2049d8318b45ad8abc29"), + "plugins/CoreHome/javascripts/popover.js" => array("9146", "344269ceac5c593298aef1a0b5c37f8c"), + "plugins/CoreHome/javascripts/require.js" => array("1311", "51935def5f73c3a156fc3c2a9711c37c"), + "plugins/CoreHome/javascripts/sparkline.js" => array("3645", "ff2d22b59ec4931d51a37db8a6822ec4"), + "plugins/CoreHome/javascripts/top_controls.js" => array("3274", "44f4eaeacc3e1849b401bbf7288ba85e"), + "plugins/CoreHome/javascripts/uiControl.js" => array("3667", "60260138e086658c1c11c48b1b9a2182"), + "plugins/CoreHome/lang/am.json" => array("293", "3f589e67838d75a6a85867bced743b73"), + "plugins/CoreHome/lang/ar.json" => array("5957", "1e6488690d6684189e2d67ff283957a5"), + "plugins/CoreHome/lang/be.json" => array("1420", "74dd7c9a19335d242227a1ba5125ec0a"), + "plugins/CoreHome/lang/bg.json" => array("6249", "6daffdb7e37de2c3ae2df6c6d682033e"), + "plugins/CoreHome/lang/bs.json" => array("191", "dcd4f69b4db7ba742ec6a208c843f1b5"), + "plugins/CoreHome/lang/ca.json" => array("3020", "756505ce395c21b74add9399c1bf3993"), + "plugins/CoreHome/lang/cs.json" => array("5214", "49306f1d64959f7d6cf59cb709c8ac8b"), + "plugins/CoreHome/lang/da.json" => array("4732", "9f500ec0ce459ae1271257d936ea9d54"), + "plugins/CoreHome/lang/de.json" => array("5865", "488193e51830e35a33eaee21948fe3bf"), + "plugins/CoreHome/lang/el.json" => array("9207", "4daac3c46140eeca1f355ddeb5478f7e"), + "plugins/CoreHome/lang/en.json" => array("5077", "7dd99ec0da369fffdb606e669350910a"), + "plugins/CoreHome/lang/es.json" => array("5181", "245f6592c1e10782cf73709fad9a7241"), + "plugins/CoreHome/lang/et.json" => array("1771", "1a673b3b63e0255658dad9fb1600dddc"), + "plugins/CoreHome/lang/eu.json" => array("572", "5953a2984961166b0a2a64800f74ad4c"), + "plugins/CoreHome/lang/fa.json" => array("4390", "5f802e7ea85acebab2837d6d8653f595"), + "plugins/CoreHome/lang/fi.json" => array("4796", "a7e15e4375e604f9946fc35291662e6d"), + "plugins/CoreHome/lang/fr.json" => array("5676", "33af17617c9b4a1cbf4426ab2edc0d96"), + "plugins/CoreHome/lang/gl.json" => array("221", "41bd1519566cce99bfb3e515b522a173"), + "plugins/CoreHome/lang/he.json" => array("1496", "da109c3438e9293a76a670722be7223b"), + "plugins/CoreHome/lang/hi.json" => array("8530", "f60490868dc8f480e021ef9c25a3ca7b"), + "plugins/CoreHome/lang/hr.json" => array("77", "13403fada02c2532bb998ad2908dae6f"), + "plugins/CoreHome/lang/hu.json" => array("1051", "c68f876f0f31babb919b65af0214f574"), + "plugins/CoreHome/lang/id.json" => array("4417", "6b775dc045f8bfabdf259dfdf23adf39"), + "plugins/CoreHome/lang/is.json" => array("470", "13475a07031c4827df7589e999cd616c"), + "plugins/CoreHome/lang/it.json" => array("5657", "b187d975fa3cc18fd3729c65f3621447"), + "plugins/CoreHome/lang/ja.json" => array("6255", "eb8f82e4715425efb4fb7e4d0e12736f"), + "plugins/CoreHome/lang/ka.json" => array("1702", "d4bf8b0b44f9c2fee1a91ff1251d852b"), + "plugins/CoreHome/lang/ko.json" => array("5753", "d0524976ca4b0f9540d5a1475a0c72c6"), + "plugins/CoreHome/lang/lt.json" => array("943", "aba801491b521fd3cd8c1ca948912085"), + "plugins/CoreHome/lang/lv.json" => array("984", "bb98adcf3b3bc291ba2b759ab8da50d2"), + "plugins/CoreHome/lang/nb.json" => array("5220", "09dc0d12ea5440472ad78b16d2d1b680"), + "plugins/CoreHome/lang/nl.json" => array("4956", "9765a994044cab8b8cccc8757e54ee53"), + "plugins/CoreHome/lang/nn.json" => array("1685", "f18b8771fe07aba94b1dac9c30624506"), + "plugins/CoreHome/lang/pl.json" => array("4770", "9003828ba5f4a99922045eb85ac065f1"), + "plugins/CoreHome/lang/pt-br.json" => array("5678", "416fe9be52b13c71ba9b4244e92b3fa3"), + "plugins/CoreHome/lang/pt.json" => array("1320", "c9dcc227cb00378fb3917d5e8fcbd962"), + "plugins/CoreHome/lang/ro.json" => array("4835", "34849359279f194e821a88a9384dc0a5"), + "plugins/CoreHome/lang/ru.json" => array("7241", "771cc56580779142c0d22f1346a0ce23"), + "plugins/CoreHome/lang/sk.json" => array("5147", "63a77dda587e34285686aad8a48197d2"), + "plugins/CoreHome/lang/sl.json" => array("773", "0f99c25bfc9b8a8b38bc2650514ce2a4"), + "plugins/CoreHome/lang/sq.json" => array("1204", "a9304618931870ec83194b2a7769e3a5"), + "plugins/CoreHome/lang/sr.json" => array("4827", "a13be300970735f469212cc8165a0d47"), + "plugins/CoreHome/lang/sv.json" => array("5039", "a36238a658e29075469ee82605fdc1d0"), + "plugins/CoreHome/lang/ta.json" => array("5082", "570180a121f912fb39ab1831bef1e3e9"), + "plugins/CoreHome/lang/te.json" => array("119", "7e9c6ec6465a602b12c4a73bafe260c0"), + "plugins/CoreHome/lang/th.json" => array("1804", "d477d76043a7a60f33033511bb8af17d"), + "plugins/CoreHome/lang/tl.json" => array("5352", "76876f1dfad73bef25ea9fc11c6b7542"), + "plugins/CoreHome/lang/tr.json" => array("3020", "0c8171405b07db58acd3f4bc65ee1717"), + "plugins/CoreHome/lang/uk.json" => array("1009", "1882ad1616f47c7beb774837ad9c7f61"), + "plugins/CoreHome/lang/vi.json" => array("5383", "782ccd38a3f50d02e6582531e72d4e6f"), + "plugins/CoreHome/lang/zh-cn.json" => array("4212", "de2edff85024110f5047c0292f5bead0"), + "plugins/CoreHome/lang/zh-tw.json" => array("662", "18e17d93964d0a413301c728ea4cc378"), + "plugins/CoreHome/Menu.php" => array("2616", "5b7abf74077706521e5a6c08ec2d3215"), + "plugins/CoreHome/Segment.php" => array("367", "eb86574f0cbe1d33fc9f449bb3ca60b8"), + "plugins/CoreHome/stylesheets/cloud.less" => array("1138", "69db3085ff95dbeb0e3d6a01bc456e2e"), "plugins/CoreHome/stylesheets/color_manager.css" => array("42", "40709b59fb63c5e7fcdcab9230c1dbaa"), - "plugins/CoreHome/stylesheets/coreHome.less" => array("4274", "966a251617979d9103c999cb5a3c56e8"), - "plugins/CoreHome/stylesheets/dataTable/_dataTable.less" => array("10157", "7fe43c52bc99e776a9e2b7de3db419f4"), + "plugins/CoreHome/stylesheets/coreHome.less" => array("3735", "f2b04ba74b9f89c94a2c881cc5a285f9"), + "plugins/CoreHome/stylesheets/dataTable/_dataTable.less" => array("11088", "ec7e511e7c080893d250c76f46cd3f78"), "plugins/CoreHome/stylesheets/dataTable.less" => array("366", "b5a4df46b9d7f8e1193f54d3aad665c8"), - "plugins/CoreHome/stylesheets/dataTable/_limitSelection.less" => array("1306", "68d63775a50badf3b9ac1992997d1c9b"), - "plugins/CoreHome/stylesheets/dataTable/_reportDocumentation.less" => array("1182", "2423f8ebb34d4a3fd818d580b176ca45"), - "plugins/CoreHome/stylesheets/dataTable/_rowActions.less" => array("656", "afe520a206b30502fa9e4c3812efa605"), - "plugins/CoreHome/stylesheets/dataTable/_subDataTable.less" => array("860", "e5b7fb01c2a7682310d5e9121960d6fa"), - "plugins/CoreHome/stylesheets/dataTable/_tableConfiguration.less" => array("1499", "5c63c6d3ff0fc046514ee80f572158a4"), - "plugins/CoreHome/stylesheets/_donate.less" => array("2251", "8a37ac490210fd01f9439e821d2f5aeb"), - "plugins/CoreHome/stylesheets/jqplotColors.less" => array("2449", "cee067a8b15cf5a393fdde65c03e0d03"), - "plugins/CoreHome/stylesheets/jquery.ui.autocomplete.css" => array("1200", "518cf68de85e435653447e9102316661"), - "plugins/CoreHome/stylesheets/menu.less" => array("2935", "de31a81958b769f798adaea6673f6fe8"), - "plugins/CoreHome/stylesheets/notification.less" => array("1612", "a5400eef0a05d0159e5675726ef8e2f3"), - "plugins/CoreHome/stylesheets/promo.less" => array("1222", "10e9dd09f69cd77f2d2b35ae86a99b7b"), - "plugins/CoreHome/stylesheets/sparklineColors.less" => array("522", "a8fe19271fefc2f1829b9e271838467a"), + "plugins/CoreHome/stylesheets/dataTable/_limitSelection.less" => array("1338", "c01e49c501ff406bb8357e1c7ee43f26"), + "plugins/CoreHome/stylesheets/dataTable/_reportDocumentation.less" => array("834", "148c2212218a0d82f9a918bda774d07d"), + "plugins/CoreHome/stylesheets/dataTable/_rowActions.less" => array("695", "b9d3af9358b6057e37fb7e4f5cad92de"), + "plugins/CoreHome/stylesheets/dataTable/_subDataTable.less" => array("816", "3cffe0b93ee08fa806da2332247509b3"), + "plugins/CoreHome/stylesheets/dataTable/_tableConfiguration.less" => array("1541", "b1495a2d90bbc42114eee327a6a158c2"), + "plugins/CoreHome/stylesheets/_donate.less" => array("2321", "4acd667f756ddf920caa98bf75bd8a03"), + "plugins/CoreHome/stylesheets/jqplotColors.less" => array("2512", "657cae47275e71b93413ca816c4bbf40"), + "plugins/CoreHome/stylesheets/jquery.ui.autocomplete.css" => array("1198", "e41c776588399505e932173bc556ef41"), + "plugins/CoreHome/stylesheets/layout.less" => array("7925", "5fbefe86cb79ae42b6a2c998305c3248"), + "plugins/CoreHome/stylesheets/notification.less" => array("87", "f82d13a39e0a90c2096db92ee62360e7"), + "plugins/CoreHome/stylesheets/promo.less" => array("1268", "13b273cc4a64ddcd1e83c24c2043adb7"), + "plugins/CoreHome/stylesheets/sparklineColors.less" => array("362", "143089281239350f78d6c71554b27e19"), + "plugins/CoreHome/stylesheets/zen-mode.less" => array("1881", "da662bcd2fe479f1a49156ec4eae234b"), + "plugins/CoreHome/templates/_adblockDetect.twig" => array("1492", "97a0b6464c0125f845e7448e1cacc52d"), "plugins/CoreHome/templates/checkForUpdates.twig" => array("45", "15faa60f49f1c4c281c76bc2a7e00ce7"), - "plugins/CoreHome/templates/_dataTableCell.twig" => array("2957", "d6b282e985fee6e9f1b9524c3d5b8346"), - "plugins/CoreHome/templates/_dataTableFooter.twig" => array("8068", "cbe13cdda27009f397de576cbab1e37d"), - "plugins/CoreHome/templates/_dataTableHead.twig" => array("821", "f39052d9ca22525d016796aa27662900"), + "plugins/CoreHome/templates/_dataTableCell.twig" => array("3108", "3e260fcbeaf3d972ceea3bc31314bfea"), + "plugins/CoreHome/templates/_dataTableFooter.twig" => array("9531", "15dc15199d866cfa78238a9d525c0c1c"), + "plugins/CoreHome/templates/_dataTableHead.twig" => array("916", "0d42c2d5a9fef9cee1565b9ad51e378a"), "plugins/CoreHome/templates/_dataTableJS.twig" => array("159", "7233ccf9c682cdb1744b43ccb9ca84d7"), - "plugins/CoreHome/templates/_dataTable.twig" => array("2016", "0e4018ea821c7e713d8f62c7e1bd31f1"), - "plugins/CoreHome/templates/_donate.twig" => array("2986", "2dc12be6edbf01dcb96bca7caf65a9d2"), - "plugins/CoreHome/templates/getDefaultIndexView.twig" => array("291", "a63dbf0283c9695118f5403b9cea9c9e"), + "plugins/CoreHome/templates/_dataTable.twig" => array("2097", "217737499d8db855ad31c8a76e8ea6b1"), + "plugins/CoreHome/templates/_donate.twig" => array("2994", "4a54eea81fc0cf58387698ef4725233f"), + "plugins/CoreHome/templates/_favicon.twig" => array("225", "b594a67cb6fbe1d2c11ba6e50b29907f"), + "plugins/CoreHome/templates/getDefaultIndexView.twig" => array("595", "5b5b3d422da2380eb75c8ae20d88a077"), "plugins/CoreHome/templates/getDonateForm.twig" => array("38", "ceda9c8c6ba889dbac8b55ca7e61e030"), - "plugins/CoreHome/templates/getMultiRowEvolutionPopover.twig" => array("1907", "2777f9e84b4d06496a779b8581566038"), - "plugins/CoreHome/templates/getPromoVideo.twig" => array("1379", "f40ed16a053efaa6df2c682c3e620b41"), - "plugins/CoreHome/templates/getRowEvolutionPopover.twig" => array("1685", "d9be8e51169aef72e1e6acf3d93b1d82"), - "plugins/CoreHome/templates/_headerMessage.twig" => array("2915", "83a77be74cbcedac5eff74e80e33be49"), - "plugins/CoreHome/templates/_indexContent.twig" => array("567", "936cb35de43d98985a05715734b634d6"), + "plugins/CoreHome/templates/getMultiRowEvolutionPopover.twig" => array("1962", "3f60ff1e39b96e9d43e157c4379f8fe5"), + "plugins/CoreHome/templates/getPromoVideo.twig" => array("2095", "5ac46827a8e56dc2c7f3bec7456963f6"), + "plugins/CoreHome/templates/getRowEvolutionPopover.twig" => array("1720", "f6205c1f580886de1530ff8f7c82fc7c"), + "plugins/CoreHome/templates/_headerMessage.twig" => array("3072", "10e030efafea8422b09d16766406aebb"), "plugins/CoreHome/templates/_javaScriptDisabled.twig" => array("134", "9f7e7b75b3dd7928b0d7db27802cec9e"), - "plugins/CoreHome/templates/_logo.twig" => array("607", "d14a3ee156c537331868b5cfe52034b9"), - "plugins/CoreHome/templates/_menu.twig" => array("952", "499786b48fe52cdc248d4ecb45a7d7dd"), + "plugins/CoreHome/templates/_logo.twig" => array("635", "bccae163fc358e71bacf6a6e3a9e4111"), + "plugins/CoreHome/templates/_menu.twig" => array("3252", "2d7afd94246daa4134694d6050e87028"), "plugins/CoreHome/templates/_notifications.twig" => array("333", "974e20fdb4837324b61cbfb052d3b911"), - "plugins/CoreHome/templates/_periodSelect.twig" => array("1794", "71ebf4a07f218ccd3a6b8e71bd9ad9a4"), - "plugins/CoreHome/templates/ReportRenderer/_htmlReportBody.twig" => array("3773", "5e44bacceb55616bd659c7c6b7883fe6"), + "plugins/CoreHome/templates/_periodSelect.twig" => array("1891", "c7277ccbbd71f75f5117b96dd4ab1692"), + "plugins/CoreHome/templates/ReportRenderer/_htmlReportBody.twig" => array("4080", "427c3830fa306ff2ea73f33e03485cf6"), "plugins/CoreHome/templates/ReportRenderer/_htmlReportFooter.twig" => array("15", "4d214dd44eaca17a5b8c100a0cee7ebc"), - "plugins/CoreHome/templates/ReportRenderer/_htmlReportHeader.twig" => array("1165", "f16ab5bc3e57cb8df3a4d5c101f506b2"), - "plugins/CoreHome/templates/ReportsByDimension/_reportsByDimension.twig" => array("1112", "7c22d58e39b426bd649d88d1199aa749"), + "plugins/CoreHome/templates/ReportRenderer/_htmlReportHeader.twig" => array("1329", "ef0db4e6f6105bcf672247b554494cb1"), + "plugins/CoreHome/templates/ReportsByDimension/_reportsByDimension.twig" => array("1111", "11776a7aec0652095a3fec01f3bc8017"), "plugins/CoreHome/templates/_singleReport.twig" => array("61", "3ed4c965676469386ba1e6142f55f049"), - "plugins/CoreHome/templates/_siteSelectHeader.twig" => array("234", "9b4fa86af9fec5ea3755b4dde733efa6"), - "plugins/CoreHome/templates/_topBarHelloMenu.twig" => array("1027", "f7dfa6faaa6a6ccc28f5f00e16874a0b"), - "plugins/CoreHome/templates/_topBarTopMenu.twig" => array("763", "ab004040fabe91dff6bc8752ddbc8ef0"), - "plugins/CoreHome/templates/_topBar.twig" => array("200", "9987986288e7ce9072205fb329f9fc87"), - "plugins/CoreHome/templates/_topScreen.twig" => array("109", "6fae551319f6efff81bf460ebad78b66"), - "plugins/CoreHome/templates/_uiControl.twig" => array("327", "35b5eef8d6bb93196947469ab721d0d6"), - "plugins/CoreHome/templates/_warningInvalidHost.twig" => array("915", "ef59e3a2d2828b269f2e9dd41238602d"), - "plugins/CorePluginsAdmin/Controller.php" => array("17319", "a0cecaa6e6c468d9a598d2e5ded186b8"), - "plugins/CorePluginsAdmin/CorePluginsAdmin.php" => array("4817", "4b53a6ef27725c8d61723351162f3156"), + "plugins/CoreHome/templates/_siteSelectHeader.twig" => array("147", "7f9e97bdeaf71f23550395e9d392c14b"), + "plugins/CoreHome/templates/_topBar.twig" => array("1400", "90abae55f024bc017d1ea877fa970c34"), + "plugins/CoreHome/templates/_topScreen.twig" => array("628", "e82984958b46890ae74c03f92c194a7a"), + "plugins/CoreHome/templates/_uiControl.twig" => array("429", "52df099a1a443d1c7ddb64334090f177"), + "plugins/CoreHome/templates/_warningInvalidHost.twig" => array("741", "60be9da447924dd2cf2df0adbe4c79f1"), + "plugins/CoreHome/Tracker/VisitRequestProcessor.php" => array("7417", "e49a02d7c36f5f975c6c9394da8092e1"), + "plugins/CoreHome/Visitor.php" => array("2864", "b83a0a8b1cc891868f5df7ef7074db1d"), + "plugins/CoreHome/Widgets.php" => array("1616", "a8bd48e2fe3d532f50b7d6d578460be0"), + "plugins/CorePluginsAdmin/Commands/ActivatePlugin.php" => array("1224", "09778e7ef87200a365894206a953f35e"), + "plugins/CorePluginsAdmin/Commands/DeactivatePlugin.php" => array("1239", "7dd77a5a7e15147f6c2d1d84a6b42710"), + "plugins/CorePluginsAdmin/Commands/ListPlugins.php" => array("1558", "1065d522c50f6143b730a9634bb05e72"), + "plugins/CorePluginsAdmin/Controller.php" => array("18767", "45840909c1571a6d0f20550ad64c084a"), + "plugins/CorePluginsAdmin/CorePluginsAdmin.php" => array("1918", "7aa8205c3b97c67f63665a3799949ef4"), + "plugins/CorePluginsAdmin/images/flattr.png" => array("1639", "fb7338392a7e06ed64c534f69f0c01f5"), + "plugins/CorePluginsAdmin/images/paypal_donate.jpg" => array("3665", "b5c0a835ae76a566b81ddacf370cf1e6"), "plugins/CorePluginsAdmin/images/plugins.png" => array("14076", "44b89bd30206dd317c032a8595e1ddeb"), "plugins/CorePluginsAdmin/images/rating_important.png" => array("673", "0e63393e13ce89a4920684b4b06f68ee"), "plugins/CorePluginsAdmin/images/themes.png" => array("79517", "67eb6beb264c9181c79246fc6f255c04"), - "plugins/CorePluginsAdmin/javascripts/pluginDetail.js" => array("2270", "5122b5212d9bd47b215b8899fcc448a2"), - "plugins/CorePluginsAdmin/javascripts/pluginExtend.js" => array("756", "4177e6ed911cce4e0c2fdf257f0b538f"), - "plugins/CorePluginsAdmin/javascripts/pluginOverview.js" => array("900", "a7221dc1b624beb54936fce19faf8fcf"), - "plugins/CorePluginsAdmin/javascripts/plugins.js" => array("2974", "e9b57f6e7a1fe31bf46672c7af2001a7"), - "plugins/CorePluginsAdmin/MarketplaceApiClient.php" => array("5380", "32b4ee5cc03f93a5cb0b437023025628"), - "plugins/CorePluginsAdmin/MarketplaceApiException.php" => array("258", "504b17a7c04736eac83d9d532c4666d9"), - "plugins/CorePluginsAdmin/Marketplace.php" => array("5717", "9ee5d691a3e9b28404b7caddfafe6fb3"), - "plugins/CorePluginsAdmin/PluginInstallerException.php" => array("286", "6f252dd00be63fc7c60035c042bfa17e"), - "plugins/CorePluginsAdmin/PluginInstaller.php" => array("9509", "6b462a3b03902bcfeeebab6491e6cfca"), - "plugins/CorePluginsAdmin/stylesheets/marketplace.less" => array("5941", "0d9e782e0476bed3714d79572289e3fb"), - "plugins/CorePluginsAdmin/stylesheets/plugins_admin.less" => array("910", "c3f90c42b657a52f81e4de1f774acfad"), - "plugins/CorePluginsAdmin/templates/browsePluginsActions.twig" => array("787", "ec50c06b24b52ac8a77e0473f35ecdf8"), - "plugins/CorePluginsAdmin/templates/browsePlugins.twig" => array("1569", "807f2a4521e8c2fa60ecb096c7733aae"), - "plugins/CorePluginsAdmin/templates/browseThemes.twig" => array("1355", "ace812045eef7acf7f999493badb3154"), - "plugins/CorePluginsAdmin/templates/extend.twig" => array("3852", "d29aebabcbd868d40e6e13703436d65e"), + "plugins/CorePluginsAdmin/javascripts/marketplace.js" => array("1404", "f418feb6ec58c63ada7c39f6dca0040c"), + "plugins/CorePluginsAdmin/javascripts/pluginExtend.js" => array("756", "c49fe7ecac64d68d909e7e9041ce598d"), + "plugins/CorePluginsAdmin/javascripts/pluginOverview.js" => array("1120", "23045bfed482808699ea17e472f97da7"), + "plugins/CorePluginsAdmin/javascripts/plugins.js" => array("2990", "f2a5b12bfda2a36b9d6ddfae16f09855"), + "plugins/CorePluginsAdmin/lang/am.json" => array("499", "12a0c8a8165a869f7111a60db060f75b"), + "plugins/CorePluginsAdmin/lang/ar.json" => array("2658", "6e0b9c11e816ef4350404b7914f49249"), + "plugins/CorePluginsAdmin/lang/be.json" => array("1005", "fc5908655de0f7c4c6c84f20ff4df850"), + "plugins/CorePluginsAdmin/lang/bg.json" => array("9517", "0fa07c938f75d1270dc3e0c704272ab8"), + "plugins/CorePluginsAdmin/lang/bn.json" => array("476", "620aef2bb387f8c701bb6d7868122f73"), + "plugins/CorePluginsAdmin/lang/bs.json" => array("443", "bee3f0b817f2aad1931acc6bfb2c85e5"), + "plugins/CorePluginsAdmin/lang/ca.json" => array("836", "945e4cb44f518e406f41b89beabdd540"), + "plugins/CorePluginsAdmin/lang/cs.json" => array("7688", "e7122f70d031104ee40a7288866247f4"), + "plugins/CorePluginsAdmin/lang/da.json" => array("7092", "cdb3281ee13587df15e694eee320886a"), + "plugins/CorePluginsAdmin/lang/de.json" => array("8000", "1728e735fbf5604f40b74ac7d4a3c401"), + "plugins/CorePluginsAdmin/lang/el.json" => array("11648", "7d458aed281cdec3f75ca658bf800b0d"), + "plugins/CorePluginsAdmin/lang/en.json" => array("7180", "8b9c4d55a411972a1803c44be1d99b3a"), + "plugins/CorePluginsAdmin/lang/es.json" => array("7573", "acf0eac9735cf05f1c725ad1522bbd8c"), + "plugins/CorePluginsAdmin/lang/et.json" => array("3216", "fbc78a4a5a8b9c9a4595583a47bbda8b"), + "plugins/CorePluginsAdmin/lang/eu.json" => array("648", "8436cfb268e81f029a5289275e526a1d"), + "plugins/CorePluginsAdmin/lang/fa.json" => array("7243", "fcdf7a5a392068f1181391d5a29e961a"), + "plugins/CorePluginsAdmin/lang/fi.json" => array("6443", "bf14ea64b8f32f53c4cef907d6d57f3c"), + "plugins/CorePluginsAdmin/lang/fr.json" => array("8222", "c50941d9f081b57bf2fd2233ba5d7612"), + "plugins/CorePluginsAdmin/lang/gl.json" => array("208", "6587713e133353087ffd81f1cf93f2ab"), + "plugins/CorePluginsAdmin/lang/he.json" => array("1136", "a4539f91699eb37baf33cc4fb6598c5b"), + "plugins/CorePluginsAdmin/lang/hi.json" => array("3538", "54a0746fffe74e6c95ab1ab65b91086d"), + "plugins/CorePluginsAdmin/lang/hr.json" => array("73", "63950bcb79c3f6dfad126e41c3cf50c2"), + "plugins/CorePluginsAdmin/lang/hu.json" => array("748", "95706052d5e33ff0422610f77fde7e52"), + "plugins/CorePluginsAdmin/lang/id.json" => array("766", "5bb5f712a14835a5c12b7e589f34cdbe"), + "plugins/CorePluginsAdmin/lang/is.json" => array("751", "6dfa32bd9af98bf4ef25b24a68011702"), + "plugins/CorePluginsAdmin/lang/it.json" => array("7492", "3bf1093c0ff91caac94b31c1f6d216a5"), + "plugins/CorePluginsAdmin/lang/ja.json" => array("9400", "8f4c1c9e20367926501f857981835f43"), + "plugins/CorePluginsAdmin/lang/ka.json" => array("1317", "04089b7cd323407bbeff1d38f312904c"), + "plugins/CorePluginsAdmin/lang/ko.json" => array("3409", "e94371c779d770bb427b961e31462984"), + "plugins/CorePluginsAdmin/lang/lt.json" => array("1619", "e4ca6e641d91026d44757231c3eae185"), + "plugins/CorePluginsAdmin/lang/lv.json" => array("740", "56cb390c2a35aa5b6add7a31eeb6bcb0"), + "plugins/CorePluginsAdmin/lang/nb.json" => array("7329", "931c24c1178c12fc8ddad4408d68f4d6"), + "plugins/CorePluginsAdmin/lang/nl.json" => array("7507", "d56e3f5cda1f15da10bbed78503d0269"), + "plugins/CorePluginsAdmin/lang/nn.json" => array("725", "e64e34604eb01e2941c33d15322fbdc6"), + "plugins/CorePluginsAdmin/lang/pl.json" => array("6434", "53c69b88156b56bc4e1be49a5ce1ab2d"), + "plugins/CorePluginsAdmin/lang/pt-br.json" => array("7673", "81da108103a4408c5f647c3c26e6a6f3"), + "plugins/CorePluginsAdmin/lang/pt.json" => array("1378", "e1c6555c2761c380062cb7301648da13"), + "plugins/CorePluginsAdmin/lang/ro.json" => array("6594", "e3453af55b4794f6c0f5ec091ce8a592"), + "plugins/CorePluginsAdmin/lang/ru.json" => array("8850", "c274ece9186441776b351da0213a34fe"), + "plugins/CorePluginsAdmin/lang/sk.json" => array("7371", "9c6ed20972cd46293cd59ef80a7bb1cc"), + "plugins/CorePluginsAdmin/lang/sl.json" => array("876", "52dc63465419116f03f75a410d968840"), + "plugins/CorePluginsAdmin/lang/sq.json" => array("871", "c3454a7733b6793cf7fa76000ead438f"), + "plugins/CorePluginsAdmin/lang/sr.json" => array("7420", "8b36cf2c9a9d70de7840320e6bcd2632"), + "plugins/CorePluginsAdmin/lang/sv.json" => array("6942", "09a6a211f1fbe535be0257b43a7fba10"), + "plugins/CorePluginsAdmin/lang/ta.json" => array("1687", "ef761f8935fcb0fb1ca7b954dbf2745b"), + "plugins/CorePluginsAdmin/lang/te.json" => array("534", "d305e9eebafc2b667beeae8d5dfcb826"), + "plugins/CorePluginsAdmin/lang/th.json" => array("1267", "acba15bde3aa64c43d9db07a1ce6bf27"), + "plugins/CorePluginsAdmin/lang/tl.json" => array("7010", "ab9ba75f1c287556c0fe4fdc8a679f81"), + "plugins/CorePluginsAdmin/lang/tr.json" => array("3600", "2cac28e90dec341442f8a4175b380c87"), + "plugins/CorePluginsAdmin/lang/uk.json" => array("996", "a45577adf7df3d1f32c3c119cf150113"), + "plugins/CorePluginsAdmin/lang/vi.json" => array("4373", "4fed138df3afafd869dc30f92c71727f"), + "plugins/CorePluginsAdmin/lang/zh-cn.json" => array("3574", "79f84724e55fe172d02da5462f024c29"), + "plugins/CorePluginsAdmin/lang/zh-tw.json" => array("614", "f168f8aff2b1d30e8e7af5b3a108aa3a"), + "plugins/CorePluginsAdmin/MarketplaceApiClient.php" => array("5024", "8720371679b50aed3504f932e98f70c3"), + "plugins/CorePluginsAdmin/MarketplaceApiException.php" => array("262", "e13982ce6c99a1cf4bb894c4b10dfe5a"), + "plugins/CorePluginsAdmin/Marketplace.php" => array("5842", "fcb81c755318ef8c58db988ffe37a108"), + "plugins/CorePluginsAdmin/Menu.php" => array("2401", "cee29267b22c5aef7211d7c570753995"), + "plugins/CorePluginsAdmin/PluginInstallerException.php" => array("290", "a4cea1cae2ebced279c90ff6f22a09f5"), + "plugins/CorePluginsAdmin/PluginInstaller.php" => array("9847", "30ef7acb085deaeb16b9ca32331f696e"), + "plugins/CorePluginsAdmin/stylesheets/marketplace.less" => array("2037", "1ae8d80fa80df6d1eeba8ada1a770666"), + "plugins/CorePluginsAdmin/stylesheets/plugin-details.less" => array("1838", "224bf6cbd8edcce54e9427d1f9c88f9a"), + "plugins/CorePluginsAdmin/stylesheets/plugins_admin.less" => array("2671", "7a1408a5a50f1d166d1a2e0539626f43"), + "plugins/CorePluginsAdmin/Tasks.php" => array("933", "a2b562d71118f42cd1f53849318d216a"), "plugins/CorePluginsAdmin/templates/installPlugin.twig" => array("1593", "678e6e40b503dfce996bd1638140dae2"), - "plugins/CorePluginsAdmin/templates/macros.twig" => array("13807", "c53678062e9729c889b4f0c964e3eed6"), - "plugins/CorePluginsAdmin/templates/pluginDetails.twig" => array("10614", "425ca997711e3a742a8c05b9a7547a40"), - "plugins/CorePluginsAdmin/templates/pluginMetadata.twig" => array("579", "573e9a85cc47e6581d4ea2c27e21f271"), - "plugins/CorePluginsAdmin/templates/pluginOverview.twig" => array("1373", "f63e81873d0894f1fe510860e0bb72ce"), - "plugins/CorePluginsAdmin/templates/plugins.twig" => array("985", "614227399bb790846563585476ab7293"), - "plugins/CorePluginsAdmin/templates/safemode.twig" => array("4851", "b7bc45d170dd3d6906251f8f66c24fd4"), - "plugins/CorePluginsAdmin/templates/themeOverview.twig" => array("1432", "7a7fe7a864cacf63b83afcb9ad1ae51a"), - "plugins/CorePluginsAdmin/templates/themes.twig" => array("1165", "ab2cb33c411d1b72aae3b2810982e603"), - "plugins/CorePluginsAdmin/templates/updatePlugin.twig" => array("1431", "509baa9e82b722e0518e31f45c85678c"), - "plugins/CorePluginsAdmin/templates/uploadPlugin.twig" => array("1640", "f61cd131dda39221cf364b82f03d727e"), - "plugins/CorePluginsAdmin/UpdateCommunication.php" => array("6203", "cd40906be1bc9082e7ae01ebe55b4434"), - "plugins/CoreUpdater/Commands/Update.php" => array("2038", "f713523751a5850ea31df6068d813a4f"), - "plugins/CoreUpdater/Controller.php" => array("15315", "baa77e239df7fe6679394684ef0db52f"), - "plugins/CoreUpdater/CoreUpdater.php" => array("5380", "7e2b3bc913e03712d30aef6eecf7af00"), - "plugins/CoreUpdater/javascripts/updateLayout.js" => array("246", "97c81a1a8c66bb5ddb6567591c623387"), - "plugins/CoreUpdater/NoUpdatesFoundException.php" => array("243", "4c4002fd3bdb636949915467b95d9607"), - "plugins/CoreUpdater/stylesheets/updateLayout.css" => array("564", "9fd4b49281584e2af9d964fd9e8a754a"), - "plugins/CoreUpdater/templates/layout.twig" => array("1774", "4764b278f8e7d90fdab9523feef448a1"), - "plugins/CoreUpdater/templates/newVersionAvailable.twig" => array("1667", "dc764e7c76232bca2fac9626e7127b6c"), - "plugins/CoreUpdater/templates/oneClickResults.twig" => array("768", "fd13410beecb2907de3e0121379fc0a2"), - "plugins/CoreUpdater/templates/runUpdaterAndExit_done_cli.twig" => array("1632", "34eaa2c5de8a88c4b320e5f8d4c0df54"), - "plugins/CoreUpdater/templates/runUpdaterAndExit_done.twig" => array("3017", "e28b48da5e46a69f714e03d6e0ad845b"), - "plugins/CoreUpdater/templates/runUpdaterAndExit_welcome_cli.twig" => array("1329", "b84d36f1cac2070b4f82efc39955e8da"), - "plugins/CoreUpdater/templates/runUpdaterAndExit_welcome.twig" => array("4394", "ec2fafcabb5b9ff34f63873f710dc259"), - "plugins/CoreUpdater/UpdateCommunication.php" => array("4260", "44ea4f46e1a34360f2b8d2b1779d1bf0"), - "plugins/CoreVisualizations/CoreVisualizations.php" => array("3098", "59fc60592977b80fecb9a22cb789a339"), - "plugins/CoreVisualizations/javascripts/jqplotBarGraph.js" => array("2624", "8663e49a42d16fcd7faaf628b88c2cbc"), - "plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js" => array("4990", "0555513c693cdcc529d45b82044646c0"), - "plugins/CoreVisualizations/javascripts/jqplot.js" => array("37901", "e06b89f2c320761abc576e24f0d73057"), - "plugins/CoreVisualizations/javascripts/jqplotPieGraph.js" => array("2675", "f51532d8a5c7ff32da50b032c31c5584"), - "plugins/CoreVisualizations/javascripts/seriesPicker.js" => array("13503", "834304040b3260bb6f708cc5f43d7547"), - "plugins/CoreVisualizations/JqplotDataGenerator/Chart.php" => array("3191", "b7b441685a7b019bb9c29b8478be0538"), - "plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php" => array("7403", "be2979d456835753937f5c10890270cc"), - "plugins/CoreVisualizations/JqplotDataGenerator.php" => array("4865", "b53f8d32bfe0d79da637d502863170ce"), + "plugins/CorePluginsAdmin/templates/macros.twig" => array("16619", "c131ee6f87823578c10fd800f51788b1"), + "plugins/CorePluginsAdmin/templates/marketplace/plugin-list.twig" => array("5556", "28e08f5105c42017b91b177ace5ddd7d"), + "plugins/CorePluginsAdmin/templates/marketplace.twig" => array("3349", "232afba27c1990540b32c36dec5577f2"), + "plugins/CorePluginsAdmin/templates/pluginDetails.twig" => array("10671", "d206f0cef3431f31431574c21fd018ad"), + "plugins/CorePluginsAdmin/templates/plugins.twig" => array("1308", "0ae428eefaca56d4a1b788e54a54b9c3"), + "plugins/CorePluginsAdmin/templates/safemode.twig" => array("5622", "91508f42dc492fde60ab2ac7e0ec7ff3"), + "plugins/CorePluginsAdmin/templates/themes.twig" => array("886", "57a10742139790b8236c6930480f996b"), + "plugins/CorePluginsAdmin/templates/updatePlugin.twig" => array("1436", "8dd906fac836c0eaa02be2441c835c2c"), + "plugins/CorePluginsAdmin/templates/uploadPlugin.twig" => array("1650", "2896e819b4881ee1a3eb619e7ca41a40"), + "plugins/CorePluginsAdmin/UpdateCommunication.php" => array("6473", "9dc7e7b5276fc01905540ad2ea4b6b1d"), + "plugins/CoreUpdater/ArchiveDownloadException.php" => array("432", "e058e85ee92b93c785f3dd694802ee33"), + "plugins/CoreUpdater/Commands/Update/CliUpdateObserver.php" => array("1407", "4c98536cf0fc75964450a2813961ac6b"), + "plugins/CoreUpdater/Commands/Update.php" => array("11085", "8a96b9f6b0637e94137c0a8f543f1d35"), + "plugins/CoreUpdater/config/config.php" => array("268", "61cba55ae65f82e007089ab59a9eca4d"), + "plugins/CoreUpdater/Controller.php" => array("9586", "9da445672bdbc93c34ca9a863f73af9a"), + "plugins/CoreUpdater/CoreUpdater.php" => array("2939", "dbeb0b227eb3575ac1c3d711fd9d9f34"), + "plugins/CoreUpdater/Diagnostic/HttpsUpdateCheck.php" => array("1160", "8b3a8ece0aa3c1de6ed4937a792d9425"), + "plugins/CoreUpdater/javascripts/updateLayout.js" => array("353", "560f94921854694ec2b4815d633fab91"), + "plugins/CoreUpdater/lang/am.json" => array("3461", "54998106a099fb3400ac41c8f46d7a43"), + "plugins/CoreUpdater/lang/ar.json" => array("6517", "748097937cba1ece478e32c5ddc66353"), + "plugins/CoreUpdater/lang/be.json" => array("7369", "5506557524a8a78e1b2764328002ff18"), + "plugins/CoreUpdater/lang/bg.json" => array("8817", "b78358b2e6f41eaa29b35c5d1ce7e65f"), + "plugins/CoreUpdater/lang/bs.json" => array("183", "acb5d6399859005cef98196a000341e1"), + "plugins/CoreUpdater/lang/ca.json" => array("5682", "64d08d26f478d2089b9ac7ae8ba817ba"), + "plugins/CoreUpdater/lang/cs.json" => array("8303", "c6cfd3cb43b331fc6397c1561f8e6e66"), + "plugins/CoreUpdater/lang/da.json" => array("6367", "5db0570e452e040c13b231c1465de2ee"), + "plugins/CoreUpdater/lang/de.json" => array("9090", "dc25269aa1675e84d8c18dfd185c419b"), + "plugins/CoreUpdater/lang/el.json" => array("13209", "944c7211de34785a16ddd8216aba1ddf"), + "plugins/CoreUpdater/lang/en.json" => array("7890", "fad3ac9d3363b3272d7a2b0a42712bc5"), + "plugins/CoreUpdater/lang/es.json" => array("8510", "786957e3854c5d55112e63369085d422"), + "plugins/CoreUpdater/lang/et.json" => array("3582", "37dc8785e22c74da692ebfea2cdd8063"), + "plugins/CoreUpdater/lang/eu.json" => array("4164", "5e3f2e1c443a953661e5a1daa99c891e"), + "plugins/CoreUpdater/lang/fa.json" => array("6911", "cf7a786a6d3f310d1df16e3044b9388f"), + "plugins/CoreUpdater/lang/fi.json" => array("6042", "d81e088f4e22d950c2bc2c603df30001"), + "plugins/CoreUpdater/lang/fr.json" => array("9143", "4e4fcb98feb108a399651a3e237ce9c7"), + "plugins/CoreUpdater/lang/gl.json" => array("1212", "ec59865095074e3e1df5b9adecaf5d8a"), + "plugins/CoreUpdater/lang/he.json" => array("5984", "0724d54f12ba1296c39ebbf3bdfbd3ea"), + "plugins/CoreUpdater/lang/hi.json" => array("9965", "1abba4b57b119d097bdc40871a6598ca"), + "plugins/CoreUpdater/lang/hu.json" => array("5792", "e9904bfa957290c49e5cf77874b4056c"), + "plugins/CoreUpdater/lang/id.json" => array("5225", "4dadf172be01b18c59d82673fdc74285"), + "plugins/CoreUpdater/lang/it.json" => array("8529", "960870cb791a4859adea8c2afd86f6c7"), + "plugins/CoreUpdater/lang/ja.json" => array("10464", "2b246aebf290ff7828fd5f6b40813183"), + "plugins/CoreUpdater/lang/ka.json" => array("10935", "f8fd3135fa081b9b743079d49573db05"), + "plugins/CoreUpdater/lang/ko.json" => array("6325", "5c3b3060ade6be5f1b129fe9bbf325c2"), + "plugins/CoreUpdater/lang/lt.json" => array("5641", "66e3b776b66e513c30a7da3db2c3984e"), + "plugins/CoreUpdater/lang/lv.json" => array("2202", "154ae076e24d47bd76d277534467c39c"), + "plugins/CoreUpdater/lang/nb.json" => array("8048", "5f54f105a8f78abcb6111c5e0c732bde"), + "plugins/CoreUpdater/lang/nl.json" => array("8026", "d094ce6d4b6c67d362045de03dcebd99"), + "plugins/CoreUpdater/lang/nn.json" => array("4522", "ce3fb0b72866dc7a3905ce905df31405"), + "plugins/CoreUpdater/lang/pl.json" => array("7290", "15d058a653705e2bfd943b9224afe6a9"), + "plugins/CoreUpdater/lang/pt-br.json" => array("8560", "0f39f16a99a1f4cf89f2ca9faf7e7614"), + "plugins/CoreUpdater/lang/pt.json" => array("5279", "21d63381328d231410cb635379e9501a"), + "plugins/CoreUpdater/lang/ro.json" => array("6434", "3ef43832f4711d1fe2b01f2f945b22e9"), + "plugins/CoreUpdater/lang/ru.json" => array("10699", "e670bf2b25c8c0ea17a6439df242109a"), + "plugins/CoreUpdater/lang/sk.json" => array("8249", "28eaafb4f6f3115fead6d3fee6cda4ec"), + "plugins/CoreUpdater/lang/sl.json" => array("2869", "ec94d35365fcce2b3a59b6be568619d2"), + "plugins/CoreUpdater/lang/sq.json" => array("5723", "8a81437c6736f2016077c301c0f4f4f3"), + "plugins/CoreUpdater/lang/sr.json" => array("7798", "f346b121536858df01201468e83fd251"), + "plugins/CoreUpdater/lang/sv.json" => array("7671", "cb2c1fe0baa814e39530881741d1918c"), + "plugins/CoreUpdater/lang/ta.json" => array("6546", "2d73edde54a31c4cb2df3d933d22e7b9"), + "plugins/CoreUpdater/lang/te.json" => array("397", "8b250115eb5dedd9f177dee41ec1a2b2"), + "plugins/CoreUpdater/lang/th.json" => array("9961", "e484397f272990b4b5fc384b382bf7a7"), + "plugins/CoreUpdater/lang/tl.json" => array("6729", "92c4326d5e05635e8589625a4d0e6af4"), + "plugins/CoreUpdater/lang/tr.json" => array("3627", "906f6c753b09bc87442875f484705354"), + "plugins/CoreUpdater/lang/uk.json" => array("7676", "d39bcdaa2691e464b928e0a8ddfcf8df"), + "plugins/CoreUpdater/lang/vi.json" => array("6364", "f1d916d30a6e4545b9b402ff603e553b"), + "plugins/CoreUpdater/lang/zh-cn.json" => array("4978", "60f51b7f9b1fd81057bbc2888c5cfd18"), + "plugins/CoreUpdater/lang/zh-tw.json" => array("7116", "b3d7e4561a7b7ae2d902ac715a3c3051"), + "plugins/CoreUpdater/Model.php" => array("942", "b0d3eadaa1ff0462a038931aeee813f3"), + "plugins/CoreUpdater/NoUpdatesFoundException.php" => array("247", "0d4c19405628a88e41b16beb3df81e24"), + "plugins/CoreUpdater/ReleaseChannel/Latest2XBeta.php" => array("681", "0e0a1615981a95636a15f970f3771c76"), + "plugins/CoreUpdater/ReleaseChannel/Latest2XStable.php" => array("687", "cb5de6ba03ceac467bcfd339be147060"), + "plugins/CoreUpdater/ReleaseChannel/LatestBeta.php" => array("558", "2225f760a6bae4d7e7a1a7789f6f82ac"), + "plugins/CoreUpdater/ReleaseChannel/LatestStable.php" => array("789", "367c4cc9f0e7a5b5a573d447c6d996ff"), + "plugins/CoreUpdater/ReleaseChannel.php" => array("1252", "0c03f5bc18c7680b61ff1d251554ad05"), + "plugins/CoreUpdater/stylesheets/updateLayout.css" => array("452", "b82835729641caf404972ae9df52d2ff"), + "plugins/CoreUpdater/Tasks.php" => array("637", "efb8c30e6fd18f92575e09e1d1faf290"), + "plugins/CoreUpdater/templates/layout.twig" => array("3158", "f979c43becd2eb3049977a70571a211d"), + "plugins/CoreUpdater/templates/newVersionAvailable.twig" => array("1995", "42d4faa7b7d229a8420ad13152cb3844"), + "plugins/CoreUpdater/templates/runUpdaterAndExit_done.twig" => array("3229", "c4d6bafdec81c459fe0b26411e3c8b36"), + "plugins/CoreUpdater/templates/runUpdaterAndExit_welcome.twig" => array("4960", "2678c737bd4c4846b5a1b18520f8e244"), + "plugins/CoreUpdater/templates/updateHttpError.twig" => array("819", "93603a7c8a11a82915748f099712e709"), + "plugins/CoreUpdater/templates/updateHttpsError.twig" => array("1652", "8d51be10531c815d0e10423aa0c3b86d"), + "plugins/CoreUpdater/templates/updateSuccess.twig" => array("1189", "aa7173080f7104b14cc8bd6cc353fc9e"), + "plugins/CoreUpdater/Test/Fixtures/DbUpdaterTestFixture.php" => array("591", "5c88368b42db58530a6123b76c27a3b8"), + "plugins/CoreUpdater/Test/Fixtures/FailUpdateHttpsFixture.php" => array("610", "3194abee5fc46adb951b414883b141f5"), + "plugins/CoreUpdater/Test/Integration/Commands/UpdateTest.php" => array("4203", "ef727902af4e09046e794b17a9f797b6"), + "plugins/CoreUpdater/Test/Integration/ReleaseChannelTest.php" => array("1666", "239e01132daeb2851ba0276398b954dc"), + "plugins/CoreUpdater/Test/Integration/UpdateCommunicationTest.php" => array("4756", "aa7267288f869f704627e3c8ab9fe817"), + "plugins/CoreUpdater/Test/Mock/UpdaterMock.php" => array("1711", "0991a9def66d21bd9e38b0e0f2f129f4"), + "plugins/CoreUpdater/Test/Unit/ModelTest.php" => array("1726", "8c9ad2c04e8d53edd59c9d620a3a0e75"), + "plugins/CoreUpdater/UpdateCommunication.php" => array("4852", "8f1b6fb1bd7648d288769fb5ed604f95"), + "plugins/CoreUpdater/UpdaterException.php" => array("746", "67b0f028ed004af1d6b8a59933324d6b"), + "plugins/CoreUpdater/Updater.php" => array("8898", "4db3e790463cec29dfdbca8d2a9b41f3"), + "plugins/CoreVisualizations/CoreVisualizations.php" => array("2950", "ad077d12621c27a2f734f87f500dd367"), + "plugins/CoreVisualizations/javascripts/jqplotBarGraph.js" => array("2670", "a5e8cf96fca8dff30a52eccaa50349c9"), + "plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js" => array("6646", "e3c3dd7d26c3077bf01fe9e84683cad6"), + "plugins/CoreVisualizations/javascripts/jqplot.js" => array("42238", "b95bd13c9dcc691f2d705f8bcbdf0dd4"), + "plugins/CoreVisualizations/javascripts/jqplotPieGraph.js" => array("2721", "2f141f5a36b394da5b73e558364aeda3"), + "plugins/CoreVisualizations/javascripts/seriesPicker.js" => array("13719", "41e8456559b6426c34f55b0ead63d1eb"), + "plugins/CoreVisualizations/JqplotDataGenerator/Chart.php" => array("3239", "e7ea8f4f1a78f92ab2be703aa56d2898"), + "plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php" => array("7390", "6cf8c16449a503df36d46475d1782871"), + "plugins/CoreVisualizations/JqplotDataGenerator.php" => array("4937", "8182bd4878f524f8d70e9c82136ab6a0"), + "plugins/CoreVisualizations/Metrics/Formatter/Numeric.php" => array("1247", "541b2d4cb515a5a0ded54171d6bc88c5"), "plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less" => array("537", "d54bd4990eb168a209dea17adfccc1df"), - "plugins/CoreVisualizations/stylesheets/jqplot.css" => array("4847", "03cf5f3e336655a1b5e43b7d7d35bc3a"), - "plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig" => array("2641", "e28eee7fdc78defe23ba01c368f153b4"), - "plugins/CoreVisualizations/templates/_dataTableViz_jqplotGraph.twig" => array("149", "ee20fc44c0c435c5dc4b876a42b5632d"), - "plugins/CoreVisualizations/templates/_dataTableViz_tagCloud.twig" => array("839", "b01f096bf415c6b90b405e5be27938dd"), - "plugins/CoreVisualizations/Visualizations/Cloud/Config.php" => array("848", "096f5191662d35f16818ec8262976315"), - "plugins/CoreVisualizations/Visualizations/Cloud.php" => array("5071", "c7e1b761725380954a056fd1b523a0aa"), - "plugins/CoreVisualizations/Visualizations/Graph/Config.php" => array("3359", "44e5bfb03fdc0a72f58f8a50e34f6e15"), - "plugins/CoreVisualizations/Visualizations/Graph.php" => array("5311", "f72e91bb0e8c6a7dd7870a330f51b028"), - "plugins/CoreVisualizations/Visualizations/HtmlTable/AllColumns.php" => array("2206", "891d8f79c7fa4509dbe51fd21d87759d"), - "plugins/CoreVisualizations/Visualizations/HtmlTable/Config.php" => array("3363", "22c395e75a04d580abd824cba212d43d"), - "plugins/CoreVisualizations/Visualizations/HtmlTable.php" => array("2170", "4281101774e7592f03d22abb332e760d"), - "plugins/CoreVisualizations/Visualizations/HtmlTable/RequestConfig.php" => array("1559", "1fa011dffcbb16625833555774fec70e"), - "plugins/CoreVisualizations/Visualizations/JqplotGraph/Bar.php" => array("1054", "d94682d59ff312d8cb3632b5f1bb34a0"), - "plugins/CoreVisualizations/Visualizations/JqplotGraph/Config.php" => array("1808", "c070eb1e52624df57e477ef395a1d8b8"), - "plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution/Config.php" => array("1146", "6d32a05c97c3ed61fcf07a4934772099"), - "plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution.php" => array("6285", "273e819c224b072f98acbd38db00dcd2"), - "plugins/CoreVisualizations/Visualizations/JqplotGraph.php" => array("1415", "802e838dd17eb03d047bf80cdf75507a"), - "plugins/CoreVisualizations/Visualizations/JqplotGraph/Pie.php" => array("1526", "a63ef246318b7af4a3f930b8ba9e1f95"), - "plugins/CoreVisualizations/Visualizations/Sparkline.php" => array("3542", "4901c3281e7997d351f27bcd868aabc9"), - "plugins/CustomVariables/API.php" => array("3729", "a14f0b41fa2c9d429f4bcb1b79604a89"), - "plugins/CustomVariables/Archiver.php" => array("8023", "67584cad7e95fcea09249f9a98d79dae"), - "plugins/CustomVariables/Commands/Info.php" => array("2337", "7ad2373927c02fc97c9dd0b718918b43"), - "plugins/CustomVariables/Commands/SetNumberOfCustomVariables.php" => array("7048", "7988044609e3aec8acb12b5adb9add94"), - "plugins/CustomVariables/Controller.php" => array("734", "4660b5e7cda48db92afab520d689c7bc"), - "plugins/CustomVariables/CustomVariables.php" => array("8766", "c0dfd6ad3cef8186ca6512524c71a604"), - "plugins/CustomVariables/Model.php" => array("4890", "ab27c1ca66ad3015d05f5fcda3ef660f"), - "plugins/Dashboard/API.php" => array("3932", "d87fdaf7673503e6014957c961d16189"), - "plugins/Dashboard/Controller.php" => array("10134", "f867dfa6d4e7c74293bfb9e44abf247b"), - "plugins/Dashboard/DashboardManagerControl.php" => array("1610", "fb1b28942d7990b8bbe90406cf800830"), - "plugins/Dashboard/Dashboard.php" => array("9928", "ffdb68de39c57e26b77d7ac9ecaf08c8"), - "plugins/Dashboard/DashboardSettingsControlBase.php" => array("671", "4979cb16799468c2e48306fc69b51a9d"), - "plugins/Dashboard/javascripts/dashboard.js" => array("10758", "b790e5f955248dec1129ef302cd268e3"), - "plugins/Dashboard/javascripts/dashboardObject.js" => array("18451", "dd99d1afae535b93176693343d7496b0"), - "plugins/Dashboard/javascripts/dashboardWidget.js" => array("11715", "60dc1080ad458f829e2ecca912fd2a34"), - "plugins/Dashboard/javascripts/widgetMenu.js" => array("16065", "45348093e269dbbfaa759ad98965ecc9"), - "plugins/Dashboard/stylesheets/dashboard.less" => array("8508", "65b1b29e4898fb49936a9c02bbcfc1fc"), - "plugins/Dashboard/stylesheets/standalone.css" => array("1163", "c05b2508df30267e2272e8998fd441d8"), - "plugins/Dashboard/templates/_dashboardSettings.twig" => array("800", "2c5218bdfc8b5500d438590af1babacd"), - "plugins/Dashboard/templates/embeddedIndex.twig" => array("4843", "6ee72f07ff096808072bd4667298bf90"), - "plugins/Dashboard/templates/_header.twig" => array("581", "5d695bcc4f1f82c41acb586eeac3d191"), - "plugins/Dashboard/templates/index.twig" => array("775", "f9cfac7175ab189a36954a9a6021796c"), - "plugins/Dashboard/templates/_widgetFactoryTemplate.twig" => array("963", "a2a1af34eef792aaa743d97e6d0f15e4"), - "plugins/DBStats/API.php" => array("9746", "7fc743a372e25e41bf222d6aaab68162"), - "plugins/DBStats/Controller.php" => array("5044", "d4efb7c5a12a4c87209c848e36766d75"), - "plugins/DBStats/DBStats.php" => array("15822", "98f174296aea7b597ff7b508944e3377"), - "plugins/DBStats/.gitignore" => array("33", "478d4f2ee44c0d87d241ed2565482e1c"), + "plugins/CoreVisualizations/stylesheets/jqplot.css" => array("4708", "2094e4bb4c802bb048745f57ead1f905"), + "plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig" => array("3369", "cc943cba18faa5e721d6749c3e344cb7"), + "plugins/CoreVisualizations/templates/_dataTableViz_jqplotGraph.twig" => array("194", "aafbb43ba063732fc2ba0056b8bc3d72"), + "plugins/CoreVisualizations/templates/_dataTableViz_tagCloud.twig" => array("886", "1387074d19e97ca8edd681fbf1447716"), + "plugins/CoreVisualizations/Visualizations/Cloud/Config.php" => array("852", "ccdfb0f61d3c12fb3a985489a286878f"), + "plugins/CoreVisualizations/Visualizations/Cloud.php" => array("5115", "1c3df12698f3d0a275edba9938c0c257"), + "plugins/CoreVisualizations/Visualizations/Graph/Config.php" => array("3363", "f784c301f7dcd33050fa4b2bf369e7cb"), + "plugins/CoreVisualizations/Visualizations/Graph.php" => array("5433", "dee47a3fbaff7a638e66b277866dea52"), + "plugins/CoreVisualizations/Visualizations/HtmlTable/AllColumns.php" => array("2138", "1e8235a59a2864111dc44c2777af56e3"), + "plugins/CoreVisualizations/Visualizations/HtmlTable/Config.php" => array("3367", "53f1b7bf01c9f25f9db63ddd0914c841"), + "plugins/CoreVisualizations/Visualizations/HtmlTable.php" => array("2209", "b6f4cc3b069bfb17920296d2e0071030"), + "plugins/CoreVisualizations/Visualizations/HtmlTable/PivotBy.php" => array("896", "8a8d0efc43ab88363f8c72cdbe3bbbfd"), + "plugins/CoreVisualizations/Visualizations/HtmlTable/RequestConfig.php" => array("1753", "146fb38093372ec0a800d6a18d9c6455"), + "plugins/CoreVisualizations/Visualizations/JqplotGraph/Bar.php" => array("1057", "f2bc5bb8216a4664269b1efcd701509f"), + "plugins/CoreVisualizations/Visualizations/JqplotGraph/Config.php" => array("1812", "5a0fc2669a91697fb527dfcb5708b1d9"), + "plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution/Config.php" => array("1150", "db0ebdddf050a307c357394f08260706"), + "plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution.php" => array("6289", "0cb8db0af48f46242882f46dfb9a42d7"), + "plugins/CoreVisualizations/Visualizations/JqplotGraph.php" => array("1418", "ff0d8a2d8fb7158cc8da87fe6f3ac5f4"), + "plugins/CoreVisualizations/Visualizations/JqplotGraph/Pie.php" => array("1529", "de48cbbfd5628f146310abc50e31afba"), + "plugins/CoreVisualizations/Visualizations/Sparkline.php" => array("3546", "87ec3e738f458d08473c3bb2bfbe9d51"), + "plugins/CustomVariables/angularjs/manage-custom-vars/manage-custom-vars.controller.js" => array("769", "a44ee2a662480b326b0cd60f573f8bb2"), + "plugins/CustomVariables/angularjs/manage-custom-vars/manage-custom-vars.directive.html" => array("2659", "91f42d6ae61d9f2aa5b69f6e98a2e97d"), + "plugins/CustomVariables/angularjs/manage-custom-vars/manage-custom-vars.directive.js" => array("724", "1a9ef7fd796b3d189dd65cfad95f5248"), + "plugins/CustomVariables/angularjs/manage-custom-vars/manage-custom-vars.directive.less" => array("240", "1761e56f3ff1380b315eacd286b9de97"), + "plugins/CustomVariables/angularjs/manage-custom-vars/manage-custom-vars.model.js" => array("1990", "4ec33a3e893e31b527b252e3d5cd2816"), + "plugins/CustomVariables/API.php" => array("5841", "812b54b593b6e552c6f148bd9416826b"), + "plugins/CustomVariables/Archiver.php" => array("10163", "24a4bf97c3a890b067697cac6628323a"), + "plugins/CustomVariables/Columns/Base.php" => array("1757", "a028ef2e07f53aa79912e5ed7e358019"), + "plugins/CustomVariables/Columns/CustomVariableName.php" => array("485", "89aae4a9208e8e4b446772f659822f17"), + "plugins/CustomVariables/Columns/CustomVariableValue.php" => array("488", "cd558d65cb5daf36301f625dd045d5a6"), + "plugins/CustomVariables/Commands/Info.php" => array("2288", "39ba3750ffb13363806f287159213a3e"), + "plugins/CustomVariables/Commands/SetNumberOfCustomVariables.php" => array("7017", "f46b53f0b7c7f0312321077d877a1f38"), + "plugins/CustomVariables/config/config.php" => array("138", "d4bbf8117bbe2cdca32140476243e2fb"), + "plugins/CustomVariables/config/test.php" => array("66", "f2e99120104729896c430fb1fa99d3c7"), + "plugins/CustomVariables/Controller.php" => array("511", "f643e01847f0ceb47026a23edb839349"), + "plugins/CustomVariables/CustomVariables.php" => array("6769", "ed3eda0f155e66ba55f2add079d13a43"), + "plugins/CustomVariables/DataTable/Filter/CustomVariablesValuesFromNameId.php" => array("1079", "6ba2bbf837323e95c1b7e3d7db456fc1"), + "plugins/CustomVariables/lang/ar.json" => array("248", "083b5e9f45f5d68ccbfb59ce0797bd61"), + "plugins/CustomVariables/lang/be.json" => array("763", "115dc5bdb0497ecfa18b6929707e6394"), + "plugins/CustomVariables/lang/bg.json" => array("924", "d9461c32e98815084aaf2efb9a5163cb"), + "plugins/CustomVariables/lang/ca.json" => array("663", "a28089ee5bfcd91c00a1dabcc1ca10d0"), + "plugins/CustomVariables/lang/cs.json" => array("2314", "044fff1b545aec5fada24429c9ac09ef"), + "plugins/CustomVariables/lang/da.json" => array("609", "422ed0e0426b5c538c84b2b2ec84f668"), + "plugins/CustomVariables/lang/de.json" => array("2501", "66b70f1da8e8f321509294174fbcbb3e"), + "plugins/CustomVariables/lang/el.json" => array("4025", "eb5f7f8f4a4ec5dc6a54b2501b248b04"), + "plugins/CustomVariables/lang/en.json" => array("2417", "086f7accde4bf5df7a72676387180e35"), + "plugins/CustomVariables/lang/es.json" => array("1229", "8a1e2e86d93df0e5278aea68dfed37c5"), + "plugins/CustomVariables/lang/et.json" => array("298", "102bc5c068cd3e56a92cff65eea94826"), + "plugins/CustomVariables/lang/fa.json" => array("329", "b757781232213982a275f8739cd10edd"), + "plugins/CustomVariables/lang/fi.json" => array("597", "3ecf11ea1b9ab515d90bbee9728ca028"), + "plugins/CustomVariables/lang/fr.json" => array("2663", "6211974c94fa2e9a9f50bec79f991a23"), + "plugins/CustomVariables/lang/he.json" => array("219", "fd0b095c9513eddcc75351d29230af40"), + "plugins/CustomVariables/lang/hi.json" => array("950", "c68ea25cfd021222f6445b2bcb21e3ac"), + "plugins/CustomVariables/lang/id.json" => array("558", "e64bc0ac45bc7386586ba6e50643af44"), + "plugins/CustomVariables/lang/it.json" => array("2407", "b8a4d0d57cb283dd24360cf20e314711"), + "plugins/CustomVariables/lang/ja.json" => array("657", "334746f9e4843cf4fd31842d36ae7a8b"), + "plugins/CustomVariables/lang/ko.json" => array("581", "7c708a5ac1aadbd31a3baff32e8631b1"), + "plugins/CustomVariables/lang/nb.json" => array("115", "402ba9cb397a5ab654c60edd136eace6"), + "plugins/CustomVariables/lang/nl.json" => array("1127", "09830b160b14a6fa7e97a02afd471a98"), + "plugins/CustomVariables/lang/pl.json" => array("620", "05fa0fd8ae23aceee494370d88770659"), + "plugins/CustomVariables/lang/pt-br.json" => array("2545", "21d71f5758d89e948321092f41a02e47"), + "plugins/CustomVariables/lang/pt.json" => array("694", "13524977179c0535d700d1b8ae86381e"), + "plugins/CustomVariables/lang/ro.json" => array("649", "6289e20a80a76e1c6e1fd9832233b280"), + "plugins/CustomVariables/lang/ru.json" => array("939", "b63342f0c7519035e46d90538bd9605f"), + "plugins/CustomVariables/lang/sk.json" => array("304", "2edbab7acbc510eeff67ce71c1406fc5"), + "plugins/CustomVariables/lang/sl.json" => array("81", "1dca80911cb675863506b6c9d48b5574"), + "plugins/CustomVariables/lang/sq.json" => array("1291", "151092af8e2fb170662bd287e43f455c"), + "plugins/CustomVariables/lang/sr.json" => array("983", "1878c99cd8e704bc6dc7b96cc08b8bc5"), + "plugins/CustomVariables/lang/sv.json" => array("693", "deaa27952aeb22e69ab71327ac8b1809"), + "plugins/CustomVariables/lang/ta.json" => array("425", "a6bccede35fc81eca5b1983ab9cdde0e"), + "plugins/CustomVariables/lang/th.json" => array("319", "ca4d00958e1f5ab315e40e552a2d6e3f"), + "plugins/CustomVariables/lang/tl.json" => array("670", "d2781738edfbeecb8fe801110ec2ad05"), + "plugins/CustomVariables/lang/tr.json" => array("286", "f5fa2c807b7446fb1639c3a3aa3dc354"), + "plugins/CustomVariables/lang/vi.json" => array("684", "82a09e0873030a76a9dc38e942ae2c13"), + "plugins/CustomVariables/lang/zh-cn.json" => array("495", "e499693ef31e7d901e9ac20d65a3f15d"), + "plugins/CustomVariables/Menu.php" => array("1038", "5fbaf10416d8a96428e61fadc6b9b33d"), + "plugins/CustomVariables/Model.php" => array("4942", "b6cbae67399cf1d69f1b8d5dfbd34487"), + "plugins/CustomVariables/Reports/Base.php" => array("398", "e9e74c61fbc7e506f95fb9ff9cda5b70"), + "plugins/CustomVariables/Reports/GetCustomVariables.php" => array("2813", "7b04fc82d41cf48872f3b5814dee87a9"), + "plugins/CustomVariables/Reports/GetCustomVariablesValuesFromNameId.php" => array("1418", "4a4e46d9a2549d18b6a72feedab5ef0f"), + "plugins/CustomVariables/templates/manage.twig" => array("296", "09c8229003b02a62cb9c14c41c824564"), + "plugins/CustomVariables/Tracker/CustomVariablesRequestProcessor.php" => array("3057", "b621698adf6732b195fba546ff4af3fd"), + "plugins/Dashboard/API.php" => array("3973", "587b704878e8973f5632beb7db171ae1"), + "plugins/Dashboard/Controller.php" => array("8130", "6dbf5481dded9237abf715bb0dbab790"), + "plugins/Dashboard/DashboardManagerControl.php" => array("1593", "fbb6289fb26fb97b30b3a9895aece757"), + "plugins/Dashboard/Dashboard.php" => array("9189", "80632159de539ed55d8677b6656ba727"), + "plugins/Dashboard/DashboardSettingsControlBase.php" => array("753", "343e3c684af7a911dc4bc83816daac2e"), + "plugins/Dashboard/javascripts/dashboard.js" => array("11253", "10786e31e1bcf0ed9c5087f1bb4a989c"), + "plugins/Dashboard/javascripts/dashboardObject.js" => array("20599", "18d4b9e2ae6c923e36152f03681f0735"), + "plugins/Dashboard/javascripts/dashboardWidget.js" => array("12743", "5f1101764f67bd1d5cb9f6438fe9ddf0"), + "plugins/Dashboard/javascripts/widgetMenu.js" => array("16628", "427ca716bb2a57ea68aa3098a24b804e"), + "plugins/Dashboard/lang/am.json" => array("569", "8fda94ca89590d74ce3fe3c53b475ce9"), + "plugins/Dashboard/lang/ar.json" => array("3653", "b47bf1563ed93ee1b962ba008cf8e9df"), + "plugins/Dashboard/lang/be.json" => array("773", "f70c775ab0902fd3d99db0036e48a143"), + "plugins/Dashboard/lang/bg.json" => array("3893", "dd4ee59c48a85cbad424a9a0c13cc25c"), + "plugins/Dashboard/lang/bn.json" => array("453", "52fabf97d2deae7f4cbcf88357a3a413"), + "plugins/Dashboard/lang/bs.json" => array("139", "d4095b9eb9e5ccc3673cafa8a39914be"), + "plugins/Dashboard/lang/ca.json" => array("2590", "7710c56dc9b1c8ad3552f91bf826805f"), + "plugins/Dashboard/lang/cs.json" => array("2571", "14a1fa96e2df6585f0e9304c74db2429"), + "plugins/Dashboard/lang/cy.json" => array("63", "590dac2bf4d5091e6d47949c7a3f1996"), + "plugins/Dashboard/lang/da.json" => array("2463", "a118aaf7370086eb5be775c6c33af381"), + "plugins/Dashboard/lang/de.json" => array("2786", "677a74004fa8947053bc1bc89898585c"), + "plugins/Dashboard/lang/el.json" => array("4790", "8aa424cb6115776fde2c837434767d57"), + "plugins/Dashboard/lang/en.json" => array("2568", "1c39519f4579b367c5a7d564c9613a62"), + "plugins/Dashboard/lang/es.json" => array("2757", "8e6fdf4a3c71785af5cc9220aaf67c12"), + "plugins/Dashboard/lang/et.json" => array("1368", "597108e065f10ca4f12b24ca1df1e10a"), + "plugins/Dashboard/lang/eu.json" => array("449", "16e72f790d19cf14874da74a7b921a40"), + "plugins/Dashboard/lang/fa.json" => array("2978", "fb5bd6881faf1d05244ac0949a615bc2"), + "plugins/Dashboard/lang/fi.json" => array("2159", "9a59c537746c503a7a5231466fa596de"), + "plugins/Dashboard/lang/fr.json" => array("3026", "d19e5200134c978edd9cdcb46c952aaa"), + "plugins/Dashboard/lang/gl.json" => array("530", "ca5f821c0b219814a9016cfff2a53755"), + "plugins/Dashboard/lang/he.json" => array("859", "417101b6e24b4db8041b241129fa20b0"), + "plugins/Dashboard/lang/hi.json" => array("4564", "c56e63d9f7c093b69c0e71170048acaf"), + "plugins/Dashboard/lang/hr.json" => array("696", "d33351c130cb6c1b01b7498893d52d4e"), + "plugins/Dashboard/lang/hu.json" => array("590", "0f2fadeda3d88c9e881e3bb2b7464d2d"), + "plugins/Dashboard/lang/id.json" => array("2382", "2e4a72dcd7ae06c097de2539ee3fcaeb"), + "plugins/Dashboard/lang/is.json" => array("531", "e04d5e1ca3ea6626335acc595fdea4bf"), + "plugins/Dashboard/lang/it.json" => array("2772", "05154604c3433fd518620f998865b4b6"), + "plugins/Dashboard/lang/ja.json" => array("3557", "4b4d050f138562eb2396aab65893caf5"), + "plugins/Dashboard/lang/ka.json" => array("980", "bd12b4d68fefde166adac26c59001f26"), + "plugins/Dashboard/lang/ko.json" => array("2863", "b79f37bf16fe47809fed19497535fccb"), + "plugins/Dashboard/lang/lt.json" => array("2636", "b382d040c2150565505aa45f47c09dd1"), + "plugins/Dashboard/lang/lv.json" => array("551", "9cbc3cd52cd5068990fe2f4cf623bafc"), + "plugins/Dashboard/lang/nb.json" => array("2612", "b1d8eb6e219e7d8c44b768c533501229"), + "plugins/Dashboard/lang/nl.json" => array("2622", "978c380a70eb0188e6c834daf3ac7fb0"), + "plugins/Dashboard/lang/nn.json" => array("2164", "1ca6c97912d271768dd8b10a9109c857"), + "plugins/Dashboard/lang/pl.json" => array("2676", "670f5f3beafbc6b427daf94229de8f97"), + "plugins/Dashboard/lang/pt-br.json" => array("2703", "32e3bee76402358a50d1471db021f69b"), + "plugins/Dashboard/lang/pt.json" => array("595", "86bc066076bf5de219c0b80b394c1f83"), + "plugins/Dashboard/lang/ro.json" => array("2712", "cc973c7880fc8053860d19d66c72e0a9"), + "plugins/Dashboard/lang/ru.json" => array("3799", "7cbd47bd8dff24137f6e85cf4c5389bd"), + "plugins/Dashboard/lang/sk.json" => array("2918", "cdf9cb016f339f9d09554ee54400bf0d"), + "plugins/Dashboard/lang/sl.json" => array("1077", "f2253a972e9badda762fe2767d64a6b7"), + "plugins/Dashboard/lang/sq.json" => array("934", "7309588d8513d0d6d853cc1c4f53a146"), + "plugins/Dashboard/lang/sr.json" => array("2449", "7c3955b50ef3ab3296cd197cd44a5642"), + "plugins/Dashboard/lang/sv.json" => array("2674", "11252d8c74a1eb3d42bb70bd022e44a8"), + "plugins/Dashboard/lang/ta.json" => array("4493", "408f09655ece3e36f391d74a895758c5"), + "plugins/Dashboard/lang/te.json" => array("304", "61a18d83af188af0f83f1056b154ce83"), + "plugins/Dashboard/lang/th.json" => array("2742", "4570b40f07aeec70ebd57fed1304c2a4"), + "plugins/Dashboard/lang/tl.json" => array("2551", "c0bf1de6c15a86585c2a09e8b54604a1"), + "plugins/Dashboard/lang/tr.json" => array("1732", "8c673f389d0d88973e55beeed1b4110b"), + "plugins/Dashboard/lang/uk.json" => array("688", "b068f8b9e5c1c108b20617eb5776b94e"), + "plugins/Dashboard/lang/vi.json" => array("3274", "82fd5f22cb38a3fa97c206aff6017529"), + "plugins/Dashboard/lang/zh-cn.json" => array("2210", "38e0418b0a1d226632c67e39707dca9d"), + "plugins/Dashboard/lang/zh-tw.json" => array("505", "5f3ede3ed060e4e8d05aa8549d5671f7"), + "plugins/Dashboard/Menu.php" => array("1879", "2408aae44c211dcb574bf369ebfa1a30"), + "plugins/Dashboard/Model.php" => array("7447", "b854ea335cbee3ace0dde1abe86c9a8e"), + "plugins/Dashboard/stylesheets/dashboard.less" => array("7443", "3f71ac78a0d7b429a304bc9cbcacdd9b"), + "plugins/Dashboard/stylesheets/standalone.css" => array("1012", "84e41d5c9b1a53e55b5b3a0a738a9c9b"), + "plugins/Dashboard/stylesheets/widget.less" => array("3191", "510062901125747e8406db34811ec17f"), + "plugins/Dashboard/templates/_dashboardSettings.twig" => array("918", "727fa6d1e866020e0e16283464845f27"), + "plugins/Dashboard/templates/embeddedIndex.twig" => array("5004", "43973e12ed01827e2720e20e448a4311"), + "plugins/Dashboard/templates/_header.twig" => array("741", "5528f135dd6df001c90b6a9c1580888d"), + "plugins/Dashboard/templates/index.twig" => array("842", "de7557260adbe304856f496800cd9035"), + "plugins/Dashboard/templates/_widgetFactoryTemplate.twig" => array("1251", "191e4b8ce42fd40f7b4ddba26bae5336"), + "plugins/DBStats/API.php" => array("9633", "8c80c4a6dfe0e153325bbcb5f779ed7d"), + "plugins/DBStats/config/test.php" => array("144", "7826aff77c3bde2340fa441f7ee285e1"), + "plugins/DBStats/Controller.php" => array("1675", "5563e410ae421343cf2f55367aa60b5b"), + "plugins/DBStats/DBStats.php" => array("1128", "28d02390b72286f0232141871c5d58b4"), "plugins/DBStats/lang/am.json" => array("544", "7baa1ff89068b48d9d30244ee87c00cd"), - "plugins/DBStats/lang/ar.json" => array("1131", "56ba42e94f87e55fe5350d50eb1be05f"), - "plugins/DBStats/lang/be.json" => array("1093", "d46871abcfc087252143e630b0658a21"), - "plugins/DBStats/lang/bg.json" => array("1470", "4529c23a8802c448433bee85ca847bbe"), + "plugins/DBStats/lang/ar.json" => array("965", "737b502e264481af4efca7e905dd64f5"), + "plugins/DBStats/lang/be.json" => array("939", "9cfcf1f97e230552f3920d099c7a466d"), + "plugins/DBStats/lang/bg.json" => array("1297", "2c67d3fe9f7efb063a72d15a6c343b90"), "plugins/DBStats/lang/bn.json" => array("61", "82e91890b454800e8d058c5f53f2e5d0"), "plugins/DBStats/lang/bs.json" => array("178", "f35b90a6e1ec759920d7b1e1eb536308"), - "plugins/DBStats/lang/ca.json" => array("1129", "25c425a101f8093240cbc5499c9343e5"), - "plugins/DBStats/lang/cs.json" => array("1008", "cffa24240285345e52e5ef6e9d1204bc"), - "plugins/DBStats/lang/da.json" => array("1025", "12d7024ec084a9be18e42dfbeade0690"), - "plugins/DBStats/lang/de.json" => array("1063", "db7842b901098a4f140678bdedebf630"), - "plugins/DBStats/lang/el.json" => array("1672", "dc03df11ce42ed0f466fccd2cf512cbe"), - "plugins/DBStats/lang/en.json" => array("971", "19ece5a2cac5a31c426fd8bb21435e89"), - "plugins/DBStats/lang/es.json" => array("1120", "68b351e3419418a1da6084016a5a05ac"), - "plugins/DBStats/lang/et.json" => array("1043", "f0cfc6c767c2659465ec0c0d909c2502"), + "plugins/DBStats/lang/ca.json" => array("1015", "82674b5b3223233a6b35e797f0f68707"), + "plugins/DBStats/lang/cs.json" => array("1091", "5a55cac31b3376e498314b1ffbb5f982"), + "plugins/DBStats/lang/cy.json" => array("50", "bba7e064e9daadf6065f4d35fac64b28"), + "plugins/DBStats/lang/da.json" => array("1058", "6b7b9614082a5daa5ef28d663c3303ea"), + "plugins/DBStats/lang/de.json" => array("1087", "3d9dff5fb6e1dd7df3cbad186ee5b659"), + "plugins/DBStats/lang/el.json" => array("1714", "d3425568a72c4b508dc1785c4e38f6f1"), + "plugins/DBStats/lang/en.json" => array("1003", "6086fed43fb11e3694a85e0e16ff0a9e"), + "plugins/DBStats/lang/es.json" => array("1194", "71640d9ec3cfea32d88f16174f1ef696"), + "plugins/DBStats/lang/et.json" => array("937", "c55b92ffa8d5ce46a01ae23494caa0b6"), "plugins/DBStats/lang/eu.json" => array("607", "ba6f32b3c0e57fb6ccaedc4ba30b64e2"), - "plugins/DBStats/lang/fa.json" => array("1512", "c2dae8fe3dfb7b109afa25ccdfedd69e"), - "plugins/DBStats/lang/fi.json" => array("971", "9fdf9bb616f21541583c6d7cbe2b98c6"), - "plugins/DBStats/lang/fr.json" => array("1183", "9cbacf1e08fdc1f5addf27e3ed1b723f"), - "plugins/DBStats/lang/he.json" => array("867", "dfa4fc8df95dd71cc99bc14ad034d229"), - "plugins/DBStats/lang/hi.json" => array("1844", "46f80ba8f44e7333f082f2f35e595b40"), - "plugins/DBStats/lang/hu.json" => array("780", "fcbd2ce72e8155f459fe5cb090f09a31"), - "plugins/DBStats/lang/id.json" => array("1034", "b9e8adfb0c460cd383755fb9f5a81987"), - "plugins/DBStats/lang/is.json" => array("480", "9610b7003f73148183d44ac3f6cc7ded"), - "plugins/DBStats/lang/it.json" => array("1080", "3b7b0b7c2eb09c2c57d748f11c2a78bc"), - "plugins/DBStats/lang/ja.json" => array("1206", "6a36a174f6cca7cc6d16e95f99f88814"), - "plugins/DBStats/lang/ka.json" => array("1486", "17d027bb3b07c7b4f6e84b138f32a668"), - "plugins/DBStats/lang/ko.json" => array("1082", "259dbf55c70e0f2dac918896e2a104fc"), - "plugins/DBStats/lang/lt.json" => array("683", "f2dd194a13234292af5c78d4f918e948"), - "plugins/DBStats/lang/lv.json" => array("667", "e22bc702ad6799f314090447d3890c4d"), - "plugins/DBStats/lang/nb.json" => array("770", "6e9614e188669135143894149cf6afb8"), - "plugins/DBStats/lang/nl.json" => array("1034", "b75930ea35d558b9b31239dabb98615b"), - "plugins/DBStats/lang/nn.json" => array("1010", "bc65370bea4018a4c4a89f471170316c"), - "plugins/DBStats/lang/pl.json" => array("741", "351e6de4ab5a458b65790e7f61374607"), - "plugins/DBStats/lang/pt-br.json" => array("1098", "35a2292924438fdabc003199c9ce0927"), - "plugins/DBStats/lang/pt.json" => array("707", "b9f87b78d97a52b9815fa071ec0f1fdd"), - "plugins/DBStats/lang/ro.json" => array("1071", "58ff2ea72c28ee743b598ff434ca1135"), - "plugins/DBStats/lang/ru.json" => array("1521", "56c5e96f586510b1c3d29fcd89f80cfb"), + "plugins/DBStats/lang/fa.json" => array("1348", "83cb60891f23e818fcfe71e6db93b29f"), + "plugins/DBStats/lang/fi.json" => array("887", "b13a0070435d0ea24c361e06dbf18c2d"), + "plugins/DBStats/lang/fr.json" => array("1222", "5877af515593d6d1d668eeea87124792"), + "plugins/DBStats/lang/gl.json" => array("52", "ae9b8e099bdaa500b4051109ab72d79d"), + "plugins/DBStats/lang/he.json" => array("733", "9d94b2dae5734eb530ee05bde0f10a70"), + "plugins/DBStats/lang/hi.json" => array("1667", "1ed4bdaace4b85b5eb6f09ba808495fa"), + "plugins/DBStats/lang/hr.json" => array("53", "6b70af91412e28cba7558a6243c6f4d5"), + "plugins/DBStats/lang/hu.json" => array("670", "8c48ebfabb03bc23e389d003f867f32f"), + "plugins/DBStats/lang/id.json" => array("935", "2c7ef06ad32b228d3cb7ff48ce2d7439"), + "plugins/DBStats/lang/is.json" => array("385", "e1c8fd764b7045f94812ad3014ef6af8"), + "plugins/DBStats/lang/it.json" => array("1125", "d5e85f4a203e7c03f771d7c654991694"), + "plugins/DBStats/lang/ja.json" => array("1270", "a8e12097fe570cf12affdae65af0e441"), + "plugins/DBStats/lang/ka.json" => array("1243", "e3c40a4b8ccc8b1be321151f6bfd1f87"), + "plugins/DBStats/lang/ko.json" => array("1148", "a03c74c6a9f4787f38f8b57ffb553acd"), + "plugins/DBStats/lang/lt.json" => array("590", "d19c84bea16e895ceae5aabb3010ed94"), + "plugins/DBStats/lang/lv.json" => array("562", "b60a85fe884b65fd929678ca9797908d"), + "plugins/DBStats/lang/nb.json" => array("1054", "33e73a392aa21354fe76dad80f77a732"), + "plugins/DBStats/lang/nl.json" => array("1079", "c96777f7e239ce09f2014a8309d181bb"), + "plugins/DBStats/lang/nn.json" => array("912", "e5a5d71fb0e94f2fdd9d1776b058692b"), + "plugins/DBStats/lang/pl.json" => array("805", "183190003eb3eb3a63ff768c5cc97698"), + "plugins/DBStats/lang/pt-br.json" => array("1143", "9741e57fa0247c08db3f621b8aeb6c63"), + "plugins/DBStats/lang/pt.json" => array("606", "ce2afc3cae44d03890fe27fc25384225"), + "plugins/DBStats/lang/ro.json" => array("971", "9a732c310cacb666b2c93708999f42c0"), + "plugins/DBStats/lang/ru.json" => array("1617", "d8d48d55ab1fae869484f2cdbb588eb4"), "plugins/DBStats/lang/sk.json" => array("486", "1df64facbdd26e49116c87e6629c71e6"), - "plugins/DBStats/lang/sl.json" => array("576", "e1e0d54c1bf14659683595826509e709"), - "plugins/DBStats/lang/sq.json" => array("796", "3d358248703abc04f0609902883fdbcb"), - "plugins/DBStats/lang/sr.json" => array("1013", "abb86e964d3e0fa91e301d6d2ea6d9fc"), - "plugins/DBStats/lang/sv.json" => array("1037", "b8bda69de93baab3d956a23d3e8a03bc"), - "plugins/DBStats/lang/ta.json" => array("993", "e1d3c2a837fa6be11f1d5668dc4d812a"), + "plugins/DBStats/lang/sl.json" => array("481", "5aa215465d66ce077bd65f5ef7ca58bb"), + "plugins/DBStats/lang/sq.json" => array("674", "11f76962822c4d2313a2c5468f81c8b4"), + "plugins/DBStats/lang/sr.json" => array("1057", "7854699be9c12f7a331c8b2302bfd4e8"), + "plugins/DBStats/lang/sv.json" => array("929", "8309dd3144ffc68f86b7f4bbbb451be1"), + "plugins/DBStats/lang/ta.json" => array("782", "7dd6f257f51b264bcd12114e6b10e28a"), "plugins/DBStats/lang/te.json" => array("125", "10cb7b33f9cab70f0cd8f12b752063df"), - "plugins/DBStats/lang/th.json" => array("1383", "efaa040a75b0b20ef860d27218d7a784"), - "plugins/DBStats/lang/tr.json" => array("651", "848ac6471a4e74f96d213e3488dae3dc"), - "plugins/DBStats/lang/uk.json" => array("978", "56d1978d76916aea7480b4414003ea41"), - "plugins/DBStats/lang/vi.json" => array("1245", "336115958d203a6e6b72beebc7278b4a"), - "plugins/DBStats/lang/zh-cn.json" => array("943", "ccfe32f44e5d3cdd8febf164e3a239f5"), - "plugins/DBStats/lang/zh-tw.json" => array("673", "760492ac770163bc1cf019e96ae1d464"), - "plugins/DBStats/MySQLMetadataDataAccess.php" => array("2279", "d6b533086e17fb9fe1eb988c4e030744"), - "plugins/DBStats/MySQLMetadataProvider.php" => array("12336", "7d8c570b43a07269a54627220ba473fe"), - "plugins/DBStats/stylesheets/dbStatsTable.less" => array("233", "4324953bbb513a4e0aa2d932d2447e5a"), - "plugins/DBStats/templates/index.twig" => array("4282", "3790e00ea5964ec59aa5b014b7a9d957"), - "plugins/DevicesDetection/API.php" => array("5930", "1d311bf37c78c7ffd3d6a55523f53e57"), - "plugins/DevicesDetection/Archiver.php" => array("2780", "9f07edb22c5f4ce1240d8d742193895a"), - "plugins/DevicesDetection/Controller.php" => array("5541", "ded6665d42e88f384cd5e45574600a52"), - "plugins/DevicesDetection/DevicesDetection.php" => array("14416", "3065fdd010d298d6ad65ef38625335ae"), - "plugins/DevicesDetection/functions.php" => array("6788", "f239b1d1cb96dfc6e4ff2ed2ef676d25"), + "plugins/DBStats/lang/th.json" => array("1208", "1e52679eb394bf99a84284f1682f0751"), + "plugins/DBStats/lang/tl.json" => array("987", "d6ef114efdbe7f7f0be28f6b4cf1b6fe"), + "plugins/DBStats/lang/tr.json" => array("942", "86cb2ca878337afe4fc40b797036bfdf"), + "plugins/DBStats/lang/uk.json" => array("828", "55846b40c9cb4b33c5da502a444ea7a2"), + "plugins/DBStats/lang/vi.json" => array("1125", "8037fc9f0c439929cb2e16ef177ffe57"), + "plugins/DBStats/lang/zh-cn.json" => array("971", "b07ef6f6b4f9466111ae0820a93f6166"), + "plugins/DBStats/lang/zh-tw.json" => array("582", "f57727910eda2738779d8cfa13a9858a"), + "plugins/DBStats/Menu.php" => array("588", "67c4359ea2cc7faa9db8540c2a483bee"), + "plugins/DBStats/MySQLMetadataDataAccess.php" => array("2282", "80e83f859973078b19e3166e65a255b7"), + "plugins/DBStats/MySQLMetadataProvider.php" => array("12173", "f3687a846082318498fa3b1cc5d47361"), + "plugins/DBStats/Reports/Base.php" => array("6496", "4325a630a1a81f17a06d067c87aa5b1b"), + "plugins/DBStats/Reports/GetAdminDataSummary.php" => array("946", "d2777cd8e5d18e15dec4fb0a78368ae9"), + "plugins/DBStats/Reports/GetDatabaseUsageSummary.php" => array("1800", "0556cb2074b07a5f6e8ff4934b273485"), + "plugins/DBStats/Reports/GetIndividualMetricsSummary.php" => array("1124", "384de4be5a012dc28fe4b076dffe0118"), + "plugins/DBStats/Reports/GetIndividualReportsSummary.php" => array("1343", "4089c68bbf6f10fae430a22e85b46e44"), + "plugins/DBStats/Reports/GetMetricDataSummaryByYear.php" => array("1064", "00d03f910c6a184da85f718b46726c5b"), + "plugins/DBStats/Reports/GetMetricDataSummary.php" => array("944", "3a95a92dd75f13cb51a26424cb6e2203"), + "plugins/DBStats/Reports/GetReportDataSummaryByYear.php" => array("1058", "30b3eecb987728f9220a34ebbcfcff8c"), + "plugins/DBStats/Reports/GetReportDataSummary.php" => array("940", "0956b5b81fda06202d1d663dd5aceeab"), + "plugins/DBStats/Reports/GetTrackerDataSummary.php" => array("793", "c955160d621c6ab4bb6c0259350bf1fc"), + "plugins/DBStats/stylesheets/dbStatsTable.less" => array("331", "3080ab1ed9301c09f0b054e44110ed22"), + "plugins/DBStats/Tasks.php" => array("894", "e592446eb19c1d799a6375db30aa228a"), + "plugins/DBStats/templates/index.twig" => array("4428", "d5baa4de9e77388c4210eb6756e3255f"), + "plugins/DevicePlugins/API.php" => array("3839", "7348c3db665ec3f5d5bc435e86eda7ef"), + "plugins/DevicePlugins/Archiver.php" => array("2916", "ecb9e7cd2e5195c5f86033de10a3852c"), + "plugins/DevicePlugins/Columns/PluginCookie.php" => array("790", "480751e55d0860e5a9cc775fbb0196d5"), + "plugins/DevicePlugins/Columns/PluginDirector.php" => array("791", "a14397e759f5582b7ccce50196d0f98e"), + "plugins/DevicePlugins/Columns/PluginFlash.php" => array("785", "6ffe1f748a2ff1c0e744b110b56ec5c4"), + "plugins/DevicePlugins/Columns/PluginGears.php" => array("787", "adeb752489b4a46a3f52e42ea253427b"), + "plugins/DevicePlugins/Columns/PluginJava.php" => array("784", "74a5c6dd03401b95140c167efb5adbc5"), + "plugins/DevicePlugins/Columns/PluginPdf.php" => array("781", "3dab1136c16f433e9687ee98cd6fd1ae"), + "plugins/DevicePlugins/Columns/Plugin.php" => array("378", "6e3528bce1d165e7c275601e98254dee"), + "plugins/DevicePlugins/Columns/PluginQuickTime.php" => array("792", "a824e47f3e23054dc21e7885d2c89ca5"), + "plugins/DevicePlugins/Columns/PluginRealPlayer.php" => array("797", "046bdf870b8feb4c8d7c68489ae836f9"), + "plugins/DevicePlugins/Columns/PluginSilverlight.php" => array("796", "4f2295a66b3e431af0e3f01611f40115"), + "plugins/DevicePlugins/Columns/PluginWindowsMedia.php" => array("799", "2cbf902a7e537f524a3eeaf5e3d4895b"), + "plugins/DevicePlugins/DevicePlugins.php" => array("1507", "75af59f89916addaddcda3901740dd8d"), + "plugins/DevicePlugins/functions.php" => array("405", "18554155487b074fb0612fdadd0b0425"), + "plugins/DevicePlugins/images/plugins/cookie.gif" => array("211", "9e564884defc036134b19ab38c192a6b"), + "plugins/DevicePlugins/images/plugins/director.gif" => array("198", "952c4a0e083ed089ca5c8ab2804c35ab"), + "plugins/DevicePlugins/images/plugins/flash.gif" => array("1018", "f315906f425f089dd4664ee530c691bd"), + "plugins/DevicePlugins/images/plugins/gears.gif" => array("558", "d4ec944ef01420637a709619f067953e"), + "plugins/DevicePlugins/images/plugins/java.gif" => array("565", "b4d24f76a64e2df762292e892e9215c4"), + "plugins/DevicePlugins/images/plugins/pdf.gif" => array("1021", "79b3d68c112942cefbb24dda9b421464"), + "plugins/DevicePlugins/images/plugins/quicktime.gif" => array("1003", "0feda8dc4ddec39e35b6f4925603c8bd"), + "plugins/DevicePlugins/images/plugins/realplayer.gif" => array("1025", "95739f527d29cab050a3d4eff35e93c7"), + "plugins/DevicePlugins/images/plugins/silverlight.gif" => array("1012", "8449c3a43f42c44bc5b82948d179b4b4"), + "plugins/DevicePlugins/images/plugins/windowsmedia.gif" => array("1026", "a374aec2d5488c5dc8d4eaf21ec59728"), + "plugins/DevicePlugins/lang/am.json" => array("91", "13c73a82d49097a743559c35c88757a8"), + "plugins/DevicePlugins/lang/ar.json" => array("87", "90af3c8900bac7aedeee737bed4754f2"), + "plugins/DevicePlugins/lang/be.json" => array("464", "f423a2c57eb19927d5140d8f7d531c83"), + "plugins/DevicePlugins/lang/bg.json" => array("610", "574fdf6d02381fc3dd726190312778fe"), + "plugins/DevicePlugins/lang/ca.json" => array("553", "6848044db393c04122a1095c5bc6968e"), + "plugins/DevicePlugins/lang/cs.json" => array("829", "b1f2e8fda4b16004cf947a2b20a5c94a"), + "plugins/DevicePlugins/lang/da.json" => array("553", "ea4be865c78556de1f8c8a7ef555fbc6"), + "plugins/DevicePlugins/lang/de.json" => array("794", "e82f68e8f829941b7f435d6fb0498f3a"), + "plugins/DevicePlugins/lang/el.json" => array("1253", "b13fe3329251d95d3748ffe905413774"), + "plugins/DevicePlugins/lang/en.json" => array("699", "ec1b88bd9006ad00cf54c7188a94abb7"), + "plugins/DevicePlugins/lang/es.json" => array("599", "0b23d7bed815c48569ff760f6a7b912e"), + "plugins/DevicePlugins/lang/et.json" => array("81", "50dac8fff45205d1f921c5ea84807eba"), + "plugins/DevicePlugins/lang/eu.json" => array("77", "f3d6446f8533008ee49e8f864ae80388"), + "plugins/DevicePlugins/lang/fa.json" => array("483", "c1319eebb2ae9b3108c0e9a279b2a259"), + "plugins/DevicePlugins/lang/fi.json" => array("480", "d4bd42192a2a19b15e341ff80e367fa8"), + "plugins/DevicePlugins/lang/fr.json" => array("542", "e81123754c8400e69eaae02a954480dc"), + "plugins/DevicePlugins/lang/gl.json" => array("76", "c1f13a2631996f19206bbdffddde400e"), + "plugins/DevicePlugins/lang/hi.json" => array("538", "aa619b15ec59dce6fea8a5f846b741ff"), + "plugins/DevicePlugins/lang/hu.json" => array("83", "c3d15c36328bcf679a6583924ed3b564"), + "plugins/DevicePlugins/lang/id.json" => array("447", "2d6d7af2d47ca23522e1d4f5b4ab8daa"), + "plugins/DevicePlugins/lang/is.json" => array("253", "8bb253981586aa8563a510cefb92db38"), + "plugins/DevicePlugins/lang/it.json" => array("548", "2622b9cc46f4dce43f01b72e37f23302"), + "plugins/DevicePlugins/lang/ja.json" => array("619", "217641502eb65ea24aee42123ac85140"), + "plugins/DevicePlugins/lang/ka.json" => array("433", "d4c4e327e6ad6ca1dce3e60d7f8f83a8"), + "plugins/DevicePlugins/lang/ko.json" => array("296", "c91dcbd4d09972db3df63881539d6220"), + "plugins/DevicePlugins/lang/lt.json" => array("396", "b385c28013b8f81aaadf140138cde505"), + "plugins/DevicePlugins/lang/lv.json" => array("317", "8048ee845aebca69775a772f3a2ea8ce"), + "plugins/DevicePlugins/lang/nb.json" => array("750", "7b8228cdbb42558a3759b4a14882052b"), + "plugins/DevicePlugins/lang/nl.json" => array("568", "745f9692b97161f5c224a914d7bc5f02"), + "plugins/DevicePlugins/lang/nn.json" => array("79", "81dd7db34c645570d9369c7f1b3601fe"), + "plugins/DevicePlugins/lang/pl.json" => array("73", "2398ee68f1b7d7d16bd78e56190c3a86"), + "plugins/DevicePlugins/lang/pt-br.json" => array("762", "364e6646903e6b51493b423c5798035e"), + "plugins/DevicePlugins/lang/pt.json" => array("303", "6d48a674821b2f28745188efdde38cfb"), + "plugins/DevicePlugins/lang/ro.json" => array("430", "d132279ab303ba95a56cb8605fa9e69e"), + "plugins/DevicePlugins/lang/ru.json" => array("709", "41472af929b11fabb3df0e3aa2ae93e9"), + "plugins/DevicePlugins/lang/sk.json" => array("74", "22b0b24c96de66872ada8899b1ae20d4"), + "plugins/DevicePlugins/lang/sl.json" => array("227", "44e0644a2e7b16952e0e4a2242f18ad9"), + "plugins/DevicePlugins/lang/sq.json" => array("324", "d28ae0652df5b532a9be89d56ac06c0e"), + "plugins/DevicePlugins/lang/sr.json" => array("552", "2b3fdc046d6eaa27dbebe5074ec248c7"), + "plugins/DevicePlugins/lang/sv.json" => array("541", "3888cf446ec5c2bf367a73fc3a7d6f14"), + "plugins/DevicePlugins/lang/th.json" => array("102", "d30a83ace0cb49601d12faf8f49f661b"), + "plugins/DevicePlugins/lang/tl.json" => array("463", "5e7b3a76410d1ae1a521e6b221f5c9fd"), + "plugins/DevicePlugins/lang/tr.json" => array("75", "4fd28baa526123ccbac87870b8188773"), + "plugins/DevicePlugins/lang/uk.json" => array("89", "dfd614e1374291d15810dbfa0421cad4"), + "plugins/DevicePlugins/lang/vi.json" => array("527", "c5a7b2ca50e35c3938d5cb5ba22eed54"), + "plugins/DevicePlugins/lang/zh-cn.json" => array("347", "c30cc7f1915e9c1a19ad273e8a641f26"), + "plugins/DevicePlugins/lang/zh-tw.json" => array("81", "dd0f98015cdf0344850d9fa0767b14c6"), + "plugins/DevicePlugins/Reports/Base.php" => array("806", "d479d4459d45aec928716b24696741da"), + "plugins/DevicePlugins/Reports/GetPlugin.php" => array("1939", "8f2b5eb8c77d6462f0e941a76c8e8f4b"), + "plugins/DevicePlugins/Visitor.php" => array("1656", "dfb44c94e04757634a6079c972453e52"), + "plugins/DevicesDetection/API.php" => array("11812", "46a024dd920f1226fdb8aefbb98049b5"), + "plugins/DevicesDetection/Archiver.php" => array("3405", "189bff83fcc7391aa27cab39cca85cfe"), + "plugins/DevicesDetection/Columns/Base.php" => array("453", "282dac10b6d93a507f7772e8fc54ca73"), + "plugins/DevicesDetection/Columns/BrowserEngine.php" => array("1527", "d08c8ba55395037df8b2f4be36764d89"), + "plugins/DevicesDetection/Columns/BrowserName.php" => array("1398", "27f30db80f33553f44cc409a79ccf131"), + "plugins/DevicesDetection/Columns/BrowserVersion.php" => array("1390", "82b6b9bba8f317b3764e6bce6d46b986"), + "plugins/DevicesDetection/Columns/DeviceBrand.php" => array("1874", "994f830bc9ae55104e61ef5e9d7c6131"), + "plugins/DevicesDetection/Columns/DeviceModel.php" => array("965", "f3c4d68f8edb0baed0f713709b6bbf8e"), + "plugins/DevicesDetection/Columns/DeviceType.php" => array("1861", "65e7342923ed7957b4983541aafbf8c7"), + "plugins/DevicesDetection/Columns/Os.php" => array("1476", "6303b811b267798c175cb4c5fa5a9dfa"), + "plugins/DevicesDetection/Columns/OsVersion.php" => array("1343", "0b2851ccf888d98cb88484c0dcad357b"), + "plugins/DevicesDetection/Controller.php" => array("6035", "87f22243ea808fbf20b4231dfa0e4fc8"), + "plugins/DevicesDetection/DevicesDetection.php" => array("2688", "a53c76dd95e66f43d76d8e53254b9299"), + "plugins/DevicesDetection/functions.php" => array("10700", "552e40994e88c324ee825bcf0eecff0c"), + "plugins/DevicesDetection/images/brand/3Q.ico" => array("577", "4e6805d57df1ebf020adb31c6ea11453"), "plugins/DevicesDetection/images/brand/Acer.ico" => array("673", "5453e4cc0e9fddd4aac446d0b10d4e36"), "plugins/DevicesDetection/images/brand/Alcatel.ico" => array("577", "df5ccd4326721199d02870aab0e225c3"), "plugins/DevicesDetection/images/brand/Apple.ico" => array("1179", "3b58ada0634f0a1687a44c15e4e1936f"), @@ -1198,22 +2554,39 @@ class Manifest { "plugins/DevicesDetection/images/brand/Audiovox.ico" => array("807", "5e684babc5c21bf7a8255a0ad0f9545a"), "plugins/DevicesDetection/images/brand/Avvio.ico" => array("964", "c4a27544c3af4093265a0cbb45cd5b31"), "plugins/DevicesDetection/images/brand/BangOlufsen.ico" => array("3692", "e062a24b4ed0af1fe513053eb2e70750"), + "plugins/DevicesDetection/images/brand/Barnes_Noble.ico" => array("799", "5cbde4b9ca8d45608511ef0a902e10bd"), + "plugins/DevicesDetection/images/brand/BBK.ico" => array("263", "a69543b93cb366344658a8ba1ba86309"), "plugins/DevicesDetection/images/brand/Becker.ico" => array("519", "6c5ed512b9de01a0d60d4efcc78b1ea0"), "plugins/DevicesDetection/images/brand/Beetel.ico" => array("1645", "034453672fc6b71827ea0126248843b8"), "plugins/DevicesDetection/images/brand/BenQ.ico" => array("846", "70f0361f99a39dcdde7e1731364e19c9"), + "plugins/DevicesDetection/images/brand/bq.ico" => array("497", "1c556a4caeeb525cd7cedfb1ef6836c4"), "plugins/DevicesDetection/images/brand/Cat.ico" => array("809", "2b5844049936b7e33220ba80accfabfb"), + "plugins/DevicesDetection/images/brand/Celkon.ico" => array("332", "57c1d3ba5d44d33fec13e0a070ecaaa5"), + "plugins/DevicesDetection/images/brand/Cherry_Mobile.ico" => array("808", "6970cd108eaae433cc3ba28efd420fb5"), "plugins/DevicesDetection/images/brand/CnM.ico" => array("421", "6a3a2122bcab660df2d010087914b7b7"), "plugins/DevicesDetection/images/brand/Compal.ico" => array("432", "a7190dfd9e3b833a17bd9576b2fc47a0"), + "plugins/DevicesDetection/images/brand/Compaq.ico" => array("453", "f1ef4d435d8255a9304a2685fd61afb1"), + "plugins/DevicesDetection/images/brand/ConCorde.ico" => array("602", "a5875a21f0de5cd257b63dec7cd891fa"), + "plugins/DevicesDetection/images/brand/Coolpad.ico" => array("485", "8820e42152f8d2e2d2dfda8343bb6944"), "plugins/DevicesDetection/images/brand/CreNova.ico" => array("3142", "b34cd9a39800b0e0109ab291368e3177"), "plugins/DevicesDetection/images/brand/Cricket.ico" => array("1483", "6b3f14fc16f8b9e61cbf4eaf4af9efbf"), + "plugins/DevicesDetection/images/brand/Crius_Mea.ico" => array("566", "105e726178c68022b7787b56be36cdd9"), + "plugins/DevicesDetection/images/brand/Crosscall.ico" => array("3236", "1bf22b8590c00df8c6c7587126b5079b"), + "plugins/DevicesDetection/images/brand/Danew.ico" => array("3221", "45a67aca345dff2b0fb805b19646f780"), "plugins/DevicesDetection/images/brand/Dell.ico" => array("886", "10a5c1a530dc6a0758dec93fea8a08ff"), "plugins/DevicesDetection/images/brand/Denver.ico" => array("552", "6efa83cf0f661e769272515970bcbc97"), "plugins/DevicesDetection/images/brand/DMM.ico" => array("3623", "3a154f04b274d2e67ac9298ca2df08fd"), "plugins/DevicesDetection/images/brand/DoCoMo.ico" => array("636", "28df0a65fbcb99b0e2ca93c6cdcc8e38"), + "plugins/DevicesDetection/images/brand/Easypix.ico" => array("881", "e206cd5a0003d2a5317f30820090098c"), "plugins/DevicesDetection/images/brand/Ericsson.ico" => array("684", "de92f235c5d09fa7b1140538058c8bcf"), "plugins/DevicesDetection/images/brand/eTouch.ico" => array("889", "779c7c6654749cffea8363ed1c6ee971"), + "plugins/DevicesDetection/images/brand/Evertek.ico" => array("571", "afd0b26b99880536bf2f2b4a108a4053"), "plugins/DevicesDetection/images/brand/Fly.ico" => array("572", "d608aa4b0b9a861c77501c744aa4c275"), + "plugins/DevicesDetection/images/brand/Fujitsu.ico" => array("298", "3f19cc02a2c34b3907bb6b05be210809"), "plugins/DevicesDetection/images/brand/Gemini.ico" => array("323", "07def536ff813416b91b196660b1f4a2"), + "plugins/DevicesDetection/images/brand/Gigabyte.ico" => array("343", "35554b850966ef76fcac3e4c51f67a0c"), + "plugins/DevicesDetection/images/brand/Gigaset.ico" => array("354", "5bbcaaa121cc02d14454374ea84f2bbf"), + "plugins/DevicesDetection/images/brand/Gionee.ico" => array("3018", "9ccf5a406b6343afaa198bdda3694437"), "plugins/DevicesDetection/images/brand/Google.ico" => array("863", "ad67a48c2dc917325f9cb38a88f8a8a3"), "plugins/DevicesDetection/images/brand/Gradiente.ico" => array("1012", "25ed337d67bf8d6fdb5012b331aadd05"), "plugins/DevicesDetection/images/brand/Grundig.ico" => array("3029", "afae28b8f05bae1e8c476fc3cdb6b5ab"), @@ -1222,6 +2595,8 @@ class Manifest { "plugins/DevicesDetection/images/brand/HTC.ico" => array("1161", "b0b6419c96392cbaa646129a9f609f51"), "plugins/DevicesDetection/images/brand/Huawei.ico" => array("1022", "cf2cca917f1b7655f69ebf439d464823"), "plugins/DevicesDetection/images/brand/Humax.ico" => array("3010", "cb8aee651c6434c0ff6630e609808db9"), + "plugins/DevicesDetection/images/brand/Hyundai.ico" => array("407", "4e16da1cebbc4ad229b3a6f564040021"), + "plugins/DevicesDetection/images/brand/iBerry.ico" => array("3613", "edf0a22ec58c58a6f55adbfc19c84139"), "plugins/DevicesDetection/images/brand/Ikea.ico" => array("3291", "56fc079b565f603be4bea0faa90dfb71"), "plugins/DevicesDetection/images/brand/i-mobile.ico" => array("615", "15a5cf09f8e2ad6d60956ae07086b50c"), "plugins/DevicesDetection/images/brand/INQ.ico" => array("1059", "7ece6e5474c9ab6fdcda278c7e341b81"), @@ -1234,6 +2609,7 @@ class Manifest { "plugins/DevicesDetection/images/brand/Kyocera.ico" => array("639", "ca13b59af47f69c5ac6917cef9512add"), "plugins/DevicesDetection/images/brand/Lanix.ico" => array("437", "fb1e54c478ce9d908a08e4a0b1005c5c"), "plugins/DevicesDetection/images/brand/Lenovo.ico" => array("237", "42abac25970f9750ec395cefffa30b9d"), + "plugins/DevicesDetection/images/brand/Le_Pan.ico" => array("408", "f1b045262e5c504b3f5ecda27c748304"), "plugins/DevicesDetection/images/brand/LG.ico" => array("1510", "79a2c6d36a1ba70423162b4c1fe46ec2"), "plugins/DevicesDetection/images/brand/LGUPlus.ico" => array("1081", "337b3f1e7368a44b7c40dd8e53940445"), "plugins/DevicesDetection/images/brand/Loewe.ico" => array("2938", "e5ec4483d155267a2944dadad9d9235e"), @@ -1246,14 +2622,17 @@ class Manifest { "plugins/DevicesDetection/images/brand/Mio.ico" => array("753", "9f53b557757e285f1de1fe7b67434406"), "plugins/DevicesDetection/images/brand/Mitsubishi.ico" => array("342", "a0d050b3c27c242d11a1a58f30e58ff9"), "plugins/DevicesDetection/images/brand/Motorola.ico" => array("465", "0561bdea0b5842d6c9bc0301bde19b0f"), + "plugins/DevicesDetection/images/brand/MSI.ico" => array("377", "4e086d7fec17fc4ff0bd721744d109b1"), "plugins/DevicesDetection/images/brand/MyPhone.ico" => array("933", "84cd21bc7363dda6be3a1181be22de6b"), "plugins/DevicesDetection/images/brand/NEC.ico" => array("450", "a3ae0709656decaaf958595aed950f4d"), "plugins/DevicesDetection/images/brand/Nexian.ico" => array("2041", "4001957b806ce17dcaca59b9b852be8a"), "plugins/DevicesDetection/images/brand/NGM.ico" => array("1298", "3cbe7ef2372c4074ee2734aa3a58b328"), + "plugins/DevicesDetection/images/brand/Nikon.ico" => array("607", "546e5c33c7901edd71435d4ff8d14662"), "plugins/DevicesDetection/images/brand/Nintendo.ico" => array("740", "7bdf9ff565d9cc45a08824a6bade6f47"), "plugins/DevicesDetection/images/brand/Nokia.ico" => array("1283", "ae41cc8ec06c6e81d9f5afadf6097f5b"), "plugins/DevicesDetection/images/brand/O2.ico" => array("768", "36b1b15cc750a45b5c2b6edc80594bbd"), "plugins/DevicesDetection/images/brand/Onda.ico" => array("732", "2a8e1c31d12c2cf27c1931a13fd178df"), + "plugins/DevicesDetection/images/brand/OnePlus.ico" => array("181", "307796de7745773249cad3962d6ce6ed"), "plugins/DevicesDetection/images/brand/OPPO.ico" => array("870", "4128f2c75414aa765adc5866bb627635"), "plugins/DevicesDetection/images/brand/Orange.ico" => array("461", "21a6df281c10a240cbc134f79ea0d6eb"), "plugins/DevicesDetection/images/brand/Panasonic.ico" => array("3649", "71da2a84a1fdf04abd130562699a9ab4"), @@ -1262,14 +2641,18 @@ class Manifest { "plugins/DevicesDetection/images/brand/Philips.ico" => array("3749", "2213ce1a4b99c4af0a53dff146aca457"), "plugins/DevicesDetection/images/brand/Polaroid.ico" => array("737", "c6703ba6a2d008aa2197df33370e124c"), "plugins/DevicesDetection/images/brand/PolyPad.ico" => array("1381", "35f641a4463636392646a476a4ffbfbd"), + "plugins/DevicesDetection/images/brand/Quechua.ico" => array("296", "f20d9d68e393691e8974cd3c6b9cf78d"), "plugins/DevicesDetection/images/brand/RIM.ico" => array("705", "e5660baaa9484cddcd779e16672d48ce"), "plugins/DevicesDetection/images/brand/Sagem.ico" => array("694", "6d0664111d655a222cf7ec86cebd7750"), "plugins/DevicesDetection/images/brand/Samsung.ico" => array("3095", "988bb1039ac4b49d767dcfbdff86172a"), "plugins/DevicesDetection/images/brand/Sanyo.ico" => array("639", "ca13b59af47f69c5ac6917cef9512add"), "plugins/DevicesDetection/images/brand/Sega.ico" => array("706", "d73c2098167d02eb81c7b83cfe5787a2"), "plugins/DevicesDetection/images/brand/Selevision.ico" => array("3497", "c920706882f4d553619e275f0d336423"), + "plugins/DevicesDetection/images/brand/Sencor.ico" => array("885", "f57169b5881e9b166c4acc761da2f503"), + "plugins/DevicesDetection/images/brand/SFR.ico" => array("686", "7de3efe1dee578907ffc1b653653be3f"), "plugins/DevicesDetection/images/brand/Sharp.ico" => array("403", "ce78aee244f948bba92bfbf91397448b"), "plugins/DevicesDetection/images/brand/Siemens.ico" => array("395", "a82f9c7c51665f04cad1a2a4a69bc590"), + "plugins/DevicesDetection/images/brand/Smartfren.ico" => array("691", "6df4c52756c13a8408062e6198f37d81"), "plugins/DevicesDetection/images/brand/Smart.ico" => array("3419", "04bc938b9b436ac140922f399767fd9f"), "plugins/DevicesDetection/images/brand/Softbank.ico" => array("381", "8c42b583b4e1b05900b06698a65678c5"), "plugins/DevicesDetection/images/brand/Sony_Ericsson.ico" => array("628", "7c8230f2d8dc34c6c5761aa9db204588"), @@ -1278,23 +2661,213 @@ class Manifest { "plugins/DevicesDetection/images/brand/TCL.ico" => array("2927", "207121f24b1b59c9e3e2ae43ad420f97"), "plugins/DevicesDetection/images/brand/TechniSat.ico" => array("3347", "08762844360b5220aeb01e23dd6ea410"), "plugins/DevicesDetection/images/brand/TechnoTrend.ico" => array("3500", "65748765610b09c759b739f1a97dc8ca"), + "plugins/DevicesDetection/images/brand/Tecno_Mobile.ico" => array("437", "7f512b7ce5211a9961df2360ef8078b1"), "plugins/DevicesDetection/images/brand/Telefunken.ico" => array("3651", "677bdb0ce07503bf11c437896148332d"), "plugins/DevicesDetection/images/brand/Telit.ico" => array("527", "419e9ae7ac193023e1b0082fd96bc5df"), + "plugins/DevicesDetection/images/brand/teXet.ico" => array("643", "cdaebbdf23e79a6f985d9778c6d7fa85"), "plugins/DevicesDetection/images/brand/Thomson.ico" => array("2974", "bff62a739264a6a81f8447a1aaf1d955"), "plugins/DevicesDetection/images/brand/TiPhone.ico" => array("1179", "d5e6eeafbbde1409d878c6b17a6bba9e"), "plugins/DevicesDetection/images/brand/T-Mobile.ico" => array("499", "d7869b7ce833ed1d9db2f85dfb0140b5"), + "plugins/DevicesDetection/images/brand/Tolino.ico" => array("321", "6b571e7487c4324191d96b7c00135612"), "plugins/DevicesDetection/images/brand/Toshiba.ico" => array("248", "f96539816d5b1d433bd5e8c4f1222970"), - "plugins/DevicesDetection/images/brand/unknown.ico" => array("1077", "32104fb21cf3f82b68d30bee64972ea7"), + "plugins/DevicesDetection/images/brand/Tunisie_Telecom.ico" => array("3463", "a4d1987b111a99c5f2099135c65ecfc4"), + "plugins/DevicesDetection/images/brand/Unknown.ico" => array("1077", "32104fb21cf3f82b68d30bee64972ea7"), "plugins/DevicesDetection/images/brand/Vertu.ico" => array("387", "f87d83b0a636bb325aefddcf436d50c4"), "plugins/DevicesDetection/images/brand/Vestel.ico" => array("3096", "33dcd35a24a21eb0f660b26fcc41bca1"), "plugins/DevicesDetection/images/brand/Videocon.ico" => array("617", "c9948a26325d7e73f4eb287e1475cb82"), "plugins/DevicesDetection/images/brand/Videoweb.ico" => array("3129", "ca8e2df12c63f4799f69f74e141319e2"), "plugins/DevicesDetection/images/brand/ViewSonic.ico" => array("605", "537cb66bf7d13d8be07efefc178af164"), "plugins/DevicesDetection/images/brand/Voxtel.ico" => array("222", "93267b0d0b38e7b47f2dccb7bd791171"), + "plugins/DevicesDetection/images/brand/Wiko.ico" => array("1558", "f62fced5dd30dca4f465dbea1e15e93c"), + "plugins/DevicesDetection/images/brand/Wolder.ico" => array("513", "2d12bf1e01f70fe84b18390490c05eef"), + "plugins/DevicesDetection/images/brand/Woxter.ico" => array("775", "2e2c29c9842568b24f30d770af122897"), "plugins/DevicesDetection/images/brand/Xiaomi.ico" => array("492", "fe0ef8c5a55aeb5e33067b051ca87fd7"), + "plugins/DevicesDetection/images/brand/Yarvik.ico" => array("439", "8976fb2d78300f08607decd40ec70d87"), "plugins/DevicesDetection/images/brand/Yuandao.ico" => array("639", "866dddeae2afafd1a86a28b3621962f3"), "plugins/DevicesDetection/images/brand/Zonda.ico" => array("371", "43bfb6a3cfca4edd0bf27bbf21e5baed"), + "plugins/DevicesDetection/images/brand/Zopo.ico" => array("397", "73f2422b25150e81c3de33740061f250"), "plugins/DevicesDetection/images/brand/ZTE.ico" => array("555", "1f124412f72d709497dd3f4ad50b0761"), + "plugins/DevicesDetection/images/browsers/36.gif" => array("1036", "2f34abe9fe98903bc202d3e8aeca8578"), + "plugins/DevicesDetection/images/browsers/AA.gif" => array("1092", "dc62eff78dac919b00e60c5fb3e6266d"), + "plugins/DevicesDetection/images/browsers/AB.gif" => array("1064", "1123da862a558b1ccd8f9008c5c4fdcb"), + "plugins/DevicesDetection/images/browsers/AG.gif" => array("351", "6e793ad6ad5c69abc499422d6a43d836"), + "plugins/DevicesDetection/images/browsers/AM.gif" => array("198", "18c54fc3197f6e1533c06b0923db3bd0"), + "plugins/DevicesDetection/images/browsers/AN.gif" => array("144", "a1264f47256ff5b0d4feeeac833e7a96"), + "plugins/DevicesDetection/images/browsers/AR.gif" => array("1057", "377249d199156ee602e669c0afc02945"), + "plugins/DevicesDetection/images/browsers/AV.gif" => array("151", "f459c84d6ab90fdf0f35d20f9f82626d"), + "plugins/DevicesDetection/images/browsers/AW.gif" => array("574", "e4e4fa2ef432f2a86086ec58f4b27ab1"), + "plugins/DevicesDetection/images/browsers/B2.gif" => array("1070", "e1b9f6b47cffbdf05b4159d6a31b21ae"), + "plugins/DevicesDetection/images/browsers/BB.gif" => array("576", "d40bd99ba1dd881b164907341ec2079d"), + "plugins/DevicesDetection/images/browsers/BD.gif" => array("1051", "4a8ebfcd7aad4c1004b7f82b954b0a69"), + "plugins/DevicesDetection/images/browsers/BE.gif" => array("1042", "103994d17de92aa261c8034a8e35a84f"), + "plugins/DevicesDetection/images/browsers/BJ.gif" => array("949", "4ddb1a5385c2954489f2cbc7dc73c8a2"), + "plugins/DevicesDetection/images/browsers/BP.gif" => array("1070", "e1b9f6b47cffbdf05b4159d6a31b21ae"), + "plugins/DevicesDetection/images/browsers/BS.gif" => array("980", "36f1831f70f7c8efc44d534c6adb63a5"), + "plugins/DevicesDetection/images/browsers/BX.gif" => array("522", "7302ad862f4007c23efb73acbd41f5c0"), + "plugins/DevicesDetection/images/browsers/CA.gif" => array("573", "739fca054b61f68657b0bd349c958a86"), + "plugins/DevicesDetection/images/browsers/CC.gif" => array("435", "adfa238c50cfb4f8c1c64b6affbf238e"), + "plugins/DevicesDetection/images/browsers/CD.gif" => array("1045", "b5484f5fc254abd52cdc94f378be977a"), + "plugins/DevicesDetection/images/browsers/CF.gif" => array("1074", "e349a7dbdadf2fca33cca13287b0eba8"), + "plugins/DevicesDetection/images/browsers/CH.gif" => array("1074", "e349a7dbdadf2fca33cca13287b0eba8"), + "plugins/DevicesDetection/images/browsers/CK.gif" => array("1024", "b6d4ebb0394c48dfcb5f21475de5c79b"), + "plugins/DevicesDetection/images/browsers/CM.gif" => array("1074", "e349a7dbdadf2fca33cca13287b0eba8"), + "plugins/DevicesDetection/images/browsers/CN.gif" => array("998", "cd878afb8cc56e7c8c6caef6d5fb2ba4"), + "plugins/DevicesDetection/images/browsers/CO.gif" => array("1042", "195487c9db2e19ffae3fd443b9266e62"), + "plugins/DevicesDetection/images/browsers/CP.gif" => array("998", "af47e47253591c272a458c4645fbaf5c"), + "plugins/DevicesDetection/images/browsers/CR.gif" => array("1007", "dd242a922d2430d211a75a1a2973e2bb"), + "plugins/DevicesDetection/images/browsers/CS.gif" => array("549", "eb5151f2f46fc09d687ce27becefb831"), + "plugins/DevicesDetection/images/browsers/CX.gif" => array("1067", "a5f9bb2739b6b66ea01568a4002f6f45"), + "plugins/DevicesDetection/images/browsers/DE.gif" => array("1073", "ee3f8b86d881b9ef130e08d90e6bcdfd"), + "plugins/DevicesDetection/images/browsers/DF.gif" => array("545", "f4b65ebcf304f1675088d029ed613d28"), + "plugins/DevicesDetection/images/browsers/DI.gif" => array("1068", "4eceaf5fd7808c422b67026c9329e16f"), + "plugins/DevicesDetection/images/browsers/EL.gif" => array("90", "b515db820d883f921d05d15a34dda7f9"), + "plugins/DevicesDetection/images/browsers/EP.gif" => array("316", "660436cc97429ef52365a01084b40ee0"), + "plugins/DevicesDetection/images/browsers/ES.gif" => array("1013", "92ada780fce5e3ffc318de634eba4d21"), + "plugins/DevicesDetection/images/browsers/FB.gif" => array("254", "24663f1949bae4fb7ecd0504c9c872fd"), + "plugins/DevicesDetection/images/browsers/FD.gif" => array("1050", "a569dc9dbe1b95ce2763bee8dbdc5850"), + "plugins/DevicesDetection/images/browsers/FE.gif" => array("550", "a016a52d476c4be7943e68ccf2056db1"), + "plugins/DevicesDetection/images/browsers/FF.gif" => array("197", "6b4eecf673d5461aebdd432bca086e8c"), + "plugins/DevicesDetection/images/browsers/FL.gif" => array("1034", "0ce889dc81db377eeb00f76f72942274"), + "plugins/DevicesDetection/images/browsers/FN.gif" => array("1033", "e87473b14680939e58a9a94e9dcd39e3"), + "plugins/DevicesDetection/images/browsers/GA.gif" => array("159", "576c4646cf6938dd2079516293d3a3f9"), + "plugins/DevicesDetection/images/browsers/GE.gif" => array("997", "a3d96e8576f273ecc4a864d24620fbf2"), + "plugins/DevicesDetection/images/browsers/HA.gif" => array("1009", "fccae707e311bd009be90a7b38000082"), + "plugins/DevicesDetection/images/browsers/HJ.gif" => array("1022", "8c3019d1e0867e8455d0abf1ad3eb531"), + "plugins/DevicesDetection/images/browsers/IA.gif" => array("391", "1836a7db0bc6a4a4d8b3d6063346e3f5"), + "plugins/DevicesDetection/images/browsers/IB.gif" => array("168", "b091c3e8ce2789017d581089028fa1cc"), + "plugins/DevicesDetection/images/browsers/IC.gif" => array("131", "26a6ff98d316092214c9dc9e7be45224"), + "plugins/DevicesDetection/images/browsers/ID.gif" => array("1057", "6f1f33dc4bd104e0a60000ce49e719a9"), + "plugins/DevicesDetection/images/browsers/IE.gif" => array("999", "5e002ee72167a3a78e2252766fde1046"), + "plugins/DevicesDetection/images/browsers/IM.gif" => array("999", "5e002ee72167a3a78e2252766fde1046"), + "plugins/DevicesDetection/images/browsers/IR.gif" => array("610", "565b1e3acd514c1c9b88d6c2b0ec0c4d"), + "plugins/DevicesDetection/images/browsers/IW.gif" => array("1066", "8d3376b6699ccd4533191b38d7f6b8c4"), + "plugins/DevicesDetection/images/browsers/KI.gif" => array("1050", "497b4bc9b58c113ae72763559321071b"), + "plugins/DevicesDetection/images/browsers/KM.gif" => array("180", "3daa5fa7553d448cd280612491e8a6ea"), + "plugins/DevicesDetection/images/browsers/KO.gif" => array("986", "0d15a2d4a73582d1df109e0ff3463fd3"), + "plugins/DevicesDetection/images/browsers/KP.gif" => array("1037", "f29fb41537f7df91d827c3f6eea076a0"), + "plugins/DevicesDetection/images/browsers/KZ.gif" => array("1061", "cdc654ad5f3f5fdda6ac69ea7d7dbe31"), + "plugins/DevicesDetection/images/browsers/LB.gif" => array("991", "ff5c4fad260f28ae39bdcac81c2a3309"), + "plugins/DevicesDetection/images/browsers/LG.gif" => array("1015", "6b282f2f040962adb07446579435f777"), + "plugins/DevicesDetection/images/browsers/LI.gif" => array("104", "a3b302b3fffc9ed032add149d4ace210"), + "plugins/DevicesDetection/images/browsers/LS.gif" => array("1086", "c61646736ea4872a7e58bcd29716d778"), + "plugins/DevicesDetection/images/browsers/LX.gif" => array("104", "a3b302b3fffc9ed032add149d4ace210"), + "plugins/DevicesDetection/images/browsers/MC.gif" => array("1023", "0e17db9ed1e06feb1d23d134ce34693d"), + "plugins/DevicesDetection/images/browsers/MF.gif" => array("190", "589361249f74319b57ea98d6408bc4b3"), + "plugins/DevicesDetection/images/browsers/MI.gif" => array("1025", "5e63fceac90a88f1db14b4e8ee44201d"), + "plugins/DevicesDetection/images/browsers/MO.gif" => array("192", "67b5dac21e8f2243a955f1d9df7ef67e"), + "plugins/DevicesDetection/images/browsers/MS.gif" => array("1094", "265861a05c27b23013cb6ae3c428dff0"), + "plugins/DevicesDetection/images/browsers/MU.gif" => array("1031", "a32232145133bfeb871f8075dc45192a"), + "plugins/DevicesDetection/images/browsers/MX.gif" => array("985", "4f6f87c42bf5c6bfc2b63925da5e40c1"), + "plugins/DevicesDetection/images/browsers/NB.gif" => array("977", "d2fac7549889df9f1c0863b424543c6f"), + "plugins/DevicesDetection/images/browsers/NF.gif" => array("612", "7cb0d2713e9faf25b766ca0d13cf456b"), + "plugins/DevicesDetection/images/browsers/NL.gif" => array("1081", "f66412328676120ba3cc0eb987c16158"), + "plugins/DevicesDetection/images/browsers/NP.gif" => array("1020", "ba7d68a0f9c11647abba2f8454a7c34c"), + "plugins/DevicesDetection/images/browsers/NS.gif" => array("98", "cd8d53ec12b64294d16769dfeeaf07c7"), + "plugins/DevicesDetection/images/browsers/OB.gif" => array("1010", "67b1d28deccc92200bdc3ce4a86612c3"), + "plugins/DevicesDetection/images/browsers/OE.gif" => array("562", "2afc1c7fbbf76cce942dd7a48f3ac63f"), + "plugins/DevicesDetection/images/browsers/OF.gif" => array("861", "289d8052246f66e09e8094de1d8a798f"), + "plugins/DevicesDetection/images/browsers/OI.gif" => array("911", "c574ee579047bd1a1b7f95f03d7be6b3"), + "plugins/DevicesDetection/images/browsers/ON.gif" => array("635", "f7d7eb7f8cec24f0c192e30fe29ea320"), + "plugins/DevicesDetection/images/browsers/OP.gif" => array("987", "ac0432440ad48154a6434675b1d9c27a"), + "plugins/DevicesDetection/images/browsers/OR.gif" => array("1024", "30e874c346325cd40cf58f98944ea603"), + "plugins/DevicesDetection/images/browsers/OV.gif" => array("978", "7e98fecee01438d561b791339281bd47"), + "plugins/DevicesDetection/images/browsers/OW.gif" => array("197", "b66e88cbb941f9ac326f43e0d993e572"), + "plugins/DevicesDetection/images/browsers/PL.gif" => array("1058", "759fa0100429b3b3a4dc88894454fd8a"), + "plugins/DevicesDetection/images/browsers/PM.gif" => array("1082", "271bd4b89d06a9f9a9553b5da9053bd3"), + "plugins/DevicesDetection/images/browsers/PO.gif" => array("1065", "ac773ea28693335de8e8ac62f7d20a0d"), + "plugins/DevicesDetection/images/browsers/PU.gif" => array("1094", "1ea4a15e1b326c28158b137e4e8d07af"), + "plugins/DevicesDetection/images/browsers/PW.gif" => array("1082", "ac66922861d77e9949a72f86622e0c2f"), + "plugins/DevicesDetection/images/browsers/PX.gif" => array("170", "0bd86aa95e1ae0d5975cdc2d93210e30"), + "plugins/DevicesDetection/images/browsers/QQ.gif" => array("1080", "72bb5a454a57da4ca306c9e3a6b41ac0"), + "plugins/DevicesDetection/images/browsers/RK.gif" => array("1035", "6b87220087449e134062214fbf72f5a8"), + "plugins/DevicesDetection/images/browsers/SA.gif" => array("1008", "921467088fc56c5c9cdd0bb6e58250bf"), + "plugins/DevicesDetection/images/browsers/SE.gif" => array("996", "c7a508db72e42174b83fb93cb85038d3"), + "plugins/DevicesDetection/images/browsers/SF.gif" => array("190", "589361249f74319b57ea98d6408bc4b3"), + "plugins/DevicesDetection/images/browsers/SH.gif" => array("1001", "ed645727ac6373acd25a66789a3533ee"), + "plugins/DevicesDetection/images/browsers/SL.gif" => array("900", "1210e399e978978390cfdfd9d79159e6"), + "plugins/DevicesDetection/images/browsers/SM.gif" => array("391", "1836a7db0bc6a4a4d8b3d6063346e3f5"), + "plugins/DevicesDetection/images/browsers/SR.gif" => array("1013", "bbc604df7eda9029c3ce279c6b804600"), + "plugins/DevicesDetection/images/browsers/TB.gif" => array("1014", "79bf7ed3ad92d3da09737d2fcd3913aa"), + "plugins/DevicesDetection/images/browsers/TI.gif" => array("595", "2c09db5f54b47472971d863e183159a8"), + "plugins/DevicesDetection/images/browsers/TZ.gif" => array("973", "5858a8b149e45749424bdf2da7ebefa3"), + "plugins/DevicesDetection/images/browsers/UC.gif" => array("994", "d9622ea01cb9093592858da53443c200"), + "plugins/DevicesDetection/images/browsers/UN.gif" => array("80", "c4a7f0e333a6079fdb0b6595e11bca74"), + "plugins/DevicesDetection/images/browsers/UNK.gif" => array("80", "c4a7f0e333a6079fdb0b6595e11bca74"), + "plugins/DevicesDetection/images/browsers/VI.gif" => array("2124", "b03cddacfc436367fd83f409249a8a75"), + "plugins/DevicesDetection/images/browsers/WE.gif" => array("1012", "cce9216ee7bd3ef52a46003d249ab540"), + "plugins/DevicesDetection/images/browsers/WO.gif" => array("1065", "ed1504717c9af523e30c33908126c4ad"), + "plugins/DevicesDetection/images/browsers/WP.gif" => array("982", "5bba1edfb42ce1b96551f81af0be08a1"), + "plugins/DevicesDetection/images/browsers/YA.gif" => array("1048", "8d94386ab4796664de7b897dd2106c9c"), + "plugins/DevicesDetection/images/os/3DS.gif" => array("1085", "262b44579aadcf90973653ff3e759cc7"), + "plugins/DevicesDetection/images/os/AIX.gif" => array("176", "58a60503a8e92493153694d1d97d2f6d"), + "plugins/DevicesDetection/images/os/AMG.gif" => array("1001", "5d67b7bb52ed746480573c1600d9a34c"), + "plugins/DevicesDetection/images/os/AMI.gif" => array("1055", "ef341c4cc2ec3bbf860c7d3bfe685326"), + "plugins/DevicesDetection/images/os/AND.gif" => array("144", "a1264f47256ff5b0d4feeeac833e7a96"), + "plugins/DevicesDetection/images/os/ARL.gif" => array("947", "913d273e01b9031f5113fb82ce63a591"), + "plugins/DevicesDetection/images/os/BBX.gif" => array("590", "e5cff6836abf100d9d8310d9dbb9f5d4"), + "plugins/DevicesDetection/images/os/BEO.gif" => array("1035", "ae4420933ac47a072d0f47759ef830c2"), + "plugins/DevicesDetection/images/os/BLB.gif" => array("576", "d40bd99ba1dd881b164907341ec2079d"), + "plugins/DevicesDetection/images/os/BMP.gif" => array("1001", "b9fa97bf32b038698bffe57974979e85"), + "plugins/DevicesDetection/images/os/BSD.gif" => array("1016", "1dc9b76bb3fc8f5529e9abe9ac8841fa"), + "plugins/DevicesDetection/images/os/BTR.gif" => array("946", "cbf9b74ee2db7714ed6d6432eff3f7c8"), + "plugins/DevicesDetection/images/os/CES.gif" => array("1011", "0cdd142972c3cc89b2ceb3b7b97e1321"), + "plugins/DevicesDetection/images/os/COS.gif" => array("1074", "e349a7dbdadf2fca33cca13287b0eba8"), + "plugins/DevicesDetection/images/os/DFB.gif" => array("326", "d61f11a900d520ef515eaa139176a5f1"), + "plugins/DevicesDetection/images/os/DSI.gif" => array("1076", "5c475f3ba76f4ec3b626e720574bcb37"), + "plugins/DevicesDetection/images/os/FED.gif" => array("1022", "e86e6f5aec6c32de7eca913a9f91c3ab"), + "plugins/DevicesDetection/images/os/FOS.gif" => array("197", "6b4eecf673d5461aebdd432bca086e8c"), + "plugins/DevicesDetection/images/os/GNT.gif" => array("1075", "4196a85df43e6a5593941dcf8262416f"), + "plugins/DevicesDetection/images/os/GTV.gif" => array("1614", "a032dd001e1a5755201a6263a669ca49"), + "plugins/DevicesDetection/images/os/HPX.gif" => array("191", "999717c37d76ca099a06cf77a781bdbf"), + "plugins/DevicesDetection/images/os/IOS.gif" => array("594", "5c21bb970373a93c8876c50d43f6f231"), + "plugins/DevicesDetection/images/os/IPA.gif" => array("587", "9f247437bc140cc6e70ec59f34bcddb1"), + "plugins/DevicesDetection/images/os/IPD.gif" => array("351", "a215ada2aefcbca876055fe7a2f7c039"), + "plugins/DevicesDetection/images/os/IPH.gif" => array("577", "49805f402375692d40635ada7cc0f472"), + "plugins/DevicesDetection/images/os/IRI.gif" => array("152", "5e631b5adc35a05ae0d85815829c6f48"), + "plugins/DevicesDetection/images/os/KBT.gif" => array("998", "6ecd8b978a51fb4fd6ec94f9b820ae1d"), + "plugins/DevicesDetection/images/os/KNO.gif" => array("985", "b4595a673edf60051636fedf418eda30"), + "plugins/DevicesDetection/images/os/LBT.gif" => array("951", "ed14ac9707a0e01f4557ac9e8508dea3"), + "plugins/DevicesDetection/images/os/LIN.gif" => array("170", "19039ee87d8fccdba6a391ada5656de7"), + "plugins/DevicesDetection/images/os/MAC.gif" => array("171", "03548481597f28751368e26be49aea99"), + "plugins/DevicesDetection/images/os/MAE.gif" => array("137", "84600277bad6751b68b15586bca50aef"), + "plugins/DevicesDetection/images/os/MDR.gif" => array("918", "62f5e501d28e25fff6c3a75631c7c208"), + "plugins/DevicesDetection/images/os/MIN.gif" => array("1009", "153385eff242c4e10ea3835a17a65f0e"), + "plugins/DevicesDetection/images/os/NBS.gif" => array("168", "fc0d4fcb57c98f3a4dd6eab8e71ebd6a"), + "plugins/DevicesDetection/images/os/NDS.gif" => array("1061", "16bc6e0960747b402441c40e419a7d53"), + "plugins/DevicesDetection/images/os/OBS.gif" => array("571", "fcdb547b7ab768e131ba592e8733c75c"), + "plugins/DevicesDetection/images/os/OS2.gif" => array("162", "ee37bab155ad46f2530e7586f9971656"), + "plugins/DevicesDetection/images/os/POS.gif" => array("1060", "96b06842dc1cc80a8bb283ee8f4be320"), + "plugins/DevicesDetection/images/os/PPY.gif" => array("1037", "1bc770c1bd83e6cfdc5087b00b44cb9d"), + "plugins/DevicesDetection/images/os/PS3.gif" => array("628", "7aca5b93e7cc8142e2ab578e7aae7dc8"), + "plugins/DevicesDetection/images/os/PSP.gif" => array("592", "f92da90c6c6ea808422184c314215465"), + "plugins/DevicesDetection/images/os/PSV.gif" => array("200", "d82e64a4f0aeec6e4931a41988680af3"), + "plugins/DevicesDetection/images/os/QNX.gif" => array("241", "51ceb87cd6268837d830b225cb6c8120"), + "plugins/DevicesDetection/images/os/RHT.gif" => array("952", "72c775bb0f2b8ad388ee513577abc8ec"), + "plugins/DevicesDetection/images/os/ROS.gif" => array("956", "9a294e5c701171c8c777ce563d88d924"), + "plugins/DevicesDetection/images/os/SAF.gif" => array("242", "600259fe739536945b6f5cd5c9d3489b"), + "plugins/DevicesDetection/images/os/SBA.gif" => array("990", "be3cfde24b6517884d73270a20e9a06f"), + "plugins/DevicesDetection/images/os/SLW.gif" => array("883", "37ca3a7bdb0eeea402252c80dca89369"), + "plugins/DevicesDetection/images/os/SOS.gif" => array("1036", "686261ea170398b2eb078b67a24f4276"), + "plugins/DevicesDetection/images/os/SSE.gif" => array("1066", "8a6b48a38ee8ecca0fd14092872dba12"), + "plugins/DevicesDetection/images/os/SYL.gif" => array("1017", "5c73fe766f50d4a697dbdeac254db9e2"), + "plugins/DevicesDetection/images/os/SYM.gif" => array("1042", "a8d404e43206c52a7e5f3e6e7e469eea"), + "plugins/DevicesDetection/images/os/T64.gif" => array("220", "8e42601e52784216ed71b8e8de17f82e"), + "plugins/DevicesDetection/images/os/TDX.gif" => array("1001", "a54b0161478c26d04b304c7849ef90df"), + "plugins/DevicesDetection/images/os/TIZ.gif" => array("958", "dbc584b7603e8865c9477b18a6fbbb7d"), + "plugins/DevicesDetection/images/os/UBT.gif" => array("986", "56d67af2d61927c290b0e5af8e8ef6fc"), + "plugins/DevicesDetection/images/os/UNK.gif" => array("80", "c4a7f0e333a6079fdb0b6595e11bca74"), + "plugins/DevicesDetection/images/os/VMS.gif" => array("572", "da6881ce3b86fdbea70ae1b405f9c40a"), + "plugins/DevicesDetection/images/os/WCE.gif" => array("185", "5e7141d138d3f7afc0a113c21a8b2d9b"), + "plugins/DevicesDetection/images/os/WII.gif" => array("617", "d8f2cae9e8e7723241c6c862f12c7511"), + "plugins/DevicesDetection/images/os/WIN.gif" => array("191", "53d14246a8e5a46e927a87648111a0d3"), + "plugins/DevicesDetection/images/os/WIU.gif" => array("310", "394c491524ac263e1c4fcedb1480d281"), + "plugins/DevicesDetection/images/os/WMO.gif" => array("1060", "721b79d12ac825df25f356f5a7714f74"), + "plugins/DevicesDetection/images/os/WOS.gif" => array("70", "31e5c59d2fc5b195c5ea4f1afd878e04"), + "plugins/DevicesDetection/images/os/WPH.gif" => array("1089", "8d69225f3ebb1fe2d2b6d4e2e1687b80"), + "plugins/DevicesDetection/images/os/WRT.gif" => array("925", "d9f78bbd9009c721cf5d64b056679a19"), + "plugins/DevicesDetection/images/os/XBT.gif" => array("968", "c0f572e03ffaf7d38c78fcbb7fba84cf"), + "plugins/DevicesDetection/images/os/XBX.gif" => array("1043", "e3e0eaa5daa2903bab1e0e9ea3ef1d46"), + "plugins/DevicesDetection/images/os/YNS.gif" => array("913", "f4d8502e11b209c209fcee3312a33139"), "plugins/DevicesDetection/images/screens/camera.png" => array("644", "f948997fff235b4bbdece324d9054969"), "plugins/DevicesDetection/images/screens/carbrowser.png" => array("3218", "fcbb6fa6a2c617418df8b17299d3d7fb"), "plugins/DevicesDetection/images/screens/computer.png" => array("550", "305f4f50137fe4e909ad4aa4f61d52a8"), @@ -1307,84 +2880,273 @@ class Manifest { "plugins/DevicesDetection/images/screens/tv.png" => array("644", "415f9d8ae19f77c0203fd82de985ab2a"), "plugins/DevicesDetection/images/screens/unknown.gif" => array("80", "c4a7f0e333a6079fdb0b6595e11bca74"), "plugins/DevicesDetection/images/screens/wide.gif" => array("1025", "c0104958e6fb23668d0406fd4d89095e"), - "plugins/DevicesDetection/lang/bg.json" => array("1918", "4efec5ddc313eff04a301805586e13f2"), - "plugins/DevicesDetection/lang/cs.json" => array("340", "9cbf609028387879af0c6a54c9fd67f1"), - "plugins/DevicesDetection/lang/da.json" => array("1309", "09ba8d9edf2c0bf40a4b78c2df0d9174"), - "plugins/DevicesDetection/lang/de.json" => array("1334", "14ab1307ce18bb680bc954764bdd8e2f"), - "plugins/DevicesDetection/lang/el.json" => array("2121", "71ba9e2ac13f5592daae2e6f8085361d"), - "plugins/DevicesDetection/lang/en.json" => array("1305", "e1cc97069255c3ed10eb9c96ab1faab9"), - "plugins/DevicesDetection/lang/es.json" => array("1487", "38413c85a6b8d00fb099c450bc215493"), - "plugins/DevicesDetection/lang/et.json" => array("1326", "925cd20b8c5560958c1c5af28fdabf03"), - "plugins/DevicesDetection/lang/fa.json" => array("1191", "f9021aacbeef48009222e6157bed9fb2"), - "plugins/DevicesDetection/lang/fi.json" => array("1360", "d35fa82abe7de2b9d6ff3b6ecb40aa90"), - "plugins/DevicesDetection/lang/fr.json" => array("1616", "304b6674bbda463695f47ef601130345"), - "plugins/DevicesDetection/lang/it.json" => array("1405", "ea2a06c896ed564b2d8fb3b3b128abf3"), - "plugins/DevicesDetection/lang/ja.json" => array("1757", "b8c584f9b316aa5be5694ea2a9452baf"), - "plugins/DevicesDetection/lang/nb.json" => array("359", "bd7d02fa1b1c53285f948b31d7aae4a6"), - "plugins/DevicesDetection/lang/nl.json" => array("1251", "b0ae612fa5e5c16ae19201e610934f68"), - "plugins/DevicesDetection/lang/pt-br.json" => array("937", "a3eb60325ac25d48584eecfb771b89e6"), - "plugins/DevicesDetection/lang/ro.json" => array("1003", "676591f9dc497e4117264d56690a5647"), - "plugins/DevicesDetection/lang/ru.json" => array("1510", "ea3b2ebb6e5c0cf0ea1944c56116a539"), - "plugins/DevicesDetection/lang/sr.json" => array("550", "b31ae00fd98c9b050823db27790b2628"), - "plugins/DevicesDetection/lang/sv.json" => array("1325", "9c96fbcb18791021e36d2ca4868830ba"), - "plugins/DevicesDetection/templates/detection.twig" => array("3599", "12494f5f5bff7b5f031e040c0f44f3df"), - "plugins/DevicesDetection/templates/index.twig" => array("632", "31c98fab8308315ac38c8c0d7800e105"), + "plugins/DevicesDetection/lang/am.json" => array("267", "c6f29a6e5f2005fdbb8929f1b826b788"), + "plugins/DevicesDetection/lang/ar.json" => array("490", "5157d46b7df32c6c5d1677aeecd16f56"), + "plugins/DevicesDetection/lang/be.json" => array("1407", "9b1c87abe1a0bb5ec4aee719d5d6bcb8"), + "plugins/DevicesDetection/lang/bg.json" => array("2740", "0f88e52372535431e77300f90c58109f"), + "plugins/DevicesDetection/lang/ca.json" => array("1067", "b140a7ecbe46951d059e9a7efe46e1c7"), + "plugins/DevicesDetection/lang/cs.json" => array("2538", "8b039d787c18b86b9f82f2837f0a53cf"), + "plugins/DevicesDetection/lang/da.json" => array("2231", "071760605527e39ea8243894e6cca42a"), + "plugins/DevicesDetection/lang/de.json" => array("2342", "f1d5f5285d1876196c18b62cbf790ef3"), + "plugins/DevicesDetection/lang/el.json" => array("3987", "c212270775e5449063327c8831c44ab4"), + "plugins/DevicesDetection/lang/en.json" => array("2309", "f80b64d60bf82e38813713aa6a954a05"), + "plugins/DevicesDetection/lang/es.json" => array("2680", "1d69d82a136acd5cebaff513270fc91a"), + "plugins/DevicesDetection/lang/et.json" => array("1453", "660af0cd6ccc885e641402c3c906ce0a"), + "plugins/DevicesDetection/lang/eu.json" => array("362", "da04391b50cceca6b2a61c11262cb2d7"), + "plugins/DevicesDetection/lang/fa.json" => array("1856", "4322e44b444a14187cbea66c99d3b839"), + "plugins/DevicesDetection/lang/fi.json" => array("1810", "ca7061f2bab7c7f4215f8473db3657d1"), + "plugins/DevicesDetection/lang/fr.json" => array("2698", "a1f4bb78b16f003f3f8ab0cfdcdc9879"), + "plugins/DevicesDetection/lang/gl.json" => array("176", "4fb65463e4d86f67be69009f9835245d"), + "plugins/DevicesDetection/lang/he.json" => array("245", "dd6ff9d4c57122f825e7c53e5bbb0b8b"), + "plugins/DevicesDetection/lang/hi.json" => array("1486", "7c0ab1450dc215b4cdb5718bbc36734f"), + "plugins/DevicesDetection/lang/hr.json" => array("274", "7f2d07c77dc4e80737b1893587e0b8bd"), + "plugins/DevicesDetection/lang/hu.json" => array("403", "4fa8c112e88fc8c63d7e8016753b6261"), + "plugins/DevicesDetection/lang/id.json" => array("988", "bf971c7c2c6233f8d57d023435527e82"), + "plugins/DevicesDetection/lang/is.json" => array("351", "ea0dbd14452b2d5eb86fcdd7d06420c0"), + "plugins/DevicesDetection/lang/it.json" => array("2457", "6061508bbebabb079ed528d755d758de"), + "plugins/DevicesDetection/lang/ja.json" => array("2956", "99fb6f6e87ba4bce22013cfff544a79b"), + "plugins/DevicesDetection/lang/ka.json" => array("577", "0c2e14e1c9b658b08501f631eab54296"), + "plugins/DevicesDetection/lang/ko.json" => array("2542", "6e90b087b531bcf0d4322890c4cd9823"), + "plugins/DevicesDetection/lang/lt.json" => array("1300", "cee9ad0250b4c3ed2d665e238569402c"), + "plugins/DevicesDetection/lang/lv.json" => array("925", "2a65b9b1af7d51e0f8c63baf2be2653b"), + "plugins/DevicesDetection/lang/nb.json" => array("2294", "d89b11c6c6e432be349fd8a516d3439f"), + "plugins/DevicesDetection/lang/nl.json" => array("2366", "643872354a13c2de690069b0d992641d"), + "plugins/DevicesDetection/lang/nn.json" => array("373", "73cd32395232882396a5637964bf18ff"), + "plugins/DevicesDetection/lang/pl.json" => array("1556", "2583e17e352be30c2f74fb5b1aea3b74"), + "plugins/DevicesDetection/lang/pt-br.json" => array("2558", "a33b16d2b4226c113d29b08069ebc5f1"), + "plugins/DevicesDetection/lang/pt.json" => array("1397", "ad2eb484247ebd5f9ac8d423d0dc7097"), + "plugins/DevicesDetection/lang/ro.json" => array("2022", "f02d6280104cdb241ac89ca4677d99d1"), + "plugins/DevicesDetection/lang/ru.json" => array("3493", "fd811853c4a3059b01816bfd90d0e37a"), + "plugins/DevicesDetection/lang/sk.json" => array("399", "02aaf8ae9077162acf99369e4a1b2687"), + "plugins/DevicesDetection/lang/sl.json" => array("755", "ad6f6062be535782650b4bf99c63983f"), + "plugins/DevicesDetection/lang/sq.json" => array("992", "b117d467f06ba8a9a8bcb3ec84510ae9"), + "plugins/DevicesDetection/lang/sr.json" => array("2319", "877a2ae85573097818f0dde16f493864"), + "plugins/DevicesDetection/lang/sv.json" => array("2265", "e9c68961a32c9a63dc61e8485156e189"), + "plugins/DevicesDetection/lang/ta.json" => array("78", "1838fa179f96685669f090fed2cc57af"), + "plugins/DevicesDetection/lang/te.json" => array("743", "666be60660daa83ec7196fa4cca3b36d"), + "plugins/DevicesDetection/lang/th.json" => array("708", "5403fabfa08b193e34244465350c1df2"), + "plugins/DevicesDetection/lang/tl.json" => array("2025", "685d9b9e3751106bd30eeb2cc1ae6915"), + "plugins/DevicesDetection/lang/tr.json" => array("1503", "6986c1169bcf8483ae70c69fba2f7715"), + "plugins/DevicesDetection/lang/uk.json" => array("504", "43d205e3ccef78b39847dee14882f197"), + "plugins/DevicesDetection/lang/vi.json" => array("1249", "c6a4236f550c1c0a7ee032f8356a8a71"), + "plugins/DevicesDetection/lang/zh-cn.json" => array("862", "955c7eb84721690aa4a3bdae033a580b"), + "plugins/DevicesDetection/lang/zh-tw.json" => array("431", "666c390b937a8509b5d6c5e524e12168"), + "plugins/DevicesDetection/Menu.php" => array("908", "da60e9e42408692728088e6f0d840f05"), + "plugins/DevicesDetection/plugin.json" => array("99", "d1b05c1f8861aba29255009dd7495ce0"), + "plugins/DevicesDetection/Reports/Base.php" => array("385", "8781b2f359f818e4e3a893fcbcfe2828"), + "plugins/DevicesDetection/Reports/GetBrand.php" => array("952", "d5ce29bc3935cc7182dc7e9ac5ba3564"), + "plugins/DevicesDetection/Reports/GetBrowserEngines.php" => array("1160", "0a1c7f66a28e452959feb54b32cb6c31"), + "plugins/DevicesDetection/Reports/GetBrowsers.php" => array("1193", "3637115b150b0c0675909472ae17dafd"), + "plugins/DevicesDetection/Reports/GetBrowserVersions.php" => array("1091", "7b8c0373a6898295646c2a3af6531428"), + "plugins/DevicesDetection/Reports/GetModel.php" => array("952", "05098f68086fafd23e5ce8d57eb538aa"), + "plugins/DevicesDetection/Reports/GetOsFamilies.php" => array("1128", "f7fdb5501cfb0088183aa34f16e9121c"), + "plugins/DevicesDetection/Reports/GetOsVersions.php" => array("1177", "778a3ef034985e4d9521b8cba286a96c"), + "plugins/DevicesDetection/Reports/GetType.php" => array("1166", "6f0e181aec6c170ec87a4b3f355d4a5e"), + "plugins/DevicesDetection/Segment.php" => array("382", "19e667070282e65ce237797aa9df4225"), + "plugins/DevicesDetection/templates/detection.twig" => array("3815", "49f669086b28527b435dc3db473e0cdd"), + "plugins/DevicesDetection/templates/devices.twig" => array("655", "7bd09aac269621a5a59f98b9675faba4"), "plugins/DevicesDetection/templates/list.twig" => array("174", "bfda35ec5629ff523fea8502339daa58"), - "plugins/DevicesDetection/Updates/1.14.php" => array("780", "e405cf2a34f8299ab31ff7f27e7cda9f"), - "plugins/Events/API.php" => array("7610", "1f1edc6d625743c1301eb05f28b94534"), - "plugins/Events/Archiver.php" => array("9467", "1058da454ca46bdcbf2751605eb434ef"), - "plugins/Events/Controller.php" => array("2819", "2744d5a2eaa6532b02fdae411518d6cc"), - "plugins/Events/Events.php" => array("11572", "eb5998643543849b6a86625296dc6f37"), - "plugins/Events/lang/bg.json" => array("342", "b6ca024b4dc8d44227d9b09687522b20"), - "plugins/Events/lang/da.json" => array("257", "53898474762bee31b2bce9efa4218033"), - "plugins/Events/lang/de.json" => array("846", "e7ffc157518edd6f563a036a33ceea6b"), - "plugins/Events/lang/el.json" => array("332", "f8fa308217f7fea4bad1b5cd1fda8dae"), - "plugins/Events/lang/en.json" => array("1388", "ccfc90445d2f2d1b2e9a03276c96e076"), - "plugins/Events/lang/es.json" => array("852", "02df421dd69675296c0f084dae5c3d0a"), + "plugins/DevicesDetection/templates/software.twig" => array("863", "58ce1b71283fe675a74186f6757de60b"), + "plugins/DevicesDetection/Updates/1.14.php" => array("863", "5352047205e0c439f0be8a154b4c3e78"), + "plugins/DevicesDetection/Visitor.php" => array("2457", "0949ef0225412b6e29d88d7f9f15e63c"), + "plugins/Diagnostics/Commands/AnalyzeArchiveTable.php" => array("3373", "a4f0e69a75e019b57a60680e39c75fff"), + "plugins/Diagnostics/Commands/Run.php" => array("2950", "c085a8cab37b95046535be2a6164f4c7"), + "plugins/Diagnostics/config/config.php" => array("2170", "2d363402eade49ca3aa82efda940e3f5"), + "plugins/Diagnostics/ConfigReader.php" => array("6026", "152445fb5c88cfc27b2030e67da10982"), + "plugins/Diagnostics/Controller.php" => array("1936", "168e3dae860cc48ddf582f0e5578d2a9"), + "plugins/Diagnostics/Diagnostic/CronArchivingCheck.php" => array("1260", "cd8c2c3c3e5057f80e99065991a32a70"), + "plugins/Diagnostics/Diagnostic/DbAdapterCheck.php" => array("3293", "ce936823d801acfdc20de4092467c48f"), + "plugins/Diagnostics/Diagnostic/Diagnostic.php" => array("1136", "3410ece2566296f015ab5bb72b2ea224"), + "plugins/Diagnostics/Diagnostic/DiagnosticResultItem.php" => array("776", "99f18f55d2eafab1132e2bd19a80f035"), + "plugins/Diagnostics/Diagnostic/DiagnosticResult.php" => array("2355", "01a51938f182851b9db32309d76a816a"), + "plugins/Diagnostics/Diagnostic/FileIntegrityCheck.php" => array("1607", "8965bcbe33b69326d9b569cebe1e3dd1"), + "plugins/Diagnostics/Diagnostic/GdExtensionCheck.php" => array("1197", "018b7e2530415a04468fd02544530b76"), + "plugins/Diagnostics/Diagnostic/HttpClientCheck.php" => array("1299", "728dbf2d95ffcc991dec527ea06fb721"), + "plugins/Diagnostics/Diagnostic/LoadDataInfileCheck.php" => array("2791", "c50a7ea77493a7dbbaec61335cfe2529"), + "plugins/Diagnostics/Diagnostic/MemoryLimitCheck.php" => array("1510", "457f917088abb92b7ff5a7b1737148cb"), + "plugins/Diagnostics/Diagnostic/NfsDiskCheck.php" => array("1596", "51b07702ba57ea16c269ac10ccaa33e5"), + "plugins/Diagnostics/Diagnostic/PageSpeedCheck.php" => array("2221", "13681d4819d90b2fea83bb990998a32b"), + "plugins/Diagnostics/Diagnostic/PhpExtensionsCheck.php" => array("2389", "239979571f61465280d56e2292ab01eb"), + "plugins/Diagnostics/Diagnostic/PhpFunctionsCheck.php" => array("3302", "7831d45780a0e0490dee339d6deb49a9"), + "plugins/Diagnostics/Diagnostic/PhpSettingsCheck.php" => array("2385", "f53e45e9c030c58e046543c2134afd89"), + "plugins/Diagnostics/Diagnostic/PhpVersionCheck.php" => array("1497", "32c688cba5a95c9e14e39d74bafa883b"), + "plugins/Diagnostics/Diagnostic/RecommendedExtensionsCheck.php" => array("2128", "675a2b30989dcce52f091a44d95a5718"), + "plugins/Diagnostics/Diagnostic/RecommendedFunctionsCheck.php" => array("2118", "e59ffd0ef41223a9b01d5ea0583c57d9"), + "plugins/Diagnostics/DiagnosticReport.php" => array("2550", "2b8dadc91401848db9e196d6aa5499c3"), + "plugins/Diagnostics/DiagnosticService.php" => array("1935", "a2c4a454a1d754c304fda17bfb80bd41"), + "plugins/Diagnostics/Diagnostics.php" => array("602", "c817efee9bcd93f4968f1bb12a925219"), + "plugins/Diagnostics/Diagnostic/TimezoneCheck.php" => array("1238", "2718b405625c4845c485e6bca6a80b52"), + "plugins/Diagnostics/Diagnostic/TrackerCheck.php" => array("1290", "0088909d1f908c6675a25cd52d7fe058"), + "plugins/Diagnostics/Diagnostic/WriteAccessCheck.php" => array("2725", "31ed361cf368e67ff9a5051b0a160a6c"), + "plugins/Diagnostics/lang/cs.json" => array("583", "344992d4635a380edaf62602e5f5e7b3"), + "plugins/Diagnostics/lang/de.json" => array("626", "1fe6161a280f5967bf81e714c5633951"), + "plugins/Diagnostics/lang/el.json" => array("945", "302e2654a5a855b3fb118b21fb6cff1f"), + "plugins/Diagnostics/lang/en.json" => array("540", "3dee2204f1a442b9bc4de8ffda216a08"), + "plugins/Diagnostics/lang/it.json" => array("113", "87baad045dc39b9e49ecf89f268a78cb"), + "plugins/Diagnostics/lang/pt-br.json" => array("626", "5e49f30ef9471ef834a258281f31f5ab"), + "plugins/Diagnostics/lang/sv.json" => array("77", "00fe9017ded864cb4e3e40874393963d"), + "plugins/Diagnostics/Menu.php" => array("807", "cb19847a3dd93d255bf357f43c31d313"), + "plugins/Diagnostics/plugin.json" => array("98", "2cbf507e2c94b90856270a1bc5efed34"), + "plugins/Diagnostics/stylesheets/configfile.less" => array("323", "1c829b3f59681747544e5c0d69512954"), + "plugins/Diagnostics/templates/configfile.twig" => array("2210", "f1056f197763b98601a79e43ca9001b8"), + "plugins/Diagnostics/Test/Integration/Commands/AnalyzeArchiveTableTest.php" => array("7213", "512bebd5df4760f7be595eb3fca64463"), + "plugins/Diagnostics/Test/Integration/ConfigReaderTest.php" => array("9934", "2c3405c2e37c56285415bba211e29ddd"), + "plugins/Diagnostics/Test/Mock/DiagnosticWithError.php" => array("538", "1d7a7a0b73e46849aebbe593846884d6"), + "plugins/Diagnostics/Test/Mock/DiagnosticWithSuccess.php" => array("539", "4ecb50422bda00df5dd8bc80972e7123"), + "plugins/Diagnostics/Test/Mock/DiagnosticWithWarning.php" => array("544", "d0bbf0be19be9c899786f87efe6ec87c"), + "plugins/Diagnostics/Test/Unit/Diagnostic/DiagnosticResultTest.php" => array("1370", "2fefafbb819a8360b1bab02375504550"), + "plugins/Diagnostics/Test/Unit/DiagnosticReportTest.php" => array("1726", "aba4d15208fb3c55bcd62dee82824bdb"), + "plugins/Diagnostics/Test/Unit/DiagnosticServiceTest.php" => array("1525", "f75442d5559cf68c2946687fd0cc957b"), + "plugins/Ecommerce/Columns/BaseConversion.php" => array("818", "6682a92d1025a293137fab76ace06e45"), + "plugins/Ecommerce/Columns/ProductCategory.php" => array("390", "ec022bbf48c6038d5118d048a3356db9"), + "plugins/Ecommerce/Columns/ProductName.php" => array("382", "52d07d84596b5f4fd0856a15f76f6b20"), + "plugins/Ecommerce/Columns/ProductSku.php" => array("380", "ee57c86d075ab91416d14cc10a5dfdf5"), + "plugins/Ecommerce/Columns/RevenueDiscount.php" => array("847", "b63214d54b691a28fa002b79924da15f"), + "plugins/Ecommerce/Columns/Revenue.php" => array("1813", "73dd4d8a404ee1090ea905233fdd90e5"), + "plugins/Ecommerce/Columns/RevenueShipping.php" => array("847", "2da1c621c34ba0efe51b4a0c160700ec"), + "plugins/Ecommerce/Columns/RevenueSubtotal.php" => array("847", "fcc5d8111362e99bd80efef36da6e4bc"), + "plugins/Ecommerce/Columns/RevenueTax.php" => array("837", "54375d5033ecbbd063ad294fd8122962"), + "plugins/Ecommerce/Controller.php" => array("3897", "20f99c23be354cb6f3141c5e470bba28"), + "plugins/Ecommerce/lang/bg.json" => array("64", "fe44cef66664b19de16d08d9effa401c"), + "plugins/Ecommerce/lang/cs.json" => array("377", "68fa995ddae26e383b461781600db59b"), + "plugins/Ecommerce/lang/da.json" => array("52", "c72674207df6477f4379d8b3a9f66d6e"), + "plugins/Ecommerce/lang/de.json" => array("410", "4c1534aa5d64655eddbe4a4ccac59992"), + "plugins/Ecommerce/lang/el.json" => array("672", "7dce788a36107ed6bd5175eeb9a40bc7"), + "plugins/Ecommerce/lang/en.json" => array("329", "77352d68e7c0b5394f6821da17a3b0c0"), + "plugins/Ecommerce/lang/es.json" => array("400", "fb9ab03cb2d49e61b9e39f4ca3c82b88"), + "plugins/Ecommerce/lang/fr.json" => array("413", "62aae699a87f65553e95c43449c0281d"), + "plugins/Ecommerce/lang/hi.json" => array("782", "97cd8e3621d893cf8bb94490079f508f"), + "plugins/Ecommerce/lang/it.json" => array("398", "e9242f3a8705bff4a145ae683e8ac400"), + "plugins/Ecommerce/lang/ja.json" => array("433", "b452e18de68a4e47df2f26d160886431"), + "plugins/Ecommerce/lang/nb.json" => array("362", "6e1c0b2bec992190772c38c856b9ac34"), + "plugins/Ecommerce/lang/nl.json" => array("414", "b6e54f65b51b491e09781f626ae538e7"), + "plugins/Ecommerce/lang/pt-br.json" => array("423", "5a786711f1757fd5086b8a9ea4587888"), + "plugins/Ecommerce/lang/pt.json" => array("54", "7801c86d09df902fbc51bf9ac2f67fff"), + "plugins/Ecommerce/lang/ru.json" => array("62", "e0f3952c8486e220e0937a362ab92452"), + "plugins/Ecommerce/lang/sk.json" => array("55", "786407454a35ec232533b2b6ca02b8bf"), + "plugins/Ecommerce/lang/sr.json" => array("344", "7a8050ba8f8d16ea90f95a83d98791bc"), + "plugins/Ecommerce/lang/sv.json" => array("152", "d8e4c5d1d5eca39b375268406ecb4ed3"), + "plugins/Ecommerce/lang/ta.json" => array("244", "94af4b0843825862a1259d48d1d72b3b"), + "plugins/Ecommerce/Menu.php" => array("1206", "ff5d0bdc10b2b593a1b56ad634a0e9ee"), + "plugins/Ecommerce/plugin.json" => array("27", "0a7a8fec04ae0ac3488ce3abf68e5a8e"), + "plugins/Ecommerce/Reports/BaseItem.php" => array("5695", "3424eaa93ab85e18795c415a43b8438c"), + "plugins/Ecommerce/Reports/Base.php" => array("1669", "aa62adcb2b1530469fe711a5cbb0cfa9"), + "plugins/Ecommerce/Reports/GetDaysToConversionAbandonedCart.php" => array("870", "51acdf9c0f04cc816e202f8960816021"), + "plugins/Ecommerce/Reports/GetDaysToConversionEcommerceOrder.php" => array("872", "badebebdc7e378b16479982c2c177b2a"), + "plugins/Ecommerce/Reports/GetEcommerceAbandonedCart.php" => array("1116", "97ec61bc29f7a47e842fdb5939373bb0"), + "plugins/Ecommerce/Reports/GetEcommerceOrder.php" => array("1165", "1700882ff54d5cfb1633672bc7e1a927"), + "plugins/Ecommerce/Reports/GetItemsCategory.php" => array("587", "b6a61ce96dcc1b279955b6846d74ab01"), + "plugins/Ecommerce/Reports/GetItemsName.php" => array("567", "4ba0c30c963bde7deb8f2ec68db7b3b0"), + "plugins/Ecommerce/Reports/GetItemsSku.php" => array("564", "875e55319269556be310ade71f5f29d2"), + "plugins/Ecommerce/Reports/GetVisitsUntilConversionAbandonedCart.php" => array("895", "a5c46d8ce131f80f07bb891bb06f820b"), + "plugins/Ecommerce/Reports/GetVisitsUntilConversionEcommerceOrder.php" => array("900", "b0ef45a20220f4bd9518ba2afce06b7a"), + "plugins/Ecommerce/templates/ecommerceLog.twig" => array("94", "bcc795f826bd46dde1a1d31a5530ae3e"), + "plugins/Ecommerce/templates/products.twig" => array("97", "64bf43a5297772ea21abe965d7ac1c62"), + "plugins/Ecommerce/templates/sales.twig" => array("101", "93b7233ddf89897205ab4bd5b742c027"), + "plugins/Ecommerce/Tracker/EcommerceRequestProcessor.php" => array("2807", "2b204a2894e19c49b396a01cd2622e13"), + "plugins/Ecommerce/Widgets.php" => array("816", "ea96879813a753bb5bdf3378b0768a9f"), + "plugins/Events/Actions/ActionEvent.php" => array("2342", "2ca5d3658116145b3e5a5193bdad7eb7"), + "plugins/Events/API.php" => array("9158", "071b04ae4182601ce4b5139034680796"), + "plugins/Events/Archiver.php" => array("9710", "57cf90028e7db313819f7094f3caf4b6"), + "plugins/Events/Columns/EventAction.php" => array("1331", "43d8a1afd25f50352a737ebb1db42f32"), + "plugins/Events/Columns/EventCategory.php" => array("1353", "98bca9429c42ca7202a24b4d7751120c"), + "plugins/Events/Columns/EventName.php" => array("1238", "124150dff9d044341e412cfe34e23356"), + "plugins/Events/Columns/Metrics/AverageEventValue.php" => array("1129", "5710361e2c4f37045033d67477f037cd"), + "plugins/Events/Columns/TotalEvents.php" => array("1905", "9a65f7ddaf0a321075f48a69a3f051b8"), + "plugins/Events/Controller.php" => array("2621", "751344eba7e46431f66ecaf35c8cb5af"), + "plugins/Events/DataTable/Filter/ReplaceEventNameNotSet.php" => array("894", "cb5fbecae1c9b87e0712dae1b0958393"), + "plugins/Events/Events.php" => array("9461", "c03e4510d53245102c4784c81e9e8c38"), + "plugins/Events/lang/bg.json" => array("1284", "fcf5f8a12776cfae018a88fcb02413e1"), + "plugins/Events/lang/ca.json" => array("652", "9e0a1d477b1253ab673727ebed6a0b7b"), + "plugins/Events/lang/cs.json" => array("1616", "6711683d42b0439a12cec82714cc05e7"), + "plugins/Events/lang/da.json" => array("1499", "88cfeb7c5e5a503e1a15d5feb4a727e2"), + "plugins/Events/lang/de.json" => array("1649", "c4a07655a75dc7561fd9ad7c5479bfc5"), + "plugins/Events/lang/el.json" => array("2227", "282e63d4456c58e4b23f84e2ea4b45d0"), + "plugins/Events/lang/en.json" => array("1484", "9a3a9590f0a34a1281539fa776577303"), + "plugins/Events/lang/es.json" => array("1641", "f0697ee63d0e351d512b01e07ee258fc"), "plugins/Events/lang/et.json" => array("262", "b6f500811defac532922653d562467c6"), - "plugins/Events/lang/fa.json" => array("291", "b11cba57d7f4654e63e864509c018bcf"), - "plugins/Events/lang/fi.json" => array("262", "d00802eb689dac69d2609cc6289017f2"), - "plugins/Events/lang/fr.json" => array("279", "39724ce65da5324898709bd365ceddee"), - "plugins/Events/lang/it.json" => array("818", "6a9e72466790e1e84a20281cf188a543"), - "plugins/Events/lang/ja.json" => array("300", "8b531ac5ac179f64c711cbfa3ca7b692"), - "plugins/Events/lang/nl.json" => array("271", "6a01d8a33c0234eee5d06c2da015e23e"), - "plugins/Events/lang/pt-br.json" => array("208", "292c740643d6491e0fe066ac5198345d"), - "plugins/Events/lang/ro.json" => array("921", "59e6c3b7b2527ec51a61fdc08b8d4754"), - "plugins/Events/lang/sr.json" => array("529", "993200724d25f114e3e500eab2459df8"), - "plugins/Events/lang/sv.json" => array("258", "c3d44c14aa30eebcad1133d6ae1a7b2c"), + "plugins/Events/lang/fa.json" => array("403", "c9178a3daec627c2049e5d1248399a17"), + "plugins/Events/lang/fi.json" => array("1256", "00ea1ba3743f4829027129fa95ff75d8"), + "plugins/Events/lang/fr.json" => array("1730", "bf032ff1586871585b9cc85e655bc9c4"), + "plugins/Events/lang/hi.json" => array("1574", "46c7db6113039244363f9ad7ba14734d"), + "plugins/Events/lang/it.json" => array("1545", "96a8be1eb696bfa9c0fbe27c5446a0de"), + "plugins/Events/lang/ja.json" => array("1705", "9190a96d5b9720c4d086194a3cb913fd"), + "plugins/Events/lang/lt.json" => array("291", "e853be98e8328103bb10a9fa8e2841cd"), + "plugins/Events/lang/nb.json" => array("1450", "4449b8d92877485d267c5af5c8e2b7c7"), + "plugins/Events/lang/nl.json" => array("1675", "be15c3662d4162fad886d47ec568624f"), + "plugins/Events/lang/pl.json" => array("1278", "794b57c6d5ebf31221ea4d2d241c3aa2"), + "plugins/Events/lang/pt-br.json" => array("1581", "71d6f7e242643599aa8c5b8e9e1aece0"), + "plugins/Events/lang/pt.json" => array("87", "b1ec6406b4c41cbec689e0b7a52d7091"), + "plugins/Events/lang/ro.json" => array("1571", "5c9485d0770e21513c0448d6f47099a8"), + "plugins/Events/lang/ru.json" => array("1988", "a3723c49edfec5e839a09b4db1a2aa8a"), + "plugins/Events/lang/sk.json" => array("675", "e961d0dec1730e7800bbcea4e5cd27c0"), + "plugins/Events/lang/sl.json" => array("104", "3129420f171aefb2dfa3554b84e9ac69"), + "plugins/Events/lang/sr.json" => array("1613", "16add36202c26aa8acb7f324da6c6ecf"), + "plugins/Events/lang/sv.json" => array("1628", "2b0fa50c85c4660a393162b160b9efd0"), "plugins/Events/lang/ta.json" => array("355", "b0e9b20f277a78aee6dc90005ec0c3f3"), + "plugins/Events/lang/tl.json" => array("1538", "18e9914e4b810ac515d4a0ebb0d9fe47"), + "plugins/Events/lang/tr.json" => array("1340", "22bf68dccbd258c1316584105677558e"), "plugins/Events/lang/vi.json" => array("254", "63d3e8849dd421c075b182f3db5b3d3e"), "plugins/Events/lang/zh-cn.json" => array("204", "f15db4c7236481d992551e7964b1cc79"), - "plugins/Events/plugin.json" => array("139", "b5998447ccf2ee34f37ebe94be0aa845"), + "plugins/Events/Menu.php" => array("428", "6fc85e124ec4ba0996ee1e420db34e0d"), + "plugins/Events/Reports/Base.php" => array("664", "cefdcc1ea7db80e881ac6cf2da011073"), + "plugins/Events/Reports/GetActionFromCategoryId.php" => array("679", "a4b0d4e07e2bf6a2a1e9165b24569e1e"), + "plugins/Events/Reports/GetActionFromNameId.php" => array("671", "2cb674da0b726451373b5d43d1e49023"), + "plugins/Events/Reports/GetAction.php" => array("1000", "ccee723cd9f76ff1bc77dbd0aeb946e8"), + "plugins/Events/Reports/GetCategoryFromActionId.php" => array("686", "cbfeb76c3a8b2e93b0d465bf35c50c6f"), + "plugins/Events/Reports/GetCategoryFromNameId.php" => array("682", "b1dc12ec52f227f0c291f60797cb65f1"), + "plugins/Events/Reports/GetCategory.php" => array("1012", "4c8bc313dd87d7928bf03edc58df4cf5"), + "plugins/Events/Reports/GetNameFromActionId.php" => array("660", "7647e512711a991fb838e36fbf6b218e"), + "plugins/Events/Reports/GetNameFromCategoryId.php" => array("669", "5189d864496e098e06aa3f766af03849"), + "plugins/Events/Reports/GetName.php" => array("990", "4b7fcc935a5bd3adb2b0a37082fb6aa1"), + "plugins/Events/Segment.php" => array("452", "16a91de51555740a50be10535e20375f"), + "plugins/Events/stylesheets/datatable.less" => array("227", "049a705c135cb3b8f9c5d22b81659a62"), "plugins/Events/templates/index.twig" => array("27", "a1080e4dc13f29008f29ac2d2e5dc8a8"), - "plugins/ExampleAPI/API.php" => array("4049", "2bda550f0e37806d7d8910789eee8199"), - "plugins/ExampleAPI/ExampleAPI.php" => array("325", "cef3615976cd2876afa2d2fc77acc5c1"), - "plugins/ExampleAPI/plugin.json" => array("448", "1a13a70c7ea7281490ed4af6a7221064"), - "plugins/ExampleCommand/Commands/HelloWorld.php" => array("1074", "70069208b271812bbf7e8faca036e7af"), - "plugins/ExampleCommand/plugin.json" => array("115", "cb68e3b4adc3511c16067f2cf02c5922"), - "plugins/ExamplePlugin/API.php" => array("815", "8bfc9244633eabb0c4f7f32e9b4a3fcf"), - "plugins/ExamplePlugin/Controller.php" => array("484", "f998699c283e1b20006c9afa8092f4ad"), - "plugins/ExamplePlugin/ExamplePlugin.php" => array("587", "7fedf67b0a08fb7fe1f8d71372ce6e91"), - "plugins/ExamplePlugin/.gitignore" => array("20", "4a9a2f0c455651c6b04479aafba04568"), - "plugins/ExamplePlugin/javascripts/plugin.js" => array("480", "eb32c805acb3e4af9370afd515a6dd1d"), - "plugins/ExamplePlugin/plugin.json" => array("208", "89a9672ed7f49b867ed88133a69120f4"), - "plugins/ExamplePlugin/README.md" => array("207", "48e2fb45e071d5d991d2b6243d9e8ff4"), - "plugins/ExamplePlugin/screenshots/.gitkeep" => array("0", "d41d8cd98f00b204e9800998ecf8427e"), - "plugins/ExamplePlugin/templates/index.twig" => array("77", "16efbdb92b36269be3801782f230f7d6"), - "plugins/ExamplePlugin/.travis.yml" => array("1056", "3a37a0ec586442058d923e1d7d640f43"), - "plugins/ExampleRssWidget/Controller.php" => array("1192", "090efd2ade281d8b58713d703b2beeda"), - "plugins/ExampleRssWidget/ExampleRssWidget.php" => array("949", "75f2273eb309a058b35e8c04cf3bfc45"), - "plugins/ExampleRssWidget/plugin.json" => array("449", "5ff4fed5b604abbae9e887aa0b45cb6a"), - "plugins/ExampleRssWidget/RssRenderer.php" => array("2078", "e2101bb2155cf6cc36ec064b2ca93b9e"), + "plugins/ExampleAPI/API.php" => array("4069", "22846d08aa52f3d321fad0abc20b7432"), + "plugins/ExampleAPI/ExampleAPI.php" => array("329", "5d5864d2f56b4c5c23fe7c462798c473"), + "plugins/ExampleAPI/plugin.json" => array("444", "9eefcb7da521f3a3ff34b4a9e3f297a1"), + "plugins/ExampleCommand/Commands/HelloWorld.php" => array("2229", "2327a4f5ffca68c0eceb196aebd41389"), + "plugins/ExampleCommand/plugin.json" => array("144", "52e2aa5cc3b8bc45e126234e123afc06"), + "plugins/ExamplePlugin/angularjs/directive-component/component.controller.js" => array("577", "02553ed47e181970123f9c39f878e144"), + "plugins/ExamplePlugin/angularjs/directive-component/component.directive.html" => array("68", "5fb2d542dc5a8b056334c084e15111a2"), + "plugins/ExamplePlugin/angularjs/directive-component/component.directive.js" => array("1169", "dd804d503806630903a5e4c8c41c8d88"), + "plugins/ExamplePlugin/angularjs/directive-component/component.directive.less" => array("28", "e40be237cbcfb81a2b615c91f2616f45"), + "plugins/ExamplePlugin/API.php" => array("1393", "e2050d2bb00803bd7d33513ce23c5d8f"), + "plugins/ExamplePlugin/Archiver.php" => array("2204", "7137f9e96fcb6b84079217af0020f845"), + "plugins/ExamplePlugin/Controller.php" => array("883", "5042138a91ea35322e18f63fa771f833"), + "plugins/ExamplePlugin/ExamplePlugin.php" => array("240", "03233993ed896d69769491b37d5f9ad0"), + "plugins/ExamplePlugin/javascripts/plugin.js" => array("502", "57410d0c6c961ab4a88ec15f58936023"), + "plugins/ExamplePlugin/Menu.php" => array("2401", "d0f7c2cfa7c7fe73ecd428819c55daf2"), + "plugins/ExamplePlugin/plugin.json" => array("427", "6b505b4c444b1dd29babd7f7a11391a6"), + "plugins/ExamplePlugin/README.md" => array("209", "57579e836071567bf0d5a0e0a5cbd723"), + "plugins/ExamplePlugin/Tasks.php" => array("1013", "5edc10bb7d49460b60ad63f5210d6a92"), + "plugins/ExamplePlugin/templates/index.twig" => array("156", "ff26515cddba80f1202e95e30e67dce3"), + "plugins/ExamplePlugin/Updates/0.0.2.php" => array("2021", "185e4d94abee2bd9a598e7bb3cfcf692"), + "plugins/ExamplePlugin/Widgets.php" => array("2538", "2b693e4b338f028a67c7932d7251c8dc"), + "plugins/ExampleReport/API.php" => array("842", "b74ba7a868aac484f4218b466908fd62"), + "plugins/ExampleReport/ExampleReport.php" => array("242", "de40b1bfca7e49e7badd59a0e3ec73bf"), + "plugins/ExampleReport/plugin.json" => array("255", "7fb7d47827068f60747737ab60716c4e"), + "plugins/ExampleReport/Reports/Base.php" => array("356", "b85d189f1a87bdce2085b0655db5005b"), + "plugins/ExampleReport/Reports/GetExampleReport.php" => array("4178", "77a65379b5a325f431b4ff5ddf5d105d"), + "plugins/ExampleRssWidget/ExampleRssWidget.php" => array("609", "2c42c7a4d9161068e5db42588d7aa594"), + "plugins/ExampleRssWidget/plugin.json" => array("436", "cf496833d1176d66d799331c29d3f171"), + "plugins/ExampleRssWidget/RssRenderer.php" => array("2083", "3895b7a232d2394c9ff270591aed9e45"), "plugins/ExampleRssWidget/stylesheets/rss.less" => array("468", "9890a5a97e8e0be796c1b993b6991218"), - "plugins/ExampleSettingsPlugin/plugin.json" => array("163", "2672083085a6a297afd769beef9107a2"), - "plugins/ExampleSettingsPlugin/Settings.php" => array("5641", "a65a623faf6b325eb9109bb480eff723"), - "plugins/ExampleTheme/plugin.json" => array("158", "7bd2577897bda6846b7c457e059766f4"), - "plugins/ExampleTheme/README.md" => array("208", "1e90083ee059ed260d3cc73e3bbff133"), - "plugins/ExampleTheme/stylesheets/theme.less" => array("0", "d41d8cd98f00b204e9800998ecf8427e"), - "plugins/ExampleUI/API.php" => array("3032", "c4622a9b0fa70d56d9eef72dbf22b546"), - "plugins/ExampleUI/Controller.php" => array("7325", "4255650a64a9cf4f3b41f98fd101dff5"), - "plugins/ExampleUI/ExampleUI.php" => array("1707", "f5ce7ad0cc8af387cc373ce590105a00"), + "plugins/ExampleRssWidget/Widgets.php" => array("1428", "046e141e37bf52bca08d7094a86e957d"), + "plugins/ExampleSettingsPlugin/plugin.json" => array("167", "7b8e8ae50f1052330dea3064f983ce3d"), + "plugins/ExampleSettingsPlugin/Settings.php" => array("5860", "e48e0254a38f3efffecf0bf62efedf69"), + "plugins/ExampleTheme/plugin.json" => array("391", "dc27f8bc5b98b1d36eefb01ae0edca06"), + "plugins/ExampleTheme/README.md" => array("206", "9fb5e612c84cae82a58977292467ca87"), + "plugins/ExampleTheme/stylesheets/theme.less" => array("1309", "6989a6a97eb005da0addf43a07bdd2e4"), + "plugins/ExampleTracker/Columns/ExampleActionDimension.php" => array("5102", "392b854f0518dfa410f3669c0b430271"), + "plugins/ExampleTracker/Columns/ExampleConversionDimension.php" => array("5389", "7f5d0330c3dc3e647c6eaefdc7e7d2a0"), + "plugins/ExampleTracker/Columns/ExampleDimension.php" => array("793", "37cfbe5258f8ed11f56e19decde257a2"), + "plugins/ExampleTracker/Columns/ExampleVisitDimension.php" => array("6669", "5db930cfa58ed8193db147514ba9f78c"), + "plugins/ExampleTracker/ExampleTracker.php" => array("244", "b2c931d7aed979a85b23328d18d47886"), + "plugins/ExampleTracker/lang/en.json" => array("78", "3d865e530ac4a60c0e46fd991ea4a812"), + "plugins/ExampleTracker/plugin.json" => array("288", "e9e95f73d4731957fe7c06a58844fa2c"), + "plugins/ExampleUI/API.php" => array("3036", "628f4bee7affd6adc0942f2917edff41"), + "plugins/ExampleUI/Controller.php" => array("7651", "6239d0c0d7b972bac4437be759d6ee48"), "plugins/ExampleUI/images/icons-planet/earth.png" => array("11823", "82a4f58618b5194ba7d50600115d36da"), "plugins/ExampleUI/images/icons-planet/jupiter.png" => array("10686", "08c6c466b8068c18e0c1859115515a5d"), "plugins/ExampleUI/images/icons-planet/LICENSE" => array("112", "5275944112cc13e4e74b2de749497988"), @@ -1394,130 +3156,502 @@ class Manifest { "plugins/ExampleUI/images/icons-planet/saturn.png" => array("10561", "9514535a2fde794c6dac3e3382fcf517"), "plugins/ExampleUI/images/icons-planet/uranus.png" => array("10358", "9ff6281138f4ebb8ca5b8b3b54b9d9b0"), "plugins/ExampleUI/images/icons-planet/venus.png" => array("10557", "1aa0425158220a53f36537f83d266231"), - "plugins/ExampleUI/plugin.json" => array("502", "fdd7feb05198028a198c8cedae0ce6b8"), + "plugins/ExampleUI/Menu.php" => array("1380", "816f0010379fb435489d743d1e486535"), + "plugins/ExampleUI/plugin.json" => array("451", "f2419252e8ab778f0a7b08003c80e869"), "plugins/ExampleUI/templates/evolutiongraph.twig" => array("90", "541ef7126cfe01ac071d053d720d2ccd"), - "plugins/ExampleUI/templates/notifications.twig" => array("447", "9525341678a5441f789cd446cbbdba56"), + "plugins/ExampleUI/templates/notifications.twig" => array("521", "f1f1b35cb6ef19616265ce278b3f5b79"), "plugins/ExampleUI/templates/sparklines.twig" => array("237", "31e796d35929d2839fe62b5d2f3460f6"), - "plugins/ExampleVisualization/ExampleVisualization.php" => array("631", "44f36c70c57d48b86a5342b7ee2e3010"), - "plugins/ExampleVisualization/images/table.png" => array("151", "327ee0e75605ab865796053f2c0aebf1"), - "plugins/ExampleVisualization/plugin.json" => array("410", "6879c05e6a2b969cfac1eb24b53e5e79"), + "plugins/ExampleVisualization/ExampleVisualization.php" => array("264", "d67ce85e32ce072ea428b891354018c2"), + "plugins/ExampleVisualization/images/table.png" => array("1056", "375eae11704a2d2e749cdefcb26f2a39"), + "plugins/ExampleVisualization/plugin.json" => array("436", "006172c6e563e91ff4ac0eb3bd0ad793"), "plugins/ExampleVisualization/README.md" => array("238", "1353685fc485ed3691c8ce507d530411"), - "plugins/ExampleVisualization/SimpleTable.php" => array("2515", "c5dcc6edbdb74ed20ffe9d36f6846c22"), "plugins/ExampleVisualization/templates/simpleTable.twig" => array("829", "abccf18a3c8bfb921a53f65cb339282a"), + "plugins/ExampleVisualization/Visualizations/SimpleTable.php" => array("2534", "3a8793f8c8487a23bfbbdaf0d2b7e318"), "plugins/Feedback/angularjs/ratefeature/icon_license" => array("268", "0a278d7bd9ac7a0022c8adfdf07418cd"), - "plugins/Feedback/angularjs/ratefeature/ratefeature-controller.js" => array("633", "4dd44a1936c30a3cfc157ed7c3fd2653"), - "plugins/Feedback/angularjs/ratefeature/ratefeature-directive.js" => array("542", "4376ff3f0cf1d5c78a92e8900fa14161"), - "plugins/Feedback/angularjs/ratefeature/ratefeature.html" => array("1554", "f61eee9f73fada95471dec5c5980b6b4"), - "plugins/Feedback/angularjs/ratefeature/ratefeature.less" => array("358", "51dbbd1fab358ef455a06d53697bffc7"), - "plugins/Feedback/angularjs/ratefeature/ratefeature-model.js" => array("543", "3807890aedaff7895eb3f3c9f1f00015"), + "plugins/Feedback/angularjs/ratefeature/ratefeature.controller.js" => array("886", "5bedc947e371824649ce0edcbc59fa98"), + "plugins/Feedback/angularjs/ratefeature/ratefeature.directive.html" => array("1650", "2b44e7af1072aec9ec08feba108ac43a"), + "plugins/Feedback/angularjs/ratefeature/ratefeature.directive.js" => array("733", "951a9d0f7d77f3daff4aa8dc87cd370f"), + "plugins/Feedback/angularjs/ratefeature/ratefeature.directive.less" => array("358", "51dbbd1fab358ef455a06d53697bffc7"), + "plugins/Feedback/angularjs/ratefeature/ratefeature-model.service.js" => array("744", "e494c05f594f9f0ef0ca9ec052c56e25"), "plugins/Feedback/angularjs/ratefeature/thumbs-down.png" => array("2188", "38c37611f821aa2671d60dc843345d12"), "plugins/Feedback/angularjs/ratefeature/thumbs-up.png" => array("2202", "9befe01114b4a5801808e8899b5010a7"), - "plugins/Feedback/API.php" => array("3265", "09a9d79dc7bbc8010239f81887d0de38"), - "plugins/Feedback/Controller.php" => array("623", "89de65e12271361e2e2aee5c59641288"), - "plugins/Feedback/Feedback.php" => array("2185", "73bd60a41e1b6221493799866a47291b"), + "plugins/Feedback/API.php" => array("2841", "ef6334e02c9e12ffadee8d0fc71d0b46"), + "plugins/Feedback/Controller.php" => array("497", "40b3096479efd2dc900fd83f671ac93a"), + "plugins/Feedback/Feedback.php" => array("1790", "6ef35f0bb2ab54926c2a33789a87a1d2"), "plugins/Feedback/images/facebook.png" => array("302", "9ae558673b40a7f3eeaad17b281ba4f8"), "plugins/Feedback/images/github.png" => array("361", "9417fe2b0664c8d8f23b1765728c46c1"), "plugins/Feedback/images/linkedin.png" => array("336", "26bebcbbef85af866892f6aa7c49e079"), "plugins/Feedback/images/newsletter.png" => array("444", "740cc523cc992a4e9c089d7dc589b3eb"), "plugins/Feedback/images/twitter.png" => array("376", "fcac98bcbf056e136c7b06557d7bcb98"), - "plugins/Feedback/stylesheets/feedback.less" => array("1539", "c0c45446610709fef33f18bcf4f6ef14"), - "plugins/Feedback/templates/index.twig" => array("5746", "e963b3fcd9eb4f753c2d944c174def7b"), - "plugins/Goals/API.php" => array("24370", "539367dfc9fbf1b43bf84839ee86eb58"), - "plugins/Goals/Archiver.php" => array("15665", "d6178a9fdfbaf1c96ae1fed57ba6a683"), - "plugins/Goals/Controller.php" => array("21788", "377dae6591eab934b51afaa0fab43aa9"), - "plugins/Goals/Goals.php" => array("31816", "0ea5f9311ee7c1d5ef051dd14f81e357"), - "plugins/Goals/javascripts/goalsForm.js" => array("6201", "acbd9a7ee2de6fd6e5a2f2b6698ece5a"), - "plugins/Goals/stylesheets/goals.css" => array("460", "6b076df7c73aa99431820279517ea9cd"), - "plugins/Goals/templates/_addEditGoal.twig" => array("4456", "980da0a6237f27c853fd9c57a3db1380"), - "plugins/Goals/templates/addNewGoal.twig" => array("432", "530107fc4f4a5fb8ea5744592e3c3646"), - "plugins/Goals/templates/_formAddGoal.twig" => array("5011", "af7bdec1ae036d7dc363cbb0977fc850"), - "plugins/Goals/templates/getGoalReportView.twig" => array("3030", "395e8c171b9268be5e4e7ccf24e7c6be"), - "plugins/Goals/templates/getOverviewView.twig" => array("1863", "afb8bced90b1de94b24546bd5ae21604"), - "plugins/Goals/templates/_listGoalEdit.twig" => array("2736", "6c0d563edf0c1971097e0c41d5516a9c"), - "plugins/Goals/templates/_listTopDimension.twig" => array("615", "86df6f4ecfa56b02c72075f7beace1d5"), - "plugins/Goals/templates/_titleAndEvolutionGraph.twig" => array("3785", "ae6e728ff64176a617f69d426834e6ba"), - "plugins/Goals/Visualizations/Goals.php" => array("11535", "13ea4bfab83f1f683db27a8f62d453f8"), - "plugins/ImageGraph/API.php" => array("22505", "b5f4b78e2b6b1e4eb667cd5601124edf"), - "plugins/ImageGraph/Controller.php" => array("2596", "028bc0f95cddb8fd42a72007ee8677d4"), + "plugins/Feedback/lang/ar.json" => array("636", "c9f21b2dc28529b6817f724306ba927e"), + "plugins/Feedback/lang/be.json" => array("675", "19e5e4c5267091de81319d29fffb06c0"), + "plugins/Feedback/lang/bg.json" => array("980", "6870551b7b03accbe3a7ed6d68f9fd47"), + "plugins/Feedback/lang/bn.json" => array("69", "267c5b50e912af191bb3d14c6add0a85"), + "plugins/Feedback/lang/ca.json" => array("596", "a85f9feb7102a4b8152ba040b5263183"), + "plugins/Feedback/lang/cs.json" => array("2820", "cf2b53a3a59c0f06ae3618a984964da2"), + "plugins/Feedback/lang/da.json" => array("1877", "e967fb8eaaa8ffaa7f8ce58a7411dfe4"), + "plugins/Feedback/lang/de.json" => array("3093", "3a5236b8a733db550f226b133322027b"), + "plugins/Feedback/lang/el.json" => array("4659", "7e33efd431f6da4e1ea2c3089fbf79f4"), + "plugins/Feedback/lang/en.json" => array("2724", "3cd89597c37ee39d30931da96fd4643e"), + "plugins/Feedback/lang/es.json" => array("2989", "2ad74c798c7e4aaaa45d6fd999ee4fac"), + "plugins/Feedback/lang/et.json" => array("103", "345251cae1fbd324cb500e55fac7f7bc"), + "plugins/Feedback/lang/fa.json" => array("857", "ce24f1c8dcd904763fdeeb8358553d30"), + "plugins/Feedback/lang/fi.json" => array("1047", "288655d918587937183c4a09b4c610ba"), + "plugins/Feedback/lang/fr.json" => array("3149", "42940a9ed24e576d8284dbb8adb548b4"), + "plugins/Feedback/lang/gl.json" => array("194", "137162a0dc3609abdd85c8e3f9340c1d"), + "plugins/Feedback/lang/hi.json" => array("1808", "0763dcaa19ef9b6fd469d61883420a5c"), + "plugins/Feedback/lang/hu.json" => array("573", "078bb1154afb6e195356cf1b08950dc0"), + "plugins/Feedback/lang/id.json" => array("592", "0d51055aca26ba2a2c30562cd2a762ec"), + "plugins/Feedback/lang/it.json" => array("2893", "0579ace5da9fa1095957cd76af08b5c0"), + "plugins/Feedback/lang/ja.json" => array("3727", "6e248438bb4888411c19e3638ff5a27e"), + "plugins/Feedback/lang/ka.json" => array("949", "23104be4b3c8498f7fec62cb775d0f18"), + "plugins/Feedback/lang/ko.json" => array("3142", "34735136a98a4dffec630e2722197e43"), + "plugins/Feedback/lang/lt.json" => array("1314", "60f2f27809368debbeef03aff0e0d7ab"), + "plugins/Feedback/lang/lv.json" => array("460", "d31689ab2cee8e16f076a687921b4bc7"), + "plugins/Feedback/lang/nb.json" => array("1170", "98dd1f6f81467247b3b141683c8feed9"), + "plugins/Feedback/lang/nl.json" => array("2855", "d4f70f0fd1f74ffc6f20ddbe174f9232"), + "plugins/Feedback/lang/nn.json" => array("477", "9ac4a7303b7d7ec5ed70c53a83b9af06"), + "plugins/Feedback/lang/pl.json" => array("1475", "a1a2186fd7bb1bcd87948332a35df6fa"), + "plugins/Feedback/lang/pt-br.json" => array("2891", "7c43ef7e7c3369e8f422b844c82baa70"), + "plugins/Feedback/lang/pt.json" => array("500", "f798545eb805343d864138d35aa1f928"), + "plugins/Feedback/lang/ro.json" => array("1702", "8330630751d0d7c3a038d4f546b86661"), + "plugins/Feedback/lang/ru.json" => array("3658", "87b5d237495988958e1c728702ff80e4"), + "plugins/Feedback/lang/sk.json" => array("107", "07afc9955d05d35a72428b2ccfc59ea0"), + "plugins/Feedback/lang/sl.json" => array("496", "896f696e7f7a045e37046cb3e167fbdb"), + "plugins/Feedback/lang/sq.json" => array("3096", "c64c513c3eb23d613e110842a0259c60"), + "plugins/Feedback/lang/sr.json" => array("2862", "1ecb863e3d83a3d823ae4f852f48b515"), + "plugins/Feedback/lang/sv.json" => array("2145", "51b6964bca358d02b973d1c6cfe57086"), + "plugins/Feedback/lang/ta.json" => array("780", "c76f589a520d322782c433c86e10c1d3"), + "plugins/Feedback/lang/th.json" => array("901", "fe2b62fa1a7a7a8bf9aebe680c9e9e15"), + "plugins/Feedback/lang/tl.json" => array("2026", "d12a2906a213f21a69c8cff449ce2053"), + "plugins/Feedback/lang/tr.json" => array("935", "d53d290f73793846b424bfc9ed36b2dd"), + "plugins/Feedback/lang/uk.json" => array("730", "4255f040455d4366dfe60591b6d61b1b"), + "plugins/Feedback/lang/vi.json" => array("690", "7e56808f8dba90fae84a6b82d952dfa3"), + "plugins/Feedback/lang/zh-cn.json" => array("524", "0ff1aea432da48bb3302fdd178363b59"), + "plugins/Feedback/lang/zh-tw.json" => array("541", "3e4af952f5a40ebdb1d4a559d6550ffe"), + "plugins/Feedback/Menu.php" => array("574", "f7bfdea0c96ff23b54d1e5593e4800b9"), + "plugins/Feedback/stylesheets/feedback.less" => array("1453", "20c642919047d5d1b4cf218a6da27d13"), + "plugins/Feedback/templates/index.twig" => array("6799", "e5f094c4c05b86cfea6b22c5577faac3"), + "plugins/Goals/API.php" => array("27293", "414b3d8d2d3ec916e7157164978dde65"), + "plugins/Goals/Archiver.php" => array("15402", "792fc1d66c67003bcb81171444869e03"), + "plugins/Goals/Columns/DaysToConversion.php" => array("382", "ed82de1a43d076d39b1f13ee65263603"), + "plugins/Goals/Columns/IdGoal.php" => array("820", "4948e53266d528003ab613c7fcbd5f45"), + "plugins/Goals/Columns/Metrics/AverageOrderRevenue.php" => array("1537", "fdd6c8454325c1e45fca23e279c82096"), + "plugins/Goals/Columns/Metrics/AveragePrice.php" => array("1705", "fb284dc40ff336d3abbca7a0ac926b90"), + "plugins/Goals/Columns/Metrics/AverageQuantity.php" => array("1194", "38cc026632b56dae8e2a2f01ad061231"), + "plugins/Goals/Columns/Metrics/GoalSpecific/AverageOrderRevenue.php" => array("1973", "3435e637d9d2b4435d6f24d669eac2a0"), + "plugins/Goals/Columns/Metrics/GoalSpecific/ConversionRate.php" => array("1741", "6a5dfa070629dc3e4e7da80f19d4cf48"), + "plugins/Goals/Columns/Metrics/GoalSpecific/Conversions.php" => array("1134", "87d24ac78898babe9b1fa65bc0227a1f"), + "plugins/Goals/Columns/Metrics/GoalSpecific/ItemsCount.php" => array("1241", "46702e9b3ca8fa511cd62681ed19ff5b"), + "plugins/Goals/Columns/Metrics/GoalSpecificProcessedMetric.php" => array("3072", "73fb7953a3f6c88226c6a8ba3ea5ac40"), + "plugins/Goals/Columns/Metrics/GoalSpecific/RevenuePerVisit.php" => array("2458", "41da5e857cd8e8ee1fe2a7eb0dee3f96"), + "plugins/Goals/Columns/Metrics/GoalSpecific/Revenue.php" => array("1670", "6bed1adb40da07865cf157dd7560392a"), + "plugins/Goals/Columns/Metrics/ProductConversionRate.php" => array("1391", "9e42694565dd94e4b487b3626dad2bc5"), + "plugins/Goals/Columns/Metrics/RevenuePerVisit.php" => array("2678", "753939d3cf5d575344813515a996c428"), + "plugins/Goals/Columns/VisitsUntilConversion.php" => array("392", "61cf0df1b4bece1b546e2d78134719d0"), + "plugins/Goals/Controller.php" => array("21420", "31e2ca4e3e5ab8e779bd0d4371178a92"), + "plugins/Goals/DataTable/Filter/AppendNameToColumnNames.php" => array("1544", "6a990a585c5e30871517a07c87dcd2f9"), + "plugins/Goals/Goals.php" => array("10191", "31e124f47f7d01e90b979c00356b9637"), + "plugins/Goals/javascripts/goalsForm.js" => array("7856", "c4c1e97de8fcc292a45759872ed9d5ac"), + "plugins/Goals/lang/am.json" => array("71", "febcb197ee67c57bae6fc0d67fb17ebf"), + "plugins/Goals/lang/ar.json" => array("4405", "a3cf10a7ac44514bae9a87a71d66f07a"), + "plugins/Goals/lang/be.json" => array("8907", "e1ba1d48df7fd9e1d4d127ff22888907"), + "plugins/Goals/lang/bg.json" => array("9810", "d78a188d4e7d1a1121e817589539650e"), + "plugins/Goals/lang/bs.json" => array("389", "448905e1a6a1d0348c6a9482013781aa"), + "plugins/Goals/lang/ca.json" => array("7266", "4634c3bc309fdcb8f991485f348ccfa9"), + "plugins/Goals/lang/cs.json" => array("8525", "31500eb011d62e5a45b478d4daa73b02"), + "plugins/Goals/lang/da.json" => array("7673", "b875ce9ffc1aa7a75aa36fb5364a3f42"), + "plugins/Goals/lang/de.json" => array("8835", "0967a3a87eb98acb6a5bbf1ec922d42a"), + "plugins/Goals/lang/el.json" => array("13952", "9dbd52783c46970f83022811b3914b1b"), + "plugins/Goals/lang/en.json" => array("8206", "f36e4dffee5c0e9b294259acdc3191d5"), + "plugins/Goals/lang/es.json" => array("8884", "dfa564a84ffaeb01fa06593413c0e7fd"), + "plugins/Goals/lang/et.json" => array("3399", "e35fdce62c10e714ea427da4d5688eba"), + "plugins/Goals/lang/eu.json" => array("949", "a96fba8b3f8aa6069232dacbb2a6a878"), + "plugins/Goals/lang/fa.json" => array("6481", "e351bd942f1f12270331ca2187d86372"), + "plugins/Goals/lang/fi.json" => array("6972", "52f957d6f49438c9b0567b1fbdad7435"), + "plugins/Goals/lang/fr.json" => array("9276", "5d34cd21ca39638f7e4eee6d10323e7a"), + "plugins/Goals/lang/gl.json" => array("398", "7bac6341a22ba0fb4675957946933aba"), + "plugins/Goals/lang/he.json" => array("66", "79417c11b0507e442f677340823dba52"), + "plugins/Goals/lang/hi.json" => array("13398", "3d0407b774732eb7ddf412751748134e"), + "plugins/Goals/lang/hu.json" => array("3234", "c8ced14246b09012157b18c7ce8c17fb"), + "plugins/Goals/lang/id.json" => array("6956", "46d8d3d02cfe7f523b158eb056606649"), + "plugins/Goals/lang/is.json" => array("1812", "557094c205c28f30844f4bf5cb0618ca"), + "plugins/Goals/lang/it.json" => array("8895", "fb6ea7c3be4a4e6cc2a832255f14cbb2"), + "plugins/Goals/lang/ja.json" => array("10002", "4da76c2e97a99fa2564932d47452a58f"), + "plugins/Goals/lang/ka.json" => array("5148", "2a14fd7fc2f7a4d24c9bed73b040e196"), + "plugins/Goals/lang/ko.json" => array("8587", "2b0c29916f451c83383be9088d83a434"), + "plugins/Goals/lang/lt.json" => array("2678", "207edfc35845fce83988d6d4410d899b"), + "plugins/Goals/lang/lv.json" => array("2367", "7ea4e57fba20cc5f27eb3dcc7766c742"), + "plugins/Goals/lang/nb.json" => array("2689", "2bbec6a2c3fbd3bf4805547972fe52f0"), + "plugins/Goals/lang/nl.json" => array("8620", "aa6cb3e5bd5f78b30b827aae07dbc562"), + "plugins/Goals/lang/nn.json" => array("2001", "61dc1905279e6e2908cb69d5d30d0abf"), + "plugins/Goals/lang/pl.json" => array("5191", "c935d9688b48908e4dafddfa9fcf168f"), + "plugins/Goals/lang/pt-br.json" => array("8928", "8f163eda7455265f39b2acfe37be81c3"), + "plugins/Goals/lang/pt.json" => array("7542", "352fc7c4f5220aa90fba0887de3ec1f8"), + "plugins/Goals/lang/ro.json" => array("7487", "604f604afff5245ea3ee1082c0d950de"), + "plugins/Goals/lang/ru.json" => array("12021", "7cd0803a6ef8c8091895f2f32d4e829d"), + "plugins/Goals/lang/sk.json" => array("3001", "efe3f5982af559c9f4d12aab957db8e6"), + "plugins/Goals/lang/sl.json" => array("1648", "3f79dfee0f48c91f7ae6d819254efe02"), + "plugins/Goals/lang/sq.json" => array("9417", "1409c9c0c0a0ba474a213a2670cf3320"), + "plugins/Goals/lang/sr.json" => array("8368", "3bedb1d42032916c522125c816563fcb"), + "plugins/Goals/lang/sv.json" => array("8063", "99ef13d1d7a8197e20d087517f963521"), + "plugins/Goals/lang/ta.json" => array("1685", "91df9cb5f6ffeaaf2d412390125692ef"), + "plugins/Goals/lang/te.json" => array("242", "4c5d0b0576b482bb8391abb7294a7fd3"), + "plugins/Goals/lang/th.json" => array("5593", "05ceea397a30cf3794a5b7edd654d4f5"), + "plugins/Goals/lang/tr.json" => array("2995", "60ebf10f0958f7fb44977ea02c260059"), + "plugins/Goals/lang/uk.json" => array("3843", "f7f91bdc53d58ee86994a6d8a5397b69"), + "plugins/Goals/lang/vi.json" => array("8632", "3bfa8b64f8168863e718266f06aa0986"), + "plugins/Goals/lang/zh-cn.json" => array("6029", "d4cc9115bc6f0fb26bd716380b893319"), + "plugins/Goals/lang/zh-tw.json" => array("2461", "edff7ebbde963ca0b0701683d5c65e13"), + "plugins/Goals/Menu.php" => array("2515", "5b4c7819ace7773690393bb046b0a9a1"), + "plugins/Goals/Model.php" => array("2380", "db71a051ec46ed8419a56db41e1d4ca1"), + "plugins/Goals/Reports/Base.php" => array("1487", "e45fa34e7c9f8ce552e5946484aa14bd"), + "plugins/Goals/Reports/GetDaysToConversion.php" => array("2076", "3211996a2c56b01efe0917497f7bb5d2"), + "plugins/Goals/Reports/GetMetrics.php" => array("796", "50aff6f80706b2dd4c4bd48af38c7e10"), + "plugins/Goals/Reports/Get.php" => array("1030", "e5ea6d8513113d5b8bbe48a7c8f40f2e"), + "plugins/Goals/Reports/GetVisitsUntilConversion.php" => array("2098", "af64afa4ba3abc017c73a3d5440cb3cd"), + "plugins/Goals/stylesheets/goals.css" => array("571", "b6a1300d4a90467c8379574ae8be92a3"), + "plugins/Goals/templates/_addEditGoal.twig" => array("3251", "f94098eef1e5713b458847fefcc89cd1"), + "plugins/Goals/templates/addNewGoal.twig" => array("890", "a885958baadf8603ac42f53fd384a7bd"), + "plugins/Goals/templates/editGoals.twig" => array("331", "17fa0c95106ad899870dadf6848121c1"), + "plugins/Goals/templates/_formAddGoal.twig" => array("5806", "1d279a3c42f3ed22c7800bb7203bb4cd"), + "plugins/Goals/templates/getGoalReportView.twig" => array("3269", "a0f5f57660aed077bdff5d64c2fc019f"), + "plugins/Goals/templates/getOverviewView.twig" => array("2021", "2fd51699b44cec934e6396dc9b4170bc"), + "plugins/Goals/templates/_listGoalEdit.twig" => array("3894", "14ee1105421bdceac6c4560d581ca150"), + "plugins/Goals/templates/_listTopDimension.twig" => array("629", "6ccbc50d4b3b501d5baa85cc35bbc94a"), + "plugins/Goals/templates/manageGoals.twig" => array("1526", "1d3a7c03ad50e87045b59217cc1b2978"), + "plugins/Goals/templates/_titleAndEvolutionGraph.twig" => array("4288", "366be17d9da15bd3cc7474ba7ab532e0"), + "plugins/Goals/Tracker/GoalsRequestProcessor.php" => array("5354", "f6ba71dbcb8846c0c2855bea776859db"), + "plugins/Goals/TranslationHelper.php" => array("3480", "063a70a7bf495a3515195cf208ccacc4"), + "plugins/Goals/Visualizations/Goals.php" => array("7273", "3ca5d0087dd32e672ac5e1ccd3f9ddf2"), + "plugins/Goals/Widgets.php" => array("915", "8e7acdd780424350fd1b8490ac1b688c"), + "plugins/Heartbeat/Heartbeat.php" => array("315", "1bcd69e82e3cbf1e7cb0b846e4b04892"), + "plugins/Heartbeat/plugin.json" => array("53", "934e6931668e548a080d085fecdd7949"), + "plugins/Heartbeat/Tracker/PingRequestProcessor.php" => array("1570", "2fb3fecbb11aeea7c4eac10893f64fc9"), + "plugins/ImageGraph/API.php" => array("23508", "4e6f9bf746766650f98cdf0c4143217a"), + "plugins/ImageGraph/Controller.php" => array("2651", "012cbc318556f2b95f32590ec090802f"), "plugins/ImageGraph/fonts/tahoma.ttf" => array("94740", "fabde5f388432a4a2bd40bc07ac53971"), - "plugins/ImageGraph/ImageGraph.php" => array("6041", "e604bd6a45f1e5c165319f0108826fbf"), - "plugins/ImageGraph/StaticGraph/Evolution.php" => array("576", "75d6bc92ee26a9153a19479fc26e81d6"), - "plugins/ImageGraph/StaticGraph/Exception.php" => array("1513", "b2e05ae76bb47496c259bbbb23632b1c"), - "plugins/ImageGraph/StaticGraph/GridGraph.php" => array("19969", "3beb0b57cb2837fac11c5f53be6605d3"), - "plugins/ImageGraph/StaticGraph/HorizontalBar.php" => array("7097", "a0b09ce105f15b874f7f867a71a80888"), - "plugins/ImageGraph/StaticGraph.php" => array("9417", "9d2e8b5b979bed66c7db0bbd64c87152"), - "plugins/ImageGraph/StaticGraph/Pie3D.php" => array("464", "f432e2a17d9c16fb86909bc596bacf44"), - "plugins/ImageGraph/StaticGraph/PieGraph.php" => array("4227", "d61366790d5b83979b81633631479e50"), - "plugins/ImageGraph/StaticGraph/Pie.php" => array("463", "4ffc2f6fbc909875f01df6159472d6c5"), - "plugins/ImageGraph/StaticGraph/VerticalBar.php" => array("700", "fa2f2c41aa965694711d69b94269f0c2"), + "plugins/ImageGraph/ImageGraph.php" => array("6796", "001fe9f452e292140e116100e8764fc9"), + "plugins/ImageGraph/lang/bg.json" => array("164", "40b9a40e87ad6fcac13eff40ad4ae240"), + "plugins/ImageGraph/lang/ca.json" => array("150", "1fe6f37114be95681fa4627db68115ed"), + "plugins/ImageGraph/lang/cs.json" => array("232", "157b2c1033672b7ea95ed133be65e528"), + "plugins/ImageGraph/lang/da.json" => array("230", "ebb795d9321b1736d7b3bf0776b5f9b0"), + "plugins/ImageGraph/lang/de.json" => array("270", "9067c75797b6d6f6d13e064680029a26"), + "plugins/ImageGraph/lang/el.json" => array("380", "da0817f582b365b51c40f62c29a2d085"), + "plugins/ImageGraph/lang/en.json" => array("232", "b20badf10740f0e664d9d922c3bdfe5e"), + "plugins/ImageGraph/lang/es.json" => array("268", "cededde68a7332b8528b1bcbdfe64b78"), + "plugins/ImageGraph/lang/fa.json" => array("152", "2fe660c925b5a0a59a1b72e77fda671b"), + "plugins/ImageGraph/lang/fi.json" => array("140", "e402c7b3bbf817e30a1ac61849355cb9"), + "plugins/ImageGraph/lang/fr.json" => array("295", "738816d87eab06cfe9648662e1e716fa"), + "plugins/ImageGraph/lang/hi.json" => array("436", "dc982400b054102df811996f6687cd05"), + "plugins/ImageGraph/lang/id.json" => array("138", "97e9847c5627323ea35092dcd51aa2a9"), + "plugins/ImageGraph/lang/it.json" => array("263", "6b4b2e936c5f296e2113fbd87908bd88"), + "plugins/ImageGraph/lang/ja.json" => array("318", "026c0794cfbd384569475037873145b8"), + "plugins/ImageGraph/lang/ko.json" => array("143", "27df6acce9882e66851c58796cbecec2"), + "plugins/ImageGraph/lang/lv.json" => array("141", "01e4f605267fbf444537f9f98754c68d"), + "plugins/ImageGraph/lang/nb.json" => array("222", "00cb76b9d51ba84c127a99f4597d21c9"), + "plugins/ImageGraph/lang/nl.json" => array("250", "22dd956078d8fca91917accc1101692f"), + "plugins/ImageGraph/lang/nn.json" => array("128", "3bca6be383e1a3902a20370ae0011514"), + "plugins/ImageGraph/lang/pt-br.json" => array("245", "0c8e0d99b0eaba1345a44b2e4bdfc69f"), + "plugins/ImageGraph/lang/pt.json" => array("133", "5c482101c5c91c3687142242da39ec29"), + "plugins/ImageGraph/lang/ro.json" => array("142", "817812eeec015e376a94b3d28312ba03"), + "plugins/ImageGraph/lang/ru.json" => array("196", "fb386cf4f486c1e11f225cbc6e55d5fc"), + "plugins/ImageGraph/lang/sl.json" => array("124", "3924f93696402c518704bdf2e3c2931a"), + "plugins/ImageGraph/lang/sq.json" => array("147", "20d1de2f6808af4cd47aa0cbc737a432"), + "plugins/ImageGraph/lang/sr.json" => array("249", "9a368f7a451029168b89ab06776e840d"), + "plugins/ImageGraph/lang/sv.json" => array("230", "0e66ea7f07ad8bd73ca8f733816443ae"), + "plugins/ImageGraph/lang/vi.json" => array("146", "4282dc22e53a05b930b0a7d82a8ecdb9"), + "plugins/ImageGraph/lang/zh-cn.json" => array("108", "95a367dc7f16634127f32910c7c13ca2"), + "plugins/ImageGraph/StaticGraph/Evolution.php" => array("579", "c94186d1c04aa6068588076e167f8a16"), + "plugins/ImageGraph/StaticGraph/Exception.php" => array("1517", "4c6a133b0727e027b1cd0d907ad5eb4a"), + "plugins/ImageGraph/StaticGraph/GridGraph.php" => array("19982", "6466972ede0946317dc9442c4171a58b"), + "plugins/ImageGraph/StaticGraph/HorizontalBar.php" => array("7100", "01f8036220239c5f89e5c966bccee565"), + "plugins/ImageGraph/StaticGraph.php" => array("9352", "263f0666d0ac79042dd7bade08662de8"), + "plugins/ImageGraph/StaticGraph/Pie3D.php" => array("468", "7e9300222c549c8c34b1c46643864dd9"), + "plugins/ImageGraph/StaticGraph/PieGraph.php" => array("4226", "4abbf922b246f16285aa9c077a6e79c4"), + "plugins/ImageGraph/StaticGraph/Pie.php" => array("466", "9e1ebb3cb89aff2fcd58e0c62cdd1f60"), + "plugins/ImageGraph/StaticGraph/VerticalBar.php" => array("703", "1074ef95a75b9d28778d3ae08f426596"), "plugins/ImageGraph/templates/index.twig" => array("148", "29658e5544cd076021ea898993b8506d"), - "plugins/ImageGraph/templates/testAllSizes.twig" => array("2557", "2eca39df92b75a1c67933d928d8815e9"), - "plugins/Insights/API.php" => array("13847", "788dd0b6a9e6d3ead74f319a706b94db"), - "plugins/Insights/Controller.php" => array("2184", "2f3d6d7f84ecddfb5b75a1ad780ace40"), - "plugins/Insights/DataTable/Filter/ExcludeLowValue.php" => array("1607", "bde83b71854b84f55894e754cc36b43f"), - "plugins/Insights/DataTable/Filter/Insight.php" => array("3876", "d5b9908e494619123f6c61d1e2a9aeb0"), - "plugins/Insights/DataTable/Filter/Limit.php" => array("1427", "896f5aaa0f1203918f09bcd96152c713"), - "plugins/Insights/DataTable/Filter/MinGrowth.php" => array("1489", "f385e31013c7e27a8b178ce5cf193aed"), - "plugins/Insights/DataTable/Filter/OrderBy.php" => array("1984", "fa2aa2e46620f2d92fa2aac30960efa1"), + "plugins/ImageGraph/templates/testAllSizes.twig" => array("2538", "e0d7b01414a8126ec7d3cbdda5f09111"), + "plugins/Insights/API.php" => array("13712", "86d3a6e694290b13facd3de37247a1c1"), + "plugins/Insights/Controller.php" => array("2280", "7895de22b6b085f935f0323560ff40f9"), + "plugins/Insights/DataTable/Filter/ExcludeLowValue.php" => array("1611", "46455071eab3b0b0fc07b81a316930f4"), + "plugins/Insights/DataTable/Filter/Insight.php" => array("3880", "0b70287dbb10551d12324c2f7f8f03e7"), + "plugins/Insights/DataTable/Filter/Limit.php" => array("1431", "6eb80c876549f33ea66827dbed49ff63"), + "plugins/Insights/DataTable/Filter/MinGrowth.php" => array("1493", "33bb8452f7037a36ff121e0530647be8"), + "plugins/Insights/DataTable/Filter/OrderBy.php" => array("1988", "6c920e99b0eca4866d9502499b44687e"), "plugins/Insights/images/idea.png" => array("364", "bc677415eb4fb941a85c568b6aa656ba"), - "plugins/Insights/InsightReport.php" => array("11233", "e74af746ab91d381bb04cb1cf5c9506a"), - "plugins/Insights/Insights.php" => array("1412", "00a6ff865dc2ed2728dd8dd58438784b"), - "plugins/Insights/javascripts/insightsDataTable.js" => array("3882", "559f938d2e4a3e48ceb0e2827c2a33c3"), - "plugins/Insights/Model.php" => array("3366", "ca38127d95af08cb9f7e7aa1744f396c"), - "plugins/Insights/plugin.json" => array("284", "15013f4f566ee639b3d4566468b46fe8"), - "plugins/Insights/stylesheets/insightVisualization.less" => array("619", "5cb33ec3db0c5d33d3d7714ec80be638"), + "plugins/Insights/InsightReport.php" => array("11237", "ae427f0dbf5e4712ada47bc1e592b76c"), + "plugins/Insights/Insights.php" => array("792", "060b03198eff6d1f17e19565c9dba157"), + "plugins/Insights/javascripts/insightsDataTable.js" => array("3897", "b55359d1e089adcc00be9fa4f975ac9c"), + "plugins/Insights/lang/bg.json" => array("885", "31cff85af21f3608d428bb58dc77aef3"), + "plugins/Insights/lang/cs.json" => array("2788", "3a56d36d182f3989e1bc5a140ae8a255"), + "plugins/Insights/lang/da.json" => array("2460", "d30c9e56afce12659754775ce0f218ee"), + "plugins/Insights/lang/de.json" => array("2941", "fdb67878c5763c4ac550e105393beb7b"), + "plugins/Insights/lang/el.json" => array("4097", "ef758cb7d3e5cdaf48465092ba9a78ff"), + "plugins/Insights/lang/en.json" => array("2636", "a0b8eb4465e901d8efd7f73486cad81d"), + "plugins/Insights/lang/es.json" => array("2875", "a23a0429b5c51847e61959c7d1351c4b"), + "plugins/Insights/lang/et.json" => array("354", "07fe868c73ef66b2668000cc0a2d3e75"), + "plugins/Insights/lang/fa.json" => array("581", "133cd20f6513dcbbfe7368d023c3f1b8"), + "plugins/Insights/lang/fi.json" => array("1240", "9216bd8baa2966f9226d6c19fd50a23d"), + "plugins/Insights/lang/fr.json" => array("2969", "e1b935b5497205c534103e1ca3fdfea7"), + "plugins/Insights/lang/gl.json" => array("404", "b88d99175e88efd14a4568b36be4a81b"), + "plugins/Insights/lang/hi.json" => array("2417", "aa9ab09aae2fa67e9eafd20f54f089ee"), + "plugins/Insights/lang/it.json" => array("2819", "0bd93bf32acf0536ce0fda71690feebe"), + "plugins/Insights/lang/ja.json" => array("2947", "b47c2502fb0de5872f5ba9e712b5aab5"), + "plugins/Insights/lang/nb.json" => array("685", "e0bb01b2e20508cb68b86677f898c151"), + "plugins/Insights/lang/nl.json" => array("2775", "74419977d60273df39b868a87c39e555"), + "plugins/Insights/lang/pl.json" => array("929", "5daca4856a87db83fe54d3fcdba6f969"), + "plugins/Insights/lang/pt-br.json" => array("2761", "851f1f50e14c121ad0f36b375ea55c89"), + "plugins/Insights/lang/pt.json" => array("396", "31f76fa16b16254d498abca339c4629e"), + "plugins/Insights/lang/ro.json" => array("2662", "36b751f266c058a6db57295307b724d4"), + "plugins/Insights/lang/ru.json" => array("1120", "efd3bca887495d9a6fe0e363656ade97"), + "plugins/Insights/lang/sq.json" => array("473", "83bd2547e8f79e6a63b28fe8f21cce98"), + "plugins/Insights/lang/sr.json" => array("2688", "28fded4f7c9852f3d5659de4510f2a71"), + "plugins/Insights/lang/sv.json" => array("2526", "a72ae467459cbf8df35d2fac1fabe613"), + "plugins/Insights/lang/ta.json" => array("546", "2fd564494b0a1c7745cef6a8c47c5f4b"), + "plugins/Insights/lang/tl.json" => array("1392", "a9ecaaab526b11a7bfdaf3176fb23155"), + "plugins/Insights/lang/tr.json" => array("526", "9e08fce590e817743aa7150c1e37727b"), + "plugins/Insights/lang/vi.json" => array("85", "ecfe4c8dcd57490f4a178800db7eafda"), + "plugins/Insights/lang/zh-cn.json" => array("396", "92b8c60c095d74c1c268f88c2d1036f8"), + "plugins/Insights/Model.php" => array("3367", "4b45524fc79cc679c0d64f6dda643b32"), + "plugins/Insights/stylesheets/insightVisualization.less" => array("665", "6b387ea5d5407decffb9a2d0951d9c44"), "plugins/Insights/templates/cannotDisplayReport.twig" => array("103", "02d257f7747a1f7734235a56faa900ba"), - "plugins/Insights/templates/insightControls.twig" => array("3190", "a1b43257232d5e18e746f3839b8e17be"), - "plugins/Insights/templates/insightsOverviewWidget.twig" => array("314", "19c200a37664de6dda1a858e2e133df4"), + "plugins/Insights/templates/insightControls.twig" => array("3193", "d776fef3d93201b4c80642cbbaaed112"), + "plugins/Insights/templates/insightsOverviewWidget.twig" => array("448", "f3eee60b34c0493abd1d6bf6184f76e6"), "plugins/Insights/templates/insightVisualization.twig" => array("1360", "7036f0548b2901935552b9929bf4bd6d"), - "plugins/Insights/templates/moversAndShakersOverviewWidget.twig" => array("672", "26093e2b2575a0b1b058d8b15e30f02e"), + "plugins/Insights/templates/moversAndShakersOverviewWidget.twig" => array("842", "310ee58e56eb9093d17d77681c7f02af"), "plugins/Insights/templates/overviewWidget.twig" => array("1270", "b420d52e333ea4ca167b897de920a029"), - "plugins/Insights/templates/table_header.twig" => array("440", "6f5ad76f20c6fa8536612ffe632a8d99"), + "plugins/Insights/templates/table_header.twig" => array("451", "247e1df4a5bcca625611397a34cb2d76"), "plugins/Insights/templates/table_row.twig" => array("1497", "955a553498046e53131a3320fab79a6f"), - "plugins/Insights/Visualizations/Insight.php" => array("3714", "028a760f1f2c2fd291f0609881aac743"), - "plugins/Insights/Visualizations/Insight/RequestConfig.php" => array("1254", "e62e3a81b32acf3e8f834ff688d9e94f"), - "plugins/Installation/Controller.php" => array("37029", "f9b9cc3440e4b8ebf9a4ba4f5eb5ebe0"), - "plugins/Installation/FormDatabaseSetup.php" => array("11591", "fb9e70d02e5a8201d1455ea6b9cedd13"), - "plugins/Installation/FormFirstWebsiteSetup.php" => array("3369", "e3e7a92773c26cedb6d7b3f9801cf7a3"), - "plugins/Installation/FormGeneralSetup.php" => array("4342", "74f162e30c4601c9bc2d06fa0a26590f"), - "plugins/Installation/Installation.php" => array("3249", "3ad87571d8d4abdc72779452f3b2bb9e"), - "plugins/Installation/javascripts/installation.js" => array("307", "4c37587c9e1d490726acb45f723ca7b9"), - "plugins/Installation/ServerFilesGenerator.php" => array("4673", "64c8e3186155de098d2b7f47cf2f6fee"), - "plugins/Installation/stylesheets/installation.css" => array("3791", "821b94028ac1602a34fce93061d3a7ca"), - "plugins/Installation/stylesheets/systemCheckPage.less" => array("778", "cae7f2f24da86ea2b691ebfed87b8e4c"), - "plugins/Installation/templates/_allSteps.twig" => array("392", "82b5478f80593011dbcf3ecf0af61302"), - "plugins/Installation/templates/databaseCheck.twig" => array("1462", "0ec6aa932fdbbb9fc9a21c498f9d38c4"), - "plugins/Installation/templates/databaseSetup.twig" => array("453", "126314587ffced4a2251edfdf9805169"), - "plugins/Installation/templates/finished.twig" => array("828", "98ccc1a91a49856f891a514a434df74c"), - "plugins/Installation/templates/firstWebsiteSetup.twig" => array("803", "c069ebc5ad0dcd2f032de51e12718437"), - "plugins/Installation/templates/generalSetup.twig" => array("450", "bfbb3e56dfd595a6085034ece031ce5c"), - "plugins/Installation/templates/_integrityDetails.twig" => array("1157", "26f9f8e6a60611f972182b9188cb40ce"), - "plugins/Installation/templates/layout.twig" => array("2369", "171a8bd7aa74aa4e0993c68c6ce69937"), - "plugins/Installation/templates/reuseTables.twig" => array("2884", "11a9acbfc09adb2bdeb2b37c9c810fe0"), - "plugins/Installation/templates/_systemCheckLegend.twig" => array("783", "868726c336470173b390efd117db3eb8"), - "plugins/Installation/templates/systemCheckPage.twig" => array("959", "2e4074ddeff70cf07b68a34500647a42"), - "plugins/Installation/templates/_systemCheckSection.twig" => array("13651", "3ae35c6efa7d38f24dc0fc0206988e91"), - "plugins/Installation/templates/systemCheck.twig" => array("654", "4ca83235ac1786803d690125beb943d5"), - "plugins/Installation/templates/tablesCreation.twig" => array("2445", "0713ca27f416427d8dde18759284e5fa"), - "plugins/Installation/templates/trackingCode.twig" => array("678", "e13755a135a80b77df5960ca92b59174"), - "plugins/Installation/templates/welcome.twig" => array("1341", "c04fe82942a7b099e7d35c32f2752b33"), - "plugins/Installation/View.php" => array("1591", "7433b9692f499c2c3f8804cd8095c788"), - "plugins/LanguagesManager/API.php" => array("9715", "bf712c41d7a1171976490570ab3dac04"), - "plugins/LanguagesManager/Commands/CreatePull.php" => array("8019", "da7be9f183aed2f38461d989a9841ac8"), - "plugins/LanguagesManager/Commands/FetchFromOTrance.php" => array("6166", "19f36b874c56ad9767ce0a3378d9351d"), - "plugins/LanguagesManager/Commands/LanguageCodes.php" => array("1063", "283814548ee804e448d6f1e0e5b90739"), - "plugins/LanguagesManager/Commands/LanguageNames.php" => array("1071", "c7df597dd84037238cba1369489ad396"), - "plugins/LanguagesManager/Commands/PluginsWithTranslations.php" => array("1192", "27e732d20ad5343ad54c2671c7f469bc"), - "plugins/LanguagesManager/Commands/SetTranslations.php" => array("3990", "0197fbf7ed6a56ec8e20edd236487cd0"), - "plugins/LanguagesManager/Commands/Update.php" => array("5748", "e71235b082bad8b9c1f67b941d4ece99"), - "plugins/LanguagesManager/Controller.php" => array("813", "ccfc729a22e4398f1caffd2b4ee32d4b"), - "plugins/LanguagesManager/javascripts/languageSelector.js" => array("2545", "8cad615cf92b73f4c3401419957c2abf"), - "plugins/LanguagesManager/LanguagesManager.php" => array("6527", "522f063e16bec7dd87d380fca5095f18"), - "plugins/LanguagesManager/templates/getLanguagesSelector.twig" => array("1015", "61337194fa9393f34a99726587c6939e"), - "plugins/LeftMenu/plugin.json" => array("128", "af274a9b50394d8054b01dd4508cfcf1"), - "plugins/LeftMenu/stylesheets/theme.less" => array("3139", "3e1b68f368095012788e07bce8e111bd"), - "plugins/Live/API.php" => array("29944", "4aa60c7abc780bb6dfa2ecb0385e6e66"), - "plugins/Live/Controller.php" => array("10331", "d0abb0c4882fd59af19da0968dc063b5"), + "plugins/Insights/Visualizations/Insight.php" => array("4103", "98867a6965e89d66bce76ff6415d8ba5"), + "plugins/Insights/Visualizations/Insight/RequestConfig.php" => array("1211", "13a703baef884bc8596a940efa4d79cc"), + "plugins/Insights/Widgets.php" => array("509", "bc27a29a2817b16007d50d520c7482d8"), + "plugins/Installation/Controller.php" => array("22430", "4a353d17a3831f9ff2e3a4d01c561e23"), + "plugins/Installation/Exception/DatabaseConnectionFailedException.php" => array("298", "ad427b8c36db4be3d838b10abf4c0589"), + "plugins/Installation/FormDatabaseSetup.php" => array("11785", "beac154eed9bd7983953809a14595efb"), + "plugins/Installation/FormDefaultSettings.php" => array("508", "d51b3027dc005df92ee2a47faced2c83"), + "plugins/Installation/FormFirstWebsiteSetup.php" => array("3488", "c7bd30e64957c47615384670024f63fe"), + "plugins/Installation/FormSuperUser.php" => array("4808", "9738112343287d2f4b6d1fcb30aea637"), + "plugins/Installation/Installation.php" => array("3516", "f330802ccc26faa2ce6fe034c3e3ae59"), + "plugins/Installation/javascripts/installation.js" => array("218", "f7fc61747c2f75144384a3ce57e28a0b"), + "plugins/Installation/lang/am.json" => array("4878", "ebd4923a3a361e0644d1ab2d62d8fe44"), + "plugins/Installation/lang/ar.json" => array("17262", "825ed2687b495379dcedcffcbec4ceac"), + "plugins/Installation/lang/be.json" => array("11390", "394c140c924212003709d912392880fa"), + "plugins/Installation/lang/bg.json" => array("15108", "f45d272a9c901cd52592b69fd4874d45"), + "plugins/Installation/lang/bs.json" => array("117", "15c746f8b9a53fa730205ebdc3bbbc31"), + "plugins/Installation/lang/ca.json" => array("10628", "37d730b28f60a6e612877b2238e6dec1"), + "plugins/Installation/lang/cs.json" => array("14378", "25adc3f1b47d5c48c034ebd9baded294"), + "plugins/Installation/lang/da.json" => array("12557", "0fd5efbb9c720cf08c9a64644e98380e"), + "plugins/Installation/lang/de.json" => array("15604", "8e2db325e658988200c231e2b1ccf902"), + "plugins/Installation/lang/el.json" => array("23702", "cdbecbccdf5f5e3d0e6ccd55d0dd7753"), + "plugins/Installation/lang/en.json" => array("13957", "354f99e8f0c7213ea403ab7bcf9721c3"), + "plugins/Installation/lang/es.json" => array("15219", "820bf1e19bb60565d598ab9619d164ab"), + "plugins/Installation/lang/et.json" => array("7428", "35c24aac9416a2f8d888c4eb19a1c2b2"), + "plugins/Installation/lang/eu.json" => array("5338", "3d24149159a692b45a5ff56a143fe4ff"), + "plugins/Installation/lang/fa.json" => array("13424", "0bbf3bef5b209accd87e98beaeb9c650"), + "plugins/Installation/lang/fi.json" => array("11498", "3f8ff8bbdec29c980afb153193e51c0a"), + "plugins/Installation/lang/fr.json" => array("15430", "71e45e1115546d0ad5756c6779bb58d6"), + "plugins/Installation/lang/gl.json" => array("2859", "bf1912d9c35ee95054922db902006dd9"), + "plugins/Installation/lang/he.json" => array("882", "5124ec440e6e94cd327e3515967475cd"), + "plugins/Installation/lang/hi.json" => array("22999", "5e36c0db5f169a6d758e8ddda850b4ba"), + "plugins/Installation/lang/hu.json" => array("8768", "5493bf2db8f70e3321fdf8ca9aa91ecb"), + "plugins/Installation/lang/id.json" => array("10684", "ca40fda28e607dc4c7fd443390280af3"), + "plugins/Installation/lang/is.json" => array("83", "50e992af7138f3c35079e850e592865d"), + "plugins/Installation/lang/it.json" => array("14808", "b327e0c2aa1ead1071b3aebe29375df1"), + "plugins/Installation/lang/ja.json" => array("18301", "771d8a69c2a629c606d7e7b5ddc49f1a"), + "plugins/Installation/lang/ka.json" => array("15842", "fc71f3330731b70f4cdfecc8c53d95c5"), + "plugins/Installation/lang/ko.json" => array("15717", "cd344b0b42d50aff1a2cf66b3f7c9d62"), + "plugins/Installation/lang/lt.json" => array("8884", "271c41b01d58775c4cb66b5d45cbefb1"), + "plugins/Installation/lang/lv.json" => array("3893", "78346c52194a82702e71b4db3157a0ab"), + "plugins/Installation/lang/nb.json" => array("13910", "3661b6e16d31142b8230bfb064422bf4"), + "plugins/Installation/lang/nl.json" => array("14265", "20e0d8ce21dba8871d65d96f611fbcc3"), + "plugins/Installation/lang/nn.json" => array("6296", "689dbf9ce6293f73c0db1f49a22f9a39"), + "plugins/Installation/lang/pl.json" => array("10652", "ac06ca063057b8de4d0dbdef25947664"), + "plugins/Installation/lang/pt-br.json" => array("14887", "294e22675dc5cf90f2e5083985bf5b61"), + "plugins/Installation/lang/pt.json" => array("8128", "f0954d223b9cab04a56ab23cb19b4c86"), + "plugins/Installation/lang/ro.json" => array("12819", "0042a1dcd3e624ed5bd0d23b1af902a6"), + "plugins/Installation/lang/ru.json" => array("20383", "0d8e165cc9621e348a69af43c5dd7fc6"), + "plugins/Installation/lang/sk.json" => array("5477", "a29ac07089dcb2d53855fd07f5635099"), + "plugins/Installation/lang/sl.json" => array("1427", "ff0311c82246af47fa3fc069e3d4b912"), + "plugins/Installation/lang/sq.json" => array("8479", "85f17f5c0a496b562c9f0e6ca273adcc"), + "plugins/Installation/lang/sr.json" => array("14232", "cde224bf1f576c5e42262ab4331c3b8a"), + "plugins/Installation/lang/sv.json" => array("13886", "ba81de0e86e77278afd905069815f172"), + "plugins/Installation/lang/ta.json" => array("6322", "59bd0a6af4f6bb39a03edb3adeefdde0"), + "plugins/Installation/lang/te.json" => array("634", "3c2a74c59e80cf5f41bb46478533727e"), + "plugins/Installation/lang/th.json" => array("14474", "8ef1f23088d0b5bcc150957f84fefce9"), + "plugins/Installation/lang/tl.json" => array("13140", "15a9a93b7659b36ae796ef21737aaa9a"), + "plugins/Installation/lang/tr.json" => array("3836", "6914d786a5a525fd40cee51ed54d5bd4"), + "plugins/Installation/lang/uk.json" => array("11093", "dfa752604f2f82f24b9b1252c5217219"), + "plugins/Installation/lang/vi.json" => array("12837", "b051e80853e390877b89511d815f9beb"), + "plugins/Installation/lang/zh-cn.json" => array("12711", "9c394cfd87c06e5e6d67c24bfa092950"), + "plugins/Installation/lang/zh-tw.json" => array("6086", "2623795a3784f6df933fb40bbd5baeb2"), + "plugins/Installation/Menu.php" => array("594", "9041123f60ee642e71622c992842b7e7"), + "plugins/Installation/ServerFilesGenerator.php" => array("9054", "b792c7ad43197f880ee2a8e844895cec"), + "plugins/Installation/stylesheets/installation.css" => array("1789", "e4fef5af446b6793668e66669308d603"), + "plugins/Installation/stylesheets/systemCheckPage.less" => array("499", "ede138b18bc9f13d5e8e7632ce12d19b"), + "plugins/Installation/templates/cannotConnectToDb.twig" => array("243", "61a077ac42009bc96b54efdf39588a02"), + "plugins/Installation/templates/databaseSetup.twig" => array("440", "bfd02cb2898320b3ce4f5a9ad69cd878"), + "plugins/Installation/templates/finished.twig" => array("1773", "67a5f71a6838d28f87c0000056197d99"), + "plugins/Installation/templates/firstWebsiteSetup.twig" => array("803", "a25759307147ba384e21ab08abf238c2"), + "plugins/Installation/templates/_integrityDetails.twig" => array("1046", "a4469bea7975373a3d7e301cd06d2015"), + "plugins/Installation/templates/layout.twig" => array("4444", "5376036fa64490fbcf2a519654a710c7"), + "plugins/Installation/templates/reuseTables.twig" => array("3096", "15edbfd05d7434b42531cc53fc0ac589"), + "plugins/Installation/templates/setupSuperUser.twig" => array("442", "ad5735327a9faa341f0a52d299effc08"), + "plugins/Installation/templates/_systemCheckLegend.twig" => array("587", "8d9765ec34df69af628bf1bbc320b4b5"), + "plugins/Installation/templates/systemCheckPage.twig" => array("951", "41b103d606d6809b18e8452c87d4efdb"), + "plugins/Installation/templates/_systemCheckSection.twig" => array("1759", "1ba0eef3e2f542337a5f75975c9aba0f"), + "plugins/Installation/templates/systemCheck.twig" => array("1046", "0d955f5974abc2b89aa7cefbeffd13c6"), + "plugins/Installation/templates/tablesCreation.twig" => array("1949", "da23e832feec7b2d9ca9aea9c6abaefb"), + "plugins/Installation/templates/trackingCode.twig" => array("740", "e4d7784d71ee4e0a3222a0899b1d8d4b"), + "plugins/Installation/templates/welcome.twig" => array("1000", "bd5e4f10d1213a96f92a93cf0d3cee64"), + "plugins/Installation/View.php" => array("1594", "fec00004367c77db0bec62d25d2d46bb"), + "plugins/Intl/Commands/GenerateIntl.php" => array("21836", "fd831ce25fc3d97f4a1b39d6fbc95af6"), + "plugins/Intl/config/config.php" => array("135", "a9d64043bb991c45ef9b4b16d18ff639"), + "plugins/Intl/DateTimeFormatProvider.php" => array("3691", "2b16c4a5fd54d1215e12456e178d2bf6"), + "plugins/Intl/Intl.php" => array("225", "40df032029a8e759da56ddbe5196c5d6"), + "plugins/Intl/lang/am.json" => array("24045", "590fedd43b0d89faeadba060de76e290"), + "plugins/Intl/lang/ar.json" => array("25705", "1a3ff85778970270973639fb8eb35b09"), + "plugins/Intl/lang/be.json" => array("24344", "aa6f7e344447b8c110c8aa848629bc97"), + "plugins/Intl/lang/bg.json" => array("25417", "79ce028da911b65e6896b20aec10f80c"), + "plugins/Intl/lang/bn.json" => array("30776", "ee2d5ae15ab436b01ff5307f714cad47"), + "plugins/Intl/lang/bs.json" => array("21215", "279f142a55e5ed9c9e1a8b82b26dc373"), + "plugins/Intl/lang/ca.json" => array("21100", "65ec00eeac00c2da68c4b5f4550bfc0a"), + "plugins/Intl/lang/cs.json" => array("21897", "e7635480b4e937d982cac42b7a7ade86"), + "plugins/Intl/lang/cy.json" => array("20933", "973eb4cc61967ae863dd7e9a7dff823f"), + "plugins/Intl/lang/da.json" => array("20957", "90f7676d4364c9878bfebfa36d3898f7"), + "plugins/Intl/lang/de.json" => array("21429", "5366dc9b518eb82b573f9014b93b96f6"), + "plugins/Intl/lang/dev.json" => array("104", "2cb82b1b19a40b37ecf0ecbb22fc5fc0"), + "plugins/Intl/lang/el.json" => array("26095", "0564f92f44d355c4cb0ed7eb804507fb"), + "plugins/Intl/lang/en.json" => array("20840", "6524428d14c4e905ef073ce7ff1cbb84"), + "plugins/Intl/lang/es.json" => array("21145", "6c366cb25521d9834bee9d42a30a6628"), + "plugins/Intl/lang/et.json" => array("20875", "f66696184f4f279ed318b4abfc14a44b"), + "plugins/Intl/lang/eu.json" => array("20439", "2067fd1637591579ee0afaace3b367bf"), + "plugins/Intl/lang/fa.json" => array("24533", "2473fef2157235da833ccb219e12e427"), + "plugins/Intl/lang/fi.json" => array("21081", "2f6b5c22d86c23eaa0419f1bf445f934"), + "plugins/Intl/lang/fr.json" => array("21246", "59bcf97e69be0c6af577b67da94ee2f9"), + "plugins/Intl/lang/gl.json" => array("19899", "dea93859a3dfc291416eb6319edba479"), + "plugins/Intl/lang/he.json" => array("24287", "712abf9af17f92a5479d5f817df968d1"), + "plugins/Intl/lang/hi.json" => array("29364", "51d4da364f210f91a6078f6979477710"), + "plugins/Intl/lang/hr.json" => array("21251", "a5a7d3c2d748dcbb3e16262adce5401b"), + "plugins/Intl/lang/hu.json" => array("21260", "c7318f7b64ada249be4fa4b493eae415"), + "plugins/Intl/lang/id.json" => array("20763", "83bc4d7f0fa5043e61ea98192508b7f3"), + "plugins/Intl/lang/is.json" => array("21615", "a362e3573d6b3079fce1711af4e00168"), + "plugins/Intl/lang/it.json" => array("20994", "9216570419deb065f29f30b93ba9cbeb"), + "plugins/Intl/lang/ja.json" => array("24221", "0175c86045848e159abc1054ffd491b7"), + "plugins/Intl/lang/ka.json" => array("29659", "81175b8a13cf72f49f173cb549b49dc2"), + "plugins/Intl/lang/ko.json" => array("22888", "7c92fbece129607021c7c14fd4976801"), + "plugins/Intl/lang/lt.json" => array("21686", "de036770d0f088ba39bba9accc162fd3"), + "plugins/Intl/lang/lv.json" => array("21509", "6a198d5a1713d506c1997df4c5657a68"), + "plugins/Intl/lang/nb.json" => array("20907", "43a43b70d779da76015ae4f728334fbf"), + "plugins/Intl/lang/nl.json" => array("21096", "3e4e804da517c1863794a4948a7dddc2"), + "plugins/Intl/lang/nn.json" => array("20746", "c2dea67688b157bb98d49d27d63a87ba"), + "plugins/Intl/lang/pl.json" => array("21338", "013c59397d949c9a82ed2a2117789e5c"), + "plugins/Intl/lang/pt-br.json" => array("21342", "9aa5e60c0c21eca7c5836705aa077153"), + "plugins/Intl/lang/pt.json" => array("21331", "c07578c1e43eccd45af9b681a7e05b22"), + "plugins/Intl/lang/ro.json" => array("21283", "f1c0900d7f126f5545b59bb117e4ab28"), + "plugins/Intl/lang/ru.json" => array("25669", "e1a41f7597c0b5e21e8f861f9f4e7be5"), + "plugins/Intl/lang/sk.json" => array("21703", "a91d95af1afe150953b2adadbc8e2c78"), + "plugins/Intl/lang/sl.json" => array("21935", "30ccfebc3cc90e34994cfe7e37515471"), + "plugins/Intl/lang/sq.json" => array("19736", "a4015aa9a61f593915c7c00e330a69c7"), + "plugins/Intl/lang/sr.json" => array("25383", "503baea030beba25cfb3cdb35c8c1f0d"), + "plugins/Intl/lang/sv.json" => array("21048", "c682caa54de6bd93dea7caed9ff29dc9"), + "plugins/Intl/lang/ta.json" => array("30749", "67b62e8afd7bb5c8c1f1a6234d9def89"), + "plugins/Intl/lang/te.json" => array("30394", "8e7372b7084f6809d9c2daf460bede10"), + "plugins/Intl/lang/th.json" => array("29696", "2bb6a1d46c84305bfe7698d068132396"), + "plugins/Intl/lang/tl.json" => array("19916", "735942eca7e0b0f537ac8c42a67f0f88"), + "plugins/Intl/lang/tr.json" => array("21136", "af91ac5e643b6bf62098a57c943042db"), + "plugins/Intl/lang/uk.json" => array("25849", "aa69153b79f00558e8acb9468ded1e69"), + "plugins/Intl/lang/vi.json" => array("22853", "dac554241b03aa259220f05e41e7b691"), + "plugins/Intl/lang/zh-cn.json" => array("22357", "2ebd754b86c69fdd3233eca5b9c7c99d"), + "plugins/Intl/lang/zh-tw.json" => array("22248", "dde8e5e16f4b588dad50c07c53983a91"), + "plugins/LanguagesManager/angularjs/languageselector/languageselector.directive.js" => array("962", "7f7472fe71b389a95f098b48ec420444"), + "plugins/LanguagesManager/angularjs/translationsearch/translationsearch.controller.js" => array("862", "74e304627a4f86efd4b1e97a8a318408"), + "plugins/LanguagesManager/angularjs/translationsearch/translationsearch.directive.html" => array("1237", "8b1be14386abe55912df2ca3517fcc9c"), + "plugins/LanguagesManager/angularjs/translationsearch/translationsearch.directive.js" => array("885", "b71bf8fbf51de16b778bf0aeac27ae08"), + "plugins/LanguagesManager/API.php" => array("11816", "02e370df43dd47be89d599c0559bf5c8"), + "plugins/LanguagesManager/Commands/CreatePull.php" => array("8074", "18087b6f9a8ef2b25afc81264c7279d2"), + "plugins/LanguagesManager/Commands/FetchTranslations.php" => array("4641", "103268769c159f8136984d6b1e4f303f"), + "plugins/LanguagesManager/Commands/LanguageCodes.php" => array("1035", "9e57c16c0f05523a6b14bbb30aadbad5"), + "plugins/LanguagesManager/Commands/LanguageNames.php" => array("1043", "d2869bebc1206c6877bd8e695004a97f"), + "plugins/LanguagesManager/Commands/PluginsWithTranslations.php" => array("1064", "62d56f90d37fbb088151c86f10533f56"), + "plugins/LanguagesManager/Commands/SetTranslations.php" => array("4310", "d9c97dee65cfe60bba79523af64e7f2e"), + "plugins/LanguagesManager/Commands/TranslationBase.php" => array("625", "aafe53000c9c88067feb7c2a9c4d8703"), + "plugins/LanguagesManager/Commands/Update.php" => array("7333", "abf27d7bda27545df17f75350fc4343c"), + "plugins/LanguagesManager/Controller.php" => array("997", "1eaf6c276ecb4dae91a9b9f5ea58a56d"), + "plugins/LanguagesManager/lang/ar.json" => array("95", "d451c8e3cfdc18161543fa0ad27a1a0a"), + "plugins/LanguagesManager/lang/be.json" => array("103", "a75e345e2ccace4ab91dbde41f1593dc"), + "plugins/LanguagesManager/lang/bg.json" => array("101", "c62d43add3454b943f71e8a9a0aabe2f"), + "plugins/LanguagesManager/lang/ca.json" => array("155", "b1576e347f86852710716b06c31f5b5c"), + "plugins/LanguagesManager/lang/cs.json" => array("153", "fd57d6b9f02fe27abb44e8404955a373"), + "plugins/LanguagesManager/lang/da.json" => array("150", "57c28ba890e7d9a073c18413a3b002b9"), + "plugins/LanguagesManager/lang/de.json" => array("149", "eb4fc57c297b0f054299d371bc7a8ba8"), + "plugins/LanguagesManager/lang/el.json" => array("166", "04b3f99d77e66834716da678f23fb7cc"), + "plugins/LanguagesManager/lang/en.json" => array("147", "d30a36d32452bb7bc4ce2b664e65ac9f"), + "plugins/LanguagesManager/lang/es.json" => array("158", "f7cf544127a5edcfe456a691e644d177"), + "plugins/LanguagesManager/lang/et.json" => array("104", "8eeb061c5c2366e8fcc24495aceeb7a9"), + "plugins/LanguagesManager/lang/fa.json" => array("169", "531580350b22ec8f64cc1742a0262dd3"), + "plugins/LanguagesManager/lang/fi.json" => array("155", "23ce7e6f8fa07fe1416903bc978aaf5d"), + "plugins/LanguagesManager/lang/fr.json" => array("162", "0d70bb674e7faaa63534f2ab8579da06"), + "plugins/LanguagesManager/lang/hi.json" => array("193", "1a9b6009fcc08fc40d367a61e20d0502"), + "plugins/LanguagesManager/lang/hu.json" => array("96", "9148652673e8306ebcb6c79385e9e4d3"), + "plugins/LanguagesManager/lang/id.json" => array("98", "c9355c9899ab7cdb78dceb19a9fca475"), + "plugins/LanguagesManager/lang/is.json" => array("92", "c986412e7392641911476323cd371ca3"), + "plugins/LanguagesManager/lang/it.json" => array("156", "158145cd7ef8450ad8fc020a45cb27b3"), + "plugins/LanguagesManager/lang/ja.json" => array("147", "b6e8e5e8fea0606eefd66472c0de649b"), + "plugins/LanguagesManager/lang/ka.json" => array("133", "98f154322e14ddf4b61fe9e43295d3f6"), + "plugins/LanguagesManager/lang/ko.json" => array("94", "1dee0151e488d568c80f5ee257d2f81f"), + "plugins/LanguagesManager/lang/lt.json" => array("91", "c6104a589e5cd028845a1ab204827755"), + "plugins/LanguagesManager/lang/lv.json" => array("93", "73451316fbde065ce13f2a5b612c46a4"), + "plugins/LanguagesManager/lang/nb.json" => array("144", "3b03197f5c013e2693afa85e6a8ff334"), + "plugins/LanguagesManager/lang/nl.json" => array("146", "22f867c4a91520aca448a952dff41ae1"), + "plugins/LanguagesManager/lang/nn.json" => array("93", "8d9ce397e087a5795ab36c76848b76bd"), + "plugins/LanguagesManager/lang/pl.json" => array("94", "37ee6e37fc9479efa0cc38b68780d06d"), + "plugins/LanguagesManager/lang/pt-br.json" => array("154", "3508c4b41fcd678f29ada9315e81501c"), + "plugins/LanguagesManager/lang/pt.json" => array("100", "c208f754263035bdd24c7b8bbdb4dc0f"), + "plugins/LanguagesManager/lang/ro.json" => array("94", "e697dc8d31676be8c0d4d3faeb663c74"), + "plugins/LanguagesManager/lang/ru.json" => array("159", "237b6bc2cff743d4d10d561a59c19daf"), + "plugins/LanguagesManager/lang/sk.json" => array("90", "a02d9ab7913a0e6e018d846fbba65bdf"), + "plugins/LanguagesManager/lang/sl.json" => array("90", "a6a219e8fca7e962cd4d72a6e4943cfe"), + "plugins/LanguagesManager/lang/sq.json" => array("98", "bb1efe9a1e8fc6b06f55d3ccd088dda1"), + "plugins/LanguagesManager/lang/sr.json" => array("144", "a54eb6a168d27601d73952d30391d5cc"), + "plugins/LanguagesManager/lang/sv.json" => array("151", "3c55c97be39377040b1a5fedc3af1fff"), + "plugins/LanguagesManager/lang/te.json" => array("137", "b0f4b5f2deb4978febecf24aa35390c2"), + "plugins/LanguagesManager/lang/th.json" => array("123", "7ed1edec4e9e0662835022d7f450353c"), + "plugins/LanguagesManager/lang/tl.json" => array("160", "912f25b804e5ee258713868519a69ef0"), + "plugins/LanguagesManager/lang/tr.json" => array("99", "20423eb6e47c733f5a5c51cb5851a21e"), + "plugins/LanguagesManager/LanguagesManager.php" => array("7598", "dfb0713f8349e68c022a6dba75260802"), + "plugins/LanguagesManager/lang/uk.json" => array("103", "c75236d212bc8809eaa7d89bd00052d9"), + "plugins/LanguagesManager/lang/vi.json" => array("89", "2d44d4a128615e9c70a2983f7949e020"), + "plugins/LanguagesManager/lang/zh-cn.json" => array("136", "eec86386290e9e9c803d7082f42e3409"), + "plugins/LanguagesManager/lang/zh-tw.json" => array("91", "873fdc5f907dfb9100011d12980d8e0d"), + "plugins/LanguagesManager/Menu.php" => array("987", "61ddcf8de6077c8725e75e4671f8bf76"), + "plugins/LanguagesManager/Model.php" => array("2691", "8c1dfd904f6d14142cde117182739516"), + "plugins/LanguagesManager/templates/getLanguagesSelector.twig" => array("1016", "b312c51dd9f3152595bdf71c207a38b6"), + "plugins/LanguagesManager/templates/searchTranslation.twig" => array("237", "f4b3f349b460251933c9d8637167abd0"), + "plugins/LanguagesManager/Test/Integration/LanguagesManagerTest.php" => array("6669", "a9926ae4b53b6314690a7558b609294f"), + "plugins/LanguagesManager/Test/Integration/ModelTest.php" => array("3607", "aa6bd57ee646745bbe29669a336d3946"), + "plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/ByBaseTranslationsTest.php" => array("4581", "d2d5d26fe60f3879944f68ddd2caf1ee"), + "plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/ByParameterCountTest.php" => array("3526", "6a477f568286d4c0e259b69ef987faa2"), + "plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/EmptyTranslationsTest.php" => array("2683", "23f10cea3196c7d65bf231938c63261e"), + "plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/EncodedEntitiesTest.php" => array("3164", "3cf764ed8f0b6258df72a0a9fb234611"), + "plugins/LanguagesManager/Test/Unit/TranslationWriter/Filter/UnnecassaryWhitespacesTest.php" => array("4634", "f7fc3b6ae6e4850c9617dda02a038bcd"), + "plugins/LanguagesManager/Test/Unit/TranslationWriter/Validate/CoreTranslationsTest.php" => array("3092", "3339a3e50305dbb54c99aea7e55adec6"), + "plugins/LanguagesManager/Test/Unit/TranslationWriter/Validate/NoScriptsTest.php" => array("2895", "0c507a2f50543e5ea3bb60f132c45301"), + "plugins/LanguagesManager/Test/Unit/TranslationWriter/WriterTest.php" => array("8150", "374b3122cd48b6efe846fc8ea1febb7e"), + "plugins/LanguagesManager/TranslationWriter/Filter/ByBaseTranslations.php" => array("1739", "0b905abf6386440f5b1de2c0d7617577"), + "plugins/LanguagesManager/TranslationWriter/Filter/ByParameterCount.php" => array("2478", "3923cea8c0afabc65d1d555201b9387d"), + "plugins/LanguagesManager/TranslationWriter/Filter/EmptyTranslations.php" => array("1168", "772a04a5fa008bfa09c74b580187cdf5"), + "plugins/LanguagesManager/TranslationWriter/Filter/EncodedEntities.php" => array("1056", "61a26c5797201da70a1bfdeeaaa3a534"), + "plugins/LanguagesManager/TranslationWriter/Filter/FilterAbstract.php" => array("680", "7545c15270395e6b623fcac9d98927c4"), + "plugins/LanguagesManager/TranslationWriter/Filter/UnnecassaryWhitespaces.php" => array("2436", "545a37d98e91cdcd06e296f2347f9b53"), + "plugins/LanguagesManager/TranslationWriter/Validate/CoreTranslations.php" => array("2837", "ba9b6f9df2b8ec8ebcaafc6dac0a88ed"), + "plugins/LanguagesManager/TranslationWriter/Validate/NoScripts.php" => array("1054", "d8ec39c2d4c28dfeacb467d2fcc41323"), + "plugins/LanguagesManager/TranslationWriter/Validate/ValidateAbstract.php" => array("714", "9929224b721d01e578e9bfac137aa72e"), + "plugins/LanguagesManager/TranslationWriter/Writer.php" => array("9648", "5b8b237c2a98c1be73d490b91ad72032"), + "plugins/LanguagesManager/Updates/2.15.1-b1.php" => array("771", "deafe44968c481ad4b921efb725bf3c8"), + "plugins/Live/API.php" => array("17166", "8505d2068aa82b39f08cd91c505a82e3"), + "plugins/Live/Controller.php" => array("8867", "c615d75d7d42a34187365f6d4d381802"), "plugins/Live/images/avatar_frame.png" => array("5375", "b9a974286bc6de507e88ffd8ad40a8dd"), "plugins/Live/images/file0.png" => array("593", "d3bd9b53340629a9ba00ff9d3c295995"), "plugins/Live/images/file1.png" => array("637", "c1844ec7c1a8acf7573a54f78bf3565a"), @@ -1530,134 +3664,425 @@ class Manifest { "plugins/Live/images/file8.png" => array("642", "c164fa32f73b6f67012b0fde60d0e756"), "plugins/Live/images/file9.png" => array("569", "0551f3605e761068b1b1169c188cb45c"), "plugins/Live/images/paperclip.png" => array("10924", "aa65189568a2cb4c9d08af6f46926ae0"), - "plugins/Live/images/pause_disabled.gif" => array("619", "5c68e96a0ce8eac7b75fff5735dc7a3e"), - "plugins/Live/images/pause.gif" => array("669", "534b3892f5bda663651f86356df69a8d"), - "plugins/Live/images/play_disabled.gif" => array("407", "3c1374c5bfc73f12d50a97e5017a120f"), - "plugins/Live/images/play.gif" => array("666", "10ab3e64171780613434062a50378714"), + "plugins/Live/images/pause.gif" => array("1142", "defa62045c5e21609cc6b36de1d299ef"), + "plugins/Live/images/play.gif" => array("1184", "8db25ba7dc1701ed2c93a483fa25ef88"), "plugins/Live/images/returningVisitor.gif" => array("995", "dbdd14d7d5528f2c74ec8508c99aacfc"), "plugins/Live/images/unknown_avatar.jpg" => array("13984", "8151db2b0f9b45ba92f9b81c2791df94"), + "plugins/Live/images/visitorlog-hover.png" => array("1275", "f9f2f1195e80785df8338375864194ee"), + "plugins/Live/images/visitorlog.png" => array("1273", "8b07aad24ebeac1745b75e51ac439816"), "plugins/Live/images/visitor_profile_background.jpg" => array("11060", "b7eaca3d0097fe8f27d6625214092fd5"), "plugins/Live/images/visitor_profile_close.png" => array("4734", "e12231490573aa1f37d7eed6eb6867f0"), "plugins/Live/images/visitor_profile_gradient.png" => array("2840", "377a58f8be3b30690845aa71b0db0a5b"), "plugins/Live/images/visitorProfileLaunch.png" => array("661", "60ac00d9db615450c8d6090521f30330"), - "plugins/Live/javascripts/live.js" => array("8926", "7208e2076c000a03a5f9ada452d8c428"), - "plugins/Live/javascripts/visitorLog.js" => array("3150", "5d8488563053c8dd0248bdbba49aa59d"), - "plugins/Live/javascripts/visitorProfile.js" => array("10635", "6d6476652f681ef87d8eda4eecffb2e1"), - "plugins/Live/Live.php" => array("2559", "3b4cbcdd287bc9bdc96c5927a15a2a4f"), - "plugins/Live/stylesheets/live.less" => array("3761", "78a7b21ca37b37518c66eda4d07c18d7"), - "plugins/Live/stylesheets/visitor_profile.less" => array("9904", "86b092ab364a0e298e8edbfa0c6418c1"), - "plugins/Live/templates/_actionsList.twig" => array("7713", "d8c003ff4ef3b36a2f9ce6426faa09ed"), + "plugins/Live/javascripts/live.js" => array("9685", "ae040ce68a963d67c22bdb022a381e6d"), + "plugins/Live/javascripts/rowaction.js" => array("5251", "18b7c3e1e4c8c8d6166a3dae4bfa5147"), + "plugins/Live/javascripts/SegmentedVisitorLog.js" => array("4364", "75797463fd51a0d6f2a5c182cd1f0d55"), + "plugins/Live/javascripts/visitorLog.js" => array("4516", "f2cf319cb244db4cf416a165a03d07bb"), + "plugins/Live/javascripts/visitorProfile.js" => array("11418", "3a5ad6e1a4ad9229f350158c3baef574"), + "plugins/Live/lang/ar.json" => array("569", "33d6bec3b301388b39c65055496ede3c"), + "plugins/Live/lang/be.json" => array("1297", "e29628a762fdf9065dc3c98ae0a14436"), + "plugins/Live/lang/bg.json" => array("3138", "ef6e4499138b0e63a0d461c02de1ebbb"), + "plugins/Live/lang/ca.json" => array("1185", "d378d5955d92ec24ceb10fae6a10bf43"), + "plugins/Live/lang/cs.json" => array("3770", "210c04c781072c2458ec088e75874358"), + "plugins/Live/lang/da.json" => array("2408", "b017b817d3ceb28640dba2302cda16ea"), + "plugins/Live/lang/de.json" => array("3229", "fe3abf055e0d6b321232b162e45c93be"), + "plugins/Live/lang/el.json" => array("5919", "3ab40c53af951838ffe8a9e29a95b0a8"), + "plugins/Live/lang/en.json" => array("3263", "723cdbc5a5f3b2a0f7c12494ac24dfbd"), + "plugins/Live/lang/es.json" => array("3156", "e8e401f96fe3171ee92a59b6294a75d2"), + "plugins/Live/lang/et.json" => array("1742", "d7384f08389dbb596c16c6825d9899ad"), + "plugins/Live/lang/eu.json" => array("109", "2cb8dfb58b06975938fbb187ba594882"), + "plugins/Live/lang/fa.json" => array("2326", "4cb5db545c75892c0ca13b48e58f8a07"), + "plugins/Live/lang/fi.json" => array("2298", "5b38b29a235e82485d88e137442dfbcd"), + "plugins/Live/lang/fr.json" => array("3128", "0322aac0ad6a41e4385cb08123d4b4f7"), + "plugins/Live/lang/hi.json" => array("3590", "21e6d5385f0f760e20139ea4e8df8e86"), + "plugins/Live/lang/hr.json" => array("493", "8152d4c4d1c74f82c81774172e7cead3"), + "plugins/Live/lang/hu.json" => array("894", "84ad5e99775b994b3b65206d822887ad"), + "plugins/Live/lang/id.json" => array("1333", "d86685d376e44ed3701dc4d68c1853b0"), + "plugins/Live/lang/is.json" => array("352", "59989477809f2b87111ec56503dbb508"), + "plugins/Live/lang/it.json" => array("3150", "dc9e2ea6d750666312ea3efada602cb3"), + "plugins/Live/lang/ja.json" => array("3493", "799db3f13b04d5f5cfa4c95a516d53b2"), + "plugins/Live/lang/ka.json" => array("540", "adffeff87a50fa59d98614291ccadf76"), + "plugins/Live/lang/ko.json" => array("2393", "d60af1be1ca8e0162c470b949bef3e6b"), + "plugins/Live/lang/lt.json" => array("1136", "ee6a9c7a3bed5247c4de886650f0204d"), + "plugins/Live/lang/lv.json" => array("1038", "6c04429237d59404a53e63e25ae0ff6e"), + "plugins/Live/lang/nb.json" => array("1223", "b86d5a50e6f8cf4746b6eeab456a19af"), + "plugins/Live/lang/nl.json" => array("2909", "2dad4fa08871162b8d82f03773d5a449"), + "plugins/Live/lang/nn.json" => array("872", "cd83e7fc98c69c0b9e4ad6dc437de3e0"), + "plugins/Live/lang/pl.json" => array("1410", "186884661b17e7e24d0d93a9a8b08355"), + "plugins/Live/lang/pt-br.json" => array("3554", "40023def46f3697137d1ba88bcc2461f"), + "plugins/Live/lang/pt.json" => array("1057", "4a8a3501508503408ec31830d949a8ac"), + "plugins/Live/lang/ro.json" => array("2381", "08f85d0ab54ab7cc78d8e1ec3a3c9706"), + "plugins/Live/lang/ru.json" => array("4309", "d03a41308c2c465f593569282df0ef60"), + "plugins/Live/lang/sk.json" => array("973", "b6ca67aac48a4cb57551fa4b6e9926a4"), + "plugins/Live/lang/sl.json" => array("460", "061b3a57bead908537e04d024ab9ceb4"), + "plugins/Live/lang/sq.json" => array("1175", "fa4962f34416a2afc5f7c6943b2f6701"), + "plugins/Live/lang/sr.json" => array("2869", "a29517d8f640873aa50580bffccb3944"), + "plugins/Live/lang/sv.json" => array("2219", "0fcd0e85b0df98aa2487dd6bad11c28d"), + "plugins/Live/lang/ta.json" => array("396", "3c9deb2a01718415b2168ce962226f18"), + "plugins/Live/lang/te.json" => array("238", "0eb3e27379f674dc79c6eca04cd584a4"), + "plugins/Live/lang/th.json" => array("1369", "060213c8e1321af5bd13c642ccbd5a79"), + "plugins/Live/lang/tl.json" => array("2387", "67ed057885c7bfb1a5d0b31db9edc1fb"), + "plugins/Live/lang/tr.json" => array("1008", "ae7bbc7109cacdd99efd32edad1d7212"), + "plugins/Live/lang/uk.json" => array("511", "2c92888d89e1f0e364277365c4fd7e66"), + "plugins/Live/lang/vi.json" => array("2822", "f33caf394b306f163f744d2752d874b3"), + "plugins/Live/lang/zh-cn.json" => array("2018", "25de63cbce031e5d1c04fa1363f8a082"), + "plugins/Live/lang/zh-tw.json" => array("378", "fa089ec022960ba191188cb36e532663"), + "plugins/Live/Live.php" => array("2000", "8108ca577715e2c568227d3fe37a4088"), + "plugins/Live/Model.php" => array("17234", "34bb6b1c51d3e20e9199a922c9b82255"), + "plugins/Live/Reports/Base.php" => array("410", "1e3a67ccfb8ec5fe8fa1f670c93a9267"), + "plugins/Live/Reports/GetLastVisitsDetails.php" => array("1190", "2d35d705b382d0496685a3a58c4370d8"), + "plugins/Live/Reports/GetLastVisits.php" => array("439", "6bfa5fc7d51c2c4d077e1716869070b8"), + "plugins/Live/Reports/GetSimpleLastVisitCount.php" => array("2031", "975da117da4924c1927ec4d2ebee5106"), + "plugins/Live/stylesheets/live.less" => array("5385", "e662ead0322ae9b28788cbffe9110bcb"), + "plugins/Live/stylesheets/visitor_profile.less" => array("9987", "22256e62ec7d6624f29e9f6ba508d98f"), + "plugins/Live/templates/_actionsList.twig" => array("7482", "6c0bbd28a10a36c6635f1f8809b17014"), "plugins/Live/templates/ajaxTotalVisitors.twig" => array("41", "5c57d90d62eab1c2c11969f14ff6faaa"), - "plugins/Live/templates/_dataTableViz_visitorLog.twig" => array("11165", "f10f764079fc95b18bee9964e505bbd5"), - "plugins/Live/templates/getLastVisitsStart.twig" => array("9637", "66723cfc37872398f74bcebc0170fa3a"), - "plugins/Live/templates/getSimpleLastVisitCount.twig" => array("1501", "ff589f5ab71d3364bbce2b50ac635304"), - "plugins/Live/templates/getSingleVisitSummary.twig" => array("3644", "a6d27ef3106ba799ee77ab6c7e52901b"), - "plugins/Live/templates/getVisitList.twig" => array("1401", "21f5605f7c91c89b27ec184d71e89a34"), - "plugins/Live/templates/getVisitorProfilePopup.twig" => array("10894", "ae422171a4dc85bae4c491b8530a04c7"), - "plugins/Live/templates/index.twig" => array("1728", "6de08f5ddb5d6802ab97258cafbf7d13"), - "plugins/Live/templates/indexVisitorLog.twig" => array("170", "8e65a17376e8d870da1fdb791bdb019b"), - "plugins/Live/templates/_totalVisitors.twig" => array("1193", "cb5f29dc2c34e531f7e8469feb201a32"), - "plugins/Live/VisitorLog.php" => array("4032", "95e464cd76d6842f656e0b4f7bb5e9d8"), - "plugins/Live/Visitor.php" => array("35950", "df9ec109f4a08c6bc5b7c33a1b544f73"), - "plugins/Login/Auth.php" => array("3935", "449818805ed2d144bfcd22d4e881318f"), - "plugins/Login/Controller.php" => array("14593", "3ad0b3aefe6b7aec78622079aa965691"), - "plugins/Login/FormLogin.php" => array("1283", "97995c1ab8eb36c1c4a899f5f87a4e23"), - "plugins/Login/FormResetPassword.php" => array("1247", "13d7b72379ff19943051061fa0e17f4b"), - "plugins/Login/javascripts/login.js" => array("3457", "ddf60c9aab3769010b4e022369337dfb"), - "plugins/Login/Login.php" => array("4566", "48ff16be33aab8f87ad9fa99c615ab71"), - "plugins/Login/stylesheets/login.css" => array("3564", "df89b474360b0e1c59f306df7c76255c"), - "plugins/Login/templates/login.twig" => array("7493", "7c6eebc0b8825c41214dba7647238660"), - "plugins/Login/templates/resetPassword.twig" => array("389", "751937bee878c24f3b72890273548069"), - "plugins/MobileMessaging/APIException.php" => array("261", "9f8b71ac2a11ea82638ff785a1368ecc"), - "plugins/MobileMessaging/API.php" => array("12198", "36de01e312125b59016d8f2f2ad3aa58"), - "plugins/MobileMessaging/Controller.php" => array("2675", "9b26ad0184726bc42e630ebe95b2a05d"), - "plugins/MobileMessaging/CountryCallingCodes.php" => array("7207", "0f3e6aa1f434d4c93ed4dc899ec751bf"), - "plugins/MobileMessaging/GSMCharset.php" => array("2978", "1a7692e6ddd18ff57f146e9c064db4c9"), + "plugins/Live/templates/_dataTableViz_visitorLog.twig" => array("13050", "05c2ddc33f4e1d4dfc7483b0566f2940"), + "plugins/Live/templates/getLastVisitsStart.twig" => array("10539", "ebcdd335c6d201ee40d1063929bb08c6"), + "plugins/Live/templates/getSimpleLastVisitCount.twig" => array("1495", "ab295fb6b5505873cedc80050c9f8a01"), + "plugins/Live/templates/getSingleVisitSummary.twig" => array("4281", "d967deef5c280b0238e3fafabca3c85b"), + "plugins/Live/templates/getVisitList.twig" => array("1589", "26b12aea63629c905c5d4fb2c960dfd4"), + "plugins/Live/templates/getVisitorProfilePopup.twig" => array("12995", "a06243c07123d7357b8c8b4d2fc38738"), + "plugins/Live/templates/index.twig" => array("1866", "569c925c3a2f521043d412ac6b95f471"), + "plugins/Live/templates/indexVisitorLog.twig" => array("89", "fa30cab277d7c45e669b709dfbee6c1b"), + "plugins/Live/templates/_totalVisitors.twig" => array("1143", "313576d8c6cb398865e8a30fba0e4f3d"), + "plugins/Live/VisitorFactory.php" => array("1605", "0dfa69c67b7655a57034ae68f692b98d"), + "plugins/Live/VisitorInterface.php" => array("379", "3d2df929580b3fbc46622906b33fc02a"), + "plugins/Live/Visitor.php" => array("18427", "d1123d1f7131cf93ca6a7ebeb63c424a"), + "plugins/Live/VisitorProfile.php" => array("13321", "91bc888d940cfbd47b8c99d1d64d2e59"), + "plugins/Live/Visualizations/VisitorLog/Config.php" => array("913", "ea50b1c371907681d7dc5dcb611053a9"), + "plugins/Live/Visualizations/VisitorLog.php" => array("3411", "9946bec2d7f63412d1a19ebcdcd8e208"), + "plugins/Live/Widgets.php" => array("653", "d287e11f5bb2ab6dd68b4f80c62dccc2"), + "plugins/Login/Auth.php" => array("4603", "665827a8c8cc35a47d35a8344fabb7e5"), + "plugins/Login/config/config.php" => array("81", "2fb09ca13d5056999cf6c92428917d13"), + "plugins/Login/Controller.php" => array("11357", "18113cac17b494f86a468fc56961a086"), + "plugins/Login/FormLogin.php" => array("1287", "945355c423596067fe5665ba7d25e9a6"), + "plugins/Login/FormResetPassword.php" => array("1251", "c01d7ab13be7620beff1bf89ae66556c"), + "plugins/Login/javascripts/login.js" => array("3473", "b0ebcc2fc73d258f5bc22935ac9fc9ae"), + "plugins/Login/lang/am.json" => array("688", "dc990a5a6dceaee1a6916b596357e4a8"), + "plugins/Login/lang/ar.json" => array("1517", "7132f3e21def5d8eb3afa15995308df5"), + "plugins/Login/lang/be.json" => array("1581", "b6fe88375fdb18b5c51e8712fd7d4cbd"), + "plugins/Login/lang/bg.json" => array("2215", "45c08fc79a876879ef7f192e138f949e"), + "plugins/Login/lang/ca.json" => array("2177", "5c526cadef18215ad7070cbc8ee8e17b"), + "plugins/Login/lang/cs.json" => array("2386", "e14e52b77fe826e4636e451d2d4abfad"), + "plugins/Login/lang/da.json" => array("2233", "aa61181d19a6e64f2e141101ef04c0bf"), + "plugins/Login/lang/de.json" => array("2263", "64cc841ea3dca745ef7cc33f310f5e86"), + "plugins/Login/lang/el.json" => array("4149", "a333a7c0783c3b759dfadf5e39f10823"), + "plugins/Login/lang/en.json" => array("2272", "f415caf93511301195ba1aa73a7535b7"), + "plugins/Login/lang/es.json" => array("2485", "9eb9648caaef3475f597f0e98df5a436"), + "plugins/Login/lang/et.json" => array("857", "8d2fcbc84518de1bf594495e59946a11"), + "plugins/Login/lang/eu.json" => array("685", "9af1cc42feb24b97f09442b23cbf5079"), + "plugins/Login/lang/fa.json" => array("1595", "3f66aac2025d2a93e1ff822007c00ae9"), + "plugins/Login/lang/fi.json" => array("1997", "b71aadbc519808b4b6c1a486b25ee348"), + "plugins/Login/lang/fr.json" => array("2485", "a0a9012c25a165a24d93cec2fd170f58"), + "plugins/Login/lang/gl.json" => array("484", "14c5384c9e21df1c744f1bf64919f5ed"), + "plugins/Login/lang/he.json" => array("517", "25b2205569f3959b74ce42d1811a6df0"), + "plugins/Login/lang/hi.json" => array("3913", "fb8e938c7020099785fd962dab854408"), + "plugins/Login/lang/hu.json" => array("2441", "8cdd0a76c61ce83cb04e9f2f922d017e"), + "plugins/Login/lang/id.json" => array("1789", "2b3a8dd2e750fedff78ef8db49831e14"), + "plugins/Login/lang/is.json" => array("767", "f57ff54debe9b4e4ea8061093d4da359"), + "plugins/Login/lang/it.json" => array("2360", "4598a1fa4a7f2396eac4ea7b155546d5"), + "plugins/Login/lang/ja.json" => array("2954", "c42b8c8be0e07008ae398bb3f5616a73"), + "plugins/Login/lang/ka.json" => array("1536", "4ec8a81e7b40fde87178d2527fbb01da"), + "plugins/Login/lang/ko.json" => array("2761", "62cd5094760ae0112c2e8e77e23b02c2"), + "plugins/Login/lang/lt.json" => array("824", "38b41f014be5fd71afcc13eea292ee3a"), + "plugins/Login/lang/lv.json" => array("1181", "0ac7e40e8e3f89d1452dd94153593145"), + "plugins/Login/lang/nb.json" => array("2218", "a7c9704dd12e54de7ed6c1719e4afff5"), + "plugins/Login/lang/nl.json" => array("2297", "87baacade0e13b6b8fd79d6d93132920"), + "plugins/Login/lang/nn.json" => array("533", "462b6859a84689b932ba03e670f082f8"), + "plugins/Login/lang/pl.json" => array("1641", "4a4d37093aa64df1c4ea7e04918d66fc"), + "plugins/Login/lang/pt-br.json" => array("2469", "c08431805395c974bab4419ebf28ce6f"), + "plugins/Login/lang/pt.json" => array("1226", "f9f03201e390a7cbb2d0002620f710fe"), + "plugins/Login/lang/ro.json" => array("2031", "decebe1261882ca5ef58c9182a133a83"), + "plugins/Login/lang/ru.json" => array("3253", "0149720430ca393c3197642a7bec4e71"), + "plugins/Login/lang/sk.json" => array("1334", "1dcea116f37de45a829a1136c57b77ce"), + "plugins/Login/lang/sl.json" => array("1444", "adf96181d01d6f1451c026dc4c2ad335"), + "plugins/Login/lang/sq.json" => array("1246", "9133772a3af38aed3cb3f2fbd80aa8b3"), + "plugins/Login/lang/sr.json" => array("2273", "c74aa34eaa4f0285b27f74adbef901a9"), + "plugins/Login/lang/sv.json" => array("2280", "ae372c0175ea16e46e7df05cd196f576"), + "plugins/Login/lang/ta.json" => array("1080", "09e7491313b0ea9d9e2c61bae9cdb02e"), + "plugins/Login/lang/te.json" => array("268", "0bd6a4006b0cbd6cdab7743ffbd76047"), + "plugins/Login/lang/th.json" => array("2597", "552ae9f9588352da976fba2b21519808"), + "plugins/Login/lang/tl.json" => array("1838", "a61d9994b5549a43f7db3b5d83b90bc4"), + "plugins/Login/lang/tr.json" => array("1106", "7dfdf974729ba08614113e8cbf28a78d"), + "plugins/Login/lang/uk.json" => array("1106", "bd722dafba022a44ee4c593ad557e9a3"), + "plugins/Login/lang/vi.json" => array("2331", "72cd53ed9b557036efc89610d6befc11"), + "plugins/Login/lang/zh-cn.json" => array("1493", "f6188121564a95dfa4129f6bc411d25a"), + "plugins/Login/lang/zh-tw.json" => array("792", "5b9759306a9cd11ad0c1a34c95da14fc"), + "plugins/Login/Login.php" => array("3513", "dc1bde3ba24bfc34fafd722054ddce61"), + "plugins/Login/PasswordResetter.php" => array("17285", "fc0f61a9b17b989fbf8b5870ff165cbc"), + "plugins/Login/SessionInitializer.php" => array("7592", "1d1d1341aef4f676fe74659185b78238"), + "plugins/Login/stylesheets/login.less" => array("4122", "f124f0184d37e53ff316ca62e22e17c4"), + "plugins/Login/stylesheets/variables.less" => array("36", "14cf6ec689fd1977297f3001a8635047"), + "plugins/Login/templates/_formErrors.twig" => array("268", "13e001ec458c18110fdf5d384fe606c7"), + "plugins/Login/templates/login.twig" => array("7069", "39d6dcb13c718b80da7d518732a34e4f"), + "plugins/Login/templates/resetPassword.twig" => array("157", "f5cd1c27479d8b0082030afd5a235b80"), + "plugins/MobileAppMeasurable/config/test.php" => array("147", "607b21d69b85e663f2780294b1d0f58f"), + "plugins/MobileAppMeasurable/lang/cs.json" => array("246", "b5d8e530894c2740abc9b6186922bb91"), + "plugins/MobileAppMeasurable/lang/de.json" => array("226", "62882982aa1d95060da9e904398542cd"), + "plugins/MobileAppMeasurable/lang/el.json" => array("365", "c5409748789d2881cf25dfe1f087c1ba"), + "plugins/MobileAppMeasurable/lang/en.json" => array("220", "f9a3d201adcbc0cc086b649021d9cd10"), + "plugins/MobileAppMeasurable/lang/es.json" => array("248", "52764ad57e89e4f786ebdc35a85e1e0c"), + "plugins/MobileAppMeasurable/lang/fr.json" => array("255", "ae12fdfcd7334ec7d43726835dfccb91"), + "plugins/MobileAppMeasurable/lang/hi.json" => array("409", "625ff7a91f7476a5aaf0f02eae3da81f"), + "plugins/MobileAppMeasurable/lang/hu.json" => array("261", "b34f91256cf41fe3389d6235925b2920"), + "plugins/MobileAppMeasurable/lang/it.json" => array("228", "1ffab2fd99c8472b7e67537d43252d23"), + "plugins/MobileAppMeasurable/lang/ja.json" => array("298", "0a93c2193f4371482f303218457029ee"), + "plugins/MobileAppMeasurable/lang/lt.json" => array("80", "b9ba100223ebab405e5541fbdb18aafe"), + "plugins/MobileAppMeasurable/lang/nb.json" => array("234", "9ca3aa2a6071668d8461343baac0c2b9"), + "plugins/MobileAppMeasurable/lang/nl.json" => array("213", "88417890d5f3e6e9cb4d76f0cbdd5a27"), + "plugins/MobileAppMeasurable/lang/pt-br.json" => array("251", "845eb12fc3a582db3661775c0c61ac42"), + "plugins/MobileAppMeasurable/lang/sk.json" => array("81", "66ad6af475b91814bee4a6c0ecfffe40"), + "plugins/MobileAppMeasurable/lang/sr.json" => array("239", "7c5171b57297cf17e2abe0e01fff2079"), + "plugins/MobileAppMeasurable/lang/sv.json" => array("197", "341d5545a5303c135cac0e330e6f5a8d"), + "plugins/MobileAppMeasurable/MobileAppMeasurable.php" => array("252", "36bd055e8b810d23462804240688b51c"), + "plugins/MobileAppMeasurable/plugin.json" => array("169", "500a1e733294ea8a1e89be5f8dd0c5bb"), + "plugins/MobileAppMeasurable/Type.php" => array("570", "9b9ac8512c6d478ed69417dfef51c6d3"), + "plugins/MobileMessaging/APIException.php" => array("265", "a5554240ef51b59ca0864301d6fdb4c8"), + "plugins/MobileMessaging/API.php" => array("11947", "cf31d7fcd100015d2a4b84fcd9e7e71c"), + "plugins/MobileMessaging/Controller.php" => array("3983", "76c222e17e9ff4c58f6c3423214a4cd3"), + "plugins/MobileMessaging/CountryCallingCodes.php" => array("7211", "2bc593523a2f0a88ff29981e702aea1b"), + "plugins/MobileMessaging/GSMCharset.php" => array("2982", "10651d8e8ffc14df6becc4fb3d35a7af"), "plugins/MobileMessaging/images/Clockwork.png" => array("3585", "c0ae966302ddca87a6cc55abf0617efe"), "plugins/MobileMessaging/images/phone.png" => array("568", "c294efbf17ecc6c3384e1cbff1a69521"), - "plugins/MobileMessaging/javascripts/MobileMessagingSettings.js" => array("10525", "eedfe8a5ae54d77c74590698e108b258"), - "plugins/MobileMessaging/MobileMessaging.php" => array("8963", "08ac0b55c51fb2e617173ce07c0ed673"), - "plugins/MobileMessaging/ReportRenderer/ReportRendererException.php" => array("1433", "864c4f04d4bc18daa94d59927fc87ca8"), - "plugins/MobileMessaging/ReportRenderer/Sms.php" => array("4337", "8c5cd9fb9e9e88ac7490db381ac414c3"), - "plugins/MobileMessaging/SMSProvider/Clockwork.php" => array("2850", "5de85a92828a6fb7ab107fd1821efb22"), - "plugins/MobileMessaging/SMSProvider.php" => array("6529", "4265464da0ce3dd71daccd3365f8b5d3"), - "plugins/MobileMessaging/SMSProvider/StubbedProvider.php" => array("591", "ec1c6ab90be8c4ee259ee51012a116af"), + "plugins/MobileMessaging/javascripts/MobileMessagingSettings.js" => array("10529", "133b4910c76b4624039a347a407a74b7"), + "plugins/MobileMessaging/lang/bg.json" => array("4568", "17aeae02922a90f06cd7f5a77211dc8e"), + "plugins/MobileMessaging/lang/ca.json" => array("2268", "f2f27ef1306c66c73b7eaf02bdb6914a"), + "plugins/MobileMessaging/lang/cs.json" => array("4010", "488fe07cf53c5713e8e8b7e2f4ce2c07"), + "plugins/MobileMessaging/lang/da.json" => array("4007", "e71120bc47d4cfa9232038c1c61477d5"), + "plugins/MobileMessaging/lang/de.json" => array("4268", "fa6f6645ea78a9e47ae386e5f3b4a97c"), + "plugins/MobileMessaging/lang/el.json" => array("6787", "9027aba787409b155e0452cc11e5a6a2"), + "plugins/MobileMessaging/lang/en.json" => array("3855", "4c18a2215b57327b2c42c5e8095ae379"), + "plugins/MobileMessaging/lang/es.json" => array("4381", "758b13e6059477cd62a1465b69241fec"), + "plugins/MobileMessaging/lang/et.json" => array("1130", "6330281ae2d363fcd575da04609eff3a"), + "plugins/MobileMessaging/lang/fa.json" => array("4313", "268e01bb81ce84fe2f6b390d1af049f6"), + "plugins/MobileMessaging/lang/fi.json" => array("3891", "b7b493aff2118ec019d964bd947e8795"), + "plugins/MobileMessaging/lang/fr.json" => array("4386", "f9bf76d617169acb7a4797e0b0b503f9"), + "plugins/MobileMessaging/lang/hi.json" => array("7747", "8ba37ae87df1304f9c5a3191748a2c38"), + "plugins/MobileMessaging/lang/id.json" => array("4076", "57fb9a859959985f6865dcfcb2e3fec1"), + "plugins/MobileMessaging/lang/it.json" => array("4121", "23ef29e35ef0148a6981b2e0afd4bf85"), + "plugins/MobileMessaging/lang/ja.json" => array("4921", "ee3f04459a96d8daa92d016b47e76fc6"), + "plugins/MobileMessaging/lang/ko.json" => array("4382", "0ece084f41d277a3b51e984049d6b762"), + "plugins/MobileMessaging/lang/lt.json" => array("507", "75070feeed3882774b94eba3bfa4c0c2"), + "plugins/MobileMessaging/lang/nb.json" => array("1175", "b229d913e6955a395a7884e0a615d9de"), + "plugins/MobileMessaging/lang/nl.json" => array("4084", "48cd9089379941de040465288a80eb56"), + "plugins/MobileMessaging/lang/pl.json" => array("652", "638d909cfc4fc22443ef1b824a18867c"), + "plugins/MobileMessaging/lang/pt-br.json" => array("4234", "f13c2e0ce81400ae30e89861bc8ef513"), + "plugins/MobileMessaging/lang/ro.json" => array("4307", "4869fcf2ce9864572d66d77ae3b6f077"), + "plugins/MobileMessaging/lang/ru.json" => array("5683", "400956a75ed91548359bf9630736abec"), + "plugins/MobileMessaging/lang/sk.json" => array("81", "2d4743783462179612a09e31f0fe81ae"), + "plugins/MobileMessaging/lang/sl.json" => array("130", "3960f0a1532cf5a887c6d3d8c1bbacd2"), + "plugins/MobileMessaging/lang/sr.json" => array("4058", "c13b0e45c0ac5013abbc7c641aa1161c"), + "plugins/MobileMessaging/lang/sv.json" => array("4193", "65a819a729eb13d9e54536ee0558701d"), + "plugins/MobileMessaging/lang/ta.json" => array("911", "40b3abab5850a61bb44c140698c699a5"), + "plugins/MobileMessaging/lang/th.json" => array("717", "44b2beee7fed2f5d1c3c6c7c89d8e5ff"), + "plugins/MobileMessaging/lang/tl.json" => array("4501", "d82db55c46e50e436334c7af7712849d"), + "plugins/MobileMessaging/lang/tr.json" => array("1370", "4b8e69ce82be74aa339a74cea16e5f7b"), + "plugins/MobileMessaging/lang/vi.json" => array("5093", "4a13e1a3a2fd18c1df5426a41a4af309"), + "plugins/MobileMessaging/lang/zh-cn.json" => array("3475", "a34228404ed78d8348be8c8647484782"), + "plugins/MobileMessaging/Menu.php" => array("783", "58df87cb88a2cf30a5b93a83a3eebc65"), + "plugins/MobileMessaging/MobileMessaging.php" => array("8684", "d66da525026533671d16c815a6fe47e3"), + "plugins/MobileMessaging/ReportRenderer/ReportRendererException.php" => array("1722", "b27fea00845b5a3f74164848512310bf"), + "plugins/MobileMessaging/ReportRenderer/Sms.php" => array("4735", "534567079124954079627ade81aa21c5"), + "plugins/MobileMessaging/SMSProvider/Clockwork.php" => array("3951", "b392d2b941eeec9df7079d86516cbe52"), + "plugins/MobileMessaging/SMSProvider/Development.php" => array("1366", "13083dc6a9bc3915a2f09e9eba612560"), + "plugins/MobileMessaging/SMSProvider.php" => array("7340", "657f72e13d1091cd418124ab07d98757"), + "plugins/MobileMessaging/SMSProvider/StubbedProvider.php" => array("888", "759d25913ced41e15cf582086a28b432"), "plugins/MobileMessaging/stylesheets/MobileMessagingSettings.less" => array("248", "b8f6c5c9dc4d2376def8d30a436f2f2d"), - "plugins/MobileMessaging/templates/index.twig" => array("8828", "ff291f3400b4d3e6548d7d72254fcaeb"), - "plugins/MobileMessaging/templates/reportParametersScheduledReports.twig" => array("2220", "facbb81972351c7bf9552bb6d23ad6a6"), - "plugins/MobileMessaging/templates/SMSReport.twig" => array("2081", "8757ecc10c18b0db36031f0ef659756a"), + "plugins/MobileMessaging/templates/index.twig" => array("2616", "88dac87c566134f157f60f99242046ce"), + "plugins/MobileMessaging/templates/macros.twig" => array("1334", "14ac82e870abb77e997890cd9ebd4f00"), + "plugins/MobileMessaging/templates/reportParametersScheduledReports.twig" => array("2357", "d4653ab40bbc54d07077627766b00cc6"), + "plugins/MobileMessaging/templates/SMSReport.twig" => array("2189", "d059a0d95d49a43f6cf07c0c42bb567a"), + "plugins/MobileMessaging/templates/userSettings.twig" => array("5053", "000e51e7df1a0a580fbc8e11e0a0967a"), + "plugins/Monolog/config/cli.php" => array("1133", "0def0c0bb058f9900d2a2249c901d448"), + "plugins/Monolog/config/config.php" => array("3873", "23ca6b09e161cf82f0a336e7692e7ebd"), + "plugins/Monolog/config/tracker.php" => array("926", "cf2e91823caf54e37005f8296e2ded48"), + "plugins/Monolog/Formatter/LineMessageFormatter.php" => array("2603", "a7d052eb262a3a453bc7fee442b90c06"), + "plugins/Monolog/Handler/DatabaseHandler.php" => array("929", "3369587b55baec1639ab26bec0442e3b"), + "plugins/Monolog/Handler/EchoHandler.php" => array("495", "7662d87dd25ec23dea2548ae89f45047"), + "plugins/Monolog/Handler/FileHandler.php" => array("692", "c742e9ad72ba3ff5ad9ec17d1b4c0017"), + "plugins/Monolog/Handler/WebNotificationHandler.php" => array("1480", "604ffb1fc502f72881615e30cf183f3e"), + "plugins/Monolog/Monolog.php" => array("240", "421a86ee2b8ed16d923f35b87eed3b35"), + "plugins/Monolog/plugin.json" => array("60", "a07af932909e291786f7806fe9aefe66"), + "plugins/Monolog/Processor/ClassNameProcessor.php" => array("1941", "9096da098ae7bf3831131914fb3838fa"), + "plugins/Monolog/Processor/ExceptionToTextProcessor.php" => array("1498", "a5ac4a3e78e2593ca72e081c5510faf5"), + "plugins/Monolog/Processor/RequestIdProcessor.php" => array("743", "d1f7bfaa3dffe589dc41b0d36f9b6850"), + "plugins/Monolog/Processor/SprintfProcessor.php" => array("1031", "49ec19decdb0f30c999cfad4dd331104"), + "plugins/Monolog/Processor/TokenProcessor.php" => array("540", "4e3ab05f6cdb40fc66009675707f31c7"), + "plugins/Morpheus/Controller.php" => array("496", "c9ba2c6f6ac41cf8ec608dbb7ebfb88f"), + "plugins/Morpheus/fonts/piwik.eot" => array("20640", "9bbc8707a807b8b1cdea99da191f5dbe"), + "plugins/Morpheus/fonts/piwik.ttf" => array("20484", "c221e9375f094dd947af2fe1de8dbc4d"), "plugins/Morpheus/images/add.png" => array("1261", "56daa2c3de8f7091e4a796940a395399"), + "plugins/Morpheus/images/affix-arrow.png" => array("3179", "542723bef4d8a61f3dc217352dc1873a"), "plugins/Morpheus/images/annotations.png" => array("1069", "d7778841e7e56de10f4d594e56f315c8"), "plugins/Morpheus/images/annotations_starred.png" => array("248", "c55f6bd966fee694d42996d73de74e0e"), + "plugins/Morpheus/images/arr_r.png" => array("195", "2708a22e4d851aff01d4db4f2fddf1da"), + "plugins/Morpheus/images/background-submit.png" => array("1347", "b1822012b50008b3e478d7ae890ead92"), "plugins/Morpheus/images/bullet.png" => array("963", "efd0e064fa9fc92187d976b08d707855"), "plugins/Morpheus/images/calendar.gif" => array("1260", "92079affe9d57713492ba8fc8991ed15"), "plugins/Morpheus/images/chart_bar.png" => array("968", "ff2414d707510b4188795cd7ba73b092"), "plugins/Morpheus/images/chart_line_edit.png" => array("421", "d7f696960ffe8f17fedefcc42415adf4"), "plugins/Morpheus/images/chart_pie.png" => array("1223", "cf703b3878e59ffde83f5cbe6d7571b3"), - "plugins/Morpheus/images/cities.png" => array("1038", "7eaa39f8e0021507b8685e3e0b2c87e9"), "plugins/Morpheus/images/close.png" => array("1122", "9276313cd6bbff312cf65222b970c1d3"), + "plugins/Morpheus/images/collapsed_arrows.gif" => array("54", "224b095cbca536e579119a47328badda"), "plugins/Morpheus/images/configure-highlight.png" => array("356", "b7e641dd5732e92000dfedcacf0fb42d"), "plugins/Morpheus/images/configure.png" => array("1210", "2241fa7107cc901a362a54b372d9e5d4"), + "plugins/Morpheus/images/dashboard_h_bg.png" => array("162", "a6cf0f7cdf6d69edca6edf479aa888e3"), + "plugins/Morpheus/images/data_table_footer_active_item.png" => array("145", "70dcc41079a073d03fee2628dcd0cf74"), "plugins/Morpheus/images/datepicker_arr_l.png" => array("963", "e770866fb6c0b6dc9318bb635cb8b25c"), "plugins/Morpheus/images/datepicker_arr_r.png" => array("968", "324f7f5e7b2d60ac543d574fec0638fe"), + "plugins/Morpheus/images/delete.png" => array("2175", "b3b9cb547a0511ff15b5371b122a6f66"), + "plugins/Morpheus/images/download.png" => array("734", "0552d1746701df879d14c2fdf3d5ac41"), + "plugins/Morpheus/images/ecommerceAbandonedCart.gif" => array("369", "51974d5002afd9b7c8009d14a1207aad"), + "plugins/Morpheus/images/ecommerceOrder.gif" => array("570", "b0c1aa6141f0047b4bcd0cc665c945ad"), + "plugins/Morpheus/images/email.png" => array("754", "baaa6accd945fcb4480b29ab2e15bded"), + "plugins/Morpheus/images/error_medium.png" => array("2622", "d789ca042860b20782cfb8c2bfd458e0"), + "plugins/Morpheus/images/error.png" => array("1150", "16ac5f1c769e78a074144f1fe9073dfb"), + "plugins/Morpheus/images/event.png" => array("164", "8e1d701795486cdc53cbf7a5c3b4d069"), + "plugins/Morpheus/images/expanded_arrows.gif" => array("60", "a9afa92168dbbe6f693f3ff71fa27b32"), "plugins/Morpheus/images/export.png" => array("1182", "4ab0af6ccaf4cbe06da8e2f19d611893"), + "plugins/Morpheus/images/feed.png" => array("691", "55bc1130d360583e2aecbcebfbf6eda7"), "plugins/Morpheus/images/forms-sprite.png" => array("1758", "c87c3f0ae1eeb5b9f928d76a019afbea"), + "plugins/Morpheus/images/fullscreen.png" => array("346", "629df6e9cfdf1bb8d0a612f651da69e9"), "plugins/Morpheus/images/goal.png" => array("1042", "13fcf0fc4f2f5c7cbeb3b954be2302cb"), "plugins/Morpheus/images/help.png" => array("1297", "2030eb18bab29a957d84f6ab578f04a1"), + "plugins/Morpheus/images/html_icon.png" => array("3503", "45a005cf3fa96037df5d7935d26b5f7c"), + "plugins/Morpheus/images/ico_alert.png" => array("1112", "63d124bf79386ebf6285926956ff7829"), "plugins/Morpheus/images/ico_delete.png" => array("1306", "fd41409c74d29bab6be2a1f1def2f1e3"), "plugins/Morpheus/images/ico_edit.png" => array("1202", "67cfadf5265b4d2c506f3db2e1ca367a"), + "plugins/Morpheus/images/ico_info.png" => array("978", "362b1589e75a151c9e4d509615851950"), "plugins/Morpheus/images/icon-calendar.gif" => array("1260", "7acac555dc283dfb65de9f7f524e662b"), "plugins/Morpheus/images/image.png" => array("1139", "70283dccd506faa0a0a4580d822a4609"), "plugins/Morpheus/images/info.png" => array("1278", "6df252a916c3df5b269732e76ecfa50e"), + "plugins/Morpheus/images/inp_bg.png" => array("137", "1f4b8e7288c5d4dc52e44c50e0d02a9b"), + "plugins/Morpheus/images/li_dbl_gray.gif" => array("48", "5b0a692984ac5b04acc0886cd374bb85"), "plugins/Morpheus/images/link.gif" => array("1147", "6098367a5cd6a3dd93c56aced55925bf"), "plugins/Morpheus/images/loading-blue.gif" => array("723", "23f0762fea3d694b579522524bd5628f"), + "plugins/Morpheus/images/login-sprite.png" => array("10200", "a2a3520f448277c3efdb871d637fc34b"), "plugins/Morpheus/images/logo-header.png" => array("2215", "9695149e76d3080dcffd78eb45cd3735"), + "plugins/Morpheus/images/logo-marketplace.png" => array("2927", "aa3dc0cd04c23a654e7a96ceabe2a6c1"), "plugins/Morpheus/images/logo.png" => array("3902", "d11828697d06b1117c49413d77a6e9ff"), - "plugins/Morpheus/images/logo.svg" => array("2290", "d19186837572bb5994e4d6e2b463348b"), + "plugins/Morpheus/images/logo.svg" => array("2290", "3e32725576d9f37e5ba4b320019dfff7"), "plugins/Morpheus/images/maximise.png" => array("1053", "b98f8497b9b242cac626128ffd4d5ef7"), "plugins/Morpheus/images/minimise.png" => array("968", "2ad0c819d2416043d07be478ac9a9156"), - "plugins/Morpheus/images/pause_disabled.gif" => array("1141", "5dcb5e11d359f2a4e180fd7b5a3a5dfd"), - "plugins/Morpheus/images/pause.gif" => array("1142", "defa62045c5e21609cc6b36de1d299ef"), - "plugins/Morpheus/images/play_disabled.gif" => array("1155", "dd7c8730e4be8128138ee68aebd0a5b2"), - "plugins/Morpheus/images/play.gif" => array("1184", "8db25ba7dc1701ed2c93a483fa25ef88"), + "plugins/Morpheus/images/minus.png" => array("208", "3f78b8b47aa11e1e7cc30bc3a4456cf7"), + "plugins/Morpheus/images/newtab.png" => array("509", "994c19f51192a18f7b3e0bc0775313f2"), + "plugins/Morpheus/images/ok.png" => array("626", "28501b0877ea15b49c6ca58677e186c3"), + "plugins/Morpheus/images/paypal_subscribe.gif" => array("3080", "a74a883239713fb5050593c20d9fd2a5"), + "plugins/Morpheus/images/plus_blue.png" => array("157", "9d61acb98c3ac639715aba6703997ad9"), + "plugins/Morpheus/images/plus.png" => array("214", "872b70c31871f96f546816f0fbfea54d"), "plugins/Morpheus/images/refresh.png" => array("1312", "1672c3502fac047c53f2e74797fe124b"), - "plugins/Morpheus/images/regions.png" => array("1265", "1fdee0b6664804d6525dfbe12931b922"), + "plugins/Morpheus/images/reload.png" => array("892", "5a0360408c248f9cde4e0d4bad31ac00"), + "plugins/Morpheus/images/row_evolution_hover.png" => array("601", "7f6833f656aaad475e02a96ae3c0adb9"), + "plugins/Morpheus/images/row_evolution.png" => array("1934", "0e3fe13d82bc0526ed94bc079152f2f2"), + "plugins/Morpheus/images/search_bg.png" => array("374", "585beb0a278508e4fcf32f65b6a18cf4"), "plugins/Morpheus/images/search_ico.png" => array("1227", "e2da6bc860b189996bdafef30bb2e3cc"), "plugins/Morpheus/images/segment-users.png" => array("1270", "95b3e6b1ed4c79c826f59da6100a27e1"), + "plugins/Morpheus/images/select_arrow.png" => array("93", "bc88371db06a70c1e0db029e758a7627"), + "plugins/Morpheus/images/signout.png" => array("345", "595dd5a6fd609a5357264e6551e040e6"), + "plugins/Morpheus/images/sites_selection.png" => array("120", "f8f6f62a17616adce09791ffb51aab1b"), + "plugins/Morpheus/images/smileyprog_0.png" => array("4045", "0b105851f9dfc4e5a3efb933c4fa01af"), + "plugins/Morpheus/images/smileyprog_1.png" => array("4268", "cd518d27567dda069dd5fff2e3c2291f"), + "plugins/Morpheus/images/smileyprog_2.png" => array("4292", "8e61661161afe5ac8a7e2caf085fb200"), + "plugins/Morpheus/images/smileyprog_3.png" => array("4589", "1eb75d0f84042d492b7ff4a515e23ff3"), + "plugins/Morpheus/images/smileyprog_4.png" => array("4733", "1f31e1ac3c6b3602cecabf0f0c15fd1b"), "plugins/Morpheus/images/sortasc_dark.png" => array("92", "96b06c701e6ef3645c39206045821495"), "plugins/Morpheus/images/sortasc.png" => array("964", "736682039b3ce45e6565e3f871f71057"), "plugins/Morpheus/images/sortdesc_dark.png" => array("98", "dbc223ec613d5bc863e897cf3b525335"), "plugins/Morpheus/images/sortdesc.png" => array("980", "968ded7fbfb0c38d1883788c07f090a4"), + "plugins/Morpheus/images/sort_subtable_asc_light.png" => array("2866", "a8d4e15a81c63b3d7f8ddde1ad5fc78d"), + "plugins/Morpheus/images/sort_subtable_asc.png" => array("173", "457908cb087009a946c99bef46643c69"), + "plugins/Morpheus/images/sort_subtable_desc_light.png" => array("286", "ba6261eca430661a8b84155460424106"), "plugins/Morpheus/images/sort_subtable_desc.png" => array("980", "bc2a98535cbcd85c256c534627f4fffc"), + "plugins/Morpheus/images/star_empty.png" => array("658", "31809a80055eb2aa02b51e6c11ecb02d"), + "plugins/Morpheus/images/star.png" => array("757", "872b7a1a8101bcf7ef6c7cf7c8f78ff7"), + "plugins/Morpheus/images/success_medium.png" => array("1346", "28e0ba1f8492374db4946d42c69e477b"), "plugins/Morpheus/images/table_more.png" => array("1157", "720c588117a9dc781d40a42e90c40466"), "plugins/Morpheus/images/table.png" => array("1056", "375eae11704a2d2e749cdefcb26f2a39"), "plugins/Morpheus/images/tagcloud.png" => array("1098", "1c9b9a43cef5e807087cf3f69544c9ef"), + "plugins/Morpheus/images/video_play.png" => array("517", "29fd1c103c9ac9987b85e053e621ab20"), + "plugins/Morpheus/images/warning_medium.png" => array("1283", "24bc193a073997740e4aa459b2bbbbdf"), + "plugins/Morpheus/images/warning.png" => array("571", "8c4ef759f46a90e7a00e1db65e49edc9"), + "plugins/Morpheus/images/warning_small.png" => array("1083", "5ff491ccd2f32beb35d96fd79a4d7329"), "plugins/Morpheus/images/zoom-out-disabled.png" => array("1297", "81d56e2c732e3ed4bc1a1c7d94632e7b"), "plugins/Morpheus/images/zoom-out.png" => array("1300", "b598e49632becfb91970083f8a9ff62e"), + "plugins/Morpheus/javascripts/ajaxHelper.js" => array("13441", "034436b4fcbd7eb52d075c25162a7135"), "plugins/Morpheus/javascripts/jquery.icheck.min.js" => array("4005", "a31ce1654416358e8d933cbf79b5ffbd"), - "plugins/Morpheus/javascripts/morpheus.js" => array("617", "ad7fbd24380028485a5e0758e4b66e81"), - "plugins/Morpheus/plugin.json" => array("468", "e7be1bd0537250d805421d36c880232a"), - "plugins/Morpheus/stylesheets/admin.less" => array("2273", "103712f53f7978b39f5898f4581815b5"), - "plugins/Morpheus/stylesheets/charts.less" => array("3046", "c72c436df07f2c5282afa95e13722d0e"), - "plugins/Morpheus/stylesheets/colors.less" => array("1454", "6fa45b1e4c87373f790dec9d4822645d"), - "plugins/Morpheus/stylesheets/components.less" => array("8430", "66b71131d92624e3a3d5cd654e29bb3b"), - "plugins/Morpheus/stylesheets/forms.less" => array("6410", "df34cc9255b55f23c7eb82d90abe06f5"), - "plugins/Morpheus/stylesheets/map.less" => array("1824", "fd90d3d1127a9ad8b8c71b611f202904"), - "plugins/Morpheus/stylesheets/mixins.less" => array("2884", "b62ca2d8a1c8c1867a60520243a417c2"), - "plugins/Morpheus/stylesheets/popups.less" => array("1122", "24fd3106538b5a84d77c516bb647651c"), - "plugins/Morpheus/stylesheets/theme.less" => array("19045", "b9c3f6250cd4d85602a319464731c47d"), - "plugins/Morpheus/stylesheets/tooltip.less" => array("725", "8801596d6ea42a576a486f82ffe41e29"), - "plugins/Morpheus/stylesheets/typography.less" => array("2196", "582c812de3aa430bb86b54b7d82d548f"), - "plugins/MultiSites/angularjs/dashboard/dashboard-controller.js" => array("955", "7cdb911b5a86c789d1545d7c16d90084"), - "plugins/MultiSites/angularjs/dashboard/dashboard-directive.js" => array("1097", "231dadf20d40d8e78622e9b97621c026"), - "plugins/MultiSites/angularjs/dashboard/dashboard-filter.js" => array("1962", "aa3286a874ef6b3aa0f982a815feb533"), - "plugins/MultiSites/angularjs/dashboard/dashboard.html" => array("6925", "6b97a82b739ea3c1309caf7ce274982e"), - "plugins/MultiSites/angularjs/dashboard/dashboard.less" => array("2810", "f9d750db11d5a06dc3cc60f9fcd0a5e8"), - "plugins/MultiSites/angularjs/dashboard/dashboard-model.js" => array("8474", "8ae117224a8c797c54f7fe8383503368"), - "plugins/MultiSites/angularjs/site/site-directive.js" => array("1984", "4944d752571c5e40cf6717bc74ef83cf"), - "plugins/MultiSites/angularjs/site/site.html" => array("2225", "e2ce56b83fd0599b73ffb2c64c8f6d55"), - "plugins/MultiSites/API.php" => array("20412", "11973574d15341554d0bec191dfea6e4"), - "plugins/MultiSites/Controller.php" => array("2577", "b68eb4db8925d0a5f3a6a445c76c7c4d"), + "plugins/Morpheus/javascripts/layout.js" => array("731", "a7de9f66a27a08c9ff13d10c10044495"), + "plugins/Morpheus/javascripts/morpheus.js" => array("980", "e0845465115093927bfb2e53d381ddae"), + "plugins/Morpheus/javascripts/piwikHelper.js" => array("16034", "14bac403806796487a7dcfb2e26ba53e"), + "plugins/Morpheus/Menu.php" => array("2078", "8a707a73327baab752f1c3fef9385135"), + "plugins/Morpheus/plugin.json" => array("384", "483e5d60aa915757857471669e8f8c43"), + "plugins/Morpheus/stylesheets/base/bootstrap.css" => array("16271", "fa040297857faf4b690694d72414374e"), + "plugins/Morpheus/stylesheets/base/colors.less" => array("1616", "217ffc05473287287917755d76154f51"), + "plugins/Morpheus/stylesheets/base/icons.css" => array("5001", "d7c5546f75818c2bdd3d924fc79f7a6e"), + "plugins/Morpheus/stylesheets/base.less" => array("1014", "38ce225671838d302ed3858bb29735b8"), + "plugins/Morpheus/stylesheets/base/mixins.less" => array("2460", "e9152b18b2940104370f682b4f3b3df7"), + "plugins/Morpheus/stylesheets/general/_admin.less" => array("1101", "6decf79e2dca4db34f9b0c2f517ecfae"), + "plugins/Morpheus/stylesheets/general/_default.less" => array("1352", "77f6f2ec3fbe2d41afebb91f3581e94c"), + "plugins/Morpheus/stylesheets/general/_form.less" => array("2345", "b1e60376ac1036184294f631b3708167"), + "plugins/Morpheus/stylesheets/general/_forms.less" => array("11264", "8a91fa1f1bd3ca3a49d0378db0c250d6"), + "plugins/Morpheus/stylesheets/general/_jqueryUI.less" => array("6010", "da08581e50343b54f235a6b2030746f2"), + "plugins/Morpheus/stylesheets/general/_misc.less" => array("533", "2cc510998e8adc056349001eaa45e888"), + "plugins/Morpheus/stylesheets/general/_typography.less" => array("1952", "c34f7c92f732723d0c44d1686eb12f47"), + "plugins/Morpheus/stylesheets/general/_utils.less" => array("391", "4793980854730904f5ce0cb7f13082bb"), + "plugins/Morpheus/stylesheets/ieonly.css" => array("520", "58ab9def841f09e1b8c52c61be7f4e7f"), + "plugins/Morpheus/stylesheets/main.less" => array("17450", "f746f47be56c612c19ff8f0d658de268"), + "plugins/Morpheus/stylesheets/simple_structure.css" => array("2990", "3b477d6baea90f1189f80dd5aed885e5"), + "plugins/Morpheus/stylesheets/theme-advanced.less" => array("470", "122dc2b15d0da7c0f2a2d3cfdd6adb84"), + "plugins/Morpheus/stylesheets/theme.less" => array("950", "a2ce235155bbf9da6327b7962f7e4653"), + "plugins/Morpheus/stylesheets/ui/_alerts.less" => array("1370", "898f9adf86d2501e065313de82817c2e"), + "plugins/Morpheus/stylesheets/uibase/_dataTable.less" => array("46", "29c2aa49f73efe255d4202c37a942ef5"), + "plugins/Morpheus/stylesheets/uibase/_header.less" => array("608", "9f01dcd5c270cbe00679c17deb84f4aa"), + "plugins/Morpheus/stylesheets/uibase/_headerMessage.less" => array("886", "2114ba7099e5e2f5e8d4c4b02b305811"), + "plugins/Morpheus/stylesheets/uibase/_languageSelect.less" => array("373", "6eaf913c68258e2832fc4cdaeaadb52d"), + "plugins/Morpheus/stylesheets/uibase/_loading.less" => array("433", "840100352ab2e6650755d0db83c0554b"), + "plugins/Morpheus/stylesheets/uibase/_periodSelect.less" => array("1195", "326d6dab33af99cc13d04d653cca835b"), + "plugins/Morpheus/stylesheets/ui/_buttons.less" => array("1872", "e2272d8b49abac3c6b54d81b1077d63d"), + "plugins/Morpheus/stylesheets/ui/_cards.less" => array("367", "1d79af4ae8277f6f12e00672f513930b"), + "plugins/Morpheus/stylesheets/ui/_charts.less" => array("3105", "9c3d23a1f00c497d58dd16b354dbcd58"), + "plugins/Morpheus/stylesheets/ui/_code.less" => array("478", "0254dbe64a2b57ac7c4f0f45f0cc6efa"), + "plugins/Morpheus/stylesheets/ui/_components.less" => array("7546", "a598798e3a958902e51c49391c46e6ff"), + "plugins/Morpheus/stylesheets/ui/_list-group.less" => array("1252", "70534ffa2fb3b863356f53b5219a70ed"), + "plugins/Morpheus/stylesheets/ui/_map.less" => array("1788", "91883aa3951cb9b4854db124fa56f907"), + "plugins/Morpheus/stylesheets/ui/_navs.less" => array("1222", "daa093bcdbfc240dc592a4d14798a564"), + "plugins/Morpheus/stylesheets/ui/_panels.less" => array("1609", "7d10b5c5add579b055d6962737f6d2e8"), + "plugins/Morpheus/stylesheets/ui/_popups.less" => array("977", "b635f27ebe2b6721651a584db3272956"), + "plugins/Morpheus/stylesheets/ui/_progress-bars.less" => array("557", "82238eae2173a9686e3beef281059fd8"), + "plugins/Morpheus/stylesheets/ui/_tables.less" => array("444", "a907b17ce170ace4c132ad94e82206f3"), + "plugins/Morpheus/stylesheets/ui/_tooltip.less" => array("739", "9cec901eb3c8517f61d36617d53f0ca9"), + "plugins/Morpheus/templates/admin.twig" => array("1678", "91c0a2f5df78feadcb20d885a4e3b803"), + "plugins/Morpheus/templates/ajaxMacros.twig" => array("1622", "c18d96573d0073ef9cd9aa23dc3fe4dd"), + "plugins/Morpheus/templates/dashboard.twig" => array("1674", "f48d1bac03dc3ec3efcb670af1f45106"), + "plugins/Morpheus/templates/demo.twig" => array("20889", "aa7eb3a5765f2a7df252e05809c88a7a"), + "plugins/Morpheus/templates/empty.twig" => array("35", "2c6a1dccfb394fef9ef03849c39a5bec"), + "plugins/Morpheus/templates/genericForm.twig" => array("1178", "6452313a007a012880a797ccd07eb01d"), + "plugins/Morpheus/templates/_iframeBuster.twig" => array("385", "1c780792c51f5d2fecbf53c0ce0d547f"), + "plugins/Morpheus/templates/javascriptCode.tpl" => array("661", "ae4b48cd094e9bf99eb0838f145a6d74"), + "plugins/Morpheus/templates/_jsCssIncludes.twig" => array("123", "06ce23308eb531695910b50736dbf171"), + "plugins/Morpheus/templates/_jsGlobalVariables.twig" => array("2239", "6e8d7b1cf491a51cbe6ba9ea3304bfba"), + "plugins/Morpheus/templates/layout.twig" => array("2046", "db1140a954be2fc0a691f8f2f1ffb53b"), + "plugins/Morpheus/templates/macros.twig" => array("951", "428e6cd290e9021ea4724e378d7485bf"), + "plugins/Morpheus/templates/maintenance.tpl" => array("835", "2f67753751067dc3ea9ad72ff2ec60ca"), + "plugins/Morpheus/templates/settingsMacros.twig" => array("4993", "8f3196c85f2590f5c335ce9eccf32922"), + "plugins/Morpheus/templates/simpleLayoutFooter.tpl" => array("126", "a9b5ab498a462178c939ed757d9e28f1"), + "plugins/Morpheus/templates/simpleLayoutHeader.tpl" => array("559", "5fa654886c016368dd3bcade8f754530"), + "plugins/Morpheus/templates/_sparklineFooter.twig" => array("102", "a9e46848aaf5613b971827cf12ab6eaa"), + "plugins/Morpheus/templates/user.twig" => array("1627", "dd431995331969fbc329cbccee065d84"), + "plugins/MultiSites/angularjs/dashboard/dashboard.controller.js" => array("1097", "302692ccf007d92f92aa5b04a7cfc3c7"), + "plugins/MultiSites/angularjs/dashboard/dashboard.directive.html" => array("7653", "b2a445d167f9efecd32f036f501dacad"), + "plugins/MultiSites/angularjs/dashboard/dashboard.directive.js" => array("1302", "fb45e36435dc7f134980b85d8c812958"), + "plugins/MultiSites/angularjs/dashboard/dashboard.directive.less" => array("4061", "aa890703c06763410e35426ecee57078"), + "plugins/MultiSites/angularjs/dashboard/dashboard-model.service.js" => array("5722", "315db631c998d5f2b9bef77c38fcbc01"), + "plugins/MultiSites/angularjs/site/site.controller.js" => array("1774", "4f624b5b9a172cff9fbffc12e1e84540"), + "plugins/MultiSites/angularjs/site/site.directive.html" => array("2229", "e6745fbd80ccbd1b39b6cccdc6ce78ea"), + "plugins/MultiSites/angularjs/site/site.directive.js" => array("1221", "084e99a1330404cc08ac6c78b35f7a88"), + "plugins/MultiSites/API.php" => array("20945", "e6349995a70f6094c415cbd33c339d48"), + "plugins/MultiSites/Columns/Metrics/EcommerceOnlyEvolutionMetric.php" => array("1592", "697e9f18d40efa372e85d42fdbd98b44"), + "plugins/MultiSites/Columns/Website.php" => array("377", "e86cc06275de49bd800913c70d47ee1c"), + "plugins/MultiSites/Controller.php" => array("3688", "586f29ee049dd1a2c20b46ab8ff4cd9b"), + "plugins/MultiSites/Dashboard.php" => array("11255", "413ebd3ead6f7fe345a053bb65adece0"), + "plugins/MultiSites/DataTable/Filter/NestedSitesLimiter.php" => array("3459", "fe39cd263fd6f8ede554f0abf9a07e7e"), "plugins/MultiSites/images/arrow_asc.gif" => array("120", "ff5921e3047f33fa2ddc2b85c960133c"), "plugins/MultiSites/images/arrow_desc.gif" => array("130", "38ab9e8c27b8ce2d1a6b6f9753206f4b"), "plugins/MultiSites/images/arrow_down_green.png" => array("221", "9b81c5a2fb3f3b979e5d941a7b51d0e5"), @@ -1665,60 +4090,278 @@ class Manifest { "plugins/MultiSites/images/arrow_up.png" => array("222", "d5642aff98a988d93f317894b665eeb2"), "plugins/MultiSites/images/arrow_up_red.png" => array("248", "906746c2060a32f4aa0ee204b60fe027"), "plugins/MultiSites/images/door_in.png" => array("693", "e20ba15525185c16acfbf043e7b4a9cd"), - "plugins/MultiSites/images/link.gif" => array("75", "b8de0b2b517e1999b32353209be4e976"), - "plugins/MultiSites/images/loading-blue.gif" => array("1849", "483d45d0beb0b5547988926b795f8190"), "plugins/MultiSites/images/stop.png" => array("307", "031e5dba74ad3b375096806ff1662402"), - "plugins/MultiSites/MultiSites.php" => array("4567", "09b8e3130ea25bbb6eaa46058c854f2d"), - "plugins/MultiSites/templates/getSitesInfo.twig" => array("791", "93ca3b820a17e74883bd777766da5660"), - "plugins/Overlay/API.php" => array("4545", "5bdb88b44970b0286c3c6476115f7422"), + "plugins/MultiSites/lang/ar.json" => array("65", "ddf66b6f9031c5f6b3693fd337afeb36"), + "plugins/MultiSites/lang/be.json" => array("63", "18910cb7799139ca0eb27422873dd50a"), + "plugins/MultiSites/lang/bg.json" => array("304", "29d08976c767f87ef682ad174550dba6"), + "plugins/MultiSites/lang/ca.json" => array("208", "5c458253317e797a8dd48add6692efd2"), + "plugins/MultiSites/lang/cs.json" => array("411", "60f6f0cb2f91ea942564a0314fbf330a"), + "plugins/MultiSites/lang/da.json" => array("234", "d0de31281c0930ae008db8d462789165"), + "plugins/MultiSites/lang/de.json" => array("365", "0bc79298f93da6b8adb3e5a80d93cb28"), + "plugins/MultiSites/lang/el.json" => array("580", "73ebb30207a6abb72c07f86d218964cc"), + "plugins/MultiSites/lang/en.json" => array("355", "8cd33e27a2ebf51c656fc40901bdb9be"), + "plugins/MultiSites/lang/es.json" => array("406", "b7111f4b1d4416cf7e227576acc9cd2f"), + "plugins/MultiSites/lang/et.json" => array("64", "cf2971e07c4ef5ff43e5b1b4eb29755c"), + "plugins/MultiSites/lang/fa.json" => array("257", "ff4362b0ea2de2d6eb8837b214b914b5"), + "plugins/MultiSites/lang/fi.json" => array("217", "63f67b4258ed52a64e2ffd16922e0f0a"), + "plugins/MultiSites/lang/fr.json" => array("384", "764604564144d117dd9b216335c71c9b"), + "plugins/MultiSites/lang/hi.json" => array("683", "d512b69c1c7471fee33e002ddcf4ceee"), + "plugins/MultiSites/lang/hu.json" => array("60", "a740ff512c9705171c4b62b333edb87c"), + "plugins/MultiSites/lang/id.json" => array("160", "ecdeff848cc71e5f527d6d7066fa206d"), + "plugins/MultiSites/lang/it.json" => array("356", "5e89ba1077a652d8aaa18756c6bba911"), + "plugins/MultiSites/lang/ja.json" => array("508", "3c42bb17589d2aeb9971abc20a396a04"), + "plugins/MultiSites/lang/ka.json" => array("77", "e51b0df3538d836cc7dabce12ef0041f"), + "plugins/MultiSites/lang/ko.json" => array("201", "679948664abc29de69ef377cec6756eb"), + "plugins/MultiSites/lang/lt.json" => array("156", "b4e52ee7d60093cf143369ee04c6309e"), + "plugins/MultiSites/lang/nb.json" => array("367", "d3dec556b08b5114cf6e4d0052ce45c5"), + "plugins/MultiSites/lang/nl.json" => array("346", "8a03f7a0738fe9136622f9a5b44743b8"), + "plugins/MultiSites/lang/nn.json" => array("62", "0e6eeb2efc00fdbb0f59438d23079acd"), + "plugins/MultiSites/lang/pl.json" => array("151", "8015ce9472feef48aa1f3b6362428bc5"), + "plugins/MultiSites/lang/pt-br.json" => array("347", "e29a855c13be142724304884ac05976a"), + "plugins/MultiSites/lang/pt.json" => array("357", "9eb948e805403fe95ea5dbc4125cc860"), + "plugins/MultiSites/lang/ro.json" => array("256", "df0bb59748027d0c30ae82b1a59558d5"), + "plugins/MultiSites/lang/ru.json" => array("289", "10d3cef168ff1021bf137752aeaef492"), + "plugins/MultiSites/lang/sk.json" => array("59", "f41a4d77c7302f289cb11542cb7c35c6"), + "plugins/MultiSites/lang/sl.json" => array("154", "b972d2d44ddac3d577357b02f389e313"), + "plugins/MultiSites/lang/sq.json" => array("107", "316b2774c2e154195156f283ab917000"), + "plugins/MultiSites/lang/sr.json" => array("353", "9faa5fc69d13e10568cf9707f4276bdd"), + "plugins/MultiSites/lang/sv.json" => array("331", "ba99733449dad6eeb9bb72d0f86bab7e"), + "plugins/MultiSites/lang/ta.json" => array("77", "a9fdfd688a90a24a4a3c4a0a985c5028"), + "plugins/MultiSites/lang/th.json" => array("98", "736af1e3b0f66c9d375ee79d78dbde1c"), + "plugins/MultiSites/lang/tl.json" => array("261", "691d60c3f91fc3eb1e2e02c1437999bb"), + "plugins/MultiSites/lang/uk.json" => array("63", "2de84d9a90c3554211e1e7c65f34c962"), + "plugins/MultiSites/lang/vi.json" => array("384", "5425d78a3b24996cfeb903a32b8cdb24"), + "plugins/MultiSites/lang/zh-cn.json" => array("122", "394150e4ab997c82997b9fdbb2d59ce3"), + "plugins/MultiSites/lang/zh-tw.json" => array("65", "9591edddb8d69ace8ec6af0cdfa68eb8"), + "plugins/MultiSites/Menu.php" => array("614", "1180e3e901e85aabb793c3290b5eb253"), + "plugins/MultiSites/MultiSites.php" => array("3489", "f4cff91123796a2049d9a6b2a9fc0d3e"), + "plugins/MultiSites/plugin.json" => array("98", "aa920881a7a8a187157030d8506bc3e9"), + "plugins/MultiSites/Reports/Base.php" => array("1108", "b3279b84c177e802f33519759e44dc7d"), + "plugins/MultiSites/Reports/GetAll.php" => array("599", "22226ebefe4a6185f91b9a1a4bdcf9a5"), + "plugins/MultiSites/Reports/GetOne.php" => array("603", "d478f20ddff110c4193f41be3e95e675"), + "plugins/MultiSites/templates/getSitesInfo.twig" => array("820", "3d83fdba685ee6113f0def37735ce611"), + "plugins/Overlay/API.php" => array("4626", "237491732f8c9fe7b0f5db03ce452d97"), "plugins/Overlay/client/client.css" => array("2403", "b89a7f8790c46666d3f1f4bd324b0842"), - "plugins/Overlay/client/client.js" => array("7986", "c91fc2d4c5af7f909a3189cb4e6bd10b"), + "plugins/Overlay/client/client.js" => array("8151", "fd044f62b5befb56e7c85e78811c430e"), "plugins/Overlay/client/close.png" => array("655", "42492684e24356a4081134894eabeb9e"), - "plugins/Overlay/client/followingpages.js" => array("20057", "017a346250b30fe408b154388439dac4"), - "plugins/Overlay/client/linktags.eps" => array("460096", "3b89babacdf68b23c14e37619af9c6a7"), + "plugins/Overlay/client/followingpages.js" => array("20154", "fe5fb90ab64eee92a7042fd29350517a"), "plugins/Overlay/client/linktags_lessshadow.png" => array("6353", "80e04f62a5efa4ec128efb40b0670d37"), "plugins/Overlay/client/linktags_noshadow.png" => array("5355", "1471510499dfc32c978ace341095f0aa"), "plugins/Overlay/client/linktags.png" => array("6489", "1e7ee586288b9aa1d1ecb3b6cc402cae"), - "plugins/Overlay/client/linktags.psd" => array("38518", "0ad1ccd72db63437365e1d2225213afe"), "plugins/Overlay/client/loading.gif" => array("723", "6ce8f9a2c650cf90261acfc98b2edf90"), "plugins/Overlay/client/translations.js" => array("756", "b25f65e35f2091e51f3846f58f0b0843"), - "plugins/Overlay/client/urlnormalizer.js" => array("5688", "4c43dc9ca32de3fcf5f6c9fb3a93082f"), - "plugins/Overlay/Controller.php" => array("8120", "0145004a547bfc7983686f765040b2c9"), - "plugins/Overlay/images/info.png" => array("778", "3750c701d2ec35a45d289b9b9c1a0667"), + "plugins/Overlay/client/urlnormalizer.js" => array("5778", "3d380cfa1eec049ab126c343fff17aa2"), + "plugins/Overlay/config/ui-test.php" => array("371", "191feb27bd47976e34bbcc313a57561f"), + "plugins/Overlay/Controller.php" => array("8025", "c621ec8d9daab690425e7458ba61a51b"), "plugins/Overlay/images/overlay_icon_hover.png" => array("360", "7021a0169999242feade5c21859185b1"), "plugins/Overlay/images/overlay_icon.png" => array("359", "dfabdc7dd24cad1b55101fa5dc77a2ad"), - "plugins/Overlay/javascripts/Overlay_Helper.js" => array("952", "76159386fdc4544d42d58af61051f655"), - "plugins/Overlay/javascripts/Piwik_Overlay.js" => array("8715", "0757e757ec98579e800c54557633f78b"), - "plugins/Overlay/javascripts/rowaction.js" => array("1904", "47d98c45ed01ec168770ea77fa4d200b"), - "plugins/Overlay/Overlay.php" => array("1239", "5f268e56246335ca2ab65fb295d537a4"), - "plugins/Overlay/stylesheets/overlay.css" => array("2736", "c45eed3e933565c3737d3500d642fa53"), + "plugins/Overlay/javascripts/Overlay_Helper.js" => array("1136", "c1f9749adbf5d53a9dd30b36c680dfa5"), + "plugins/Overlay/javascripts/Piwik_Overlay.js" => array("10295", "e367e7590db6217c56146f69bc90939b"), + "plugins/Overlay/javascripts/rowaction.js" => array("3066", "fb1005cfd1b100fb28591743cff88182"), + "plugins/Overlay/lang/ar.json" => array("61", "d9e9d7e4e03074877e7f658201a3e52f"), + "plugins/Overlay/lang/be.json" => array("61", "51693289b7e80b0724575533dfb797b7"), + "plugins/Overlay/lang/bg.json" => array("1343", "e4e5e67ab4676e07f77f127d97b0b7df"), + "plugins/Overlay/lang/ca.json" => array("1416", "f0d332dde0e9fd7e1feab34c902437a8"), + "plugins/Overlay/lang/cs.json" => array("1619", "a9175b369dbc62769856c672ef259586"), + "plugins/Overlay/lang/da.json" => array("1305", "99cebc955006b1c7b80b27d36f5c95c9"), + "plugins/Overlay/lang/de.json" => array("1654", "8b54c56092ad479eca8fe7488f1dd8c2"), + "plugins/Overlay/lang/el.json" => array("2715", "e8d189fb3e382d38ab24b6deddc5dab1"), + "plugins/Overlay/lang/en.json" => array("1491", "775e0d6f4906cb06483e48d253bf189a"), + "plugins/Overlay/lang/es.json" => array("1744", "86bb9a733c3c22224d7cb8127ec7f321"), + "plugins/Overlay/lang/et.json" => array("340", "496965f48bbc64c4826458530dea1b2f"), + "plugins/Overlay/lang/fa.json" => array("1095", "eb903d39763174fd3878bbf6f8f74296"), + "plugins/Overlay/lang/fi.json" => array("1309", "ce317e6b6194324cf3a9b0c6989abe92"), + "plugins/Overlay/lang/fr.json" => array("1667", "8f43cb6510d8e62736a8d5fac430fbc0"), + "plugins/Overlay/lang/he.json" => array("132", "e450c25c037976df42d0ff687e9fbaa3"), + "plugins/Overlay/lang/hi.json" => array("3060", "e0ca166075ad25dcca9528384b035772"), + "plugins/Overlay/lang/hr.json" => array("57", "7c4d845d6cbbb81371b6b94e5862499d"), + "plugins/Overlay/lang/hu.json" => array("53", "ce33470291b0ba08af3e3b4a01844e9e"), + "plugins/Overlay/lang/id.json" => array("1378", "1facc291b2f244cb9c4df545575fa064"), + "plugins/Overlay/lang/is.json" => array("61", "0bb96b3ae06094bdd8c8a4a4c7d173f8"), + "plugins/Overlay/lang/it.json" => array("1584", "147a2decd9544d69be62e9f29c86064b"), + "plugins/Overlay/lang/ja.json" => array("1967", "c54512a5fb473b7661cee05b71086623"), + "plugins/Overlay/lang/ka.json" => array("79", "1387c89a97eb0d3c0a2445d5bcd60470"), + "plugins/Overlay/lang/ko.json" => array("1807", "1165bfa8dabef44280e6341817a490c6"), + "plugins/Overlay/lang/lt.json" => array("96", "799349e43625ea16725380f990484f78"), + "plugins/Overlay/lang/lv.json" => array("59", "aa06f56a303f0b4e3bd10508279e0d5b"), + "plugins/Overlay/lang/nb.json" => array("249", "895138ba151f8b2bd43c081db802afed"), + "plugins/Overlay/lang/nl.json" => array("1559", "f9c6283de64f18c0719459f56394b016"), + "plugins/Overlay/lang/nn.json" => array("54", "91153b8385cce7420ff1210b7d2aa679"), + "plugins/Overlay/lang/pl.json" => array("292", "4dcf4dbc1dba6bc04bb564d063b00b8e"), + "plugins/Overlay/lang/pt-br.json" => array("1659", "74861a9d80bfd4ceb2f8f1729f9b00f6"), + "plugins/Overlay/lang/pt.json" => array("62", "faa03c93ea22e30268f939a2d3bc9314"), + "plugins/Overlay/lang/ro.json" => array("1439", "dc0909ab3a8da37b966011e9663b0445"), + "plugins/Overlay/lang/ru.json" => array("901", "476fa3fad5ad2035f502bf6e43e8752a"), + "plugins/Overlay/lang/sk.json" => array("207", "4bd756cd1f408afb4cba35f91208c688"), + "plugins/Overlay/lang/sl.json" => array("57", "7c4d845d6cbbb81371b6b94e5862499d"), + "plugins/Overlay/lang/sq.json" => array("53", "bb9f7689f24f12acd4fdd3d34923890e"), + "plugins/Overlay/lang/sr.json" => array("1486", "f7c7f83e19ac83cf549b5515a43e1442"), + "plugins/Overlay/lang/sv.json" => array("1378", "6336acda0c7ed252f0f00f116cb72b69"), + "plugins/Overlay/lang/ta.json" => array("189", "8c3160c4bdff593a5f22026334989aab"), + "plugins/Overlay/lang/te.json" => array("151", "2282353d44e78a465898c7a26987e301"), + "plugins/Overlay/lang/th.json" => array("238", "37cd0ae58a99ffefe2f871522d83f197"), + "plugins/Overlay/lang/tl.json" => array("1393", "441e2bee7e3beb59f2c07e3110c00293"), + "plugins/Overlay/lang/tr.json" => array("54", "c09f92a572b3559df989db0b889c7950"), + "plugins/Overlay/lang/uk.json" => array("81", "e07ee2805b4e5eeea6b4c67b4787aedd"), + "plugins/Overlay/lang/vi.json" => array("1520", "84a7b61227e44ba8e93e0763e80cd4ae"), + "plugins/Overlay/lang/zh-cn.json" => array("1351", "b4faba5ad5c27e70907c9e7063c11d9f"), + "plugins/Overlay/lang/zh-tw.json" => array("55", "2384b9539d80f596d0e0dcd69f854b8e"), + "plugins/Overlay/Overlay.php" => array("996", "22c3ec1fac70537ccf132c8d9539e976"), + "plugins/Overlay/stylesheets/overlay.css" => array("2878", "9e4bcc8b98984346c868570ad81fc9b0"), "plugins/Overlay/stylesheets/showErrorWrongDomain.css" => array("225", "c816e5c78ca1e4b4cffd6a94defcd42d"), - "plugins/Overlay/templates/index_noframe.twig" => array("651", "c52979a6cf4b4271976930fee8d75c9c"), - "plugins/Overlay/templates/index.twig" => array("2884", "39ccaab561b54a8b08d2928a111df52d"), - "plugins/Overlay/templates/notifyParentIframe.twig" => array("408", "511ff3cd9499c53ca4c83bf12ca4a2d5"), - "plugins/Overlay/templates/renderSidebar.twig" => array("872", "2692f857cb9ed8c4e57e42e32f9e9d79"), - "plugins/Overlay/templates/showErrorWrongDomain.twig" => array("434", "5ea0fec6f13ac36e82316bd98faffa17"), - "plugins/PrivacyManager/Config.php" => array("3733", "cb83704c0bef57bfd13926749ff933c0"), - "plugins/PrivacyManager/Controller.php" => array("12791", "f42e2f56e22820fc57379e2ca5445728"), - "plugins/PrivacyManager/DoNotTrackHeaderChecker.php" => array("2463", "f8225b87bffb42e0f2e37677b3887991"), - "plugins/PrivacyManager/IPAnonymizer.php" => array("2588", "b1d6687046c4138308f8906e29f5aa27"), - "plugins/PrivacyManager/javascripts/privacySettings.js" => array("7303", "6bc9a333ab44580364f515dabaff87d5"), - "plugins/PrivacyManager/LogDataPurger.php" => array("11350", "1615d14d06032ab180b20ec90d07475e"), - "plugins/PrivacyManager/PrivacyManager.php" => array("17475", "20d15ebced957a28e59abfda6bb85c9b"), - "plugins/PrivacyManager/ReportsPurger.php" => array("14516", "f5927d9196cb3efb7804c537b243a0f4"), + "plugins/Overlay/templates/index_noframe.twig" => array("672", "b3133954c1e583910e749a95c3544237"), + "plugins/Overlay/templates/index.twig" => array("2926", "d6a7439f248eb604f68e261e4a5082bf"), + "plugins/Overlay/templates/notifyParentIframe.twig" => array("460", "0cec680bb7d2720b609e03da66c369b3"), + "plugins/Overlay/templates/renderSidebar.twig" => array("1132", "b12fd5abdcdc66ca5524ee34c5712538"), + "plugins/Overlay/templates/showErrorWrongDomain.twig" => array("459", "7fb5c6c719735505cf6f2b1798062c32"), + "plugins/Overlay/templates/startOverlaySession.twig" => array("1872", "c2c84428140f7416ed3db1dc54405139"), + "plugins/PiwikPro/config/test.php" => array("310", "a19efcd9ea673f4d22ed045abc7abdc2"), + "plugins/PiwikPro/images/promo.png" => array("2349", "02e043f4426dba3d50e78a3fab19134c"), + "plugins/PiwikPro/lang/en.json" => array("143", "f7a7302c6dd80293ad5e204a4ae39bd1"), + "plugins/PiwikPro/PiwikPro.php" => array("576", "72b07947a71b431c12a6b2777a685b46"), + "plugins/PiwikPro/plugin.json" => array("159", "3e06e175584efa64d393fe85cb6fdd85"), + "plugins/PiwikPro/Promo.php" => array("2424", "534c88e23069eefa0358ce7d58848394"), + "plugins/PiwikPro/stylesheets/widget.less" => array("442", "c5778dfac63acec7fccee58a208bfe11"), + "plugins/PiwikPro/templates/promoPiwikProWidget.twig" => array("375", "6276dfced7c3dd0b07ec4a2a720a2d46"), + "plugins/PiwikPro/Widgets.php" => array("1905", "5267b454d5603a822e34adbc0870a8a6"), + "plugins/PrivacyManager/Config.php" => array("3738", "282509f1a10316199332af4eeba0b94d"), + "plugins/PrivacyManager/Controller.php" => array("13262", "1b59d8c3cd2a54f9cf432454afdf7ae4"), + "plugins/PrivacyManager/DoNotTrackHeaderChecker.php" => array("4074", "f2aedad628548d446a23d9ffd8068710"), + "plugins/PrivacyManager/IPAnonymizer.php" => array("2189", "8ab77a6befebfbbff1469c6006a9d989"), + "plugins/PrivacyManager/javascripts/privacySettings.js" => array("7568", "ed58cbf9b5060eca9122bcf9b074863a"), + "plugins/PrivacyManager/lang/ar.json" => array("217", "809314835844e1a2acd3660edb06e308"), + "plugins/PrivacyManager/lang/be.json" => array("2098", "a51fd95f8bd90985ecdfcc54436a021d"), + "plugins/PrivacyManager/lang/bg.json" => array("9456", "6c7dd523d08a513832eae1a575dfc30c"), + "plugins/PrivacyManager/lang/ca.json" => array("6409", "ef1549d01c0017a0fc21a2c8129196c1"), + "plugins/PrivacyManager/lang/cs.json" => array("7420", "44bb045d6edf70386280e41b78a0d0ba"), + "plugins/PrivacyManager/lang/da.json" => array("6867", "87f9034563b262cf8a66177a992e181c"), + "plugins/PrivacyManager/lang/de.json" => array("8013", "dfb15ca264f16c6ab0d6de6b5686b889"), + "plugins/PrivacyManager/lang/el.json" => array("12893", "509d450f49afd5f7b1552eca6b7ee63e"), + "plugins/PrivacyManager/lang/en.json" => array("6934", "ed39baead0dbace2e81edf9fcb511fcd"), + "plugins/PrivacyManager/lang/es.json" => array("8203", "eb380e389adb6edd7e47df0bbce299ef"), + "plugins/PrivacyManager/lang/et.json" => array("956", "3689e009ae424e7c684eb62fe048b242"), + "plugins/PrivacyManager/lang/fa.json" => array("6883", "6deee971d6528dee2762551339a02642"), + "plugins/PrivacyManager/lang/fi.json" => array("6312", "d029e1ae5db4840fd144be9ece09c1dd"), + "plugins/PrivacyManager/lang/fr.json" => array("8389", "79a41d9b06d7dc39d75d76b05b4dc6e3"), + "plugins/PrivacyManager/lang/he.json" => array("224", "df7a315985bde3ce28adafecd1073557"), + "plugins/PrivacyManager/lang/hi.json" => array("12599", "188f4e0ce54a45407ddd0800174eba34"), + "plugins/PrivacyManager/lang/hr.json" => array("97", "f58ba946aea666e8de8d90d8f51bd1d0"), + "plugins/PrivacyManager/lang/hu.json" => array("246", "ef9e3c9c281550f0d21f6f7bad7824f2"), + "plugins/PrivacyManager/lang/id.json" => array("5916", "181e39c45e474fbbbede5a80670537bd"), + "plugins/PrivacyManager/lang/is.json" => array("159", "c457a96dc3dc2263bc65ef21c47d0f58"), + "plugins/PrivacyManager/lang/it.json" => array("7625", "df7db12eed549e60907d54797bb4faff"), + "plugins/PrivacyManager/lang/ja.json" => array("9096", "7e163a1b27a45afc2d8b5803ed767e2e"), + "plugins/PrivacyManager/lang/ka.json" => array("401", "7e76815d9e4f8ebd35e2abf63ca15918"), + "plugins/PrivacyManager/lang/ko.json" => array("8045", "84c18fdace5769b8979336462e536e7d"), + "plugins/PrivacyManager/lang/lt.json" => array("633", "862fed853b43f7562f0ac910eb04bd05"), + "plugins/PrivacyManager/lang/lv.json" => array("946", "77e7902ee18aa72a48b4b80947f77d43"), + "plugins/PrivacyManager/lang/nb.json" => array("1449", "9bb17e4c0b3bfafe423b0c6066c36be7"), + "plugins/PrivacyManager/lang/nl.json" => array("7051", "ab937ec1571403de261b8e276b7a8752"), + "plugins/PrivacyManager/lang/nn.json" => array("320", "bd5e881458c7ab1107cd72253b170e63"), + "plugins/PrivacyManager/lang/pl.json" => array("3458", "4b0bad12fdce5833b9ff6483366dbd92"), + "plugins/PrivacyManager/lang/pt-br.json" => array("7898", "e74ad54687aede8a990bc79db0782914"), + "plugins/PrivacyManager/lang/pt.json" => array("1493", "a8a3c9080275aa554f9d26a345569c13"), + "plugins/PrivacyManager/lang/ro.json" => array("7133", "c23cb39e3070bc337cfa6e4d49632c85"), + "plugins/PrivacyManager/lang/ru.json" => array("10681", "b6df20958a064d2c53b8893d9b2d1245"), + "plugins/PrivacyManager/lang/sk.json" => array("648", "2ed5e10c6b66b9408291c954ae7c3326"), + "plugins/PrivacyManager/lang/sl.json" => array("760", "e83135609652c31f8cf7de4143c83265"), + "plugins/PrivacyManager/lang/sq.json" => array("1863", "83dc4674da2a8ddf58040ec7154338e1"), + "plugins/PrivacyManager/lang/sr.json" => array("7281", "e8c64363ae78576cf27ee50b168c8cdf"), + "plugins/PrivacyManager/lang/sv.json" => array("7205", "87262d9034b1c810a2d8ab6a72a26c37"), + "plugins/PrivacyManager/lang/te.json" => array("228", "0dc1d05f984d24fd3353bd6928956c8d"), + "plugins/PrivacyManager/lang/th.json" => array("2472", "f09b5bd3978a92c08a0c887af46cee99"), + "plugins/PrivacyManager/lang/tl.json" => array("6727", "4b9478c0ef60f8340d26e7868b4b2196"), + "plugins/PrivacyManager/lang/tr.json" => array("616", "8891d799cf1d6ac86a21292be8a85f79"), + "plugins/PrivacyManager/lang/uk.json" => array("338", "8cc8f459154e9c00ebea21e68875e51c"), + "plugins/PrivacyManager/lang/vi.json" => array("7436", "18cb179b7c73a248bdfed9e47fb9e7a6"), + "plugins/PrivacyManager/lang/zh-cn.json" => array("5430", "ef1e9ea2f06ab6702c6b75cd8925525e"), + "plugins/PrivacyManager/lang/zh-tw.json" => array("151", "1f620a10e9468b3a43d289b6e0899506"), + "plugins/PrivacyManager/LogDataPurger.php" => array("5214", "b5645d500435dd86f046fa98aef11f8d"), + "plugins/PrivacyManager/Menu.php" => array("606", "c7e16bc01fb2af947113a7a9b218ce23"), + "plugins/PrivacyManager/PrivacyManager.php" => array("18559", "38fd81610c837c1c44613aca057f003f"), + "plugins/PrivacyManager/ReportsPurger.php" => array("14850", "ad32d80402f1e78dde30cac6b18f62a0"), + "plugins/PrivacyManager/Tasks.php" => array("701", "7f52adc528600755f855eed3f0cb1e45"), "plugins/PrivacyManager/templates/getDatabaseSize.twig" => array("394", "b20b279686f6084c9b6d1e054711991c"), - "plugins/PrivacyManager/templates/privacySettings.twig" => array("20783", "b213cd581d7f612668eded5b4daf9875"), - "plugins/Provider/API.php" => array("1282", "02ed45d84f2acf2d31d1ec3dfa657715"), - "plugins/Provider/Archiver.php" => array("885", "12721c47c4a162253ef42338f86d6407"), - "plugins/Provider/Controller.php" => array("442", "074ac889d8bbd8521336f33cafc5980b"), - "plugins/Provider/functions.php" => array("1874", "fcce854c96b6a99d906cb348833c6f79"), - "plugins/Provider/Provider.php" => array("7890", "bcf48128ceabc04b1295ba16cffbaf20"), - "plugins/Proxy/Controller.php" => array("3809", "92af78c5373bb29f60fa3dbbdc6e96de"), - "plugins/Proxy/Proxy.php" => array("748", "7fa5c2c560d621ea057cb3258f6ed0e4"), - "plugins/Referrers/API.php" => array("21979", "18add5717656db81281bf2aab318f4bd"), - "plugins/Referrers/Archiver.php" => array("11276", "312f7099cd0f95a633a20a20d9e0e7a3"), - "plugins/Referrers/Controller.php" => array("21890", "3d8ee0bbab081d220d5501ba2535e9a6"), - "plugins/Referrers/functions.php" => array("6981", "5bb99c15c9912d20d94aa399831c3cc3"), + "plugins/PrivacyManager/templates/privacySettings.twig" => array("19214", "0b1e72a93260dca6be36a73b5e7697b9"), + "plugins/Provider/API.php" => array("1192", "85d8114d8929a55e92121aa1bfe4b17e"), + "plugins/Provider/Archiver.php" => array("1206", "0301de3ad875ba8338828df7faa6f342"), + "plugins/Provider/Columns/Provider.php" => array("3121", "40235bcd2b003f4dc4331bb5fd81b720"), + "plugins/Provider/Controller.php" => array("257", "14d269796ec2e34356a31ba0f65dbe66"), + "plugins/Provider/functions.php" => array("1718", "abb7bbb4edbfaae06df9ded1a5c7aba8"), + "plugins/Provider/lang/am.json" => array("117", "291791d66aa097caf78e730fd9350367"), + "plugins/Provider/lang/ar.json" => array("744", "aadefe1dcba85f865c97cdb923b5b48e"), + "plugins/Provider/lang/be.json" => array("669", "49023708c4548f775f3a5691ede3e2ee"), + "plugins/Provider/lang/bg.json" => array("640", "00e1ffbe4b613d244c685b20c360caa1"), + "plugins/Provider/lang/ca.json" => array("399", "f32df5e63ff134c70e8fc5207a90c295"), + "plugins/Provider/lang/cs.json" => array("651", "aac7a90e28676047872712b9fb0ebe22"), + "plugins/Provider/lang/da.json" => array("503", "348264e58c95aa5a6f70270efa277bfe"), + "plugins/Provider/lang/de.json" => array("621", "74bb0afb1819c983349af23201f2e01b"), + "plugins/Provider/lang/el.json" => array("1002", "f9f146d5698e5dd243ce14732a0c32c7"), + "plugins/Provider/lang/en.json" => array("544", "f54b24dd2004282a8e02fca0da2e1db7"), + "plugins/Provider/lang/es.json" => array("638", "4d5fcbcb6994f9082504c10a0b349334"), + "plugins/Provider/lang/et.json" => array("116", "3017fc79c15823d758d7b2c40e84638a"), + "plugins/Provider/lang/eu.json" => array("112", "44fd1acd44276234a30c226dd6d0e22b"), + "plugins/Provider/lang/fa.json" => array("144", "cc86e295bb45ad3ba9ba0d86162c9f59"), + "plugins/Provider/lang/fi.json" => array("381", "d7ce76d8b11ec09abe316762e0e36001"), + "plugins/Provider/lang/fr.json" => array("638", "cc17e6f767d637c0bb4bb142845e8862"), + "plugins/Provider/lang/gl.json" => array("67", "026e7ddfea388c6b7a8fd61656dfae91"), + "plugins/Provider/lang/hi.json" => array("1191", "7d92824326e773c2e372646ce9f359db"), + "plugins/Provider/lang/hu.json" => array("130", "0f5819a027e320364b4fbc0ab3ee4276"), + "plugins/Provider/lang/id.json" => array("405", "d47aea611c92bdfbb8792bfb76aefd69"), + "plugins/Provider/lang/is.json" => array("104", "7949c9b77ffa7ec26ae1c2d2a525f21a"), + "plugins/Provider/lang/it.json" => array("611", "a2a84e970b4f9322c781ac2b7c71b942"), + "plugins/Provider/lang/ja.json" => array("764", "38fe8a42d281b1809c009ade75f132bf"), + "plugins/Provider/lang/ka.json" => array("153", "70ee5247470cb637de4f61733fd2359f"), + "plugins/Provider/lang/ko.json" => array("631", "530c195c5270e95942c11afe9b4f9ed9"), + "plugins/Provider/lang/lt.json" => array("111", "6f234e24add9a01756f6e23f59806b76"), + "plugins/Provider/lang/lv.json" => array("131", "aa48cda969c2dc6bb40fbc7d3cfff287"), + "plugins/Provider/lang/nb.json" => array("595", "14976d530cee609030bb168400d167b1"), + "plugins/Provider/lang/nl.json" => array("584", "e3ac8f22d3a88e3ea87ac53eb5f796a4"), + "plugins/Provider/lang/pl.json" => array("103", "4fa4a69ebd2df5bdeaa0595ee8bcec10"), + "plugins/Provider/lang/pt-br.json" => array("616", "78247792331827d10f8f5bad65a379f3"), + "plugins/Provider/lang/pt.json" => array("377", "a5d76f1e16933a5d4911e60ac95d2765"), + "plugins/Provider/lang/ro.json" => array("408", "7afb2d3cb7171ecac03c6705c3b8f1be"), + "plugins/Provider/lang/ru.json" => array("677", "72e611073439af083857bd73faa45c75"), + "plugins/Provider/lang/sk.json" => array("114", "70a0fa24d8673f272d46a63aaef4f895"), + "plugins/Provider/lang/sl.json" => array("642", "c2aa1fd3276cb51ada69c89244f21210"), + "plugins/Provider/lang/sq.json" => array("439", "a8ea30b514a2be021b4bdc7b5bb6f9b1"), + "plugins/Provider/lang/sr.json" => array("571", "3b028e20cc9201b606570f59514bb9f6"), + "plugins/Provider/lang/sv.json" => array("496", "a10b89c343ea45c4d17dd4a72768c36d"), + "plugins/Provider/lang/ta.json" => array("144", "73d291097b6b445fa30a0dc12ce91e9b"), + "plugins/Provider/lang/th.json" => array("159", "be98f042cd6704b939791bb01c20bfe9"), + "plugins/Provider/lang/tl.json" => array("541", "28722ac542adf1468039a35e645190cd"), + "plugins/Provider/lang/tr.json" => array("114", "f8893e4e3c32b792881a8f4591f7070c"), + "plugins/Provider/lang/uk.json" => array("125", "17af9a194a78480067f6ce95dde0bcc5"), + "plugins/Provider/lang/vi.json" => array("488", "ae88ce12dc83f5f364f388c6123e25b1"), + "plugins/Provider/lang/zh-cn.json" => array("319", "9a709d71a4833b889e28ffade6fa8609"), + "plugins/Provider/lang/zh-tw.json" => array("123", "472b7d00e00d26a2e056ea69210c3aae"), + "plugins/Provider/Provider.php" => array("3953", "5a2afbf9bcd08342e5a3e9e0b797e54b"), + "plugins/Provider/Reports/GetProvider.php" => array("1441", "c639c63375a4ad17277973ea392f99a4"), + "plugins/Provider/Visitor.php" => array("871", "f02f476db53ef3d649845920ba45339a"), + "plugins/Proxy/Controller.php" => array("3823", "9a1b507a1501508bb88fe820e5eb256a"), + "plugins/Proxy/plugin.json" => array("39", "390f9fd5abb660cc3b56bcda43d7e615"), + "plugins/Proxy/Proxy.php" => array("266", "a46c59933b4ce1d2accbf905a8969aa7"), + "plugins/Referrers/API.php" => array("23201", "00bf351b1310953eb2a8696c0b5ae774"), + "plugins/Referrers/Archiver.php" => array("11610", "60765c90ac1cd163eb1faa07b8e487a9"), + "plugins/Referrers/Columns/Base.php" => array("20323", "b32b820ef90292ba45e6b8fae3616fff"), + "plugins/Referrers/Columns/Campaign.php" => array("1863", "8897c4e353e3d3a82595a0c6bbb03fcc"), + "plugins/Referrers/Columns/Keyword.php" => array("1673", "6d0dc2c2b18ed489db2ba43e426d8f3b"), + "plugins/Referrers/Columns/ReferrerName.php" => array("1581", "2190fac10435a8909534b72a4eb2a936"), + "plugins/Referrers/Columns/Referrer.php" => array("380", "4a1b3ebff433500a2d83366dbe89d11a"), + "plugins/Referrers/Columns/ReferrerType.php" => array("1599", "27ed36a1b4df2916e90559948d118524"), + "plugins/Referrers/Columns/ReferrerUrl.php" => array("1101", "54b0899aed9da7863bb49efd0f07f60a"), + "plugins/Referrers/Columns/SearchEngine.php" => array("394", "9763aeabe02ca6c5372746aedfe5cbd8"), + "plugins/Referrers/Columns/SocialNetwork.php" => array("389", "755ebaae2e5308e15b0df56527380f5e"), + "plugins/Referrers/Columns/WebsitePage.php" => array("392", "23e0ad84c75bee3eceabcbdc8666bda6"), + "plugins/Referrers/Columns/Website.php" => array("1599", "060093f62dd2e98b4e39e933e37b34bb"), + "plugins/Referrers/Controller.php" => array("20234", "199b2eccea22beaee54db9a2d165503f"), + "plugins/Referrers/DataTable/Filter/KeywordNotDefined.php" => array("582", "da0b8960eac350bbffd910e94c4b1db9"), + "plugins/Referrers/DataTable/Filter/KeywordsFromSearchEngineId.php" => array("1819", "967d80878e965d628c30457ac9155363"), + "plugins/Referrers/DataTable/Filter/SearchEnginesFromKeywordId.php" => array("2043", "80f6cef775b5a8fb5d9baf32e35a2392"), + "plugins/Referrers/DataTable/Filter/SetGetReferrerTypeSubtables.php" => array("2820", "6ddb30bdc98f97d21f2ff027d4acf6bd"), + "plugins/Referrers/DataTable/Filter/UrlsForSocial.php" => array("1141", "8380453b8feb440d2747ff69ae2e0fae"), + "plugins/Referrers/DataTable/Filter/UrlsFromWebsiteId.php" => array("1270", "e813b81e0c93863324f1e6b885a5fa14"), + "plugins/Referrers/functions.php" => array("2234", "1df5df4efb92a343684ff5b45fb38d87"), "plugins/Referrers/images/searchEngines/1.cz.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"), "plugins/Referrers/images/searchEngines/abcsok.no.png" => array("734", "46c308c614869c6a7b6dea023df9fb7a"), "plugins/Referrers/images/searchEngines/alexa.com.png" => array("878", "dea186e4049c87f56d90aa473cc58e65"), @@ -1738,6 +4381,7 @@ class Manifest { "plugins/Referrers/images/searchEngines/busca.orange.es.png" => array("430", "da209f011cf118792af6682db448e0cc"), "plugins/Referrers/images/searchEngines/busca.uol.com.br.png" => array("701", "b7108a19b9c249b774f6a5777580cf18"), "plugins/Referrers/images/searchEngines/cgi.search.biglobe.ne.jp.png" => array("716", "806a4ed72d78c7b84472567d28589894"), + "plugins/Referrers/images/searchEngines/chercherfr.aguea.com.png" => array("4303", "067083a7accc683ee2d34392959887c6"), "plugins/Referrers/images/searchEngines/claro-search.com.png" => array("564", "302c71d5eb2d7216713dc6bec51d51d4"), "plugins/Referrers/images/searchEngines/daemon-search.com.png" => array("718", "c14a1c8b35e78a1d6661f7e25e370f9f"), "plugins/Referrers/images/searchEngines/digg.com.png" => array("465", "1755793491662ad23c71eb1623706dc2"), @@ -1747,6 +4391,7 @@ class Manifest { "plugins/Referrers/images/searchEngines/ecosia.org.png" => array("667", "2a93403cf58fb4ad1f7393abca387180"), "plugins/Referrers/images/searchEngines/encrypted.google.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"), "plugins/Referrers/images/searchEngines/eo.st.png" => array("445", "aa76f4c0c414201782d015ee304643d0"), + "plugins/Referrers/images/searchEngines/extern.peoplecheck.de.png" => array("347", "10accdc8511ec172f9a9b1b5443c996f"), "plugins/Referrers/images/searchEngines/forestle.org.png" => array("814", "3849505c44dd9af3564964b2c7f72c24"), "plugins/Referrers/images/searchEngines/fr.dir.com.png" => array("735", "a08719a48edcf34246e545276a9537b6"), "plugins/Referrers/images/searchEngines/friendfeed.com.png" => array("524", "91638ac895b524e32425e742cfd98dd0"), @@ -1757,7 +4402,9 @@ class Manifest { "plugins/Referrers/images/searchEngines/google.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"), "plugins/Referrers/images/searchEngines/googlesyndicatedsearch.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"), "plugins/Referrers/images/searchEngines/holmes.ge.png" => array("774", "89027cbd84500007fb09c4ed1a2c8273"), + "plugins/Referrers/images/searchEngines/image.search.yahoo.co.jp.png" => array("522", "76ac9b6fda30c632b0c4258d72072680"), "plugins/Referrers/images/searchEngines/images.google.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"), + "plugins/Referrers/images/searchEngines/images.search.biglobe.ne.jp.png" => array("808", "64a4a3d5a3487a29c35434ae1e5a9915"), "plugins/Referrers/images/searchEngines/images.search.yahoo.com.png" => array("538", "9c23aa1c6d5ad4fdea3738632364cbb2"), "plugins/Referrers/images/searchEngines/images.yandex.ru.png" => array("497", "0ff0e2ee9e0b08fbfba2eaa9f4612848"), "plugins/Referrers/images/searchEngines/infospace.com.png" => array("940", "c269cb5b0658fe1679db465613a7c406"), @@ -1765,12 +4412,15 @@ class Manifest { "plugins/Referrers/images/searchEngines/ixquick.com.png" => array("613", "203d372a14add8806719d1fe499210e4"), "plugins/Referrers/images/searchEngines/junglekey.com.png" => array("580", "498a589e8b6b01a45be9b5b090897c03"), "plugins/Referrers/images/searchEngines/jyxo.1188.cz.png" => array("401", "f1845d795944ceb17f0d2490e771331d"), + "plugins/Referrers/images/searchEngines/k9safesearch.com.png" => array("562", "ea7a3bf74a549a7d618255b94215eaf3"), "plugins/Referrers/images/searchEngines/ko.search.need2find.com.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"), + "plugins/Referrers/images/searchEngines/kwzf.net.png" => array("265", "34652cdc25d70bba01ce9ac01d929d3b"), "plugins/Referrers/images/searchEngines/lo.st.png" => array("828", "cebd310f1c9e95a2afab3699e8877e4a"), "plugins/Referrers/images/searchEngines/maps.google.com.png" => array("1132", "b95321cca7e7e4745c9478060113984e"), "plugins/Referrers/images/searchEngines/metager2.de.png" => array("556", "147f78f79ab041ce6131f310a94ad772"), "plugins/Referrers/images/searchEngines/meta.rrzn.uni-hannover.de.png" => array("281", "c851ac18187f08ac7f5860f31f5a8231"), "plugins/Referrers/images/searchEngines/meta.ua.png" => array("873", "190caab492691e94df900c74f9445f85"), + "plugins/Referrers/images/searchEngines/m.sm.cn.png" => array("649", "48f49c22225145764595e204269893a5"), "plugins/Referrers/images/searchEngines/news.google.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"), "plugins/Referrers/images/searchEngines/nigma.ru.png" => array("673", "41743688398e2ee8a5e30bb32f74ad34"), "plugins/Referrers/images/searchEngines/nova.rambler.ru.png" => array("765", "f74b4c361dfae2e7a1b4116ad7b28925"), @@ -1792,6 +4442,7 @@ class Manifest { "plugins/Referrers/images/searchEngines/searchalot.com.png" => array("369", "6dad79f26644dfc7640af20743994c82"), "plugins/Referrers/images/searchEngines/search.aol.com.png" => array("713", "52bf4d61fe18c1e8564c75709cfee36a"), "plugins/Referrers/images/searchEngines/searchatlas.centrum.cz.png" => array("716", "fac3ddbc5e34c229aac77d4297d09e8b"), + "plugins/Referrers/images/searchEngines/search.auone.jp.png" => array("3609", "11aec5432959244cfa3e5046cc68ce3e"), "plugins/Referrers/images/searchEngines/search.babylon.com.png" => array("930", "5db9449cdc8ab7964896f62c26bd5799"), "plugins/Referrers/images/searchEngines/search.bluewin.ch.png" => array("349", "4ce3ac28b1d3446d71c8db2335f79506"), "plugins/Referrers/images/searchEngines/search.centrum.cz.png" => array("599", "4a90a6fd8cd5a587ee3f887fb2e91a87"), @@ -1800,8 +4451,10 @@ class Manifest { "plugins/Referrers/images/searchEngines/search.daum.net.png" => array("800", "fc91668b8deb23b4b7290dceb75f2428"), "plugins/Referrers/images/searchEngines/search.earthlink.net.png" => array("564", "889aec3d7357d1c7cbb6d52225644907"), "plugins/Referrers/images/searchEngines/search.excite.it.png" => array("323", "5c915ccd6526168e81de15c7fefb050a"), + "plugins/Referrers/images/searchEngines/search.fooooo.com.png" => array("457", "39cb6a4a8b4a023c8e427b50f3179202"), "plugins/Referrers/images/searchEngines/search.freecause.com.png" => array("951", "834f2c111eed35eaf997913c20ab56ff"), "plugins/Referrers/images/searchEngines/search.free.fr.png" => array("881", "02f2297d212c26bd136a99fbc5488170"), + "plugins/Referrers/images/searchEngines/search.genieo.com.png" => array("674", "172a9fbe9867dea9a0926af1f79b3516"), "plugins/Referrers/images/searchEngines/search.goo.ne.jp.png" => array("730", "8d650ba94af30886600ca236f9fbb443"), "plugins/Referrers/images/searchEngines/search.imesh.com.png" => array("644", "2dd75a109986a2337893e3b0d0a621a1"), "plugins/Referrers/images/searchEngines/search.ke.voila.fr.png" => array("331", "5a2240c18b117cf38cc00914d01ee34e"), @@ -1812,6 +4465,7 @@ class Manifest { "plugins/Referrers/images/searchEngines/search.peoplepc.com.png" => array("832", "67f88efd19919d2292405319f6d5092d"), "plugins/Referrers/images/searchEngines/search.qip.ru.png" => array("556", "8bc854ef9a247905ed29938e69d2cea0"), "plugins/Referrers/images/searchEngines/search.rr.com.png" => array("794", "3e9a94cd34825c4246eb666338b22e6b"), + "plugins/Referrers/images/searchEngines/search.seesaa.jp.png" => array("3368", "6cb490bde2df941e985765de150d799a"), "plugins/Referrers/images/searchEngines/searchservice.myspace.com.png" => array("610", "96ecdce6052dd3a535f95ed92c056bc2"), "plugins/Referrers/images/searchEngines/search.seznam.cz.png" => array("553", "4b4f7b4eec38531fe662f678682041f8"), "plugins/Referrers/images/searchEngines/search.smartaddressbar.com.png" => array("624", "13ce4ccf571cd2f09553318b719948b2"), @@ -1820,12 +4474,14 @@ class Manifest { "plugins/Referrers/images/searchEngines/search.tiscali.it.png" => array("548", "f9fea4cbf8bc6f5748c7b1e8e5317b0e"), "plugins/Referrers/images/searchEngines/search.winamp.com.png" => array("753", "3210bbc6d253913396ca23a76ef7ae51"), "plugins/Referrers/images/searchEngines/search.www.ee.png" => array("625", "d0b310f299b5fe2f13f925b8a7fd8e66"), + "plugins/Referrers/images/searchEngines/search.yahoo.co.jp.png" => array("522", "76ac9b6fda30c632b0c4258d72072680"), "plugins/Referrers/images/searchEngines/search.yahoo.com.png" => array("522", "76ac9b6fda30c632b0c4258d72072680"), "plugins/Referrers/images/searchEngines/search.yam.com.png" => array("186", "5585e8456efc56e4ccd870fce71c349a"), "plugins/Referrers/images/searchEngines/search.yippy.com.png" => array("654", "348c872e7f49261c025074587210c494"), "plugins/Referrers/images/searchEngines/sm.aport.ru.png" => array("469", "956d1ffb844dcd9c22fc0f82dd02f6d8"), "plugins/Referrers/images/searchEngines/smart.delfi.lv.png" => array("543", "46e62fb3ebddb902b5b426dda74bdc07"), "plugins/Referrers/images/searchEngines/so.360.cn.png" => array("480", "083a258a36d5571cbb3cdbca50231f89"), + "plugins/Referrers/images/searchEngines/sp-image.search.auone.jp.png" => array("3609", "11aec5432959244cfa3e5046cc68ce3e"), "plugins/Referrers/images/searchEngines/startgoogle.startpagina.nl.png" => array("801", "f60a50c67088bd1387631ff895df1b9a"), "plugins/Referrers/images/searchEngines/start.iplay.com.png" => array("293", "96909e49478da43937ced8de17bb7b43"), "plugins/Referrers/images/searchEngines/suche.freenet.de.png" => array("719", "4fb7eeb8dbd2eefd6bb5572d87ca4be9"), @@ -1837,7 +4493,11 @@ class Manifest { "plugins/Referrers/images/searchEngines/szukaj.wp.pl.png" => array("672", "a14bb3c9901c8158d7051ed50b40a6df"), "plugins/Referrers/images/searchEngines/technorati.com.png" => array("567", "23fbfff1215e8bf0270b529849200214"), "plugins/Referrers/images/searchEngines/translate.google.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"), + "plugins/Referrers/images/searchEngines/videa.seznam.cz.png" => array("553", "4b4f7b4eec38531fe662f678682041f8"), "plugins/Referrers/images/searchEngines/video.google.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"), + "plugins/Referrers/images/searchEngines/videosearch.nifty.com.png" => array("565", "f38b9e99892cf2fdf146a8f6e2731858"), + "plugins/Referrers/images/searchEngines/video.search.yahoo.co.jp.png" => array("522", "76ac9b6fda30c632b0c4258d72072680"), + "plugins/Referrers/images/searchEngines/video.so-net.ne.jp.png" => array("3609", "22aaf56cf759644e7f0a851ad36c6ac9"), "plugins/Referrers/images/searchEngines/web.canoe.ca.png" => array("721", "7645213daa2213dc1b1361a42c11700c"), "plugins/Referrers/images/searchEngines/websearch.cs.com.png" => array("806", "daf75065560d60f37511023d8b2a4212"), "plugins/Referrers/images/searchEngines/websearch.rakuten.co.jp.png" => array("513", "e32baedde4b2a9b508336e2560a7b8ec"), @@ -1868,6 +4528,7 @@ class Manifest { "plugins/Referrers/images/searchEngines/www.exalead.fr.png" => array("880", "6cb4ba9a2fda31f0063972e6d608412e"), "plugins/Referrers/images/searchEngines/www.facebook.com.png" => array("349", "34811a0b31ca7dd2934cc02cffbcdc95"), "plugins/Referrers/images/searchEngines/www.fastbrowsersearch.com.png" => array("965", "d1616dfa7bf9d20634b52857f1c91177"), + "plugins/Referrers/images/searchEngines/www.findhurtig.dk.png" => array("615", "ff904e7690351e547b4c5d254184ae18"), "plugins/Referrers/images/searchEngines/www.fireball.de.png" => array("275", "7831b2d7ebbbacacf8ae272bb407e599"), "plugins/Referrers/images/searchEngines/www.firstsfind.com.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"), "plugins/Referrers/images/searchEngines/www.fixsuche.de.png" => array("975", "4701f16ae042e18187b99ad23da4aef0"), @@ -1878,6 +4539,7 @@ class Manifest { "plugins/Referrers/images/searchEngines/www.google.interia.pl.png" => array("690", "fa07f267bcb77246a0cf5e76a77b5c77"), "plugins/Referrers/images/searchEngines/www.goyellow.de.png" => array("778", "d410a511b82055b8817585031292756c"), "plugins/Referrers/images/searchEngines/www.gulesider.no.png" => array("575", "0eae2709e4afbe459883bcb690dbdeef"), + "plugins/Referrers/images/searchEngines/www.haosou.com.png" => array("655", "14d0039471d3d744fed5005ebd9c1f8c"), "plugins/Referrers/images/searchEngines/www.highbeam.com.png" => array("705", "d6fd2b1bd62893f37451bc0fe4d8b0a0"), "plugins/Referrers/images/searchEngines/www.hooseek.com.png" => array("548", "610bbf7fd42486596a7edf046c0aa3ba"), "plugins/Referrers/images/searchEngines/www.hotbot.com.png" => array("275", "7831b2d7ebbbacacf8ae272bb407e599"), @@ -1885,8 +4547,10 @@ class Manifest { "plugins/Referrers/images/searchEngines/www.ilse.nl.png" => array("854", "7321baaf2ff095c525fb1a27dd8a3256"), "plugins/Referrers/images/searchEngines/www.jungle-spider.de.png" => array("867", "6c675278c93d7f5145c116fd12b83076"), "plugins/Referrers/images/searchEngines/www.kataweb.it.png" => array("273", "b9f2ffb235b4459c6e2b6f3f8c811b1c"), + "plugins/Referrers/images/searchEngines/www.kensaq.com.png" => array("278", "43a8c6d6be8c6413cdd8ab0ec0f12a85"), "plugins/Referrers/images/searchEngines/www.kvasir.no.png" => array("395", "2be12aaef3172e501bb5c2960c4bde26"), "plugins/Referrers/images/searchEngines/www.latne.lv.png" => array("756", "89197fc28e878fb017d1fe0f2be63b26"), + "plugins/Referrers/images/searchEngines/www.lookany.com.png" => array("635", "3c7970ddd1509bcaa70164722e41e2e7"), "plugins/Referrers/images/searchEngines/www.looksmart.com.png" => array("423", "c63999bc972ee4181b3aed5506727684"), "plugins/Referrers/images/searchEngines/www.maailm.com.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"), "plugins/Referrers/images/searchEngines/www.mamma.com.png" => array("931", "fbd287ad4cb8ff307ddfde1a61ba6b18"), @@ -1901,14 +4565,18 @@ class Manifest { "plugins/Referrers/images/searchEngines/www.picsearch.com.png" => array("661", "7fd004f30bee9ed392c3dfdd36bc7cce"), "plugins/Referrers/images/searchEngines/www.plazoo.com.png" => array("471", "ce3c7526febd2d33468292a2cc18fd05"), "plugins/Referrers/images/searchEngines/www.qualigo.at.png" => array("627", "c793911b3efed6122c975023c6c86369"), + "plugins/Referrers/images/searchEngines/www.qwant.com.png" => array("991", "0facf32fc2519c93c1a93e11ddef4115"), "plugins/Referrers/images/searchEngines/www.searchcanvas.com.png" => array("867", "a148aa4f2ebd3a9d627b156c261b11f1"), "plugins/Referrers/images/searchEngines/www.search.ch.png" => array("344", "cbf2de2cac7cf39897ed488301b88687"), "plugins/Referrers/images/searchEngines/www.search.com.png" => array("517", "7675624d816df97773003fd968d3fb92"), "plugins/Referrers/images/searchEngines/www.searchy.co.uk.png" => array("656", "e6018e106ad0190fff95a73bb3bb31d9"), "plugins/Referrers/images/searchEngines/www.sharelook.fr.png" => array("433", "85be47a9b158874e7831982d2872c4f3"), "plugins/Referrers/images/searchEngines/www.skynet.be.png" => array("855", "9561e59ff5068d31f7a3a95bbed6bb8f"), + "plugins/Referrers/images/searchEngines/www.sm.de.png" => array("806", "4802f1828c79834917b06392673ddcfb"), "plugins/Referrers/images/searchEngines/www.sogou.com.png" => array("636", "babba0fbf8f3dc3f8e332a9bc0e0b616"), + "plugins/Referrers/images/searchEngines/www.so-net.ne.jp.png" => array("3609", "22aaf56cf759644e7f0a851ad36c6ac9"), "plugins/Referrers/images/searchEngines/www.soso.com.png" => array("895", "4cb24e3d5b5b4f434e9e77b78283fb6e"), + "plugins/Referrers/images/searchEngines/www.sputnik.ru.png" => array("372", "b8303047b8de975eed7d4b2484c1b4bb"), "plugins/Referrers/images/searchEngines/www.startsiden.no.png" => array("285", "4d50ad01809bb118e0e14b690c2898c2"), "plugins/Referrers/images/searchEngines/www.suchmaschine.com.png" => array("605", "fec00799f617313dcec453d470bcc1ec"), "plugins/Referrers/images/searchEngines/www.suchnase.de.png" => array("289", "c0f4d4e1a6d633a2de161d847e51ae38"), @@ -1918,6 +4586,7 @@ class Manifest { "plugins/Referrers/images/searchEngines/www.tixuma.de.png" => array("933", "ac483ba65b637e352bd9b7edf11e462e"), "plugins/Referrers/images/searchEngines/www.toile.com.png" => array("868", "7e46a21790c2ca55a8adacaa3ecb860f"), "plugins/Referrers/images/searchEngines/www.toolbarhome.com.png" => array("617", "d39570e56b848781f614ae70f8ccd8a9"), + "plugins/Referrers/images/searchEngines/www.toppreise.ch.png" => array("599", "58f7f59a0866454fc990edb5db2edb5f"), "plugins/Referrers/images/searchEngines/www.trouvez.com.png" => array("498", "cfcae76baa6008dbdad8d65736ca9c07"), "plugins/Referrers/images/searchEngines/www.trovarapido.com.png" => array("540", "25010fae9beae3454550b78cd1c62cca"), "plugins/Referrers/images/searchEngines/www.trusted-search.com.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"), @@ -1930,6 +4599,7 @@ class Manifest { "plugins/Referrers/images/searchEngines/www.weborama.fr.png" => array("978", "c56c4180238352c79c39105ca60c8d41"), "plugins/Referrers/images/searchEngines/www.websearch.com.png" => array("830", "b7275242d17e492422677fd66e4fe38a"), "plugins/Referrers/images/searchEngines/www.witch.de.png" => array("384", "3471981c242407d47d9786ee4b9dea4a"), + "plugins/Referrers/images/searchEngines/www.woopie.jp.png" => array("319", "6b796434e9c51a65fcfc3b51ec275ba8"), "plugins/Referrers/images/searchEngines/www.x-recherche.com.png" => array("546", "1f424da61da3eb848ecef393a6bccfc5"), "plugins/Referrers/images/searchEngines/www.yasni.de.png" => array("747", "01c5182fc922793a5b408411b53a3c6e"), "plugins/Referrers/images/searchEngines/www.yatedo.com.png" => array("647", "aa0d9cbbe207eaa5bcab22dee20185ce"), @@ -1937,6 +4607,7 @@ class Manifest { "plugins/Referrers/images/searchEngines/www.zapmeta.com.png" => array("220", "1707e427049d54e0046fdbe8d6b90250"), "plugins/Referrers/images/searchEngines/www.zoeken.nl.png" => array("598", "d26fe06a02e1b7e33e0f519af929d9a6"), "plugins/Referrers/images/searchEngines/www.zoznam.sk.png" => array("349", "b690e483bd1a89acd54f0f21531453f6"), + "plugins/Referrers/images/searchEngines/www.zxuso.com.png" => array("814", "cd6f1421a1e792776cc45fe635027f92"), "plugins/Referrers/images/searchEngines/xx.gif" => array("55", "a7bc48d335263fe532d80efb25afb2d5"), "plugins/Referrers/images/searchEngines/xx.png" => array("265", "34652cdc25d70bba01ce9ac01d929d3b"), "plugins/Referrers/images/searchEngines/yandex.ru.png" => array("497", "0ff0e2ee9e0b08fbfba2eaa9f4612848"), @@ -1948,6 +4619,7 @@ class Manifest { "plugins/Referrers/images/socials/buzznet.com.png" => array("462", "3b3e4ee1fe990041080a93bff2d34f98"), "plugins/Referrers/images/socials/classmates.com.png" => array("861", "fc6ae2a786dba146d8279e269e39bb48"), "plugins/Referrers/images/socials/douban.com.png" => array("530", "be1b7fb618ab9ea1eeb04e15b4428b1a"), + "plugins/Referrers/images/socials/dribbble.com.png" => array("3468", "b06a7ae17321542ef4a1144bc5c4e64a"), "plugins/Referrers/images/socials/facebook.com.png" => array("300", "cadba1be0b6e8d4e3ef1755f3cee55ed"), "plugins/Referrers/images/socials/flickr.com.png" => array("1120", "4bf2b10ac1b80eac5f749ea65ff808b0"), "plugins/Referrers/images/socials/flixster.com.png" => array("908", "2011245e7a16ab25fc4f0c5d21487f6a"), @@ -2011,21 +4683,207 @@ class Manifest { "plugins/Referrers/images/socials/xing.com.png" => array("675", "3764aea0fd7c26bf8bf8e95958d94fff"), "plugins/Referrers/images/socials/xx.png" => array("265", "34652cdc25d70bba01ce9ac01d929d3b"), "plugins/Referrers/images/socials/youtube.com.png" => array("695", "81125e13b69e308800a98d9752e3f48b"), - "plugins/Referrers/Referrers.php" => array("29947", "909f867aa6cc8daf48218c512f3c48de"), - "plugins/Referrers/templates/getSearchEnginesAndKeywords.twig" => array("264", "0dff654fb12896c6e24d6579f099c244"), - "plugins/Referrers/templates/index.twig" => array("6007", "644a0b2555e61f18dabca2e7239b800f"), - "plugins/Referrers/templates/indexWebsites.twig" => array("252", "bf3a7aa50faea341e0603b53ff14e4d5"), - "plugins/ScheduledReports/API.php" => array("36820", "527f23578d1ff32edacf572ebef32fb0"), - "plugins/ScheduledReports/config/tcpdf_config.php" => array("6402", "923e6b4e2b61bbc44fb7882d911e438c"), - "plugins/ScheduledReports/Controller.php" => array("3576", "10ce18981e80bb5f89eb9532805f46cb"), - "plugins/ScheduledReports/javascripts/pdf.js" => array("7001", "f42863d0418f199dcef8af7e46d1c0bc"), - "plugins/ScheduledReports/ScheduledReports.php" => array("25874", "523d4cdbf0cc7407353b5958875acb9e"), - "plugins/ScheduledReports/templates/_addReport.twig" => array("8636", "00ecae5fe8b8aa647bcd0efa248e2bc0"), - "plugins/ScheduledReports/templates/index.twig" => array("1937", "0aaa4e1d71cbcb85794f920f6530e953"), - "plugins/ScheduledReports/templates/_listReports.twig" => array("4267", "b554291be9228ef8cb7d250da9449288"), + "plugins/Referrers/lang/am.json" => array("1319", "74aeb79061a9f7980e4a479e667f99f0"), + "plugins/Referrers/lang/ar.json" => array("1376", "105da72dfff5e89e6ce49fd2262f89c4"), + "plugins/Referrers/lang/be.json" => array("4815", "6fd2956224c3c7c5c850a181acee5e80"), + "plugins/Referrers/lang/bg.json" => array("5454", "e98e050bdb024e9dce5992b9a36d9118"), + "plugins/Referrers/lang/bn.json" => array("80", "db197e85fdbbd4e756b819124366bc69"), + "plugins/Referrers/lang/bs.json" => array("68", "a90299482b2190c29a37b7001978899e"), + "plugins/Referrers/lang/ca.json" => array("4162", "acd480649a55d65a5b4fb9636d77b547"), + "plugins/Referrers/lang/cs.json" => array("4634", "156abc5e71c370f39e060f47e965b0fd"), + "plugins/Referrers/lang/cy.json" => array("62", "2afe272bf41b7ebc82c2d3b04cfab1ce"), + "plugins/Referrers/lang/da.json" => array("4386", "7d41d5d4428d2b9ced6a41a8d7acb045"), + "plugins/Referrers/lang/de.json" => array("4675", "d3b56923df2d534748b9bca9b00d9f23"), + "plugins/Referrers/lang/el.json" => array("7531", "0a7a49b8036380d12b6cb58daf1eac45"), + "plugins/Referrers/lang/en.json" => array("4292", "897aff931f8ef2338becf6a1479b27c9"), + "plugins/Referrers/lang/es.json" => array("4899", "f985b18a5bc1ae7bf97e55e3218ae7a9"), + "plugins/Referrers/lang/et.json" => array("1546", "bcf02932fa3afc8cee6c29a1d51efb14"), + "plugins/Referrers/lang/eu.json" => array("1112", "5978f6a531b85f7f90aeb686cda1852f"), + "plugins/Referrers/lang/fa.json" => array("2958", "dc2a43201c2fbe11817c142d6b1300f0"), + "plugins/Referrers/lang/fi.json" => array("4005", "8c5b13f1e94f37a274fc6c988abf9f27"), + "plugins/Referrers/lang/fr.json" => array("4876", "3d34028beec5207de8550dc556f1012d"), + "plugins/Referrers/lang/gl.json" => array("661", "81146e1ab1c11e0b68dd5acac7859e13"), + "plugins/Referrers/lang/he.json" => array("623", "42ddffa848048372355b3fa7c969e6f1"), + "plugins/Referrers/lang/hi.json" => array("8121", "37a67f3937d581252f8e036b30c490b5"), + "plugins/Referrers/lang/hr.json" => array("68", "a90299482b2190c29a37b7001978899e"), + "plugins/Referrers/lang/hu.json" => array("1229", "2228b133e88f8fe9c00eb9f6fb33c2b2"), + "plugins/Referrers/lang/id.json" => array("4224", "9f5385e3e204ed68979993126da34ce2"), + "plugins/Referrers/lang/is.json" => array("1135", "9f87814372127588c947ce3551781f1f"), + "plugins/Referrers/lang/it.json" => array("4642", "f6edd2f734f653cd93d601ee14ce5d1b"), + "plugins/Referrers/lang/ja.json" => array("5434", "1e23caf248428b86169e6dec0a4e03f1"), + "plugins/Referrers/lang/ka.json" => array("2120", "6b901993e679c9b51d2047b74c075b8f"), + "plugins/Referrers/lang/ko.json" => array("4683", "5435783c11c0446113bb692484494a09"), + "plugins/Referrers/lang/lt.json" => array("1335", "06ad375be2c47dbb598c90e2abe738f3"), + "plugins/Referrers/lang/lv.json" => array("878", "8a9e610587f18a681335a542f71b2d96"), + "plugins/Referrers/lang/nb.json" => array("1529", "a02dc36387fa88c8f46c1bb1c75c5b0b"), + "plugins/Referrers/lang/nl.json" => array("3865", "f1a9344ddf36e7bf660f25807b25db5f"), + "plugins/Referrers/lang/nn.json" => array("1095", "4abea66972f71de7c83446b05fa61ff0"), + "plugins/Referrers/lang/pl.json" => array("1295", "c7ca997c45ba0267fd49a0e11035d5f3"), + "plugins/Referrers/lang/pt-br.json" => array("4730", "32d57b0f7df32ce622822f1d635dd0eb"), + "plugins/Referrers/lang/pt.json" => array("3228", "199a7dd0fb2040cef23ff4350f6d25b8"), + "plugins/Referrers/lang/ro.json" => array("4568", "a129d19cbc0220c554374fabd35c7fce"), + "plugins/Referrers/lang/ru.json" => array("6044", "b371412481bbb12456eded1bc33eeca1"), + "plugins/Referrers/lang/sk.json" => array("1157", "f922110ffec6d07f0a13a71b356ddc6c"), + "plugins/Referrers/lang/sl.json" => array("1205", "4025d2526859f214b977c0b7aaff812e"), + "plugins/Referrers/lang/sq.json" => array("4740", "d0726f05e47e5f0c3e1ea308fed4a170"), + "plugins/Referrers/lang/sr.json" => array("4299", "5ffe015cace29fa5f7e0779500d896cc"), + "plugins/Referrers/lang/sv.json" => array("4564", "067975ce23e047a1bed0c3ae5fbaa26d"), + "plugins/Referrers/lang/ta.json" => array("92", "a60df0b9b505623acf5a71b8d291b838"), + "plugins/Referrers/lang/te.json" => array("728", "78101568ff9880e1f27718c2a60daad5"), + "plugins/Referrers/lang/th.json" => array("2074", "94d13608c29f5cccecd88d9541b4b918"), + "plugins/Referrers/lang/tl.json" => array("4819", "8d62f7492426c047fd9250249806cc2f"), + "plugins/Referrers/lang/tr.json" => array("803", "9712aed9bb762998d6bb1d61467faca0"), + "plugins/Referrers/lang/uk.json" => array("1514", "7a5477c6e4de34e531a3ecb00133bb32"), + "plugins/Referrers/lang/vi.json" => array("5215", "1124a168b18ec3de04c4036f85a8263b"), + "plugins/Referrers/lang/zh-cn.json" => array("3703", "51aa372912a13a5a902ef779e465d97c"), + "plugins/Referrers/lang/zh-tw.json" => array("1214", "43fa3a38fa4324edf273b8fa55524891"), + "plugins/Referrers/Menu.php" => array("801", "27065aa16ab18837785157fcfc9451ba"), + "plugins/Referrers/Referrers.php" => array("3792", "a6c200ff5d701c6c471e9de007cb292a"), + "plugins/Referrers/Reports/Base.php" => array("345", "5994a3be08e49ec36e0dd7a355521dfe"), + "plugins/Referrers/Reports/GetAll.php" => array("1577", "32039fa958d2cca13f424ab530cf9820"), + "plugins/Referrers/Reports/GetCampaigns.php" => array("1248", "6a50b8b3a532f2b1798c00f9590722fe"), + "plugins/Referrers/Reports/GetKeywordsFromCampaignId.php" => array("1082", "67239b696cac58542f462713f0794abe"), + "plugins/Referrers/Reports/GetKeywordsFromSearchEngineId.php" => array("957", "bd5d8e7adc3a92773fef2472bcc3bd0f"), + "plugins/Referrers/Reports/GetKeywords.php" => array("1268", "a84d93a581095b5cac5c8d4493d5b678"), + "plugins/Referrers/Reports/GetReferrerType.php" => array("3193", "e7a5d81bab9db51345e431743b8b420b"), + "plugins/Referrers/Reports/GetSearchEnginesFromKeywordId.php" => array("957", "bfea66b6b2f00c1f2829b56360e82e61"), + "plugins/Referrers/Reports/GetSearchEngines.php" => array("1328", "ad1622ee6e7ba9fe061c143dba0902e4"), + "plugins/Referrers/Reports/GetSocials.php" => array("1689", "0c66e975c41cc44899bd12e82a8a0e61"), + "plugins/Referrers/Reports/GetUrlsForSocial.php" => array("990", "a7625d5bf3e903790113ffbdc73ea91e"), + "plugins/Referrers/Reports/GetUrlsFromWebsiteId.php" => array("1013", "d43e3f00f394626b3ccfc08b52f88e25"), + "plugins/Referrers/Reports/GetWebsites.php" => array("1368", "e4d92bc946e5066e62f574998039b153"), + "plugins/Referrers/SearchEngine.php" => array("16436", "330441a60f79d6ec50cf0b0a77688580"), + "plugins/Referrers/Segment.php" => array("378", "15bbfa8d4f76728d090205460d729761"), + "plugins/Referrers/Social.php" => array("4854", "e0268c6580feca9f8e24a3b35415bed8"), + "plugins/Referrers/Tasks.php" => array("1538", "0f729d7ea814561cb28418f85bfb79cd"), + "plugins/Referrers/templates/allReferrers.twig" => array("334", "7ffdef581995e3e54d753c4d99683310"), + "plugins/Referrers/templates/getSearchEnginesAndKeywords.twig" => array("324", "062ef8b47602e9f3a0d8df19ec2a435f"), + "plugins/Referrers/templates/index.twig" => array("5062", "1c6a02d8f411168e845c1367085f5f00"), + "plugins/Referrers/templates/indexWebsites.twig" => array("312", "642f16c5967e4b240e91a694629576f7"), + "plugins/Referrers/Visitor.php" => array("3334", "e3917b74d5efb808e314d6ee141d5004"), + "plugins/Referrers/Widgets.php" => array("495", "0d5c03f515c6e8d89abb9f54b2f2e506"), + "plugins/Resolution/API.php" => array("1596", "bc1de72bda535c962bf0e9a318d4a7d9"), + "plugins/Resolution/Archiver.php" => array("2402", "ffe3cd86c92db72352dba826e3f184a0"), + "plugins/Resolution/Columns/Configuration.php" => array("398", "ce5b99c5387b19038a1f2d3669d04e86"), + "plugins/Resolution/Columns/Resolution.php" => array("1316", "cb8bb69e41bd94680e93682a20cb87c8"), + "plugins/Resolution/functions.php" => array("696", "fb2714f750edecaf56aad0def12c1783"), + "plugins/Resolution/lang/am.json" => array("332", "a5f3bf7460697faec0a4c8c8eb3e88c9"), + "plugins/Resolution/lang/ar.json" => array("397", "61869f55444144c98d63279f19fbdbe7"), + "plugins/Resolution/lang/be.json" => array("797", "b535208adff9daa2a056a95e54d03873"), + "plugins/Resolution/lang/bg.json" => array("870", "fd8ab42de4993732c999637be35e3af2"), + "plugins/Resolution/lang/ca.json" => array("553", "1674f467618a607cdfd2afd1e7bee4fd"), + "plugins/Resolution/lang/cs.json" => array("618", "e31cb163800ee1c9eec646fb34a3f4cc"), + "plugins/Resolution/lang/da.json" => array("616", "2575e8b31373bc06a85afdc8a3fe18ec"), + "plugins/Resolution/lang/de.json" => array("631", "4697d61c7a60c8a0c17aa2603612f6cc"), + "plugins/Resolution/lang/el.json" => array("907", "8cced77232ed90ba584a4466b1133ba0"), + "plugins/Resolution/lang/en.json" => array("616", "35c5154ca93e6bee0dd65ad78614da06"), + "plugins/Resolution/lang/es.json" => array("669", "77b6a5b24e4270f2ef9ab001558264b0"), + "plugins/Resolution/lang/et.json" => array("337", "51f1f638c0ef8bba6c02cbcec69a2c7a"), + "plugins/Resolution/lang/eu.json" => array("316", "a1268cb3436febb91efec821d0dd07f5"), + "plugins/Resolution/lang/fa.json" => array("664", "6b4e01dbe0fed17a19e54007337b7c16"), + "plugins/Resolution/lang/fi.json" => array("523", "812565161f2588c8754460f2c5448b54"), + "plugins/Resolution/lang/fr.json" => array("664", "130ffc148c155835d6ef993127c11af9"), + "plugins/Resolution/lang/gl.json" => array("660", "5ce5b18f801442cda007a7a92f84cbdb"), + "plugins/Resolution/lang/he.json" => array("86", "d63356e1508251174c66489f50349e9f"), + "plugins/Resolution/lang/hi.json" => array("975", "47882dbecc8c68c5a9a7a3c5ce0dc684"), + "plugins/Resolution/lang/hr.json" => array("311", "b16e21708d86ab3ca2e9de72f2fd30ec"), + "plugins/Resolution/lang/hu.json" => array("716", "ddd0b91b351e773bbe0516222313435f"), + "plugins/Resolution/lang/id.json" => array("499", "9ca71c77145db6617d09c34f8cb30f79"), + "plugins/Resolution/lang/is.json" => array("306", "d10368aa7f387ad841c3cd71e43ff446"), + "plugins/Resolution/lang/it.json" => array("648", "2fe0f92718c647c4dd26f1f615b0ebfc"), + "plugins/Resolution/lang/ja.json" => array("645", "aebc0e9888d6d94875e7a21e81bf8c38"), + "plugins/Resolution/lang/ka.json" => array("512", "f76803729a6b3e722191f1704fe18c1f"), + "plugins/Resolution/lang/ko.json" => array("599", "9f75b6bc9d8d5fca18ec90b290695908"), + "plugins/Resolution/lang/lt.json" => array("322", "e15eb9e5a3a65deb19a1df38a9987271"), + "plugins/Resolution/lang/lv.json" => array("584", "7c60fa28a96d540aacdb4db37925d45b"), + "plugins/Resolution/lang/nb.json" => array("617", "974906f074e19afdc01906131bde53c5"), + "plugins/Resolution/lang/nl.json" => array("611", "f818f260415b4c6b9bd572d29c514742"), + "plugins/Resolution/lang/nn.json" => array("316", "15ca6132f3639a184e541a7866a62221"), + "plugins/Resolution/lang/pl.json" => array("333", "bc6d92ce275584cb651e87231c499009"), + "plugins/Resolution/lang/pt-br.json" => array("658", "698fce24416cde994650122affa9f2ce"), + "plugins/Resolution/lang/pt.json" => array("586", "bf282d2c6fdb8e322b62369440a67894"), + "plugins/Resolution/lang/ro.json" => array("564", "b1ff4c92abaf30fcd411362b627f7baa"), + "plugins/Resolution/lang/ru.json" => array("936", "90c36b6e0e6b891bf5ab71245d65a9e4"), + "plugins/Resolution/lang/sk.json" => array("666", "e720e2ffa64915e520808890f3c3e58e"), + "plugins/Resolution/lang/sl.json" => array("243", "a55b17fce3ea1c43ab8a28ac46a4f215"), + "plugins/Resolution/lang/sq.json" => array("632", "a59733a22faad4a4de47803bff9d4c7a"), + "plugins/Resolution/lang/sr.json" => array("607", "0004a1429a6d652488dde934b10d7369"), + "plugins/Resolution/lang/sv.json" => array("638", "9b3427c7ef39e97d268e0e5cf116ed2f"), + "plugins/Resolution/lang/te.json" => array("144", "ce28dff437ac7da3430f22c405256b1a"), + "plugins/Resolution/lang/th.json" => array("473", "42769e0c67ddabfe7c78a3d2a540b37f"), + "plugins/Resolution/lang/tl.json" => array("551", "19122bb8e56af5566c491830fbde8bc8"), + "plugins/Resolution/lang/tr.json" => array("342", "0d527a33d6aebca932cf8a4c32217936"), + "plugins/Resolution/lang/uk.json" => array("446", "79ecd5946e0530617662a42c6413f635"), + "plugins/Resolution/lang/vi.json" => array("639", "d92029180d9281466b1a86ac7e5f3733"), + "plugins/Resolution/lang/zh-cn.json" => array("477", "f4dbb9150ea97c42f0f3efe1af453203"), + "plugins/Resolution/lang/zh-tw.json" => array("549", "021fdd2c78d7e2158d1b9a00ff4e3ea4"), + "plugins/Resolution/Reports/Base.php" => array("800", "254465a552f6f00b337537e67d9a69b1"), + "plugins/Resolution/Reports/GetConfiguration.php" => array("1141", "933d4976dccac955d986791288e0ef29"), + "plugins/Resolution/Reports/GetResolution.php" => array("1016", "553bfba338a53b0d83eb7bd6bc3a6c1c"), + "plugins/Resolution/Resolution.php" => array("1108", "92561250f314054883e0af687b1cc8e5"), + "plugins/Resolution/Segment.php" => array("374", "6748efd168f63060b7e83ac63b142ce1"), + "plugins/Resolution/Visitor.php" => array("532", "db940e92fe48b762a810308670920963"), + "plugins/ScheduledReports/API.php" => array("37088", "a15da1f8801068b2ed8ed7f4ce6183ae"), + "plugins/ScheduledReports/config/tcpdf_config.php" => array("6380", "279f679a4f0c2f7651fac7d597c62773"), + "plugins/ScheduledReports/Controller.php" => array("3554", "bd2e9c819deb7221f7734bcbb8dddd4f"), + "plugins/ScheduledReports/javascripts/pdf.js" => array("7129", "d981d220fa7c3fe008dcfb7393c0a952"), + "plugins/ScheduledReports/lang/ar.json" => array("2011", "a5643a5f968fafa5c6e6111b68fa2c6f"), + "plugins/ScheduledReports/lang/be.json" => array("2670", "aa24633e81e98c7a29f4d4819b5e8962"), + "plugins/ScheduledReports/lang/bg.json" => array("3963", "cfb6e82fa19d751d89efb99e6ad39e64"), + "plugins/ScheduledReports/lang/ca.json" => array("2877", "51c03a09db41280b2980a2a0fca2cde4"), + "plugins/ScheduledReports/lang/cs.json" => array("3909", "eacf5175c34bd22dfccb98c59ca63a4d"), + "plugins/ScheduledReports/lang/da.json" => array("3404", "963ae79e25bf3a4be18a870dbe110e02"), + "plugins/ScheduledReports/lang/de.json" => array("3982", "2a7bba84a8a0cdd80c5523745ab911e6"), + "plugins/ScheduledReports/lang/el.json" => array("6252", "f186b86420c4d1b774bcb4a98c5ec3eb"), + "plugins/ScheduledReports/lang/en.json" => array("3572", "6e1cb16cc05348a401e27a8a5c250121"), + "plugins/ScheduledReports/lang/es.json" => array("4035", "69365ecf63489ec3a20ad79e08305cdb"), + "plugins/ScheduledReports/lang/et.json" => array("1832", "96194e052f9987192b02345a7af4bd3f"), + "plugins/ScheduledReports/lang/fa.json" => array("3676", "3c9d2e229691d6cab0f9b800aa2d7b28"), + "plugins/ScheduledReports/lang/fi.json" => array("3353", "a121ccb2d2002e89685c25b3c65ee21b"), + "plugins/ScheduledReports/lang/fr.json" => array("4221", "7621378d0ea7007caff0867da7d9e402"), + "plugins/ScheduledReports/lang/he.json" => array("197", "e4595abb26084e88c0cb46cc8d7091f6"), + "plugins/ScheduledReports/lang/hi.json" => array("5950", "0c043642de03e50a37e4511b0998ce91"), + "plugins/ScheduledReports/lang/hu.json" => array("852", "4cc5ff246abfa6a2dfe9a8f519656572"), + "plugins/ScheduledReports/lang/id.json" => array("3059", "4198c7dafcf7cedc06c5be96327f7c85"), + "plugins/ScheduledReports/lang/is.json" => array("783", "df435a4e0341dbdfc8256deb4fce7b7f"), + "plugins/ScheduledReports/lang/it.json" => array("3885", "dc1392acd384fc2f59ddc6f479fa8a05"), + "plugins/ScheduledReports/lang/ja.json" => array("4473", "05cdc92c78ae4741837e0bcf6105b247"), + "plugins/ScheduledReports/lang/ka.json" => array("1475", "0e4238accd4e08de3ddd1f2f612f0d8a"), + "plugins/ScheduledReports/lang/ko.json" => array("4023", "a648949eb73d495c50b1fd4ef899e1d0"), + "plugins/ScheduledReports/lang/lt.json" => array("1239", "2304ded28ceb3550ec974a9d4ca889dd"), + "plugins/ScheduledReports/lang/lv.json" => array("654", "8c0fc54d27ed8b89892b4e9ed2c4dd5a"), + "plugins/ScheduledReports/lang/nb.json" => array("3703", "12e045b2a57d52611d8329198d3ac2e2"), + "plugins/ScheduledReports/lang/nl.json" => array("3207", "cf311d9878a84625aa6f2e668c056987"), + "plugins/ScheduledReports/lang/pl.json" => array("1998", "baee59f38f4f18c303ed898058b7a1f8"), + "plugins/ScheduledReports/lang/pt-br.json" => array("4037", "e08f31627c98077e8ede397649601048"), + "plugins/ScheduledReports/lang/pt.json" => array("2237", "903e89c5e99968dce107bc2524ffacae"), + "plugins/ScheduledReports/lang/ro.json" => array("3619", "1a9be01a75c0773bc3928f6b703e0a24"), + "plugins/ScheduledReports/lang/ru.json" => array("5035", "57ad2fd03ac17158ce5532904522ff34"), + "plugins/ScheduledReports/lang/sk.json" => array("838", "9c77d454833847609446581085c22144"), + "plugins/ScheduledReports/lang/sl.json" => array("1142", "c02b13f49ccb879cf80d063b3b00fbbc"), + "plugins/ScheduledReports/lang/sq.json" => array("2208", "6cb8a855ec4835b903ca5aa01ca9a209"), + "plugins/ScheduledReports/lang/sr.json" => array("3809", "5081b7daf22e0f1706960c7479dde823"), + "plugins/ScheduledReports/lang/sv.json" => array("3561", "a6766ee96351e434b9eef4e4e47f376d"), + "plugins/ScheduledReports/lang/ta.json" => array("337", "edf463627a0e5b6041be29dd0594f196"), + "plugins/ScheduledReports/lang/te.json" => array("224", "f9f54eb2166fd2fe22fd20fcbfd00523"), + "plugins/ScheduledReports/lang/th.json" => array("1692", "5abcb1c05cb73720124094fc9ae320c2"), + "plugins/ScheduledReports/lang/tl.json" => array("3602", "5021ee48e1bf0a716bf38b9c0f92acea"), + "plugins/ScheduledReports/lang/tr.json" => array("3421", "37c2e07355ff686ca84340233b6b06ee"), + "plugins/ScheduledReports/lang/uk.json" => array("1000", "f0ab0565835b9593193d85b19cba803c"), + "plugins/ScheduledReports/lang/vi.json" => array("4065", "344658b9e7bf03f3a160a66be6d74d65"), + "plugins/ScheduledReports/lang/zh-cn.json" => array("3276", "b51d3f3b99084093ab92302081240db4"), + "plugins/ScheduledReports/lang/zh-tw.json" => array("706", "88859a3f3b89830edb1a98301c1a5d0e"), + "plugins/ScheduledReports/Menu.php" => array("2574", "aaf18b6851c4c2b61c97c18208b74eb4"), + "plugins/ScheduledReports/Model.php" => array("2326", "f777a1aa75c08372001dbcd6b931b5e6"), + "plugins/ScheduledReports/ScheduledReports.php" => array("25886", "a59fe7ef757eaa170880932f41ac14fe"), + "plugins/ScheduledReports/stylesheets/scheduledreports.less" => array("138", "da856e2be51af7fb4f4b0d52361de00b"), + "plugins/ScheduledReports/Tasks.php" => array("872", "63f4b96bbcb9a970be79a9cd804936d7"), + "plugins/ScheduledReports/templates/_addReport.twig" => array("8379", "692bf8fa770d6a4e0d22d7ec209bd3dd"), + "plugins/ScheduledReports/templates/index.twig" => array("1918", "c701765a258c320eb092a44f22d32f0a"), + "plugins/ScheduledReports/templates/_listReports.twig" => array("4315", "2b64617f52037898a293c92088f34146"), "plugins/ScheduledReports/templates/reportParametersScheduledReports.twig" => array("3848", "4b32be0f5bb38a54ac06b276a962aae6"), - "plugins/SegmentEditor/API.php" => array("10874", "52a80f940f2bd60881732aa28d7def09"), - "plugins/SegmentEditor/Controller.php" => array("386", "f565883b4e48442253c328ab00016755"), + "plugins/SegmentEditor/API.php" => array("13087", "f1e1b35f3a8dd2cf63071910571f25e9"), + "plugins/SegmentEditor/config/config.php" => array("129", "5fa87c0b335d27e96dcd2b011cb53187"), "plugins/SegmentEditor/images/ajax-loader.gif" => array("847", "30d8e72bfdae694b1938658e1b087df0"), "plugins/SegmentEditor/images/bg-inverted-corners.png" => array("968", "b441529c920b6ce5ca36d62b08a73fa2"), "plugins/SegmentEditor/images/bg-segment-search.png" => array("1068", "938234a77114aff5541b1b9a47acf1f9"), @@ -2033,48 +4891,263 @@ class Manifest { "plugins/SegmentEditor/images/close_btn.png" => array("928", "58c02c2bf48632f89a571f8065dfedfa"), "plugins/SegmentEditor/images/close.png" => array("288", "122f9ccffeba88ccdc3eb2ef41b518bc"), "plugins/SegmentEditor/images/dashboard_h_bg_hover.png" => array("378", "41c5d393f8c12cd2cc727fada94b154a"), + "plugins/SegmentEditor/images/edit_segment.png" => array("1138", "9c4ac04dcfec6a9afa73d2f81c225a0a"), "plugins/SegmentEditor/images/icon-users.png" => array("1728", "89e68113ed647295901322eea82ff8ed"), "plugins/SegmentEditor/images/reset_search.png" => array("1021", "7e761f3444bf4edd4cd1779801c963bd"), "plugins/SegmentEditor/images/search_btn.png" => array("2825", "2f11b8a2a361aa4eb350d33048ea56c9"), "plugins/SegmentEditor/images/segment-close.png" => array("1302", "28e429768ba7f35ea4c48930848c2a26"), "plugins/SegmentEditor/images/segment-move.png" => array("1447", "5c8a0111446ce9eaf70cd0cb82211ded"), - "plugins/SegmentEditor/javascripts/Segmentation.js" => array("50044", "f8db88f49b88e433bb74ad004b81b2e1"), - "plugins/SegmentEditor/Model.php" => array("2610", "b8de12b8dcb6d85d01ce48c2c7d35f4d"), - "plugins/SegmentEditor/SegmentEditor.php" => array("3196", "88b61079a22b9489913274251b4b0539"), - "plugins/SegmentEditor/SegmentSelectorControl.php" => array("4496", "9f540610044cf02578bfdfbcf3d1edff"), - "plugins/SegmentEditor/stylesheets/segmentation.less" => array("14770", "3bccb8665591b5904b8fd1641aae90a2"), - "plugins/SegmentEditor/templates/_segmentSelector.twig" => array("9000", "46fd47d6e427123362e9f39dd2c58099"), - "plugins/SEO/API.php" => array("3604", "29f8089a50cfe5e5796c097813a92341"), - "plugins/SEO/Controller.php" => array("1195", "93cc25b095aa6bfe89d91088ee620964"), + "plugins/SegmentEditor/javascripts/Segmentation.js" => array("58430", "36fa36297a8afdf6901b7af64215b1c1"), + "plugins/SegmentEditor/lang/bg.json" => array("1969", "3ecf292ed923c0e1905a7ad5365f6d95"), + "plugins/SegmentEditor/lang/cs.json" => array("2594", "c7ac21951e8a995ae4547d2b8374afce"), + "plugins/SegmentEditor/lang/da.json" => array("2068", "a0c63eaf285e6b53c0e558a640cfd948"), + "plugins/SegmentEditor/lang/de.json" => array("2620", "b551a17f563afe63edf48e4cc4e2f216"), + "plugins/SegmentEditor/lang/el.json" => array("3964", "6d0274ff4f25b033eab1ed0d7dc05d9c"), + "plugins/SegmentEditor/lang/en.json" => array("2509", "5ce5e3ce90fa52a90b7019e3ae383846"), + "plugins/SegmentEditor/lang/es.json" => array("2290", "e82d17bf0579bc01e0fbe8fd6a6cd14e"), + "plugins/SegmentEditor/lang/et.json" => array("994", "bc1adb9ff16258a98d5d2a4852993bbd"), + "plugins/SegmentEditor/lang/fa.json" => array("1682", "c5295dcc6d0f9ff7bc3a2205975c5050"), + "plugins/SegmentEditor/lang/fi.json" => array("1680", "cd1ce69735e5d349b4d6265b179b1f8a"), + "plugins/SegmentEditor/lang/fr.json" => array("2689", "3516e7d63d8a734a539cbd755a7d387a"), + "plugins/SegmentEditor/lang/he.json" => array("138", "87bb59ad5aac18e12d4352305b1eac65"), + "plugins/SegmentEditor/lang/hi.json" => array("2583", "67cc865a83fb5abb56e4beafe1e2b31e"), + "plugins/SegmentEditor/lang/id.json" => array("1315", "e2011d85fe9463f919981caa6e2f4508"), + "plugins/SegmentEditor/lang/it.json" => array("2593", "8af4ea90e82f320a4c910c94179c30f3"), + "plugins/SegmentEditor/lang/ja.json" => array("2801", "23c4b4dee6ec28b7c0313aca28ecae50"), + "plugins/SegmentEditor/lang/lt.json" => array("60", "a032bad1e567a5a433e38c5d01823c4d"), + "plugins/SegmentEditor/lang/nb.json" => array("711", "c0de93d5afc3ecdf59aa8c5bf5bd79ac"), + "plugins/SegmentEditor/lang/nl.json" => array("2290", "0918b51b9fa209ac220e1d9a173702f3"), + "plugins/SegmentEditor/lang/pl.json" => array("646", "576c5dd4d6c04858c3c95e83b69d8547"), + "plugins/SegmentEditor/lang/pt-br.json" => array("2665", "39bd0a728763ec5a58950635f0bc73a7"), + "plugins/SegmentEditor/lang/ro.json" => array("2058", "e1bb24a50c72c9ffe0d3d9cb5d1b6393"), + "plugins/SegmentEditor/lang/ru.json" => array("1869", "f483ae42c1f41087a776c7b2dfcce102"), + "plugins/SegmentEditor/lang/sk.json" => array("131", "854b921f49ec7cccaf215c90181c4e16"), + "plugins/SegmentEditor/lang/sl.json" => array("73", "664b8f5e885759b9f29913dc11dee146"), + "plugins/SegmentEditor/lang/sr.json" => array("2139", "259d6b34fa6025188f5858a297f34989"), + "plugins/SegmentEditor/lang/sv.json" => array("2146", "b0c222607314aaf0137cde368c5047de"), + "plugins/SegmentEditor/lang/tl.json" => array("1951", "31a54f7116495d8af13aaee3fa22ff03"), + "plugins/SegmentEditor/lang/tr.json" => array("185", "6948557ebdaecde3119e7dec722cf00b"), + "plugins/SegmentEditor/lang/vi.json" => array("1475", "aa433a3c8c8efe97c8d27d53d3691068"), + "plugins/SegmentEditor/lang/zh-cn.json" => array("1194", "0a6cd9707feca364a4b4d612f3f87ff8"), + "plugins/SegmentEditor/Model.php" => array("5228", "47d5471dc65332bef99e54a6acff730e"), + "plugins/SegmentEditor/SegmentEditor.php" => array("2698", "229034aad9ea97b32c3399623201037d"), + "plugins/SegmentEditor/SegmentFormatter.php" => array("4873", "dd3d69a381e4d25690cd874dfbdb2401"), + "plugins/SegmentEditor/SegmentList.php" => array("660", "a61dbd361a5efd8ca330420b0acf541a"), + "plugins/SegmentEditor/SegmentQueryDecorator.php" => array("2154", "f091c4d9762cf20aee9bd6782d6ac842"), + "plugins/SegmentEditor/SegmentSelectorControl.php" => array("4921", "cb6d94e089431ba6a5b9690484b210af"), + "plugins/SegmentEditor/Services/StoredSegmentService.php" => array("1176", "99e5c5b08a8594f0ff8b87897b88e486"), + "plugins/SegmentEditor/stylesheets/segmentation.less" => array("16182", "a0d5729f895d5907f0b40ac68db43338"), + "plugins/SegmentEditor/templates/_segmentSelector.twig" => array("10683", "261a3f3e8540c5ecd53051ba447d5c1d"), + "plugins/SEO/API.php" => array("2091", "6d827230e8d7a5eabdf04f24c54fb1ba"), "plugins/SEO/images/majesticseo.png" => array("674", "a771319c2aa22f4b4744f76e59ec5fb3"), "plugins/SEO/images/whois.png" => array("928", "ef67a4e9689efda71625a2ef894fb700"), - "plugins/SEO/javascripts/rank.js" => array("802", "8a49f8635a4d501d73868dcfc415e4e5"), - "plugins/SEO/MajesticClient.php" => array("3132", "2973973ceba8173cab44fd0cac09f869"), - "plugins/SEO/RankChecker.php" => array("10231", "72e8c9795cb7a537ff80bee9e5890d77"), - "plugins/SEO/SEO.php" => array("1194", "bb40b45a7b5ef22684fc1370fd5553ba"), - "plugins/SEO/templates/getRank.twig" => array("2900", "a686edd3171736d456f4ec6062206819"), - "plugins/SitesManager/API.php" => array("55565", "1d1299fa3efe085855b42f4134a8b0fc"), - "plugins/SitesManager/Controller.php" => array("6317", "bac508220402a58b5c67442b641d8c09"), - "plugins/SitesManager/javascripts/SitesManager.js" => array("22790", "153a9bd5ec22c2f98791989235a5289d"), - "plugins/SitesManager/SitesManager.php" => array("7094", "4e0830e699db4d59d697efac994a8858"), - "plugins/SitesManager/stylesheets/SitesManager.less" => array("1059", "2c47402c996f5783383b5dd9bdb41048"), + "plugins/SEO/javascripts/rank.js" => array("818", "63808a20196095da1b3eed55c2f33ea9"), + "plugins/SEO/lang/ar.json" => array("303", "de4823a6371f37cb9adba7d39a28b248"), + "plugins/SEO/lang/be.json" => array("245", "e86487c0e51a146ce77d5d2ff3b3b65c"), + "plugins/SEO/lang/bg.json" => array("716", "843fe90327b61feda6733036a3bcf110"), + "plugins/SEO/lang/ca.json" => array("369", "d73c3c164dd89bf750cb9317fe132790"), + "plugins/SEO/lang/cs.json" => array("598", "4087086c642a4e0156519c9351a6166b"), + "plugins/SEO/lang/da.json" => array("362", "2b2e9eb29db7359cbce7865ec04a79db"), + "plugins/SEO/lang/de.json" => array("570", "ef941c4516cc98cb5622d68a49070eb0"), + "plugins/SEO/lang/el.json" => array("793", "1a1b127a744449aba538751030e176a3"), + "plugins/SEO/lang/en.json" => array("529", "b62ae0f0d3f92e4bd0aafcb72acd658d"), + "plugins/SEO/lang/es.json" => array("599", "be1a22181b6538c94ccf4ac07bf8eef7"), + "plugins/SEO/lang/et.json" => array("332", "7202407bb7ee67cc812199deb37a7dc9"), + "plugins/SEO/lang/fa.json" => array("464", "ef3bbfd749c448c41dd54446bfc213a0"), + "plugins/SEO/lang/fi.json" => array("357", "d6d2155d3733febc94af23a6c7682ce9"), + "plugins/SEO/lang/fr.json" => array("582", "ec429758d298a82bf47f7a94f1812ac7"), + "plugins/SEO/lang/he.json" => array("51", "5073510f41062ead5e88a925347e5130"), + "plugins/SEO/lang/hi.json" => array("1007", "a5c1a9549f79a7d009e7525e208fc6a3"), + "plugins/SEO/lang/hu.json" => array("260", "6e8ac7427fd4aec458fa49734ef7daf3"), + "plugins/SEO/lang/id.json" => array("362", "730855aa5109a5de479cd8a78152472c"), + "plugins/SEO/lang/it.json" => array("552", "10567a190422e8b9ffdc191cfeef03ac"), + "plugins/SEO/lang/ja.json" => array("648", "d6d7b786c3d683ac0ac148ae6f7f51e5"), + "plugins/SEO/lang/ka.json" => array("292", "597df00e8a846b024ecf1a3b40bfb3cc"), + "plugins/SEO/lang/ko.json" => array("582", "ac4c000cf07bc091a2bd9eb7251f3a0a"), + "plugins/SEO/lang/lt.json" => array("231", "690a0e34489d072fd0c5b00c0cd763b9"), + "plugins/SEO/lang/lv.json" => array("199", "aee4271562513439c06a1a7c0fea3cab"), + "plugins/SEO/lang/nb.json" => array("566", "32a47e709b9a5a06ef53d0706ccab394"), + "plugins/SEO/lang/nl.json" => array("554", "9605807b46a8d15e2af014d4033c9a5e"), + "plugins/SEO/lang/pl.json" => array("208", "876c984ecad0d4c2cfcd36b60387bb37"), + "plugins/SEO/lang/pt-br.json" => array("555", "c0b66c24ef4648d123077b223d4e3236"), + "plugins/SEO/lang/pt.json" => array("249", "464372e1e620cb30e93925ec70e2cd3a"), + "plugins/SEO/lang/ro.json" => array("381", "cf1ae825a24c07696a02412895f57f5c"), + "plugins/SEO/lang/ru.json" => array("462", "948d41720f82b243bcb1fe6088f36b29"), + "plugins/SEO/lang/sk.json" => array("220", "515e684909aa69a5d645134cd67f8c6e"), + "plugins/SEO/lang/sl.json" => array("332", "ab75da704cec1c78d542c41fb8367b59"), + "plugins/SEO/lang/sq.json" => array("221", "b65a052c181a47c2d4d20023818920cf"), + "plugins/SEO/lang/sr.json" => array("571", "517f2d4ad69302a6058ffeb685b5ddb5"), + "plugins/SEO/lang/sv.json" => array("347", "787e301e7dcf35cb6e6711e80f27a6cc"), + "plugins/SEO/lang/te.json" => array("130", "8a281c2dfa6f4e2dcc3e5858ec66c0cc"), + "plugins/SEO/lang/th.json" => array("297", "cfe2ac84521475629b6bf611fdc27415"), + "plugins/SEO/lang/tl.json" => array("383", "d21e2a94d98155b1a7733a28da09c8aa"), + "plugins/SEO/lang/tr.json" => array("226", "cd2c642e70b9a3e14a56d0c5076c3ee5"), + "plugins/SEO/lang/uk.json" => array("229", "778ffbc113ad96a352a6ad90b4341dab"), + "plugins/SEO/lang/vi.json" => array("424", "e66c4d8b38389daa7de73cf23d887672"), + "plugins/SEO/lang/zh-cn.json" => array("512", "ba71162ff226dfccfb19226605b00500"), + "plugins/SEO/lang/zh-tw.json" => array("211", "aeeacf894db92f5687e58ddda18b241e"), + "plugins/SEO/Metric/Aggregator.php" => array("1610", "7dfcb8851083af11976b81e180121909"), + "plugins/SEO/Metric/Alexa.php" => array("1457", "bad9fc70b86cc8648290804b6b4d3c8d"), + "plugins/SEO/Metric/Bing.php" => array("1550", "94a87eec8b697b66d975b251471d9e2a"), + "plugins/SEO/Metric/Dmoz.php" => array("1628", "35a86d4b5878fa151f3511d4cad69cc2"), + "plugins/SEO/Metric/DomainAge.php" => array("3748", "d9d27a46635dedccc04a22c3cf5ce3e8"), + "plugins/SEO/Metric/Google.php" => array("5144", "c67c209775737c359827c204ff236256"), + "plugins/SEO/Metric/Metric.php" => array("2594", "c7b1d3d18142fd1ffd9cbb132809e63e"), + "plugins/SEO/Metric/MetricsProvider.php" => array("377", "e9f1e43594708cfaca9ab95075412eba"), + "plugins/SEO/Metric/ProviderCache.php" => array("932", "7fb255339d181acbaa5f27f357e0eef3"), + "plugins/SEO/SEO.php" => array("230", "1e2706a6b02044ac609ebdd8f4b62a1e"), + "plugins/SEO/templates/getRank.twig" => array("2584", "753dd24642b9a5d31771fa5591208f62"), + "plugins/SEO/Widgets.php" => array("1325", "275e1f7a24ce53b766b9c776ba308c79"), + "plugins/SitesManager/angularjs/sites-manager/api-core.service.js" => array("741", "1131a9cc43d9683c7848ae6ea06f6cfe"), + "plugins/SitesManager/angularjs/sites-manager/api-helper.service.js" => array("2051", "2ada3913ab7512fb0a1cec6111991f08"), + "plugins/SitesManager/angularjs/sites-manager/api-site.service.js" => array("1409", "7e3086e9bd490e05a0007dd2ba9f9080"), + "plugins/SitesManager/angularjs/sites-manager/edit-trigger.directive.js" => array("767", "eb0325962d424df3291dfb535b5ac2a4"), + "plugins/SitesManager/angularjs/sites-manager/multiline-field.directive.html" => array("138", "1bd07714603c13ac08dede575488e978"), + "plugins/SitesManager/angularjs/sites-manager/multiline-field.directive.js" => array("1418", "9c6ab6cbeb40dc773ee8366b4ed43e0d"), + "plugins/SitesManager/angularjs/sites-manager/sites-manager-admin-sites-model.js" => array("3148", "efcc94e3ffd2629e6700d7fbfbc252b4"), + "plugins/SitesManager/angularjs/sites-manager/sites-manager.controller.js" => array("10214", "e634f4b6883df2e2efc6a26c93d5c0b9"), + "plugins/SitesManager/angularjs/sites-manager/sites-manager-site.controller.js" => array("7249", "1f751cb056b3ac98440eef886abeb9fe"), + "plugins/SitesManager/angularjs/sites-manager/sites-manager-type-model.js" => array("1400", "ad362878482a3ddd04c67121e9061300"), + "plugins/SitesManager/API.php" => array("56101", "208be0295749a8b97b5e5f30dbad0463"), + "plugins/SitesManager/Controller.php" => array("6386", "a09423058a3c7c80ccff48643f0dc1ea"), + "plugins/SitesManager/lang/am.json" => array("1486", "36f212cae0e47f317b1b787bcbae8bce"), + "plugins/SitesManager/lang/ar.json" => array("6061", "b5230290b25886199c11da570d6b12f3"), + "plugins/SitesManager/lang/be.json" => array("7108", "e63e3ced4717673ac5c2b93844b155f7"), + "plugins/SitesManager/lang/bg.json" => array("8582", "8e46d5c7912cd4223b22add1bebb736b"), + "plugins/SitesManager/lang/bn.json" => array("118", "120dbaa081631531efcfbd39bc28fa80"), + "plugins/SitesManager/lang/bs.json" => array("84", "d62fe6a4a6608b971b6aea2effeaf6ae"), + "plugins/SitesManager/lang/ca.json" => array("6913", "0c45f458f98e61cfc1e4626f7b7d1077"), + "plugins/SitesManager/lang/cs.json" => array("8987", "c4f3ca9e3bf72eead5427ccb7eb0ec49"), + "plugins/SitesManager/lang/cy.json" => array("68", "a9847ef3b4ee461e40f655a614c975a1"), + "plugins/SitesManager/lang/da.json" => array("7169", "7751a4e7006ef94e75f68fcc3806ebf0"), + "plugins/SitesManager/lang/de.json" => array("9102", "e7563669e60eedca4b985c86669ceb91"), + "plugins/SitesManager/lang/el.json" => array("15424", "431159db9cbf2fc1e614c3689166cdf2"), + "plugins/SitesManager/lang/en.json" => array("8480", "dfabd1722e326b9dd37b3018df98bbac"), + "plugins/SitesManager/lang/es.json" => array("8690", "663a7ab8567c941299b2f819b7e91b99"), + "plugins/SitesManager/lang/et.json" => array("1976", "ff15f29e191a05c5f4f0a69f4d778da6"), + "plugins/SitesManager/lang/eu.json" => array("1452", "f0885dd53ced5c9b6fb3d8bb462cab05"), + "plugins/SitesManager/lang/fa.json" => array("7533", "69d417927feb2082f19d226a20837804"), + "plugins/SitesManager/lang/fi.json" => array("7025", "bd5652ee51bb76ea3461334e0a8b054b"), + "plugins/SitesManager/lang/fr.json" => array("8729", "ef1d31832a19699f3010715734756ceb"), + "plugins/SitesManager/lang/gl.json" => array("694", "917fe86b1c57670acb23cfe4dc1bd228"), + "plugins/SitesManager/lang/he.json" => array("132", "b14a42cdf5fa4dbef115e2fcece32630"), + "plugins/SitesManager/lang/hi.json" => array("13887", "bbe28a4c3b5ece854ad67074a35e2927"), + "plugins/SitesManager/lang/hr.json" => array("274", "aef9ad84b218dea0492b22004f5a2ee5"), + "plugins/SitesManager/lang/hu.json" => array("4501", "66040f051d7617851dbd9b7b71df7483"), + "plugins/SitesManager/lang/id.json" => array("7059", "2fe54619e6e55e0c11bfc26a7f159b4b"), + "plugins/SitesManager/lang/is.json" => array("447", "76a1b6e1eac4473756b39e88e52f234a"), + "plugins/SitesManager/lang/it.json" => array("8545", "b4798b9eebb53120748475950d6d9a36"), + "plugins/SitesManager/lang/ja.json" => array("10389", "d4d4b1d4cd7a694b17eb7a0b82e58ff0"), + "plugins/SitesManager/lang/ka.json" => array("8684", "5a7cbe257e41fb20e70d9c8888a8a030"), + "plugins/SitesManager/lang/ko.json" => array("9443", "1f90c9483520d75f237dbb6efd19d12b"), + "plugins/SitesManager/lang/lt.json" => array("4031", "f4e6ef62637daaaadf27f4291e72c7b0"), + "plugins/SitesManager/lang/lv.json" => array("2925", "3e75617ab4d21ea49b8f6cb625382f55"), + "plugins/SitesManager/lang/nb.json" => array("7900", "75ac2a3a52f1f2c64196c00d3d7ae335"), + "plugins/SitesManager/lang/nl.json" => array("6593", "2cd7f16bc3782ab56b46ebf085047531"), + "plugins/SitesManager/lang/nn.json" => array("1748", "25371bf5dbcb7d642a69b92e49527202"), + "plugins/SitesManager/lang/pl.json" => array("4943", "16772f0c955bc106c4806d89e820cd6c"), + "plugins/SitesManager/lang/pt-br.json" => array("9296", "148a69513cb17811f631c0f777c0120f"), + "plugins/SitesManager/lang/pt.json" => array("4941", "bc5a430d5d1b7f9c99b5d6e1be7d7356"), + "plugins/SitesManager/lang/ro.json" => array("7847", "6fdf4bf4df0b93d547dab77c96093cc9"), + "plugins/SitesManager/lang/ru.json" => array("11726", "fecbbf83188a6ad2cdf280878200de99"), + "plugins/SitesManager/lang/sk.json" => array("1144", "d996cc9792e837b953beab528a29faaf"), + "plugins/SitesManager/lang/sl.json" => array("2187", "223abd60fa8a3ea2fe6fd9afd8699017"), + "plugins/SitesManager/lang/sq.json" => array("7243", "90decf712ee0230def4dba81eca2053a"), + "plugins/SitesManager/lang/sr.json" => array("8068", "f245eda5b504562367011d047cf0655d"), + "plugins/SitesManager/lang/sv.json" => array("8192", "024df9cb6630f5491e90f24fe6bf92f0"), + "plugins/SitesManager/lang/ta.json" => array("127", "5f9c30b2a49b0d4f836a1548782d4769"), + "plugins/SitesManager/lang/te.json" => array("303", "bb38cf75220b9cd0912f83c59b90438e"), + "plugins/SitesManager/lang/th.json" => array("7613", "a6d01b8dbfb48c47cf02a8e8046994af"), + "plugins/SitesManager/lang/tl.json" => array("7827", "5f8f02a32e3a1eb71ebce7611ae1b3aa"), + "plugins/SitesManager/lang/tr.json" => array("2195", "9026c6b17bb3941c5398dae41518e0de"), + "plugins/SitesManager/lang/uk.json" => array("5986", "04cf77c7f67702f5801b288e4f2e6c2e"), + "plugins/SitesManager/lang/vi.json" => array("8653", "713e75f7e3ec471660ccaad77c5027f5"), + "plugins/SitesManager/lang/zh-cn.json" => array("6010", "62a4bc3c975d11d35ffa1bd31cdd2560"), + "plugins/SitesManager/lang/zh-tw.json" => array("3541", "288c1c1273963548908ab8aaaf45c31f"), + "plugins/SitesManager/Menu.php" => array("1564", "ae8a759ae208e0952673d700297cdfb0"), + "plugins/SitesManager/Model.php" => array("11891", "def7b0b0a0cce1e31de2d334ddaa256e"), + "plugins/SitesManager/SitesManager.php" => array("14692", "8d6bd3a1f7473d0938e63aa177f4409f"), + "plugins/SitesManager/SiteUrls.php" => array("5727", "ef055d33644c1805646f28cca87151d3"), + "plugins/SitesManager/stylesheets/SitesManager.less" => array("2376", "dcb0347253a14600120146e9e147b2f7"), + "plugins/SitesManager/templates/dialogs/dialogs.html" => array("169", "7f31f2ae1a6d9957ee957c18fd595748"), + "plugins/SitesManager/templates/dialogs/edit-dialog.html" => array("185", "a1311883efc1468013ad6a276be3953b"), + "plugins/SitesManager/templates/dialogs/remove-dialog.html" => array("286", "df63b9966b7f91e850d01cf169106cac"), "plugins/SitesManager/templates/displayJavascriptCode.twig" => array("121", "ab4d91574f554b6e4ff05cae26373902"), - "plugins/SitesManager/templates/_displayJavascriptCode.twig" => array("731", "cc93bf9bffdbd5efc0ff98a0bee83e86"), - "plugins/SitesManager/templates/index.twig" => array("19864", "71b4a9079d4e90951e1b83571f72a64f"), - "plugins/Transitions/API.php" => array("25342", "1d0bf029a35fa8cad6444271571d8388"), - "plugins/Transitions/Controller.php" => array("3778", "f926b1496c2db36deb6f7f2ee1d48e36"), + "plugins/SitesManager/templates/_displayJavascriptCode.twig" => array("742", "97d7f49f49c8d08f90292ccd870d45e6"), + "plugins/SitesManager/templates/global-settings.html" => array("4853", "4e0c44e362de36f6d72d291c5a811f12"), + "plugins/SitesManager/templates/help/excluded-ip-help.html" => array("240", "148a2b037fdc6f2171956b32260e88c5"), + "plugins/SitesManager/templates/help/excluded-query-parameters-help.html" => array("230", "9dd06435c7171f949b197dd39a7ef3f7"), + "plugins/SitesManager/templates/help/excluded-user-agents-help.html" => array("241", "7679168a9072470f7a3008667ead635a"), + "plugins/SitesManager/templates/help/timezone-help.html" => array("534", "c0603d04709369ca3f61d56bb4c104c0"), + "plugins/SitesManager/templates/index.html" => array("879", "cc23f0d33cfc39d0c10a9ca1dd4464bd"), + "plugins/SitesManager/templates/index.twig" => array("237", "d6f90ea01c3c4dcee0c21d2b9387a1e5"), + "plugins/SitesManager/templates/loading.html" => array("267", "1bc83652de333a8c6c69ccf806d5f101"), + "plugins/SitesManager/templates/measurable_type_settings.twig" => array("196", "c1d747512209de1a85e8fd7c76749173"), + "plugins/SitesManager/templates/sites-list/add-entity-dialog.html" => array("718", "c78f02bba8e5b5427cb19acc45671dcc"), + "plugins/SitesManager/templates/sites-list/add-site-link.html" => array("1795", "3af8acdd8005abe8651586f260cc754a"), + "plugins/SitesManager/templates/sites-list/site-fields.html" => array("7448", "41af2f047263099a353508bafb8764ef"), + "plugins/SitesManager/templates/sites-list/site-search-field.html" => array("2446", "8ed6d30747ad0f30686a980ef727f67a"), + "plugins/SitesManager/templates/sites-list/sites-list.html" => array("580", "884f853d72b92e4e572ade62df8febc8"), + "plugins/SitesManager/templates/sites-manager-header.html" => array("739", "22852a615d0e43cb810d35915559f5ab"), + "plugins/SitesManager/templates/siteWithoutData.twig" => array("1521", "a1ae84f720a36eb3e763f9232dced1cb"), + "plugins/SitesManager/Tracker/SitesManagerRequestProcessor.php" => array("2515", "8f75aaf544c4fd51e21ee042fbf4c679"), + "plugins/Transitions/API.php" => array("25718", "d68bb5c44a87a35beb45af581450fcc9"), + "plugins/Transitions/Controller.php" => array("3782", "fcd68506281bcbb0ccf3cd5986b66611"), "plugins/Transitions/images/transitions_icon_hover.png" => array("647", "2ec41fe6df63da14f7c187b067b94cc9"), "plugins/Transitions/images/transitions_icon.png" => array("643", "fbf901428436a1ba3bbcc5c3081b6f24"), - "plugins/Transitions/javascripts/transitions.js" => array("56161", "97aee28fdcf5a08f9ca8cc9e744c3eb8"), - "plugins/Transitions/stylesheets/_transitionColors.less" => array("1677", "4c6eb4b6425faabe6d2eb4e1821904ef"), - "plugins/Transitions/stylesheets/transitions.less" => array("3702", "b58c35379c79ce4060216479e0310c2f"), + "plugins/Transitions/javascripts/transitions.js" => array("57413", "cb7ca73a1f44e4fd9eee60ee3b9419c4"), + "plugins/Transitions/lang/bg.json" => array("1622", "926261ec980e03506aa55c4100a5d6cf"), + "plugins/Transitions/lang/ca.json" => array("1337", "d0267f9c934c24ba8ef5d95cb496e8d0"), + "plugins/Transitions/lang/cs.json" => array("1546", "af0bee5caaf1ae79b72e89672bc9f266"), + "plugins/Transitions/lang/da.json" => array("1346", "e476282900d8787f8549b1ce4105e75c"), + "plugins/Transitions/lang/de.json" => array("1571", "e02e9c83c3317c518ca4a37072e2c611"), + "plugins/Transitions/lang/el.json" => array("2253", "2c4d48d060a26d6339ed117f8b0be981"), + "plugins/Transitions/lang/en.json" => array("1469", "fa5ea3dfbf32d9fdbb11910092046e7f"), + "plugins/Transitions/lang/es.json" => array("1597", "d342fcae3dc8fbc06ef7aae9107adb47"), + "plugins/Transitions/lang/et.json" => array("1218", "ad19775e0e8dd8a7f7dd4c037fad0e8f"), + "plugins/Transitions/lang/fa.json" => array("1474", "04aecb3bbf1e98d35dc94ff7a0435ad6"), + "plugins/Transitions/lang/fi.json" => array("1351", "131e353f5c2aef6d30ad8d9bc658d6ce"), + "plugins/Transitions/lang/fr.json" => array("1627", "3090f3efed5aaa701c307d4885286007"), + "plugins/Transitions/lang/hi.json" => array("1986", "1061f4a650baf71b14c3f1b0688df286"), + "plugins/Transitions/lang/id.json" => array("1364", "ae6f9b163163fecff43bc9c4dd2f0dcb"), + "plugins/Transitions/lang/it.json" => array("1519", "a0f70813bca9daefeceeda3555a41434"), + "plugins/Transitions/lang/ja.json" => array("1658", "188636e33b73c8ca3d53dbc8e4ac7689"), + "plugins/Transitions/lang/ko.json" => array("1588", "8c9e8933fac03d148b0522e1fd331ace"), + "plugins/Transitions/lang/lt.json" => array("91", "bd63b630d828acfa66e88f5bbd8816a4"), + "plugins/Transitions/lang/nb.json" => array("1495", "bc4e7a1668e84b1dfeae15ac2f1afb3f"), + "plugins/Transitions/lang/nl.json" => array("1542", "93986cbf2169f0bd3d472b30402dbb40"), + "plugins/Transitions/lang/pl.json" => array("625", "ab4416854faf53567f7a464cd0a36a1d"), + "plugins/Transitions/lang/pt-br.json" => array("1590", "7daedb026521640c54da7c1f53157ba7"), + "plugins/Transitions/lang/ro.json" => array("1422", "942dbf654d122aac487a8f1143c14bb9"), + "plugins/Transitions/lang/ru.json" => array("1803", "3be0be5b403b09765408819d28adcf1a"), + "plugins/Transitions/lang/sl.json" => array("789", "3731f5b31b1e801e69047b0f126ff7ea"), + "plugins/Transitions/lang/sr.json" => array("1420", "5e8d3ab9f787c1e6dc89b3c745040aaf"), + "plugins/Transitions/lang/sv.json" => array("1365", "efef60fe66d17dde1e38c8a11ba82d8c"), + "plugins/Transitions/lang/th.json" => array("240", "44265d1594c5ef469b39fec39889c0a4"), + "plugins/Transitions/lang/tl.json" => array("1442", "172803664a2819d2afa20d4f76524848"), + "plugins/Transitions/lang/tr.json" => array("86", "44b9124a4f1dc5c0331871d904c4926c"), + "plugins/Transitions/lang/vi.json" => array("1581", "877ccb46f8ee65dca0f3634bdcd15c8a"), + "plugins/Transitions/lang/zh-cn.json" => array("1413", "c5809e6f23a7312f758537e0dd109174"), + "plugins/Transitions/stylesheets/_transitionColors.less" => array("1675", "5505d5f0b87669aff5031329217a886b"), + "plugins/Transitions/stylesheets/transitions.less" => array("3712", "e37a4e328945fe0358862fcc0ba9d742"), "plugins/Transitions/templates/renderPopover.twig" => array("2576", "81bba58121f174ab9edf66c6560f1815"), - "plugins/Transitions/Transitions.php" => array("1125", "efe0685071d15cd7ce16c94116ed71bc"), - "plugins/UserCountry/API.php" => array("8863", "a79d65ab6a040442be62b43d687e01cb"), - "plugins/UserCountry/Archiver.php" => array("6221", "191beb3c68d3dddc8a3407f0e4c33594"), - "plugins/UserCountry/Controller.php" => array("14875", "3b186fb085a8147c30078f722f7bccfc"), - "plugins/UserCountry/functions.php" => array("5232", "de661ba6505b4d27286e6e8543295d65"), - "plugins/UserCountry/GeoIPAutoUpdater.php" => array("23171", "102290b02300a18c821aa1b46ef294d5"), + "plugins/Transitions/Transitions.php" => array("1113", "2916d6fd161bb8b881597667ccbabe5c"), + "plugins/UserCountry/API.php" => array("9154", "0fdd794cf2990d991b664cd002908cca"), + "plugins/UserCountry/Archiver.php" => array("6582", "7cacc1e82a4df5be2b9a36ea2a3e2ff9"), + "plugins/UserCountry/Columns/Base.php" => array("2483", "90698596f0dce9b56ee99927d9a68bc3"), + "plugins/UserCountry/Columns/City.php" => array("1993", "68c489b47e02f3948d79d58ae63deab4"), + "plugins/UserCountry/Columns/Continent.php" => array("386", "2f3b66902c9e183014455826e1c39ad2"), + "plugins/UserCountry/Columns/Country.php" => array("4594", "2819bc14f193d92b1eb94f9225fb824a"), + "plugins/UserCountry/Columns/Latitude.php" => array("2140", "94b80ed029f3c8587037b5777245d5f8"), + "plugins/UserCountry/Columns/Longitude.php" => array("2045", "d035e537179cad24291df586384a0c62"), + "plugins/UserCountry/Columns/Provider.php" => array("1460", "307308bf7c810ecc28c47eb331ee5ea0"), + "plugins/UserCountry/Columns/Region.php" => array("2027", "709a8d32a4e1f1f9fcfe9a30355f228e"), + "plugins/UserCountry/Commands/AttributeHistoricalDataWithLocations.php" => array("7120", "833ccd4390615b3893a50a699f34bfb6"), + "plugins/UserCountry/config/config.php" => array("154", "15ae36c439bf4fe91a5676847723c74d"), + "plugins/UserCountry/Controller.php" => array("14198", "b57147c18111eca0abd0b5687656fa29"), + "plugins/UserCountry/Diagnostic/GeolocationDiagnostic.php" => array("2438", "f440781fa310b2b736270a80553d2a08"), + "plugins/UserCountry/functions.php" => array("5585", "8549c101ea50e8abd5d5d83820a9c1c3"), + "plugins/UserCountry/GeoIPAutoUpdater.php" => array("23487", "410a4b6b63485c59ff2c95bd0cbd2104"), "plugins/UserCountry/images/flags/a1.png" => array("290", "f1c31042bade52bcf13d8bd778fcf37c"), "plugins/UserCountry/images/flags/a2.png" => array("290", "f1c31042bade52bcf13d8bd778fcf37c"), "plugins/UserCountry/images/flags/ac.png" => array("545", "373b8f3d6c92338b228a160e2a0d87fc"), @@ -2349,27 +5422,123 @@ class Manifest { "plugins/UserCountry/images/flags/zm.png" => array("359", "96b272c89f9775f2a8aca88497d660ae"), "plugins/UserCountry/images/flags/zr.png" => array("380", "7bd1bcc513aac082fa6f6292b72edc0c"), "plugins/UserCountry/images/flags/zw.png" => array("462", "29cc6d7fa4fb68eb909da5b1ee142bdd"), - "plugins/UserCountry/javascripts/userCountry.js" => array("8251", "cba3f6cb36fe9e75404d48a96ec2349b"), - "plugins/UserCountry/LocationProvider/Default.php" => array("3251", "31708eccdddce97672027891f9a6888b"), - "plugins/UserCountry/LocationProvider/GeoIp/Pecl.php" => array("11193", "e705086e0d6a9ed88a0571e58c7ee291"), - "plugins/UserCountry/LocationProvider/GeoIp.php" => array("8983", "f2c5d51c4d954ba83a6b79f52142e270"), - "plugins/UserCountry/LocationProvider/GeoIp/Php.php" => array("13353", "524c43039566f77e0bb99b97d0d89408"), - "plugins/UserCountry/LocationProvider/GeoIp/ServerBased.php" => array("10066", "cd4ab8bff2cfc6a1de0b660622771679"), - "plugins/UserCountry/LocationProvider.php" => array("16287", "a884a71eb8a2c99dd5e856d0c0120710"), - "plugins/UserCountryMap/Controller.php" => array("11481", "c1083ef590a08229977f908487188697"), - "plugins/UserCountryMap/images/cities.png" => array("267", "05566bd830e4a3225fa746eafc2534b1"), + "plugins/UserCountry/javascripts/userCountry.js" => array("8267", "cb3c3cb212c03e00536cbebc888deeb5"), + "plugins/UserCountry/lang/am.json" => array("209", "4fe1372aa4aa41e3aeb59dd6ae925542"), + "plugins/UserCountry/lang/ar.json" => array("2228", "da0167056dfeadaab40ed609fa247b80"), + "plugins/UserCountry/lang/be.json" => array("529", "688c6bbec8c9080f2444764fa1f856bd"), + "plugins/UserCountry/lang/bg.json" => array("12884", "0e15b91460d0d9069a41cfeeba92bb87"), + "plugins/UserCountry/lang/ca.json" => array("10390", "752a8be8d7826cd7b4a92eb1049a176f"), + "plugins/UserCountry/lang/cs.json" => array("10898", "b7f79cfca0a3e183393a23c313c56e50"), + "plugins/UserCountry/lang/da.json" => array("10550", "3be401c0b5a90efebbfc55bd2e6b0eac"), + "plugins/UserCountry/lang/de.json" => array("11891", "bccb6abd9a42a1ed0a8b686ab2b22ee4"), + "plugins/UserCountry/lang/el.json" => array("18727", "e48cc40b72bd5da77cae6eaa83b2a92d"), + "plugins/UserCountry/lang/en.json" => array("10458", "7052b8b6a0ea19839743b696d65f1502"), + "plugins/UserCountry/lang/es.json" => array("11542", "9f9bba21fef8e16700602a2af8393888"), + "plugins/UserCountry/lang/et.json" => array("1608", "833789878b781618e3ed2fdcd4629085"), + "plugins/UserCountry/lang/eu.json" => array("195", "0d2f93219b13b870cb96d505a621e928"), + "plugins/UserCountry/lang/fa.json" => array("4712", "f59aa17d52e23804715af4ad162708cd"), + "plugins/UserCountry/lang/fi.json" => array("10486", "e640144b64c01495a57d7a2336b1d8b3"), + "plugins/UserCountry/lang/fr.json" => array("12210", "88c822fc759f119ef06df1d9ce22aecb"), + "plugins/UserCountry/lang/gl.json" => array("868", "ce930a5574ebee9a475869836167c9dd"), + "plugins/UserCountry/lang/he.json" => array("510", "2d92b7104ca95bde8dbe89dd577469d9"), + "plugins/UserCountry/lang/hi.json" => array("17407", "af0a076e360fe64e3feb8ac638226e38"), + "plugins/UserCountry/lang/hr.json" => array("323", "9898a220a28b06b9eb2dd9603ceadc3f"), + "plugins/UserCountry/lang/hu.json" => array("392", "2872053a604b9664d62959c88adf12f0"), + "plugins/UserCountry/lang/id.json" => array("9783", "e3b86c4279c11462f14e9062d1d21a9e"), + "plugins/UserCountry/lang/is.json" => array("353", "d606b0b36de35ae12dc63e3d8a278f95"), + "plugins/UserCountry/lang/it.json" => array("11524", "60978bdf77c20037eb4b419ba5b40e77"), + "plugins/UserCountry/lang/ja.json" => array("14056", "4a6413aac4a31b296610c17e526dcaff"), + "plugins/UserCountry/lang/ka.json" => array("566", "de4b81622c4cb07e3c9da2f46af7b641"), + "plugins/UserCountry/lang/ko.json" => array("12182", "dc31f65812ea48ca261943134e944dd4"), + "plugins/UserCountry/lang/lt.json" => array("776", "cf0b9ac7158294dd828bc11d40fcd957"), + "plugins/UserCountry/lang/lv.json" => array("370", "e0a637cbc85dd855cba3c41bc66c57fd"), + "plugins/UserCountry/lang/nb.json" => array("1954", "fb3a0e91c6d6a79115dfb7dcdfbf0e99"), + "plugins/UserCountry/lang/nl.json" => array("7645", "155a5fb86f41059788b27cab3a4b31b5"), + "plugins/UserCountry/lang/nn.json" => array("376", "037dc5fcc10f8c3275effbd4008350f1"), + "plugins/UserCountry/lang/pl.json" => array("5433", "9f8794a7b14a34a0a414f6e179c6b417"), + "plugins/UserCountry/lang/pt-br.json" => array("11585", "9b864ebc8f5fad3f366a45df330c4375"), + "plugins/UserCountry/lang/pt.json" => array("405", "8243fce35e7a3cd627c77a75eba2d100"), + "plugins/UserCountry/lang/ro.json" => array("11165", "1859ea0135e6c3434c15b35a2c6eed4a"), + "plugins/UserCountry/lang/ru.json" => array("15412", "2469ffe73d14bfc3074e27b7835729c7"), + "plugins/UserCountry/lang/sk.json" => array("325", "9d0fce406a63ebfad432f05fbc3cfac9"), + "plugins/UserCountry/lang/sl.json" => array("488", "632d0e5632b77400e892d8d90d04eb7e"), + "plugins/UserCountry/lang/sq.json" => array("390", "fe9617663560bb393d4f6a0c30afff2d"), + "plugins/UserCountry/lang/sr.json" => array("10318", "4c482db8ad7eb31bfff7e843b5016f60"), + "plugins/UserCountry/lang/sv.json" => array("10883", "827c3c0f1b20ac88c3913d082d629a74"), + "plugins/UserCountry/lang/ta.json" => array("282", "93670005ee2ef3c0bc3784d4bc4d3142"), + "plugins/UserCountry/lang/te.json" => array("395", "b23c84f0d9b2124cdb4b530bfeef4e78"), + "plugins/UserCountry/lang/th.json" => array("1085", "e2ebc562abf5d6bda48bee8bbcd0f673"), + "plugins/UserCountry/lang/tl.json" => array("10338", "312a953f8d84dba5d1cb9b90100438ea"), + "plugins/UserCountry/lang/tr.json" => array("561", "624f86102cc1566053ccd30e94f3f898"), + "plugins/UserCountry/lang/uk.json" => array("467", "c83c72d7ec724a545f5fe9407fc18f05"), + "plugins/UserCountry/lang/vi.json" => array("12709", "7e077aa291b29e4e577cb98b8cb6d7e1"), + "plugins/UserCountry/lang/zh-cn.json" => array("9201", "d892ed415e60d4a37b3eb78e9b832de9"), + "plugins/UserCountry/lang/zh-tw.json" => array("271", "70ee304be66e431549262d5db6dc49c8"), + "plugins/UserCountry/LocationProvider/DefaultProvider.php" => array("3273", "debc74a9a4ad8a478c8e85ba0f365131"), + "plugins/UserCountry/LocationProvider/GeoIp/Pecl.php" => array("11216", "a3615106f9cffc1428b7909f4e6bfec0"), + "plugins/UserCountry/LocationProvider/GeoIp.php" => array("8987", "7d6ed9d777eff0ab7ed2682185fc39e1"), + "plugins/UserCountry/LocationProvider/GeoIp/Php.php" => array("14620", "2453b05419c7256257873efd93bd54f4"), + "plugins/UserCountry/LocationProvider/GeoIp/ServerBased.php" => array("10430", "466b505acf078a26ddbd496373292fd8"), + "plugins/UserCountry/LocationProvider.php" => array("16366", "4ca54d7b5d3e50c48f076e08998dc705"), + "plugins/UserCountryMap/Controller.php" => array("13061", "dfa6f8cfb6fa9ede9039b23a1cc4d0d0"), + "plugins/UserCountryMap/images/cities.png" => array("1038", "7eaa39f8e0021507b8685e3e0b2c87e9"), "plugins/UserCountryMap/images/realtimemap-loading.gif" => array("308", "a41ca826560fe6eaeb46dd69b6d9dba2"), - "plugins/UserCountryMap/images/regions.png" => array("296", "98c1643253f8198b91297c5db80be6b0"), - "plugins/UserCountryMap/images/zoom-out-disabled.png" => array("270", "153e42ba1158ec475aa0e7e0f67acaac"), - "plugins/UserCountryMap/javascripts/realtime-map.js" => array("27190", "de5fbfd35a558cc047e3491a7f4f2e08"), - "plugins/UserCountryMap/javascripts/vendor/chroma.min.js" => array("25434", "33d7d9ebf37530751174e8ac63d83c95"), + "plugins/UserCountryMap/images/regions.png" => array("1265", "1fdee0b6664804d6525dfbe12931b922"), + "plugins/UserCountryMap/images/zoom-out-disabled.png" => array("1297", "81d56e2c732e3ed4bc1a1c7d94632e7b"), + "plugins/UserCountryMap/javascripts/realtime-map.js" => array("28098", "d13861676e7eb84433694494cbbb3af2"), "plugins/UserCountryMap/javascripts/vendor/jquery.qtip.min.js" => array("24441", "9b50d81ac4cb3194777a05429abb39d3"), - "plugins/UserCountryMap/javascripts/vendor/kartograph.min.js" => array("67544", "457051a2a34be85bbc921d0fdcb94351"), + "plugins/UserCountryMap/javascripts/vendor/kartograph.min.js" => array("67046", "25dd1ee7ea9be84160d1cee89ffe7d5d"), "plugins/UserCountryMap/javascripts/vendor/raphael.min.js" => array("90648", "3af49700d08ae8f43d613218eec1f754"), - "plugins/UserCountryMap/javascripts/visitor-map.js" => array("71971", "d9d908d5c59527221ccd4098fcbcea48"), - "plugins/UserCountryMap/stylesheets/map.css" => array("1504", "843c9412e98b9da0ece4b8533017ddd8"), - "plugins/UserCountryMap/stylesheets/realtime-map.less" => array("3192", "c367e7663058456db63ffdcac5af6fea"), - "plugins/UserCountryMap/stylesheets/visitor-map.less" => array("4529", "659ac501b37cd5b2ee9dbdf78829f8ed"), + "plugins/UserCountryMap/javascripts/visitor-map.js" => array("74657", "1689420292b24bceffb6c7deaf9bfa55"), + "plugins/UserCountryMap/lang/ar.json" => array("61", "0487964b7483feb043b25762c5c3f072"), + "plugins/UserCountryMap/lang/be.json" => array("61", "dd4263fdd6cc1caf9a3cb571687b8aa4"), + "plugins/UserCountryMap/lang/bg.json" => array("1090", "9d2e775c7af3aaecb97ce860f1426e9d"), + "plugins/UserCountryMap/lang/ca.json" => array("55", "e1103b85f2500b0409fef3e1555b24c6"), + "plugins/UserCountryMap/lang/cs.json" => array("1053", "1ccff2afb609d419fba82b1cf1aafcda"), + "plugins/UserCountryMap/lang/da.json" => array("850", "99ea8e23b82e8f4876f9e57bd8d1d0db"), + "plugins/UserCountryMap/lang/de.json" => array("1047", "72b138467a631973f0799ee1ab7828f8"), + "plugins/UserCountryMap/lang/el.json" => array("1556", "e4eb04f2f0729a7c7ec7c861ac75017a"), + "plugins/UserCountryMap/lang/en.json" => array("976", "97d903ef5ea29aeec00be157aa595d11"), + "plugins/UserCountryMap/lang/es.json" => array("1072", "ce15e88373b52fa3021958daa5f1f2bd"), + "plugins/UserCountryMap/lang/et.json" => array("700", "9bb7503698ea74afb0f4c88bdc7cf6bd"), + "plugins/UserCountryMap/lang/fa.json" => array("770", "0e3e4c0d8de72d0cb8a290c202589f05"), + "plugins/UserCountryMap/lang/fi.json" => array("786", "3690e874d2e989d29160aaa9ed4f2218"), + "plugins/UserCountryMap/lang/fr.json" => array("1063", "f8cb6faee400e22bea30e9eadd4d42cc"), + "plugins/UserCountryMap/lang/he.json" => array("257", "932f6202a5d3b6be73fc6325618d5e15"), + "plugins/UserCountryMap/lang/hi.json" => array("1460", "3e9ed24df39e30f8f10bdd46125b71d0"), + "plugins/UserCountryMap/lang/hr.json" => array("75", "dec57d727e509b3fd6980cac1e2657ba"), + "plugins/UserCountryMap/lang/hu.json" => array("59", "86b3d3d6f7163c4075149e89b19cb28f"), + "plugins/UserCountryMap/lang/id.json" => array("1026", "5ba82b93951526dc1cfc796289bba212"), + "plugins/UserCountryMap/lang/it.json" => array("1029", "58be5179a4e7c3cead5e9edcf0c19f38"), + "plugins/UserCountryMap/lang/ja.json" => array("1139", "6b8ae0fafe98835b883d46be0fc33f4d"), + "plugins/UserCountryMap/lang/ka.json" => array("63", "f951e48941a86f2ce75027addc276681"), + "plugins/UserCountryMap/lang/ko.json" => array("1048", "0f00191b51d265026705193d8a903bc7"), + "plugins/UserCountryMap/lang/lt.json" => array("235", "3a8fb4db724569c968fd1d19b4ee358d"), + "plugins/UserCountryMap/lang/lv.json" => array("56", "c7647191700c173f1ceb998e12d933a5"), + "plugins/UserCountryMap/lang/nb.json" => array("1001", "d1dad7c76cc07320665fbb1cd7af661e"), + "plugins/UserCountryMap/lang/nl.json" => array("1064", "b5bbc4bbeee4725f642dbf8be03965c9"), + "plugins/UserCountryMap/lang/pl.json" => array("661", "127a61efb09654d880c6084495ffe805"), + "plugins/UserCountryMap/lang/pt-br.json" => array("1040", "b12e2a9b6f3480705e4c67298327d973"), + "plugins/UserCountryMap/lang/pt.json" => array("55", "e1103b85f2500b0409fef3e1555b24c6"), + "plugins/UserCountryMap/lang/ro.json" => array("775", "d89c75bde81f6bc507943f76d0448cb1"), + "plugins/UserCountryMap/lang/ru.json" => array("1147", "45920ebd58f478e13e6320132a0008d4"), + "plugins/UserCountryMap/lang/sk.json" => array("299", "576950d21eb3a97e131e6c2ffb19d6cd"), + "plugins/UserCountryMap/lang/sl.json" => array("222", "522a0ab2237f8310eac6784cb398040f"), + "plugins/UserCountryMap/lang/sq.json" => array("57", "21286ba5f979c7aef7424078910869c6"), + "plugins/UserCountryMap/lang/sr.json" => array("987", "778b30fb4645224ce50fc44b9ea57412"), + "plugins/UserCountryMap/lang/sv.json" => array("948", "bffa47fd41329547ff352d72c128a733"), + "plugins/UserCountryMap/lang/te.json" => array("60", "ca24573a8e35e07e8e62829533521e30"), + "plugins/UserCountryMap/lang/th.json" => array("69", "175502debba30f502be3c7baa7c647a2"), + "plugins/UserCountryMap/lang/tl.json" => array("932", "515929ed758ce6597a659232eaeade6b"), + "plugins/UserCountryMap/lang/tr.json" => array("103", "121a79353e2bfd8ac7ca0f62fd0d57e0"), + "plugins/UserCountryMap/lang/uk.json" => array("61", "dd4263fdd6cc1caf9a3cb571687b8aa4"), + "plugins/UserCountryMap/lang/vi.json" => array("917", "0c3ecbd95d01a4bf71ae6e6d8a8b2997"), + "plugins/UserCountryMap/lang/zh-cn.json" => array("714", "6b9e57bf513673a399a47416ba2c4c73"), + "plugins/UserCountryMap/lang/zh-tw.json" => array("57", "36988d75192ecdfe4fea0421209dcac5"), + "plugins/UserCountryMap/Menu.php" => array("667", "544c906d2c0506c785ec21051f7949b3"), + "plugins/UserCountryMap/stylesheets/map.css" => array("1503", "3dde855d124691ccd8b31b6e2d741f39"), + "plugins/UserCountryMap/stylesheets/realtime-map.less" => array("3391", "2a16f028ef313a3d7243a29134eba270"), + "plugins/UserCountryMap/stylesheets/visitor-map.less" => array("4974", "19cadbdd00b7731a37ddbfe16b4d7c8c"), "plugins/UserCountryMap/svg/AFG.svg" => array("23731", "2093e033509062e7928f7917fa7a33c5"), "plugins/UserCountryMap/svg/AF.svg" => array("40727", "4bb303e7ee6eb2b29caec82db8843123"), "plugins/UserCountryMap/svg/AGO.svg" => array("11968", "820cf71105c9c3c1339ba3740ccc9ea8"), @@ -2550,393 +5719,675 @@ class Manifest { "plugins/UserCountryMap/svg/ZAF.svg" => array("10216", "ea4566c8b873e02a443efde301ebafd3"), "plugins/UserCountryMap/svg/ZMB.svg" => array("11277", "e79d992f232139d9bb4c971c57fd1803"), "plugins/UserCountryMap/svg/ZWE.svg" => array("10120", "00eb861cd623329b917a94aacaf09b97"), - "plugins/UserCountryMap/templates/realtimeMap.twig" => array("1455", "aa4b22cc760a6dcc5690a6420aaafce8"), - "plugins/UserCountryMap/templates/visitorMap.twig" => array("5543", "0b26ebd13591d082358483483427eaf6"), - "plugins/UserCountryMap/UserCountryMap.php" => array("2953", "6cf2a6f2c7e90fbbcfae3edd21419454"), + "plugins/UserCountryMap/templates/realtimeMap.twig" => array("1454", "2cb85cc3414ff5ba3e42a369cadc5606"), + "plugins/UserCountryMap/templates/visitorMap.twig" => array("5185", "ba8626133460e538cfc1b54fec23c808"), + "plugins/UserCountryMap/UserCountryMap.php" => array("2749", "86d7911e914a5461afdedbcab8b5cd94"), + "plugins/UserCountry/Menu.php" => array("830", "37abf12815a42db274d30881c5ede89f"), + "plugins/UserCountry/Reports/Base.php" => array("2602", "2b708cab0863d1e3ce3240f33d489c02"), + "plugins/UserCountry/Reports/GetCity.php" => array("1312", "dad364dd1108e7e2bddf12b32c9bb8ea"), + "plugins/UserCountry/Reports/GetContinent.php" => array("1403", "152c4a7afa114c6afdf8fd45c745928a"), + "plugins/UserCountry/Reports/GetCountry.php" => array("1822", "c6834895f42b93ab6b268f96d41b5504"), + "plugins/UserCountry/Reports/GetRegion.php" => array("1324", "cb75b052f580ad70802963f82249b733"), + "plugins/UserCountry/Segment.php" => array("376", "91525bd815987da3760803330424db5b"), "plugins/UserCountry/stylesheets/userCountry.less" => array("1122", "17952f305542ff7aa15daf21f7d1deda"), - "plugins/UserCountry/templates/adminIndex.twig" => array("6899", "4ec9965bec4e48d45de0e029b51ab6d9"), + "plugins/UserCountry/Tasks.php" => array("466", "686a2d7cd2b7d9282369e3bfed4c530d"), + "plugins/UserCountry/templates/adminIndex.twig" => array("6686", "b543e048170c631386740a86b56ef4c4"), "plugins/UserCountry/templates/getGeoIpUpdaterManageScreen.twig" => array("48", "b3ffc6408921cada77599d32ae546511"), - "plugins/UserCountry/templates/index.twig" => array("785", "12f762bb3dff29bb477eff7fff7238c2"), - "plugins/UserCountry/templates/_updaterManage.twig" => array("3168", "87f3d8ef21402d491ec9c19203fd4ed7"), + "plugins/UserCountry/templates/index.twig" => array("929", "3986e5864bf49ee5c9f1108af3d420db"), + "plugins/UserCountry/templates/_updaterManage.twig" => array("3004", "048c9980cbce0a3f84c0a6ac02354211"), "plugins/UserCountry/templates/_updaterNextRunTime.twig" => array("386", "9761b8bd24bd0ebd0461a560dcb23d22"), - "plugins/UserCountry/UserCountry.php" => array("21375", "6a03bbebc2f88b09ddc859d37e8b1aa3"), - "plugins/UserSettings/API.php" => array("10763", "f32d5ea9844711996d18085b7d5ec1b9"), - "plugins/UserSettings/Archiver.php" => array("6825", "1cd51e9b6b66f6a066e301b058c1fbd4"), - "plugins/UserSettings/Controller.php" => array("2023", "0628ed431135bd3fac8ad989088df785"), - "plugins/UserSettings/functions.php" => array("6653", "3444e02fdef24b992dd36e164e3fa10a"), - "plugins/UserSettings/images/browsers/AA.gif" => array("1092", "dc62eff78dac919b00e60c5fb3e6266d"), - "plugins/UserSettings/images/browsers/AB.gif" => array("1064", "1123da862a558b1ccd8f9008c5c4fdcb"), - "plugins/UserSettings/images/browsers/AG.gif" => array("351", "6e793ad6ad5c69abc499422d6a43d836"), - "plugins/UserSettings/images/browsers/AM.gif" => array("198", "18c54fc3197f6e1533c06b0923db3bd0"), - "plugins/UserSettings/images/browsers/AN.gif" => array("144", "a1264f47256ff5b0d4feeeac833e7a96"), - "plugins/UserSettings/images/browsers/AR.gif" => array("1057", "377249d199156ee602e669c0afc02945"), - "plugins/UserSettings/images/browsers/AV.gif" => array("151", "f459c84d6ab90fdf0f35d20f9f82626d"), - "plugins/UserSettings/images/browsers/AW.gif" => array("574", "e4e4fa2ef432f2a86086ec58f4b27ab1"), - "plugins/UserSettings/images/browsers/B2.gif" => array("1070", "e1b9f6b47cffbdf05b4159d6a31b21ae"), - "plugins/UserSettings/images/browsers/BB.gif" => array("576", "d40bd99ba1dd881b164907341ec2079d"), - "plugins/UserSettings/images/browsers/BD.gif" => array("1051", "4a8ebfcd7aad4c1004b7f82b954b0a69"), - "plugins/UserSettings/images/browsers/BE.gif" => array("1042", "103994d17de92aa261c8034a8e35a84f"), - "plugins/UserSettings/images/browsers/BP.gif" => array("1070", "e1b9f6b47cffbdf05b4159d6a31b21ae"), - "plugins/UserSettings/images/browsers/BX.gif" => array("522", "7302ad862f4007c23efb73acbd41f5c0"), - "plugins/UserSettings/images/browsers/CA.gif" => array("573", "739fca054b61f68657b0bd349c958a86"), - "plugins/UserSettings/images/browsers/CD.gif" => array("1045", "b5484f5fc254abd52cdc94f378be977a"), - "plugins/UserSettings/images/browsers/CF.gif" => array("1074", "e349a7dbdadf2fca33cca13287b0eba8"), - "plugins/UserSettings/images/browsers/CH.gif" => array("1074", "e349a7dbdadf2fca33cca13287b0eba8"), - "plugins/UserSettings/images/browsers/CK.gif" => array("1024", "b6d4ebb0394c48dfcb5f21475de5c79b"), - "plugins/UserSettings/images/browsers/CM.gif" => array("1074", "e349a7dbdadf2fca33cca13287b0eba8"), - "plugins/UserSettings/images/browsers/CN.gif" => array("998", "cd878afb8cc56e7c8c6caef6d5fb2ba4"), - "plugins/UserSettings/images/browsers/CO.gif" => array("1042", "195487c9db2e19ffae3fd443b9266e62"), - "plugins/UserSettings/images/browsers/CP.gif" => array("998", "af47e47253591c272a458c4645fbaf5c"), - "plugins/UserSettings/images/browsers/CS.gif" => array("549", "eb5151f2f46fc09d687ce27becefb831"), - "plugins/UserSettings/images/browsers/DF.gif" => array("545", "f4b65ebcf304f1675088d029ed613d28"), - "plugins/UserSettings/images/browsers/DI.gif" => array("1068", "4eceaf5fd7808c422b67026c9329e16f"), - "plugins/UserSettings/images/browsers/EL.gif" => array("90", "b515db820d883f921d05d15a34dda7f9"), - "plugins/UserSettings/images/browsers/EP.gif" => array("316", "660436cc97429ef52365a01084b40ee0"), - "plugins/UserSettings/images/browsers/ES.gif" => array("1013", "92ada780fce5e3ffc318de634eba4d21"), - "plugins/UserSettings/images/browsers/FB.gif" => array("254", "24663f1949bae4fb7ecd0504c9c872fd"), - "plugins/UserSettings/images/browsers/FD.gif" => array("1050", "a569dc9dbe1b95ce2763bee8dbdc5850"), - "plugins/UserSettings/images/browsers/FE.gif" => array("550", "a016a52d476c4be7943e68ccf2056db1"), - "plugins/UserSettings/images/browsers/FF.gif" => array("197", "6b4eecf673d5461aebdd432bca086e8c"), - "plugins/UserSettings/images/browsers/FL.gif" => array("1034", "0ce889dc81db377eeb00f76f72942274"), - "plugins/UserSettings/images/browsers/FN.gif" => array("1033", "e87473b14680939e58a9a94e9dcd39e3"), - "plugins/UserSettings/images/browsers/GA.gif" => array("159", "576c4646cf6938dd2079516293d3a3f9"), - "plugins/UserSettings/images/browsers/GE.gif" => array("997", "a3d96e8576f273ecc4a864d24620fbf2"), - "plugins/UserSettings/images/browsers/HA.gif" => array("1009", "fccae707e311bd009be90a7b38000082"), - "plugins/UserSettings/images/browsers/HJ.gif" => array("1022", "8c3019d1e0867e8455d0abf1ad3eb531"), - "plugins/UserSettings/images/browsers/IA.gif" => array("391", "1836a7db0bc6a4a4d8b3d6063346e3f5"), - "plugins/UserSettings/images/browsers/IB.gif" => array("168", "b091c3e8ce2789017d581089028fa1cc"), - "plugins/UserSettings/images/browsers/IC.gif" => array("131", "26a6ff98d316092214c9dc9e7be45224"), - "plugins/UserSettings/images/browsers/ID.gif" => array("1057", "6f1f33dc4bd104e0a60000ce49e719a9"), - "plugins/UserSettings/images/browsers/IE.gif" => array("999", "5e002ee72167a3a78e2252766fde1046"), - "plugins/UserSettings/images/browsers/IR.gif" => array("610", "565b1e3acd514c1c9b88d6c2b0ec0c4d"), - "plugins/UserSettings/images/browsers/IW.gif" => array("1066", "8d3376b6699ccd4533191b38d7f6b8c4"), - "plugins/UserSettings/images/browsers/KI.gif" => array("1050", "497b4bc9b58c113ae72763559321071b"), - "plugins/UserSettings/images/browsers/KM.gif" => array("180", "3daa5fa7553d448cd280612491e8a6ea"), - "plugins/UserSettings/images/browsers/KO.gif" => array("986", "0d15a2d4a73582d1df109e0ff3463fd3"), - "plugins/UserSettings/images/browsers/KP.gif" => array("1037", "f29fb41537f7df91d827c3f6eea076a0"), - "plugins/UserSettings/images/browsers/KZ.gif" => array("1061", "cdc654ad5f3f5fdda6ac69ea7d7dbe31"), - "plugins/UserSettings/images/browsers/LI.gif" => array("104", "a3b302b3fffc9ed032add149d4ace210"), - "plugins/UserSettings/images/browsers/LS.gif" => array("1086", "c61646736ea4872a7e58bcd29716d778"), - "plugins/UserSettings/images/browsers/LX.gif" => array("104", "a3b302b3fffc9ed032add149d4ace210"), - "plugins/UserSettings/images/browsers/MC.gif" => array("1023", "0e17db9ed1e06feb1d23d134ce34693d"), - "plugins/UserSettings/images/browsers/MI.gif" => array("1025", "5e63fceac90a88f1db14b4e8ee44201d"), - "plugins/UserSettings/images/browsers/MO.gif" => array("192", "67b5dac21e8f2243a955f1d9df7ef67e"), - "plugins/UserSettings/images/browsers/MS.gif" => array("1094", "265861a05c27b23013cb6ae3c428dff0"), - "plugins/UserSettings/images/browsers/MX.gif" => array("985", "4f6f87c42bf5c6bfc2b63925da5e40c1"), - "plugins/UserSettings/images/browsers/NB.gif" => array("977", "d2fac7549889df9f1c0863b424543c6f"), - "plugins/UserSettings/images/browsers/NF.gif" => array("612", "7cb0d2713e9faf25b766ca0d13cf456b"), - "plugins/UserSettings/images/browsers/NL.gif" => array("1081", "f66412328676120ba3cc0eb987c16158"), - "plugins/UserSettings/images/browsers/NP.gif" => array("1020", "ba7d68a0f9c11647abba2f8454a7c34c"), - "plugins/UserSettings/images/browsers/NS.gif" => array("98", "cd8d53ec12b64294d16769dfeeaf07c7"), - "plugins/UserSettings/images/browsers/OB.gif" => array("1010", "67b1d28deccc92200bdc3ce4a86612c3"), - "plugins/UserSettings/images/browsers/ON.gif" => array("635", "f7d7eb7f8cec24f0c192e30fe29ea320"), - "plugins/UserSettings/images/browsers/OP.gif" => array("987", "ac0432440ad48154a6434675b1d9c27a"), - "plugins/UserSettings/images/browsers/OR.gif" => array("1024", "30e874c346325cd40cf58f98944ea603"), - "plugins/UserSettings/images/browsers/OV.gif" => array("978", "7e98fecee01438d561b791339281bd47"), - "plugins/UserSettings/images/browsers/OW.gif" => array("197", "b66e88cbb941f9ac326f43e0d993e572"), - "plugins/UserSettings/images/browsers/PL.gif" => array("1058", "759fa0100429b3b3a4dc88894454fd8a"), - "plugins/UserSettings/images/browsers/PM.gif" => array("1082", "271bd4b89d06a9f9a9553b5da9053bd3"), - "plugins/UserSettings/images/browsers/PO.gif" => array("1065", "ac773ea28693335de8e8ac62f7d20a0d"), - "plugins/UserSettings/images/browsers/PU.gif" => array("1094", "1ea4a15e1b326c28158b137e4e8d07af"), - "plugins/UserSettings/images/browsers/PW.gif" => array("1082", "ac66922861d77e9949a72f86622e0c2f"), - "plugins/UserSettings/images/browsers/PX.gif" => array("170", "0bd86aa95e1ae0d5975cdc2d93210e30"), - "plugins/UserSettings/images/browsers/RK.gif" => array("1035", "6b87220087449e134062214fbf72f5a8"), - "plugins/UserSettings/images/browsers/SA.gif" => array("1008", "921467088fc56c5c9cdd0bb6e58250bf"), - "plugins/UserSettings/images/browsers/SF.gif" => array("190", "589361249f74319b57ea98d6408bc4b3"), - "plugins/UserSettings/images/browsers/SL.gif" => array("900", "1210e399e978978390cfdfd9d79159e6"), - "plugins/UserSettings/images/browsers/SM.gif" => array("391", "1836a7db0bc6a4a4d8b3d6063346e3f5"), - "plugins/UserSettings/images/browsers/TB.gif" => array("1014", "79bf7ed3ad92d3da09737d2fcd3913aa"), - "plugins/UserSettings/images/browsers/TI.gif" => array("595", "2c09db5f54b47472971d863e183159a8"), - "plugins/UserSettings/images/browsers/TZ.gif" => array("973", "5858a8b149e45749424bdf2da7ebefa3"), - "plugins/UserSettings/images/browsers/UC.gif" => array("994", "d9622ea01cb9093592858da53443c200"), - "plugins/UserSettings/images/browsers/UN.gif" => array("80", "c4a7f0e333a6079fdb0b6595e11bca74"), - "plugins/UserSettings/images/browsers/UNK.gif" => array("80", "c4a7f0e333a6079fdb0b6595e11bca74"), - "plugins/UserSettings/images/browsers/WE.gif" => array("1012", "cce9216ee7bd3ef52a46003d249ab540"), - "plugins/UserSettings/images/browsers/WO.gif" => array("1065", "ed1504717c9af523e30c33908126c4ad"), - "plugins/UserSettings/images/browsers/WP.gif" => array("982", "5bba1edfb42ce1b96551f81af0be08a1"), - "plugins/UserSettings/images/browsers/YA.gif" => array("1048", "8d94386ab4796664de7b897dd2106c9c"), - "plugins/UserSettings/images/os/3DS.gif" => array("1085", "262b44579aadcf90973653ff3e759cc7"), - "plugins/UserSettings/images/os/AIX.gif" => array("176", "58a60503a8e92493153694d1d97d2f6d"), - "plugins/UserSettings/images/os/AMG.gif" => array("1001", "5d67b7bb52ed746480573c1600d9a34c"), - "plugins/UserSettings/images/os/AMI.gif" => array("1055", "ef341c4cc2ec3bbf860c7d3bfe685326"), - "plugins/UserSettings/images/os/AND.gif" => array("144", "a1264f47256ff5b0d4feeeac833e7a96"), - "plugins/UserSettings/images/os/ARL.gif" => array("947", "913d273e01b9031f5113fb82ce63a591"), - "plugins/UserSettings/images/os/BBX.gif" => array("590", "e5cff6836abf100d9d8310d9dbb9f5d4"), - "plugins/UserSettings/images/os/BEO.gif" => array("1035", "ae4420933ac47a072d0f47759ef830c2"), - "plugins/UserSettings/images/os/BLB.gif" => array("576", "d40bd99ba1dd881b164907341ec2079d"), - "plugins/UserSettings/images/os/BSD.gif" => array("1016", "1dc9b76bb3fc8f5529e9abe9ac8841fa"), - "plugins/UserSettings/images/os/BTR.gif" => array("946", "cbf9b74ee2db7714ed6d6432eff3f7c8"), - "plugins/UserSettings/images/os/CES.gif" => array("1011", "0cdd142972c3cc89b2ceb3b7b97e1321"), - "plugins/UserSettings/images/os/COS.gif" => array("1074", "e349a7dbdadf2fca33cca13287b0eba8"), - "plugins/UserSettings/images/os/DFB.gif" => array("326", "d61f11a900d520ef515eaa139176a5f1"), - "plugins/UserSettings/images/os/DSI.gif" => array("1076", "5c475f3ba76f4ec3b626e720574bcb37"), - "plugins/UserSettings/images/os/FED.gif" => array("1022", "e86e6f5aec6c32de7eca913a9f91c3ab"), - "plugins/UserSettings/images/os/FOS.gif" => array("197", "6b4eecf673d5461aebdd432bca086e8c"), - "plugins/UserSettings/images/os/GNT.gif" => array("1075", "4196a85df43e6a5593941dcf8262416f"), - "plugins/UserSettings/images/os/GTV.gif" => array("1614", "a032dd001e1a5755201a6263a669ca49"), - "plugins/UserSettings/images/os/HPX.gif" => array("191", "999717c37d76ca099a06cf77a781bdbf"), - "plugins/UserSettings/images/os/IOS.gif" => array("591", "0bd1b8e09506ae6f9ed6ab1b18acf552"), - "plugins/UserSettings/images/os/IPA.gif" => array("587", "9f247437bc140cc6e70ec59f34bcddb1"), - "plugins/UserSettings/images/os/IPD.gif" => array("351", "a215ada2aefcbca876055fe7a2f7c039"), - "plugins/UserSettings/images/os/IPH.gif" => array("577", "49805f402375692d40635ada7cc0f472"), - "plugins/UserSettings/images/os/IRI.gif" => array("152", "5e631b5adc35a05ae0d85815829c6f48"), - "plugins/UserSettings/images/os/KBT.gif" => array("998", "6ecd8b978a51fb4fd6ec94f9b820ae1d"), - "plugins/UserSettings/images/os/KNO.gif" => array("985", "b4595a673edf60051636fedf418eda30"), - "plugins/UserSettings/images/os/LBT.gif" => array("951", "ed14ac9707a0e01f4557ac9e8508dea3"), - "plugins/UserSettings/images/os/LIN.gif" => array("170", "19039ee87d8fccdba6a391ada5656de7"), - "plugins/UserSettings/images/os/MAC.gif" => array("171", "03548481597f28751368e26be49aea99"), - "plugins/UserSettings/images/os/MAE.gif" => array("137", "84600277bad6751b68b15586bca50aef"), - "plugins/UserSettings/images/os/MDR.gif" => array("918", "62f5e501d28e25fff6c3a75631c7c208"), - "plugins/UserSettings/images/os/MIN.gif" => array("1009", "153385eff242c4e10ea3835a17a65f0e"), - "plugins/UserSettings/images/os/NBS.gif" => array("168", "fc0d4fcb57c98f3a4dd6eab8e71ebd6a"), - "plugins/UserSettings/images/os/NDS.gif" => array("1061", "16bc6e0960747b402441c40e419a7d53"), - "plugins/UserSettings/images/os/OBS.gif" => array("571", "fcdb547b7ab768e131ba592e8733c75c"), - "plugins/UserSettings/images/os/OS2.gif" => array("162", "ee37bab155ad46f2530e7586f9971656"), - "plugins/UserSettings/images/os/POS.gif" => array("1060", "96b06842dc1cc80a8bb283ee8f4be320"), - "plugins/UserSettings/images/os/PPY.gif" => array("1037", "1bc770c1bd83e6cfdc5087b00b44cb9d"), - "plugins/UserSettings/images/os/PS3.gif" => array("628", "7aca5b93e7cc8142e2ab578e7aae7dc8"), - "plugins/UserSettings/images/os/PSP.gif" => array("592", "f92da90c6c6ea808422184c314215465"), - "plugins/UserSettings/images/os/PSV.gif" => array("200", "d82e64a4f0aeec6e4931a41988680af3"), - "plugins/UserSettings/images/os/QNX.gif" => array("241", "51ceb87cd6268837d830b225cb6c8120"), - "plugins/UserSettings/images/os/RHT.gif" => array("952", "72c775bb0f2b8ad388ee513577abc8ec"), - "plugins/UserSettings/images/os/ROS.gif" => array("956", "9a294e5c701171c8c777ce563d88d924"), - "plugins/UserSettings/images/os/SAF.gif" => array("242", "600259fe739536945b6f5cd5c9d3489b"), - "plugins/UserSettings/images/os/SBA.gif" => array("990", "be3cfde24b6517884d73270a20e9a06f"), - "plugins/UserSettings/images/os/SLW.gif" => array("883", "37ca3a7bdb0eeea402252c80dca89369"), - "plugins/UserSettings/images/os/SOS.gif" => array("1036", "686261ea170398b2eb078b67a24f4276"), - "plugins/UserSettings/images/os/SSE.gif" => array("1066", "8a6b48a38ee8ecca0fd14092872dba12"), - "plugins/UserSettings/images/os/SYL.gif" => array("1017", "5c73fe766f50d4a697dbdeac254db9e2"), - "plugins/UserSettings/images/os/SYM.gif" => array("1042", "a8d404e43206c52a7e5f3e6e7e469eea"), - "plugins/UserSettings/images/os/T64.gif" => array("220", "8e42601e52784216ed71b8e8de17f82e"), - "plugins/UserSettings/images/os/TIZ.gif" => array("958", "dbc584b7603e8865c9477b18a6fbbb7d"), - "plugins/UserSettings/images/os/UBT.gif" => array("986", "56d67af2d61927c290b0e5af8e8ef6fc"), - "plugins/UserSettings/images/os/UNK.gif" => array("80", "c4a7f0e333a6079fdb0b6595e11bca74"), - "plugins/UserSettings/images/os/VMS.gif" => array("572", "da6881ce3b86fdbea70ae1b405f9c40a"), - "plugins/UserSettings/images/os/W2K.gif" => array("185", "5e7141d138d3f7afc0a113c21a8b2d9b"), - "plugins/UserSettings/images/os/W61.gif" => array("1060", "721b79d12ac825df25f356f5a7714f74"), - "plugins/UserSettings/images/os/W65.gif" => array("1060", "721b79d12ac825df25f356f5a7714f74"), - "plugins/UserSettings/images/os/W75.gif" => array("1089", "8d69225f3ebb1fe2d2b6d4e2e1687b80"), - "plugins/UserSettings/images/os/W95.gif" => array("185", "5e7141d138d3f7afc0a113c21a8b2d9b"), - "plugins/UserSettings/images/os/W98.gif" => array("185", "5e7141d138d3f7afc0a113c21a8b2d9b"), - "plugins/UserSettings/images/os/WCE.gif" => array("185", "5e7141d138d3f7afc0a113c21a8b2d9b"), - "plugins/UserSettings/images/os/WI7.gif" => array("191", "53d14246a8e5a46e927a87648111a0d3"), - "plugins/UserSettings/images/os/WI8.gif" => array("925", "d9f78bbd9009c721cf5d64b056679a19"), - "plugins/UserSettings/images/os/WII.gif" => array("617", "d8f2cae9e8e7723241c6c862f12c7511"), - "plugins/UserSettings/images/os/WIN.gif" => array("191", "53d14246a8e5a46e927a87648111a0d3"), - "plugins/UserSettings/images/os/WIU.gif" => array("310", "394c491524ac263e1c4fcedb1480d281"), - "plugins/UserSettings/images/os/WME.gif" => array("1025", "27c3894e3585f86f9283374ed7776c3a"), - "plugins/UserSettings/images/os/WMO.gif" => array("1060", "721b79d12ac825df25f356f5a7714f74"), - "plugins/UserSettings/images/os/WNT.gif" => array("185", "5e7141d138d3f7afc0a113c21a8b2d9b"), - "plugins/UserSettings/images/os/WOS.gif" => array("70", "31e5c59d2fc5b195c5ea4f1afd878e04"), - "plugins/UserSettings/images/os/WP7.gif" => array("1089", "8d69225f3ebb1fe2d2b6d4e2e1687b80"), - "plugins/UserSettings/images/os/WPH.gif" => array("1089", "8d69225f3ebb1fe2d2b6d4e2e1687b80"), - "plugins/UserSettings/images/os/WRT.gif" => array("925", "d9f78bbd9009c721cf5d64b056679a19"), - "plugins/UserSettings/images/os/WS3.gif" => array("191", "53d14246a8e5a46e927a87648111a0d3"), - "plugins/UserSettings/images/os/WVI.gif" => array("191", "53d14246a8e5a46e927a87648111a0d3"), - "plugins/UserSettings/images/os/WXP.gif" => array("191", "53d14246a8e5a46e927a87648111a0d3"), - "plugins/UserSettings/images/os/XBT.gif" => array("968", "c0f572e03ffaf7d38c78fcbb7fba84cf"), - "plugins/UserSettings/images/os/XBX.gif" => array("1043", "e3e0eaa5daa2903bab1e0e9ea3ef1d46"), - "plugins/UserSettings/images/os/YNS.gif" => array("913", "f4d8502e11b209c209fcee3312a33139"), - "plugins/UserSettings/images/plugins/cookie.gif" => array("211", "9e564884defc036134b19ab38c192a6b"), - "plugins/UserSettings/images/plugins/director.gif" => array("198", "952c4a0e083ed089ca5c8ab2804c35ab"), - "plugins/UserSettings/images/plugins/flash.gif" => array("1018", "f315906f425f089dd4664ee530c691bd"), - "plugins/UserSettings/images/plugins/gears.gif" => array("558", "d4ec944ef01420637a709619f067953e"), - "plugins/UserSettings/images/plugins/java.gif" => array("565", "b4d24f76a64e2df762292e892e9215c4"), - "plugins/UserSettings/images/plugins/pdf.gif" => array("1021", "79b3d68c112942cefbb24dda9b421464"), - "plugins/UserSettings/images/plugins/quicktime.gif" => array("1003", "0feda8dc4ddec39e35b6f4925603c8bd"), - "plugins/UserSettings/images/plugins/realplayer.gif" => array("1025", "95739f527d29cab050a3d4eff35e93c7"), - "plugins/UserSettings/images/plugins/silverlight.gif" => array("1012", "8449c3a43f42c44bc5b82948d179b4b4"), - "plugins/UserSettings/images/plugins/windowsmedia.gif" => array("1026", "a374aec2d5488c5dc8d4eaf21ec59728"), - "plugins/UserSettings/images/screens/dual.gif" => array("1082", "7903e070f4c4aa7933030e547e78073e"), - "plugins/UserSettings/images/screens/mobile.gif" => array("324", "04942ef60dd75380cfd682314e87e857"), - "plugins/UserSettings/images/screens/normal.gif" => array("1088", "b0c3de8704745e58c67844e54282ee42"), - "plugins/UserSettings/images/screens/unknown.gif" => array("80", "c4a7f0e333a6079fdb0b6595e11bca74"), - "plugins/UserSettings/images/screens/wide.gif" => array("1025", "c0104958e6fb23668d0406fd4d89095e"), - "plugins/UserSettings/templates/index.twig" => array("985", "64dbe064f25bce041a9ea0ac4b401509"), - "plugins/UserSettings/UserSettings.php" => array("16493", "546f136ebeb5cbe7e1ffa364be6f1fdc"), - "plugins/UsersManager/API.php" => array("21685", "ff11d871854f92e6e309b563ba4ed8cd"), - "plugins/UsersManager/Controller.php" => array("12751", "27d35da3855e76319f08f5cfeb4e24cd"), - "plugins/UsersManager/images/add.png" => array("1366", "79f8c85304af007fb571bd67b5b84a65"), + "plugins/UserCountry/UserCountry.php" => array("4285", "bffbc382752561cc071e9b4341b83ec5"), + "plugins/UserCountry/VisitorGeolocator.php" => array("10882", "d05221ed2456380605b5ee6857634e27"), + "plugins/UserCountry/Visitor.php" => array("2558", "0dbbfa0030de5317128be97d97c2b475"), + "plugins/UserLanguage/API.php" => array("2116", "1314ee077057973e23efe20f7447e050"), + "plugins/UserLanguage/Archiver.php" => array("3003", "2eca610090869ff2f14aead682d90f51"), + "plugins/UserLanguage/Columns/Language.php" => array("1777", "f7639231c9d299b15ad77589a647bf4c"), + "plugins/UserLanguage/functions.php" => array("1726", "c105d7757352549ff9d55a4e72349dd9"), + "plugins/UserLanguage/lang/am.json" => array("77", "50ea2510c809e5191cf9005f906f609f"), + "plugins/UserLanguage/lang/ar.json" => array("240", "e064c44491011846dbd5fa40cd2f7690"), + "plugins/UserLanguage/lang/be.json" => array("73", "7838a25874e18e0e93f41a1a53db9bcb"), + "plugins/UserLanguage/lang/bg.json" => array("141", "06cbf050cd902edf4c1a41a6e7359143"), + "plugins/UserLanguage/lang/ca.json" => array("74", "517d9a647ca634e725d934ec3080b4a1"), + "plugins/UserLanguage/lang/cs.json" => array("208", "46b68267c8ed894345aa6b207ca65444"), + "plugins/UserLanguage/lang/da.json" => array("203", "31a371933ea1723393bdda93c71307d8"), + "plugins/UserLanguage/lang/de.json" => array("222", "f6960e2af8690d3a534b91b184668858"), + "plugins/UserLanguage/lang/el.json" => array("339", "4cfc41ed7db488ba5dbd54b9e63a528b"), + "plugins/UserLanguage/lang/en.json" => array("204", "fc78445517e40377464d578229eb376c"), + "plugins/UserLanguage/lang/es.json" => array("224", "2be0cb2d3c5e4f173d0cfd223ad54706"), + "plugins/UserLanguage/lang/et.json" => array("116", "82eb2cbee9421ec8b46cfa84cc81720d"), + "plugins/UserLanguage/lang/eu.json" => array("73", "156cfb5560818d5004456163b6665e26"), + "plugins/UserLanguage/lang/fa.json" => array("123", "69593e002f7db732fee224a35256a0e3"), + "plugins/UserLanguage/lang/fi.json" => array("113", "41bc41ca424b42b5488b36f904064df4"), + "plugins/UserLanguage/lang/fr.json" => array("223", "b4b952edf04e549d4cb9727bb99902af"), + "plugins/UserLanguage/lang/gl.json" => array("221", "6cec01749ee22927adeb7883700e6709"), + "plugins/UserLanguage/lang/he.json" => array("71", "7885719b1507af9af987e2a61d6d2cef"), + "plugins/UserLanguage/lang/hi.json" => array("342", "5396ac5662df6bff367adb5b9c7665d4"), + "plugins/UserLanguage/lang/hr.json" => array("211", "4d38af3ae41a3e9d47947f6caa30e327"), + "plugins/UserLanguage/lang/hu.json" => array("69", "5150359cdf58dc2816fc2d398eab725f"), + "plugins/UserLanguage/lang/id.json" => array("200", "5cea9c9d2f9c4fda153979b9d13c0235"), + "plugins/UserLanguage/lang/is.json" => array("75", "944b8498ab8f90c72e71386ed88bd8cb"), + "plugins/UserLanguage/lang/it.json" => array("220", "1fbf80f80d056f978a179a80ba87aeb0"), + "plugins/UserLanguage/lang/ja.json" => array("245", "45de0b2711432c65fbe0ffb22ed7605d"), + "plugins/UserLanguage/lang/ka.json" => array("83", "1611da157c35c952be9613cea9d60e68"), + "plugins/UserLanguage/lang/ko.json" => array("203", "751f2e18aa2a5febc323f77c0e747128"), + "plugins/UserLanguage/lang/lt.json" => array("118", "45a234ce3d8e6d30d561d819c32c7f1e"), + "plugins/UserLanguage/lang/lv.json" => array("70", "443d78e2ba274a9fe627d652cd890d83"), + "plugins/UserLanguage/lang/nb.json" => array("211", "1068d9a06f96f67d811b8d69f5bb0857"), + "plugins/UserLanguage/lang/nl.json" => array("206", "5e1a3ddde02fa61bffd3640b3929d2ec"), + "plugins/UserLanguage/lang/nn.json" => array("68", "dafd2eddbee8c3f8675af50aeda6e597"), + "plugins/UserLanguage/lang/pl.json" => array("215", "aa81c2937cbf5150a5cba693cdba793a"), + "plugins/UserLanguage/lang/pt-br.json" => array("214", "6008163185e1c3980153851af96c3d41"), + "plugins/UserLanguage/lang/pt.json" => array("75", "bdc7c957b8a00b6bae753cec6f77e098"), + "plugins/UserLanguage/lang/ro.json" => array("117", "62924385db942a0bc1d7a2f1f1c05e8a"), + "plugins/UserLanguage/lang/ru.json" => array("268", "5849e16f274ad6d1162f071bdd21d066"), + "plugins/UserLanguage/lang/sk.json" => array("210", "46cc32df6fa5d8c27cc4dbf2dbe27dc0"), + "plugins/UserLanguage/lang/sl.json" => array("118", "61fc91fbdb8a80e1e34f38edd929f1fa"), + "plugins/UserLanguage/lang/sq.json" => array("67", "c706a736e887902410bed30205cddeed"), + "plugins/UserLanguage/lang/sr.json" => array("210", "bc1412286e2300440cad5297b86bb283"), + "plugins/UserLanguage/lang/sv.json" => array("193", "9c9980b72928ef2e9cacb99330c73b2e"), + "plugins/UserLanguage/lang/ta.json" => array("308", "be2eda126052cf72cf873826f8cb1e42"), + "plugins/UserLanguage/lang/te.json" => array("89", "a4badd5969e35c356b8040ca0d679688"), + "plugins/UserLanguage/lang/th.json" => array("82", "3791e882e71c4c7c54d306bb1e47b076"), + "plugins/UserLanguage/lang/tl.json" => array("116", "5be2bcd21245c36a5181fc6d88765cba"), + "plugins/UserLanguage/lang/tr.json" => array("66", "5986ed03ce0c063eef3955d7c72d8992"), + "plugins/UserLanguage/lang/uk.json" => array("73", "edebfb5f4507b44ad19149cbe77ca606"), + "plugins/UserLanguage/lang/vi.json" => array("130", "660023f218a9ef33161d88b5c2f4fa91"), + "plugins/UserLanguage/lang/zh-cn.json" => array("116", "daadb80a393e6527df81fe8aca18cb70"), + "plugins/UserLanguage/lang/zh-tw.json" => array("185", "c2c575d43b2da8c9503fc21479419a6c"), + "plugins/UserLanguage/Reports/Base.php" => array("411", "203a69e3d30dad6caed2d323a3e7c4ff"), + "plugins/UserLanguage/Reports/GetLanguageCode.php" => array("773", "ef24e69a8558efbcb62cd08c4e422b0d"), + "plugins/UserLanguage/Reports/GetLanguage.php" => array("1254", "0ae540fb19b4bd3dbbf9ff38418b11e8"), + "plugins/UserLanguage/UserLanguage.php" => array("1525", "26277ce1b1312efa6337ae3b14a0f2a6"), + "plugins/UserLanguage/Visitor.php" => array("637", "682e01ae0c2046efc8de13e638809b9c"), + "plugins/UsersManager/API.php" => array("26904", "4ddfd742012972557b4000749eb3937e"), + "plugins/UsersManager/Controller.php" => array("17108", "7b256dd874a1d14f8662b46c87ac7c4d"), "plugins/UsersManager/images/no-access.png" => array("653", "e92421cec0f4c4f344b00a2ff72f96ec"), "plugins/UsersManager/images/ok.png" => array("851", "94672a5d1482b1afaafce7802155f518"), - "plugins/UsersManager/javascripts/usersManager.js" => array("10779", "e4711ac89dcb705f5002c39a26608dbe"), - "plugins/UsersManager/javascripts/usersSettings.js" => array("3254", "6f10ea2cac2509e07988389078687af4"), - "plugins/UsersManager/LastSeenTimeLogger.php" => array("2296", "11eb62ec3cc2588fb571769330aefbba"), - "plugins/UsersManager/Model.php" => array("9294", "93aa4779b992fc0991aaa9a6cc8e533e"), - "plugins/UsersManager/stylesheets/usersManager.less" => array("400", "34e7c27854b8a5e2b5c2e55df5de755c"), - "plugins/UsersManager/templates/index.twig" => array("9706", "50e07c93f864be7e52418c05d96c5588"), - "plugins/UsersManager/templates/userSettings.twig" => array("9580", "25708d48635daf6f1114220df0c43fc2"), - "plugins/UsersManager/UsersManager.php" => array("5028", "42c49b25bd17cc81567855b42e0e4736"), - "plugins/VisitFrequency/API.php" => array("2542", "bd9a2e5d38ce65bc72af6abfe65166a6"), - "plugins/VisitFrequency/Controller.php" => array("4225", "9316375706f2da88c7243aa2e703d362"), + "plugins/UsersManager/javascripts/giveViewAccess.js" => array("5315", "9cd65b92613cc3199b7383483e0d50f7"), + "plugins/UsersManager/javascripts/usersManager.js" => array("11248", "6daffb27ea6a2015249fb5806143fa9f"), + "plugins/UsersManager/javascripts/usersSettings.js" => array("3341", "3ab8e2b943e9243915dd162d28653a44"), + "plugins/UsersManager/lang/am.json" => array("1835", "3d0c0aa3aab5c0562cd1ed0fa1716525"), + "plugins/UsersManager/lang/ar.json" => array("4096", "a0ee1715e62db81681ab991c501a6eef"), + "plugins/UsersManager/lang/be.json" => array("5049", "42a56bd1b8c3f79d30064a1b961a1c02"), + "plugins/UsersManager/lang/bg.json" => array("8143", "3e420c866a0892442e8987715a7988d2"), + "plugins/UsersManager/lang/bs.json" => array("97", "4dfd11c5c330b2adf981bc2c1bcbe4c8"), + "plugins/UsersManager/lang/ca.json" => array("3909", "5060b33f6e6ba32a7915c6e89307e672"), + "plugins/UsersManager/lang/cs.json" => array("7790", "11c02b05cb4915c515109c6a755e4173"), + "plugins/UsersManager/lang/da.json" => array("5738", "744f37c4c2b09c4e20b62d3b15c99881"), + "plugins/UsersManager/lang/de.json" => array("7206", "7154583aba4ef0b6a8cf4c336772f7e7"), + "plugins/UsersManager/lang/el.json" => array("12719", "695852310a9dc98abdef80981cbeecec"), + "plugins/UsersManager/lang/en.json" => array("7192", "962d7ffa4610a6eeba787d3fbd15738a"), + "plugins/UsersManager/lang/es.json" => array("6353", "eaae4c6fac6763badac90feced16ea6b"), + "plugins/UsersManager/lang/et.json" => array("2761", "b3fba88a6633d1559753efac1de3568c"), + "plugins/UsersManager/lang/eu.json" => array("1524", "d9f296ac1e15b969b3f05830da31cd9b"), + "plugins/UsersManager/lang/fa.json" => array("5617", "199d0fa6796bb5db093ea3cf4b95c0b7"), + "plugins/UsersManager/lang/fi.json" => array("5844", "3c32208081ed506c0b5d5789bdba17c4"), + "plugins/UsersManager/lang/fr.json" => array("6633", "90451fc72ef58fc94ac08ca7c10d31ab"), + "plugins/UsersManager/lang/gl.json" => array("1272", "d93072ac7f3695fa877bdca302ec4005"), + "plugins/UsersManager/lang/he.json" => array("381", "4c2311bec3c380920eadf0dc3daa6bb8"), + "plugins/UsersManager/lang/hi.json" => array("2279", "06a3c3da358376eb0518b9e1de237c4b"), + "plugins/UsersManager/lang/hr.json" => array("397", "062659f1845602d8e3085d0f3176ebad"), + "plugins/UsersManager/lang/hu.json" => array("3763", "9494e22107493145ac5545b15f5fc3d2"), + "plugins/UsersManager/lang/id.json" => array("4152", "33a98d86a6cc80c1343eb276f3ab0f08"), + "plugins/UsersManager/lang/is.json" => array("915", "5e7a748872f8b468a6a8a74bb41b57c3"), + "plugins/UsersManager/lang/it.json" => array("6459", "eb3178db7e5a06bcb218ce810c166925"), + "plugins/UsersManager/lang/ja.json" => array("7650", "5d324b04def21e6b338e9cb3445ae143"), + "plugins/UsersManager/lang/ka.json" => array("7783", "6cba383ab0dec6d60bcfe5a2e4d2cdf9"), + "plugins/UsersManager/lang/ko.json" => array("7182", "9a5e7e94aadb15f959b79d4e832d3787"), + "plugins/UsersManager/lang/lt.json" => array("4352", "f1fc1013e8dc3e51b5b7f991ad3e8270"), + "plugins/UsersManager/lang/lv.json" => array("3518", "aa74674018fce7f0ffc906377c7564c8"), + "plugins/UsersManager/lang/nb.json" => array("6035", "76c94a91f96c81ca4c24534524dc9249"), + "plugins/UsersManager/lang/nl.json" => array("4963", "440e1d01c6f56bff5a9bee31b15387c8"), + "plugins/UsersManager/lang/nn.json" => array("2846", "cfd8265eb96cb596b19602f72e28dc9e"), + "plugins/UsersManager/lang/pl.json" => array("4662", "90f8bc467d5af79ce2b6ca7cf98b0a2c"), + "plugins/UsersManager/lang/pt-br.json" => array("7815", "325dc7f1083cbf4351a90716c215bbea"), + "plugins/UsersManager/lang/pt.json" => array("4070", "4ff409b51db56fd59da780a57edf9241"), + "plugins/UsersManager/lang/ro.json" => array("6137", "a268fc98779cac49f0c5bb6b9de93bf4"), + "plugins/UsersManager/lang/ru.json" => array("9243", "25e74abf9d744eaba3614833a2353e43"), + "plugins/UsersManager/lang/sk.json" => array("2058", "0817dad62b9feb5288845797e0ba2ec3"), + "plugins/UsersManager/lang/sl.json" => array("1759", "212508815037820774d081cbb79e7db7"), + "plugins/UsersManager/lang/sq.json" => array("6490", "d47bb1f909c1961cffd2f9313df6410e"), + "plugins/UsersManager/lang/sr.json" => array("6034", "39199015c046fa4d7b93e3d0c4f3d3a3"), + "plugins/UsersManager/lang/sv.json" => array("7189", "7c4f2a41ac73e9615fc0ff32dee994a8"), + "plugins/UsersManager/lang/ta.json" => array("190", "9979b6abfcecefa85689b816a60ae616"), + "plugins/UsersManager/lang/te.json" => array("1105", "367dac75ecf70e15604315645e3da89a"), + "plugins/UsersManager/lang/th.json" => array("5669", "bbb513ec7fdb801efe50d3b6f730bde9"), + "plugins/UsersManager/lang/tl.json" => array("6424", "5e221e8da6a1d013d8524e9e5801cbab"), + "plugins/UsersManager/lang/tr.json" => array("2402", "02c132faa99d618a4427e209d9b754cd"), + "plugins/UsersManager/lang/uk.json" => array("4866", "0d4b98dd7920a8cb476cffd8cb71f4fd"), + "plugins/UsersManager/lang/vi.json" => array("5089", "f7abdc3c84dc642a5284352076c05504"), + "plugins/UsersManager/lang/zh-cn.json" => array("3647", "91f9e787e6ffb9ac6ffc2c2e154d0821"), + "plugins/UsersManager/lang/zh-tw.json" => array("5457", "18cb02cac0bd4c120f53cd729da85dd9"), + "plugins/UsersManager/LastSeenTimeLogger.php" => array("2242", "3b6e4ff03a54a9bf8d9cf9a6ce041b41"), + "plugins/UsersManager/Menu.php" => array("1010", "5e086c12e6bc6bb495b7099998c6430b"), + "plugins/UsersManager/Model.php" => array("9600", "ad998674846ffad441089d2e68c36fe4"), + "plugins/UsersManager/stylesheets/usersManager.less" => array("956", "054253daacd3aab5b21ef4e58ab537a5"), + "plugins/UsersManager/Tasks.php" => array("1509", "702658103000bd2464c3ed624d561816"), + "plugins/UsersManager/templates/anonymousSettings.twig" => array("3393", "c28fdcd11fc0cd0e7b3c1aa8f0f1c503"), + "plugins/UsersManager/templates/index.twig" => array("10810", "33285385f311ba9b04aec43df9dec955"), + "plugins/UsersManager/templates/noWebsiteAdminAccess.twig" => array("180", "0c0214158072b04b1d14c52b22bd434e"), + "plugins/UsersManager/templates/userSettings.twig" => array("5912", "93ea4ccd70a4caf66b5d94b7e44d1e93"), + "plugins/UsersManager/UserAccessFilter.php" => array("6105", "91cc7237150225eba83680096476e3c3"), + "plugins/UsersManager/UserPreferences.php" => array("4781", "0047722509afe73752d470ba80ceb2cc"), + "plugins/UsersManager/UsersManager.php" => array("6130", "02b42bcc2c4c72af584b622d1115cd69"), + "plugins/VisitFrequency/API.php" => array("2494", "6c28ba5bf7257a4e6221680b2ef1ee7e"), + "plugins/VisitFrequency/Columns/Metrics/ReturningMetric.php" => array("1719", "8e84485b2fea401070935da2a3f49e8c"), + "plugins/VisitFrequency/Controller.php" => array("4705", "8ee641b7023c537354660da86c13fab1"), + "plugins/VisitFrequency/lang/am.json" => array("798", "a9859a73bc78617ffd803e95bcd2ff79"), + "plugins/VisitFrequency/lang/ar.json" => array("1198", "deb18579c7dfaf65ffa0ef33f949ec35"), + "plugins/VisitFrequency/lang/be.json" => array("1799", "bdbdcdc6d183ef9a522ee597a5d031de"), + "plugins/VisitFrequency/lang/bg.json" => array("2463", "3f9fcb66521f74ee65a9546a6ba14d1d"), + "plugins/VisitFrequency/lang/ca.json" => array("1749", "23e895f0e37c0fee0ecfe34396fe783d"), + "plugins/VisitFrequency/lang/cs.json" => array("1993", "3957b6f8acf88374ec950a67e871a082"), + "plugins/VisitFrequency/lang/da.json" => array("1942", "c2a832ff46010fa5a0170f3f88b039f6"), + "plugins/VisitFrequency/lang/de.json" => array("1955", "459e05fea9a2c5d3594c5fa31cc448ee"), + "plugins/VisitFrequency/lang/el.json" => array("3063", "6ae86fc68cf1cef613f4b6e9a67d7cd6"), + "plugins/VisitFrequency/lang/en.json" => array("1760", "3b9f1cd6153315a58bc87344ac44dee4"), + "plugins/VisitFrequency/lang/es.json" => array("2009", "6f8c86c7d432d4a97b3ee5e66b9daebe"), + "plugins/VisitFrequency/lang/et.json" => array("1490", "4b1338d3cc516798e8bf61d4f3ba20d4"), + "plugins/VisitFrequency/lang/eu.json" => array("629", "80d25672d23f99900d600e74c41a9dae"), + "plugins/VisitFrequency/lang/fa.json" => array("2051", "fc7d6db8e3938727a5eb7001ad53b41e"), + "plugins/VisitFrequency/lang/fi.json" => array("1719", "f8aca2bab1f30235a50a390ba6e1a37a"), + "plugins/VisitFrequency/lang/fr.json" => array("1924", "82a428868de4deb20037fc025bc21202"), + "plugins/VisitFrequency/lang/gl.json" => array("330", "6045c8bd81c8d43858e3dc36af5bed75"), + "plugins/VisitFrequency/lang/he.json" => array("1370", "dfda4eef55185249389951ef98e40761"), + "plugins/VisitFrequency/lang/hi.json" => array("1929", "c2c8ea8a22ce24a4fdf31a80a358eb7e"), + "plugins/VisitFrequency/lang/hu.json" => array("2108", "112b6f75cfa92a5c54aaf193fa6723dc"), + "plugins/VisitFrequency/lang/id.json" => array("1802", "0a6ccab9fdc91c9bd56cca673637a2f1"), + "plugins/VisitFrequency/lang/is.json" => array("917", "e68a026be89ee9311b72cf05c12be207"), + "plugins/VisitFrequency/lang/it.json" => array("1895", "76b7b17ec1ad9998de6e8fc953c04a3d"), + "plugins/VisitFrequency/lang/ja.json" => array("2067", "164e5522f1eadc10b3fbe30194e85039"), + "plugins/VisitFrequency/lang/ka.json" => array("1861", "1e4b03a089f4027d2873d22e09d986ea"), + "plugins/VisitFrequency/lang/ko.json" => array("1754", "654f966b91026e065ec36f65fe29b150"), + "plugins/VisitFrequency/lang/lt.json" => array("1056", "2682b7818b5d94480549d4d5994fa6ad"), + "plugins/VisitFrequency/lang/lv.json" => array("346", "d17722b45616b503c30f7911fbb50162"), + "plugins/VisitFrequency/lang/nb.json" => array("1905", "42c2e3b332d4fc4ccbfb7f3c1d34b748"), + "plugins/VisitFrequency/lang/nl.json" => array("1897", "f827574fe70366591b3a789de8b0bdff"), + "plugins/VisitFrequency/lang/nn.json" => array("831", "019470ce322dd67ec38f8eb8bc1f488f"), + "plugins/VisitFrequency/lang/pl.json" => array("1404", "b24df0deddc51993113ccec9b8a14c04"), + "plugins/VisitFrequency/lang/pt-br.json" => array("1921", "7a6a045393dc746ee1f5ee036ede8643"), + "plugins/VisitFrequency/lang/pt.json" => array("1745", "30b8356585dbef8c1dd66a109fcd46e3"), + "plugins/VisitFrequency/lang/ro.json" => array("1694", "84af23d7f4010aa88a61af2e40aa32fb"), + "plugins/VisitFrequency/lang/ru.json" => array("2579", "fe177dd25410bad14a8196a7e95d92e3"), + "plugins/VisitFrequency/lang/sk.json" => array("1063", "2b50df44f0ac54a815a8d106119259b0"), + "plugins/VisitFrequency/lang/sl.json" => array("676", "379c4fbeec8ed70ce48ffa21da5dda48"), + "plugins/VisitFrequency/lang/sq.json" => array("1915", "be4cc1fe0b79dab9eef965b8df432145"), + "plugins/VisitFrequency/lang/sr.json" => array("1862", "0b1507255059863347122e5480cee4ab"), + "plugins/VisitFrequency/lang/sv.json" => array("1914", "8877825e6b1f1d402f101d3d8f36c0bb"), + "plugins/VisitFrequency/lang/te.json" => array("162", "0dd6ceeec79a95fda3f32b77df471a2d"), + "plugins/VisitFrequency/lang/th.json" => array("1755", "2bd6b9a79fc4634d6b1f9f1fb5f2ef2b"), + "plugins/VisitFrequency/lang/tl.json" => array("1883", "7fbd1b7a02b97b60d44c677a6cd3763b"), + "plugins/VisitFrequency/lang/uk.json" => array("1452", "5548d05e52df69d1e56e60d678ed1b63"), + "plugins/VisitFrequency/lang/vi.json" => array("2121", "3106558cabcc4f39f1b6d2a6308087f6"), + "plugins/VisitFrequency/lang/zh-cn.json" => array("1449", "fe61a6fea25c19789e69f5206df5a248"), + "plugins/VisitFrequency/lang/zh-tw.json" => array("912", "253a1b8535ae50b253c6f657f8b49872"), + "plugins/VisitFrequency/Menu.php" => array("456", "f7c343844315ced9fd4e3044e56a504c"), + "plugins/VisitFrequency/Reports/Get.php" => array("1233", "eeffeef5d65fa53e6d8cda49b943f131"), "plugins/VisitFrequency/templates/getSparklines.twig" => array("48", "1d2ec22a548fb6662979f19d9d3fc329"), "plugins/VisitFrequency/templates/index.twig" => array("338", "3eca543636e9b14ecb0208b6e03bf283"), - "plugins/VisitFrequency/templates/_sparklines.twig" => array("1064", "09eecc1ac6d13181fbec549185cacb88"), - "plugins/VisitFrequency/VisitFrequency.php" => array("2625", "84fd90736051331df11ca37a64917cea"), - "plugins/VisitorInterest/API.php" => array("4802", "3ea0c6255ab8ebb2deb7c600e300460a"), - "plugins/VisitorInterest/Archiver.php" => array("5175", "66968c131f5b948ecb49dbf5bdb4fc5f"), - "plugins/VisitorInterest/Controller.php" => array("1730", "99828a062bfe59a5dccafa741e001bff"), - "plugins/VisitorInterest/templates/index.twig" => array("533", "5f1579ad5941e520b87275aff6cbb729"), - "plugins/VisitorInterest/VisitorInterest.php" => array("10661", "d019fd8854d7087dd4948f230f6ae158"), - "plugins/VisitsSummary/API.php" => array("6176", "0780ead1ba4050b502a18c37f8c0f322"), - "plugins/VisitsSummary/Controller.php" => array("8277", "31b7cdca8c5b9293af2e2eeedf6adf60"), - "plugins/VisitsSummary/stylesheets/datatable.less" => array("113", "8ffed702781ee6746a4701fc5f5ff9f1"), + "plugins/VisitFrequency/templates/_sparklines.twig" => array("1487", "3ce7ebe07cfe9337971db098cc135df1"), + "plugins/VisitFrequency/VisitFrequency.php" => array("1397", "e47b957afe5e64e6d0b54f3adacd8860"), + "plugins/VisitFrequency/Widgets.php" => array("594", "7646664109f21ffc118b2ce85c66a7df"), + "plugins/VisitorInterest/API.php" => array("3958", "9137dfafe816d1608f20528ff2c93cef"), + "plugins/VisitorInterest/Archiver.php" => array("5527", "060776e5305e07c01c01e051945ead09"), + "plugins/VisitorInterest/Columns/PagesPerVisit.php" => array("408", "fa0253477d763a94a9cb1aaade3d6a4b"), + "plugins/VisitorInterest/Columns/VisitDuration.php" => array("408", "7eac5c276a814fc60094d09edaaf7278"), + "plugins/VisitorInterest/Columns/VisitsByDaysSinceLastVisit.php" => array("1234", "29760577f0c11fa637004fc340c776c8"), + "plugins/VisitorInterest/Columns/VisitsbyVisitNumber.php" => array("413", "7d1b07dbd69cbc756b29b918496af7e5"), + "plugins/VisitorInterest/Controller.php" => array("822", "1485acb7828a011fd838c6ef569abb5b"), + "plugins/VisitorInterest/lang/am.json" => array("498", "afdfea0e933b04482bc373c006f13a65"), + "plugins/VisitorInterest/lang/ar.json" => array("637", "1f1d823648ba3f171cd0ff6f586d8cbf"), + "plugins/VisitorInterest/lang/be.json" => array("1643", "0181fe9941e495bfce160b5bdf4ebe17"), + "plugins/VisitorInterest/lang/bg.json" => array("2820", "a5e9d04df1857d75aaef21225f5ede79"), + "plugins/VisitorInterest/lang/ca.json" => array("1672", "529d1689bc1819f0b3c82515a5fa3e6c"), + "plugins/VisitorInterest/lang/cs.json" => array("1824", "8eceff8a5e140e0b7169203fcfe42ecf"), + "plugins/VisitorInterest/lang/da.json" => array("1532", "fc4c9a91ac394fe24e0904a33a5502cd"), + "plugins/VisitorInterest/lang/de.json" => array("1843", "14a32a4855c62d0fcabc1289d03defcf"), + "plugins/VisitorInterest/lang/el.json" => array("2995", "f181eb3e7e3e2871608bbe3652d01ccf"), + "plugins/VisitorInterest/lang/en.json" => array("1692", "2a8f5210478e60165901708989446c2f"), + "plugins/VisitorInterest/lang/es.json" => array("1917", "544493024d3e7dccd2b3d5badeb08c9d"), + "plugins/VisitorInterest/lang/et.json" => array("853", "31c91bcdd93c6aa3fda8b7580e24ee4c"), + "plugins/VisitorInterest/lang/eu.json" => array("399", "ff5a86f49b98ba62ed463a30eeb5d483"), + "plugins/VisitorInterest/lang/fa.json" => array("2463", "da4857ff78103b7ecf095968a62680d2"), + "plugins/VisitorInterest/lang/fi.json" => array("1566", "8149e552599cc1aab9bfb202c9633b68"), + "plugins/VisitorInterest/lang/fr.json" => array("1911", "596e512c54b5a210ec2242cc89afa56f"), + "plugins/VisitorInterest/lang/gl.json" => array("265", "10d2e5e5891b97f278cf016e7a9f8e2c"), + "plugins/VisitorInterest/lang/he.json" => array("914", "0806ed577bfb934546a2b68b0d149bc1"), + "plugins/VisitorInterest/lang/hi.json" => array("1458", "f94b3777b165e7bdae161992c82127b7"), + "plugins/VisitorInterest/lang/hu.json" => array("576", "4814e4f7c51de86440335075b9db9ade"), + "plugins/VisitorInterest/lang/id.json" => array("1723", "acbb1cf16f7d671139105972e4d3ac44"), + "plugins/VisitorInterest/lang/is.json" => array("594", "9c997e535f53fd53c847d06a2d063484"), + "plugins/VisitorInterest/lang/it.json" => array("1888", "554aa06794f2253ea5640a54daf3d46b"), + "plugins/VisitorInterest/lang/ja.json" => array("1997", "2c8b553d60095a8a207732db7c2e48ad"), + "plugins/VisitorInterest/lang/ka.json" => array("907", "fe850779ddceb12f5f06ff2ec9a92474"), + "plugins/VisitorInterest/lang/ko.json" => array("1791", "a350eee566fc2170cde9ed969c7bc5fa"), + "plugins/VisitorInterest/lang/lt.json" => array("591", "defa79e7db518b0f02b47e689b7e7be4"), + "plugins/VisitorInterest/lang/lv.json" => array("629", "91a3ec187fd616b4f2aa21b97c38705e"), + "plugins/VisitorInterest/lang/nb.json" => array("522", "f00612c03981b13c275da2039bd43817"), + "plugins/VisitorInterest/lang/nl.json" => array("1796", "a6d9331c4d6abcce288a9d13a3fc3559"), + "plugins/VisitorInterest/lang/nn.json" => array("441", "3007e17d0f9e356c470b9722519d5b6a"), + "plugins/VisitorInterest/lang/pl.json" => array("829", "2776307a742aa80afc6b1b344986c2a0"), + "plugins/VisitorInterest/lang/pt-br.json" => array("1874", "1e53ad464d172a9dbaecf64fb8fcbf0c"), + "plugins/VisitorInterest/lang/pt.json" => array("1625", "2b18a3b066e37320b3dae8559c6e2aa4"), + "plugins/VisitorInterest/lang/ro.json" => array("1863", "c2f2419c3231615ad20080c6f533ca58"), + "plugins/VisitorInterest/lang/ru.json" => array("2618", "a707d8056b92b5b394c2b6285e2516f4"), + "plugins/VisitorInterest/lang/sk.json" => array("562", "910500aaf48cdfddf1590d92b8540f07"), + "plugins/VisitorInterest/lang/sl.json" => array("414", "6baaefa875ac81abf3ff4dd7ff48e764"), + "plugins/VisitorInterest/lang/sq.json" => array("1874", "94b583a751564bc1e4629df2feb00533"), + "plugins/VisitorInterest/lang/sr.json" => array("1782", "6c7b51cf03c3413c23ed47600445f55a"), + "plugins/VisitorInterest/lang/sv.json" => array("1546", "48a62afa1ca5bb629e3848a7b451956f"), + "plugins/VisitorInterest/lang/te.json" => array("716", "36f8b9dda75a02a390ee8423dbdd4e38"), + "plugins/VisitorInterest/lang/th.json" => array("806", "55256a306457753ce0ea351ab00a02e6"), + "plugins/VisitorInterest/lang/tl.json" => array("1749", "66b1a2039ebc58011f4c8f329c7626a2"), + "plugins/VisitorInterest/lang/tr.json" => array("77", "49a918373e5e6ee2107f1cbb6411910e"), + "plugins/VisitorInterest/lang/uk.json" => array("768", "fae5267e3a6056c21927ade3e315a3ff"), + "plugins/VisitorInterest/lang/vi.json" => array("2107", "66fe048c34313d5ee8da4de1bc5ceac7"), + "plugins/VisitorInterest/lang/zh-cn.json" => array("1564", "ebdad05e43f93969d7cb252b1f6b55e8"), + "plugins/VisitorInterest/lang/zh-tw.json" => array("566", "51bc1ac32fb46ada56e03c7e432dd3bb"), + "plugins/VisitorInterest/Menu.php" => array("497", "0452125d2d3ab8282525e16440a3947e"), + "plugins/VisitorInterest/Reports/Base.php" => array("348", "7f833e7b0b4cb002b23b795c51dd9732"), + "plugins/VisitorInterest/Reports/GetNumberOfVisitsByDaysSinceLast.php" => array("1751", "5d84b654a0706b5c7bf837b45bd59c8b"), + "plugins/VisitorInterest/Reports/GetNumberOfVisitsByVisitCount.php" => array("2149", "8be0eda6024ed40c5c71c06ef16247f2"), + "plugins/VisitorInterest/Reports/GetNumberOfVisitsPerPage.php" => array("2191", "a2f0e5ba10fbc497a5ba5c4e0cd5e4ee"), + "plugins/VisitorInterest/Reports/GetNumberOfVisitsPerVisitDuration.php" => array("2178", "715febeef8de9ecf5ceaeaa44c0d4a17"), + "plugins/VisitorInterest/templates/index.twig" => array("797", "b156d17107b581ab0561a093258faf9e"), + "plugins/VisitorInterest/VisitorInterest.php" => array("1154", "fc2248cc0407547b7db88d6ec52684ab"), + "plugins/VisitsSummary/API.php" => array("5049", "214a3706209f84e1f9c4b76ee3482079"), + "plugins/VisitsSummary/Controller.php" => array("9868", "7423407f657042195d81f78457ec6569"), + "plugins/VisitsSummary/lang/am.json" => array("683", "95bb188cae7671973785a03cd7141b41"), + "plugins/VisitsSummary/lang/ar.json" => array("999", "d0469f57660ddc2e53a4643d2a6d9edc"), + "plugins/VisitsSummary/lang/be.json" => array("1197", "316cd998746f6b5d49bdd616f8c4710f"), + "plugins/VisitsSummary/lang/bg.json" => array("1998", "afca35714f91a922d3e9918dc7d667b7"), + "plugins/VisitsSummary/lang/ca.json" => array("1459", "bb74dfabbddfe36a3266035c42f238bb"), + "plugins/VisitsSummary/lang/cs.json" => array("1681", "d45c2e323bcad9d70ebe8e8def9b29b1"), + "plugins/VisitsSummary/lang/da.json" => array("1511", "ee3363348f80c06fbcc04efe39a7cfa3"), + "plugins/VisitsSummary/lang/de.json" => array("1636", "944f6b097be7bc032bc49067583ac6e0"), + "plugins/VisitsSummary/lang/el.json" => array("2284", "ab5ac17a7425d9ca6b32ebc6b269027b"), + "plugins/VisitsSummary/lang/en.json" => array("1487", "59e85705ac628c7cc62600e029c03875"), + "plugins/VisitsSummary/lang/es.json" => array("1724", "8ccec3ba38e4a6cc24983f879af374d0"), + "plugins/VisitsSummary/lang/et.json" => array("1498", "b68ca41e08e12cad7f5fabae0102f3b4"), + "plugins/VisitsSummary/lang/eu.json" => array("570", "30a19c808a0900e8b2224957c64fb53c"), + "plugins/VisitsSummary/lang/fa.json" => array("1758", "bacdc64912aa3ed0f29cd65b86e9bfa6"), + "plugins/VisitsSummary/lang/fi.json" => array("1435", "74c29d5a69da3a9e71ffa6868d762caf"), + "plugins/VisitsSummary/lang/fr.json" => array("1605", "a0626a91ae4c36296fd884c5b254a953"), + "plugins/VisitsSummary/lang/gl.json" => array("430", "3c44a9ce700a5eba98bd899135907019"), + "plugins/VisitsSummary/lang/he.json" => array("1671", "748f3e58ef11f1ab392ca15f1327fe55"), + "plugins/VisitsSummary/lang/hi.json" => array("1332", "1fa9f07ca3ef6ad2dd9f6a8d651f8911"), + "plugins/VisitsSummary/lang/hr.json" => array("232", "10a6841c24a56e63761167d7aba590ad"), + "plugins/VisitsSummary/lang/hu.json" => array("930", "7db40333dfc4f7beab9fda4d1f5ae0f4"), + "plugins/VisitsSummary/lang/id.json" => array("1430", "26ccbec8c76f7112a898d6c3e53aa92c"), + "plugins/VisitsSummary/lang/is.json" => array("808", "36f485cfbeb59de812b4955a8f8ea4d8"), + "plugins/VisitsSummary/lang/it.json" => array("1587", "e4ba42096a5e91905a20374bd947d84a"), + "plugins/VisitsSummary/lang/ja.json" => array("1725", "72680af67935ac523352cca11cfc386e"), + "plugins/VisitsSummary/lang/ka.json" => array("1396", "e18109b461fb37ed5e25ee2cf3b33b35"), + "plugins/VisitsSummary/lang/ko.json" => array("1421", "40029cb612d69f30d5caef9042f7901f"), + "plugins/VisitsSummary/lang/lt.json" => array("831", "1aa05ed183d3cc0e092ab5f9c38142de"), + "plugins/VisitsSummary/lang/lv.json" => array("869", "1566193268254c5ac4ea525de8e074ad"), + "plugins/VisitsSummary/lang/nb.json" => array("1544", "17c8f53a35093a031614225774262d0a"), + "plugins/VisitsSummary/lang/nl.json" => array("1462", "c520e3ab809e31ab2a7e5dae337bb3d5"), + "plugins/VisitsSummary/lang/nn.json" => array("491", "46f1d9417854a76b9e73313ca7805d2a"), + "plugins/VisitsSummary/lang/pl.json" => array("1362", "fc72929ab84f35a06d4cf972bf5203af"), + "plugins/VisitsSummary/lang/pt-br.json" => array("1677", "265b2ca1de10d0f4660e62c8e8965017"), + "plugins/VisitsSummary/lang/pt.json" => array("1284", "10cc9e895c9df899b2f593886221294a"), + "plugins/VisitsSummary/lang/ro.json" => array("1441", "8e2ecc5c23fc7e11dff151fef38e672e"), + "plugins/VisitsSummary/lang/ru.json" => array("1945", "cfa4de675f6c6fbdca14b3dce0839d17"), + "plugins/VisitsSummary/lang/sk.json" => array("1423", "6532d18470ca616282efd9306acfb2c0"), + "plugins/VisitsSummary/lang/sl.json" => array("1520", "9fb2d68168c96f0119664325da2911d6"), + "plugins/VisitsSummary/lang/sq.json" => array("1621", "4ce588f7382a41d2bed48102f584cfe7"), + "plugins/VisitsSummary/lang/sr.json" => array("1567", "d9cd3917d83d90c375350c551c0c00b7"), + "plugins/VisitsSummary/lang/sv.json" => array("1353", "b6507b7a0bd88d1abb8a60a6573e905f"), + "plugins/VisitsSummary/lang/te.json" => array("307", "1944e2cb1637e070151edce17324fb6f"), + "plugins/VisitsSummary/lang/th.json" => array("1977", "d3ae23b81f246c720c628968ddf3aa27"), + "plugins/VisitsSummary/lang/tl.json" => array("1560", "56ce03bca820bae83fe6ce20e1653db6"), + "plugins/VisitsSummary/lang/tr.json" => array("847", "97da6cae97b07bf589d81b2b36a96c64"), + "plugins/VisitsSummary/lang/uk.json" => array("1200", "d554426c5ea139b9966e8c915cf38d69"), + "plugins/VisitsSummary/lang/vi.json" => array("1700", "daa62d16b28e8d3ca5c2752a05e3765c"), + "plugins/VisitsSummary/lang/zh-cn.json" => array("1407", "ec61165828183621eb9f5a55f9c1bb4a"), + "plugins/VisitsSummary/lang/zh-tw.json" => array("733", "598ade1ce62708d54fcf5a5b5860625d"), + "plugins/VisitsSummary/Menu.php" => array("506", "0710e47aeb8b3c349b9ea40030da7ca8"), + "plugins/VisitsSummary/Reports/Get.php" => array("2366", "4e6c3c79f9e717c6b317e6fa75e404cd"), + "plugins/VisitsSummary/stylesheets/datatable.less" => array("151", "a57b5fd08b0941f5005128ba0551cd69"), "plugins/VisitsSummary/templates/getSparklines.twig" => array("47", "f8630d90a9c3ab9ba0d5929d537ec5a1"), - "plugins/VisitsSummary/templates/index.twig" => array("634", "6ff7b1f46d57ca9e0a4b7e6293ffa81c"), - "plugins/VisitsSummary/templates/_sparklines.twig" => array("3505", "6f587c7c41c00b990065c5cde5a505cd"), - "plugins/VisitsSummary/VisitsSummary.php" => array("2864", "820091576b5aaef9873de6b4d1a8ad43"), - "plugins/VisitTime/API.php" => array("5806", "04226b406a95196b3ac7d653cb79b814"), - "plugins/VisitTime/Archiver.php" => array("2924", "cb73804d891689ebc1a79cf3ff6d6e2a"), - "plugins/VisitTime/Controller.php" => array("960", "6dce46cb47d8892f3e96e13d606b2f99"), - "plugins/VisitTime/functions.php" => array("800", "02b598cca67def3060e538d359da9c76"), - "plugins/VisitTime/templates/index.twig" => array("316", "483e2fc50e9e905201576c1638326782"), - "plugins/VisitTime/VisitTime.php" => array("9503", "867e6b57fb7e4f64219471c6f4ffe6b1"), - "plugins/Widgetize/Controller.php" => array("2404", "0d55e61621a887b086c60d086b66a271"), - "plugins/Widgetize/javascripts/widgetize.js" => array("3815", "b142cb72e9609949b895f517f1f19271"), - "plugins/Widgetize/stylesheets/widgetize.less" => array("523", "92ccef42a032893e61115dc1804a9d64"), + "plugins/VisitsSummary/templates/index.twig" => array("349", "5cbe6df89771d9549db8faafb746bad8"), + "plugins/VisitsSummary/templates/_sparklines.twig" => array("4398", "53c6da3a10a869c566cdb779c8bc971f"), + "plugins/VisitsSummary/VisitsSummary.php" => array("2145", "5a1ee76b23a9ce0b9601f59bcd6b9fea"), + "plugins/VisitsSummary/Widgets.php" => array("608", "2d6e35f428d1e5280d161650ae20bdc5"), + "plugins/VisitTime/API.php" => array("6074", "010ae0b7781b783bdce3a52ac4ab74cd"), + "plugins/VisitTime/Archiver.php" => array("3276", "9b987a4345d50fe02080f425fb474eae"), + "plugins/VisitTime/Columns/DayOfTheWeek.php" => array("385", "59e81797ab2a10eaf11a0349c96b34ec"), + "plugins/VisitTime/Columns/LocalTime.php" => array("1255", "8966f16bd24d3713fb365e8099398e7a"), + "plugins/VisitTime/Columns/ServerTime.php" => array("881", "eef4020f76634e51531077331c423936"), + "plugins/VisitTime/Controller.php" => array("616", "d32dcd20b20c59052a3263a05d61bc8e"), + "plugins/VisitTime/DataTable/Filter/AddSegmentByLabelInUTC.php" => array("1615", "ab85eb8bedf769a5657350caa617ccb6"), + "plugins/VisitTime/functions.php" => array("814", "1a1e626c952c35280b4597e4fa698d9d"), + "plugins/VisitTime/lang/am.json" => array("482", "1eaa48e10fd46aa9141202948faac467"), + "plugins/VisitTime/lang/ar.json" => array("518", "0fb1bbda44532b3e9856e17b12133564"), + "plugins/VisitTime/lang/be.json" => array("919", "15986698eb18b2e9e087139221906d3e"), + "plugins/VisitTime/lang/bg.json" => array("1330", "829c0b77f6aaf1d27ebd99feedd8f36d"), + "plugins/VisitTime/lang/ca.json" => array("1078", "7f429c772c0649e401558a44f5b78d8b"), + "plugins/VisitTime/lang/cs.json" => array("1061", "edd403c4231200553b316645097bfd48"), + "plugins/VisitTime/lang/da.json" => array("823", "d3e939b8bc7091225ea1485938d8564d"), + "plugins/VisitTime/lang/de.json" => array("1022", "e3616fbd13084f5b93ad01450c3e5c64"), + "plugins/VisitTime/lang/el.json" => array("1628", "bcd424c580d590c3e2c3032c33719a3f"), + "plugins/VisitTime/lang/en.json" => array("980", "de0fb9e0b33a27416c14726a3770f1d7"), + "plugins/VisitTime/lang/es.json" => array("1098", "0ef15eaa7c146ba28c5b40ec923a11d8"), + "plugins/VisitTime/lang/et.json" => array("535", "3662f0859e1af6708fb219cb8d3150c2"), + "plugins/VisitTime/lang/eu.json" => array("393", "9f5ecbb6d22606ea3c2d6023e8ad2972"), + "plugins/VisitTime/lang/fa.json" => array("791", "8764d8a0acc845389625bf2afd70ed1c"), + "plugins/VisitTime/lang/fi.json" => array("917", "9796ae4df82ea679b491e14e6e8dead6"), + "plugins/VisitTime/lang/fr.json" => array("1094", "8a3bb592a1f712437ef2b1ce7b5cdc7d"), + "plugins/VisitTime/lang/gl.json" => array("275", "e6d4286f68a37aa7d7f1d199d720c820"), + "plugins/VisitTime/lang/he.json" => array("697", "280fd4ba5d51b4444de2fef21ce2090e"), + "plugins/VisitTime/lang/hi.json" => array("965", "9aed89a9b6414c25df1948a2aaa47720"), + "plugins/VisitTime/lang/hr.json" => array("184", "e9e7516866e20384b70cb593c2786fed"), + "plugins/VisitTime/lang/hu.json" => array("399", "65e91f558b76fb802a09e499fb67d3ac"), + "plugins/VisitTime/lang/id.json" => array("936", "ac2c49496871e94abb959d8fe806d54b"), + "plugins/VisitTime/lang/is.json" => array("434", "3518d51dbd6dea8bcf57d24bd57c6dd2"), + "plugins/VisitTime/lang/it.json" => array("1035", "bb65167c969ed9a9fc78b41f0ce5a0ee"), + "plugins/VisitTime/lang/ja.json" => array("1129", "ecf14c867fe3ba60956f55132cb5d0ee"), + "plugins/VisitTime/lang/ka.json" => array("638", "84043e3892c979036cf6136b28b3777f"), + "plugins/VisitTime/lang/ko.json" => array("1043", "2387b4ef39343156633025e584a0e637"), + "plugins/VisitTime/lang/lt.json" => array("400", "5c7aa20069a61d497ac8ff4464d0a8fa"), + "plugins/VisitTime/lang/lv.json" => array("356", "8006109a2c5b7d50f331390f14ed7aff"), + "plugins/VisitTime/lang/nb.json" => array("970", "ee4580069107496e616d3327b19877f2"), + "plugins/VisitTime/lang/nl.json" => array("1047", "c9ade79ff3e45553c4b500d847ae196b"), + "plugins/VisitTime/lang/nn.json" => array("370", "f800a45a1be2412d45cc92eb1c5a597a"), + "plugins/VisitTime/lang/pl.json" => array("941", "b8f172d048662f6d488457b43857c594"), + "plugins/VisitTime/lang/pt-br.json" => array("1066", "2a6e717d03ab2df03ecaad894ce81084"), + "plugins/VisitTime/lang/pt.json" => array("671", "c89aea6a09dfb23ab31f54c03e964d66"), + "plugins/VisitTime/lang/ro.json" => array("921", "fcf1f9cc5d77efd77a19108ec99f4afd"), + "plugins/VisitTime/lang/ru.json" => array("1514", "fedada6c82c2df94a7c110056105de9a"), + "plugins/VisitTime/lang/sk.json" => array("1135", "1acb5e15832f250ecdbc2623f2a18ef5"), + "plugins/VisitTime/lang/sl.json" => array("403", "fe14d32caff9b64877d2324fda7b6a06"), + "plugins/VisitTime/lang/sq.json" => array("1062", "29de50a0144b38a0cb8a46862978b160"), + "plugins/VisitTime/lang/sr.json" => array("1004", "c4e88f7a7aee5415848920ea167a5c23"), + "plugins/VisitTime/lang/sv.json" => array("816", "7b08f8edad6e16ebf9047666dad73321"), + "plugins/VisitTime/lang/te.json" => array("121", "292486d6c3dabf467531a70ab9b1ae9e"), + "plugins/VisitTime/lang/th.json" => array("771", "8e708b71f78ca16a1815e23da1e366d5"), + "plugins/VisitTime/lang/tl.json" => array("1025", "1109aba0ca4bd1fb458d29b6efee5acb"), + "plugins/VisitTime/lang/tr.json" => array("482", "4e8e6df7cb01d0bd2f7b1b616470ab55"), + "plugins/VisitTime/lang/uk.json" => array("603", "44e42f732f705f5735aae016eebc0ef7"), + "plugins/VisitTime/lang/vi.json" => array("1165", "92db76916cf5237f390c8e5555897fe9"), + "plugins/VisitTime/lang/zh-cn.json" => array("943", "4e599e4fcc90c513691d34dc94a1182d"), + "plugins/VisitTime/lang/zh-tw.json" => array("457", "5fce2cf23ee4619d236e6769fac4705e"), + "plugins/VisitTime/Menu.php" => array("442", "42932ba63205e7be66f5d2a0922630d8"), + "plugins/VisitTime/Reports/Base.php" => array("1166", "bc120438f994f8c80aa63fda1a4bbc42"), + "plugins/VisitTime/Reports/GetByDayOfWeek.php" => array("2464", "b716571c46f6bf99cc8d1634b96c5fed"), + "plugins/VisitTime/Reports/GetVisitInformationPerLocalTime.php" => array("1660", "bb960f50c1d116f8f562fec276c8cf58"), + "plugins/VisitTime/Reports/GetVisitInformationPerServerTime.php" => array("1395", "d06171b29984d756e1eff0438f23dc40"), + "plugins/VisitTime/Segment.php" => array("364", "2b87bc68bc89dd748fe4c4c1292e9307"), + "plugins/VisitTime/templates/index.twig" => array("376", "282d1db3099e4873ebdb62ff4626230f"), + "plugins/VisitTime/VisitTime.php" => array("770", "6e144b89f92ca6502e37641e0f3a3992"), + "plugins/WebsiteMeasurable/lang/ar.json" => array("287", "9fceb0168b7ae814f3db1c0905b900af"), + "plugins/WebsiteMeasurable/lang/cs.json" => array("234", "b1ca395da313b787c5bd776fe379a806"), + "plugins/WebsiteMeasurable/lang/de.json" => array("241", "a88c319060045f8102b3e778a80db5c0"), + "plugins/WebsiteMeasurable/lang/el.json" => array("318", "056249dba1b5e37cd2350123d3e2402c"), + "plugins/WebsiteMeasurable/lang/en.json" => array("205", "b5b236a0200702e47de4e3442b7688c9"), + "plugins/WebsiteMeasurable/lang/es.json" => array("248", "b55bbe3ed7c7a96ea4ca8b758129300a"), + "plugins/WebsiteMeasurable/lang/fr.json" => array("233", "2b982424900b23ecb41c51a90a344b2c"), + "plugins/WebsiteMeasurable/lang/hi.json" => array("345", "a0308856b388de1cbb6b10b01ce1b0d2"), + "plugins/WebsiteMeasurable/lang/it.json" => array("213", "0996c72c89196bb4a7054de5466d5c33"), + "plugins/WebsiteMeasurable/lang/ja.json" => array("268", "ac4a735ad4dc4b18ebbecc698d5e4032"), + "plugins/WebsiteMeasurable/lang/ko.json" => array("239", "69849b5fc39ec4146d2b75089cd5f513"), + "plugins/WebsiteMeasurable/lang/lt.json" => array("67", "1c7597052fed5f2b6e23d41db0631c1a"), + "plugins/WebsiteMeasurable/lang/nb.json" => array("206", "d7ad676270c449f757d1c5916ecc3f7f"), + "plugins/WebsiteMeasurable/lang/nl.json" => array("224", "c1c6fc151842f4c96a9602129f6ef82b"), + "plugins/WebsiteMeasurable/lang/pt-br.json" => array("218", "9f16ef13cf9ff52276fada1d1e06dac6"), + "plugins/WebsiteMeasurable/lang/sk.json" => array("70", "4d159495da698d8c89c9c2f7d1f4d9cf"), + "plugins/WebsiteMeasurable/lang/sq.json" => array("91", "6971d9b145bbf208a8d722ef0900eae6"), + "plugins/WebsiteMeasurable/lang/sr.json" => array("195", "76be52d2eccc109d33645856c7eb1c0a"), + "plugins/WebsiteMeasurable/lang/sv.json" => array("203", "e5b776a87c0b70317e11626e92b66d00"), + "plugins/WebsiteMeasurable/lang/zh-tw.json" => array("194", "60b9e75d35d0dc739604bb76585d7cb0"), + "plugins/WebsiteMeasurable/plugin.json" => array("115", "7b57e4c193b1dadadf5bbe594e50cb9b"), + "plugins/WebsiteMeasurable/Type.php" => array("617", "305d1866aed3319ac2cbae25eb36f4b3"), + "plugins/WebsiteMeasurable/WebsiteMeasurable.php" => array("248", "5c4838b5efab69a3ccdee2488fecb4cf"), + "plugins/Widgetize/Controller.php" => array("1483", "97ff09cc8ccc84c5d5e431985bf7cec3"), + "plugins/Widgetize/javascripts/widgetize.js" => array("3849", "cc6cb621ea94dcf209dd0f168d6daa8b"), + "plugins/Widgetize/lang/ar.json" => array("91", "2e866e6ca9ac7423ea6dfb3022a0daaf"), + "plugins/Widgetize/lang/be.json" => array("95", "b82c96a69e9e2adb903f801aa0c73bfc"), + "plugins/Widgetize/lang/bg.json" => array("283", "784986c5791da36845e7ebcca2693f47"), + "plugins/Widgetize/lang/bn.json" => array("111", "a0496832d1185c1d7e0f9c08c130c74b"), + "plugins/Widgetize/lang/bs.json" => array("80", "cecce6df714236a71e26e4cf711f536e"), + "plugins/Widgetize/lang/ca.json" => array("219", "c5f9487c34e64886a301938ee0c6994b"), + "plugins/Widgetize/lang/cs.json" => array("341", "ddf3650a718eb72da2a5a582e8515972"), + "plugins/Widgetize/lang/cy.json" => array("83", "244418c080929068ea509c751a7e8245"), + "plugins/Widgetize/lang/da.json" => array("325", "4e5560af41a0473e4279e844fd8f4772"), + "plugins/Widgetize/lang/de.json" => array("343", "6863f2acf4bc08ef4bc302f0c3c3cd79"), + "plugins/Widgetize/lang/el.json" => array("557", "d52daed2944c5f27a95dba781565e2ec"), + "plugins/Widgetize/lang/en.json" => array("299", "ae26c857aef0f39c6e43d6e38b2464e1"), + "plugins/Widgetize/lang/es.json" => array("374", "c636c0581d8331d459103d8355e3a1c4"), + "plugins/Widgetize/lang/et.json" => array("72", "1a12e6d331bff21deda92908466b86c4"), + "plugins/Widgetize/lang/fa.json" => array("103", "50a0eb99db2519d488b134b3ebd60311"), + "plugins/Widgetize/lang/fi.json" => array("187", "62308e80c4d3a738ee0937ce86635c8a"), + "plugins/Widgetize/lang/fr.json" => array("360", "d2b73c5d4f6cd1e7765b88b3938b4046"), + "plugins/Widgetize/lang/gl.json" => array("78", "adb1f2936252409ffb735625a8377acf"), + "plugins/Widgetize/lang/he.json" => array("230", "0646e7104eb5033b71f62b778c28068a"), + "plugins/Widgetize/lang/hi.json" => array("662", "d38065a18769f8c858702b18e4d69635"), + "plugins/Widgetize/lang/hr.json" => array("80", "cecce6df714236a71e26e4cf711f536e"), + "plugins/Widgetize/lang/hu.json" => array("81", "2071513091be50b5ecc6bb11e9c8a033"), + "plugins/Widgetize/lang/id.json" => array("215", "1ea959dffec2c88103b7e544505e7823"), + "plugins/Widgetize/lang/is.json" => array("79", "e6f66da3731e91b279a0814ec7c3a283"), + "plugins/Widgetize/lang/it.json" => array("313", "901d1124af046ff979754723411be4b3"), + "plugins/Widgetize/lang/ja.json" => array("465", "4c9e9db01634528eda2b838beabe9df4"), + "plugins/Widgetize/lang/ka.json" => array("117", "9f5f6cd4c0830bf4d773a05853642f62"), + "plugins/Widgetize/lang/ko.json" => array("238", "bd6d600f45e69e1ccf4f1040b3ceeaa9"), + "plugins/Widgetize/lang/lt.json" => array("81", "43064f72119027d705552d01a907b62f"), + "plugins/Widgetize/lang/lv.json" => array("78", "b1806cc2716a1374c005b6f6c494c7eb"), + "plugins/Widgetize/lang/nb.json" => array("329", "19a21cdd6a1e6f81ffaedb392308f801"), + "plugins/Widgetize/lang/nl.json" => array("309", "6b8baa5e3e9940f6b0f397c1e1351c64"), + "plugins/Widgetize/lang/nn.json" => array("77", "6067d7ac7d157def1d54912902dc2f90"), + "plugins/Widgetize/lang/pl.json" => array("79", "93a8e5b46e7ae26f266f1284022eaac1"), + "plugins/Widgetize/lang/pt-br.json" => array("333", "37cbb10a9150d32b70c30f25970aad9d"), + "plugins/Widgetize/lang/pt.json" => array("78", "6ab16058baa2943b6a88221b2c6431f3"), + "plugins/Widgetize/lang/ro.json" => array("214", "15193282c8563815c59b1a6012e7a63a"), + "plugins/Widgetize/lang/ru.json" => array("501", "ebf6f3713dc00c9dfff91b5ac6f2f16f"), + "plugins/Widgetize/lang/sk.json" => array("79", "f28df735ea65e1c83aa36cfb6d015657"), + "plugins/Widgetize/lang/sl.json" => array("76", "96e61c13660c3168abb8a2b90f5f5f13"), + "plugins/Widgetize/lang/sq.json" => array("81", "1d22364c4ec0de96fa6d875efc8474d9"), + "plugins/Widgetize/lang/sr.json" => array("309", "9590a0a3718f7fdefc6c205abb10bf71"), + "plugins/Widgetize/lang/sv.json" => array("214", "01ad02d3f2bb1c7f14d762043fcfda21"), + "plugins/Widgetize/lang/th.json" => array("112", "775812ca6cf15e1d4f55b34f5ee560a5"), + "plugins/Widgetize/lang/tl.json" => array("213", "cf180efdb48d1665edbca98a68e788a6"), + "plugins/Widgetize/lang/tr.json" => array("76", "785efc82871ca9d3540b8692ce116641"), + "plugins/Widgetize/lang/uk.json" => array("94", "5cb2179f8e7b2e5b5b88f11ecb44ceb9"), + "plugins/Widgetize/lang/vi.json" => array("249", "e04cc5b270885aee0da4b2fe75d9738b"), + "plugins/Widgetize/lang/zh-cn.json" => array("202", "3ad7870c7a3679c88816e99416b4cbe1"), + "plugins/Widgetize/lang/zh-tw.json" => array("140", "b3e480ae12469801fd4e71b5d58f43f2"), + "plugins/Widgetize/Menu.php" => array("566", "3734232593cf3775e43f9e9165c4315e"), + "plugins/Widgetize/stylesheets/widgetize.less" => array("623", "a4404bf0c1fb2b21918d96fbac277c9d"), "plugins/Widgetize/templates/iframe_empty.twig" => array("17", "8a9aa8c925c824c53de88a0feca71be5"), - "plugins/Widgetize/templates/iframe.twig" => array("740", "b0e678fca11277a139e66eeaba321684"), - "plugins/Widgetize/templates/index.twig" => array("3680", "2e7dd9cc68bf352c6a219c989f020b16"), - "plugins/Widgetize/templates/testJsInclude1.twig" => array("662", "be3cc3e650ab96eade1172057d2577dc"), - "plugins/Widgetize/templates/testJsInclude2.twig" => array("709", "429886f9491d07389f73d803a0737a9b"), - "plugins/Widgetize/Widgetize.php" => array("2089", "9f1aa8d49cd9bdc7dd6e42991c8b982a"), - "plugins/Zeitgeist/images/affix-arrow.png" => array("3179", "542723bef4d8a61f3dc217352dc1873a"), - "plugins/Zeitgeist/images/annotations.png" => array("1627", "d8dab24c4203b462aa522642fcdd6947"), - "plugins/Zeitgeist/images/annotations_starred.png" => array("1618", "9fcbf35415857fb2a89435106641b0be"), - "plugins/Zeitgeist/images/arr_r.png" => array("195", "2708a22e4d851aff01d4db4f2fddf1da"), - "plugins/Zeitgeist/images/background-submit.png" => array("1347", "b1822012b50008b3e478d7ae890ead92"), - "plugins/Zeitgeist/images/chart_bar.png" => array("170", "05be19310c2236ac9c62e661bae95989"), - "plugins/Zeitgeist/images/chart_line_edit.png" => array("993", "7d7239985b27ced21c55d6de9810284c"), - "plugins/Zeitgeist/images/chart_pie.png" => array("355", "de043ca76bd7419b20150ceafbcf9312"), - "plugins/Zeitgeist/images/close.png" => array("1089", "47fc9f47a998804a1cb4e5728377a1fe"), - "plugins/Zeitgeist/images/collapsed_arrows.gif" => array("54", "224b095cbca536e579119a47328badda"), - "plugins/Zeitgeist/images/configure-highlight.png" => array("933", "a975c7cf6f594786581d997c1b2cc485"), - "plugins/Zeitgeist/images/configure.png" => array("387", "1e0d61849e3012c025554edd97f9cdd8"), - "plugins/Zeitgeist/images/dashboard_h_bg_hover.png" => array("333", "d062d8b2b65f012a196878c2f05db335"), - "plugins/Zeitgeist/images/dashboard_h_bg.png" => array("162", "a6cf0f7cdf6d69edca6edf479aa888e3"), - "plugins/Zeitgeist/images/data_table_footer_active_item.png" => array("145", "70dcc41079a073d03fee2628dcd0cf74"), - "plugins/Zeitgeist/images/datepicker_arr_l.png" => array("191", "10c37d1f62b0c21bc99ef3c3fabe4f7e"), - "plugins/Zeitgeist/images/datepicker_arr_r.png" => array("196", "e2f8672222d50df21b718654c424dd5f"), - "plugins/Zeitgeist/images/delete.png" => array("2175", "b3b9cb547a0511ff15b5371b122a6f66"), - "plugins/Zeitgeist/images/download.png" => array("734", "0552d1746701df879d14c2fdf3d5ac41"), - "plugins/Zeitgeist/images/ecommerceAbandonedCart.gif" => array("369", "51974d5002afd9b7c8009d14a1207aad"), - "plugins/Zeitgeist/images/ecommerceOrder.gif" => array("570", "b0c1aa6141f0047b4bcd0cc665c945ad"), - "plugins/Zeitgeist/images/email.png" => array("754", "baaa6accd945fcb4480b29ab2e15bded"), - "plugins/Zeitgeist/images/error_medium.png" => array("2622", "d789ca042860b20782cfb8c2bfd458e0"), - "plugins/Zeitgeist/images/error.png" => array("1150", "16ac5f1c769e78a074144f1fe9073dfb"), - "plugins/Zeitgeist/images/event.png" => array("164", "8e1d701795486cdc53cbf7a5c3b4d069"), - "plugins/Zeitgeist/images/expanded_arrows.gif" => array("60", "a9afa92168dbbe6f693f3ff71fa27b32"), - "plugins/Zeitgeist/images/export.png" => array("219", "bfcbff8f64765ec78685d04283857b9b"), - "plugins/Zeitgeist/images/feed.png" => array("691", "55bc1130d360583e2aecbcebfbf6eda7"), - "plugins/Zeitgeist/images/fullscreen.png" => array("346", "629df6e9cfdf1bb8d0a612f651da69e9"), - "plugins/Zeitgeist/images/goal.png" => array("270", "bc6edcd9d776933ea6b6cea5be1c6383"), - "plugins/Zeitgeist/images/help.png" => array("942", "471c83040c41ec0922eb4540992bcd1f"), - "plugins/Zeitgeist/images/html_icon.png" => array("3503", "45a005cf3fa96037df5d7935d26b5f7c"), - "plugins/Zeitgeist/images/ico_alert.png" => array("1112", "63d124bf79386ebf6285926956ff7829"), - "plugins/Zeitgeist/images/ico_delete.png" => array("231", "4065203e7b7471539eeff819b4cf443b"), - "plugins/Zeitgeist/images/ico_edit.png" => array("255", "32e1c11ad294948cfedff7c4daf76dad"), - "plugins/Zeitgeist/images/ico_info.png" => array("978", "362b1589e75a151c9e4d509615851950"), - "plugins/Zeitgeist/images/icon-calendar.gif" => array("331", "a0cffdd9fcf6552dea43658fc217732b"), - "plugins/Zeitgeist/images/image.png" => array("306", "2ea0351a26cdc37e0359ab011b832902"), - "plugins/Zeitgeist/images/inp_bg.png" => array("137", "1f4b8e7288c5d4dc52e44c50e0d02a9b"), - "plugins/Zeitgeist/images/li_dbl_gray.gif" => array("48", "5b0a692984ac5b04acc0886cd374bb85"), - "plugins/Zeitgeist/images/link.gif" => array("75", "b8de0b2b517e1999b32353209be4e976"), - "plugins/Zeitgeist/images/loading-blue.gif" => array("723", "6ce8f9a2c650cf90261acfc98b2edf90"), - "plugins/Zeitgeist/images/login-sprite.png" => array("10200", "a2a3520f448277c3efdb871d637fc34b"), - "plugins/Zeitgeist/images/logo-header.png" => array("3682", "f3a7921898ffcd44e413015b596c4e68"), - "plugins/Zeitgeist/images/logo-marketplace.png" => array("2927", "aa3dc0cd04c23a654e7a96ceabe2a6c1"), - "plugins/Zeitgeist/images/logo.png" => array("11152", "bc2b73b0541589e617fbb66b54ccc7ad"), - "plugins/Zeitgeist/images/logo.svg" => array("4044", "7f9586818f589f658c4dd7e7e5802274"), - "plugins/Zeitgeist/images/maximise.png" => array("3182", "e0018bf302603d234ae3d31c44fb3d1e"), - "plugins/Zeitgeist/images/minimise.png" => array("2869", "e746d4f1bfcc4d565a526e40918bcd0e"), - "plugins/Zeitgeist/images/minus.png" => array("176", "e010a638230ff96610f0ebffdfced0ac"), - "plugins/Zeitgeist/images/newtab.png" => array("509", "994c19f51192a18f7b3e0bc0775313f2"), - "plugins/Zeitgeist/images/ok.png" => array("626", "28501b0877ea15b49c6ca58677e186c3"), - "plugins/Zeitgeist/images/paypal_subscribe.gif" => array("3080", "a74a883239713fb5050593c20d9fd2a5"), - "plugins/Zeitgeist/images/plus_blue.png" => array("157", "9d61acb98c3ac639715aba6703997ad9"), - "plugins/Zeitgeist/images/plus.png" => array("174", "f867099b8f18cd9f989f1fcfb77cba37"), - "plugins/Zeitgeist/images/refresh.png" => array("2978", "b821bde25f3e4dcd0a7f6b9bd37f57b3"), - "plugins/Zeitgeist/images/reload.png" => array("892", "5a0360408c248f9cde4e0d4bad31ac00"), - "plugins/Zeitgeist/images/row_evolution_hover.png" => array("601", "7f6833f656aaad475e02a96ae3c0adb9"), - "plugins/Zeitgeist/images/row_evolution.png" => array("1934", "0e3fe13d82bc0526ed94bc079152f2f2"), - "plugins/Zeitgeist/images/search_bg.png" => array("384", "3105cf7e5419bef110507a682f28fab7"), - "plugins/Zeitgeist/images/search_ico.png" => array("175", "b74de7cb2b8cfddb82b95e63cd492286"), - "plugins/Zeitgeist/images/sites_selection.png" => array("120", "f8f6f62a17616adce09791ffb51aab1b"), - "plugins/Zeitgeist/images/smileyprog_0.png" => array("4045", "0b105851f9dfc4e5a3efb933c4fa01af"), - "plugins/Zeitgeist/images/smileyprog_1.png" => array("4268", "cd518d27567dda069dd5fff2e3c2291f"), - "plugins/Zeitgeist/images/smileyprog_2.png" => array("4292", "8e61661161afe5ac8a7e2caf085fb200"), - "plugins/Zeitgeist/images/smileyprog_3.png" => array("4589", "1eb75d0f84042d492b7ff4a515e23ff3"), - "plugins/Zeitgeist/images/smileyprog_4.png" => array("4733", "1f31e1ac3c6b3602cecabf0f0c15fd1b"), - "plugins/Zeitgeist/images/sortasc.png" => array("173", "bf15b38f5921052cd7d402a5a6e7542d"), - "plugins/Zeitgeist/images/sortdesc.png" => array("171", "3c918bc6390e75b19094de3b3db01578"), - "plugins/Zeitgeist/images/sort_subtable_asc_light.png" => array("2866", "a8d4e15a81c63b3d7f8ddde1ad5fc78d"), - "plugins/Zeitgeist/images/sort_subtable_asc.png" => array("173", "457908cb087009a946c99bef46643c69"), - "plugins/Zeitgeist/images/sort_subtable_desc_light.png" => array("286", "ba6261eca430661a8b84155460424106"), - "plugins/Zeitgeist/images/sort_subtable_desc.png" => array("171", "63b74c05d158e1bf7ed0172142991b24"), - "plugins/Zeitgeist/images/star_empty.png" => array("658", "31809a80055eb2aa02b51e6c11ecb02d"), - "plugins/Zeitgeist/images/star.png" => array("757", "872b7a1a8101bcf7ef6c7cf7c8f78ff7"), - "plugins/Zeitgeist/images/success_medium.png" => array("1346", "28e0ba1f8492374db4946d42c69e477b"), - "plugins/Zeitgeist/images/table_more.png" => array("200", "a8d8a758c97a1342864b646469059a02"), - "plugins/Zeitgeist/images/table.png" => array("151", "327ee0e75605ab865796053f2c0aebf1"), - "plugins/Zeitgeist/images/tagcloud.png" => array("202", "127389e4f7146b1322dd1873ee89235b"), - "plugins/Zeitgeist/images/video_play.png" => array("517", "29fd1c103c9ac9987b85e053e621ab20"), - "plugins/Zeitgeist/images/warning_medium.png" => array("1283", "24bc193a073997740e4aa459b2bbbbdf"), - "plugins/Zeitgeist/images/warning.png" => array("571", "8c4ef759f46a90e7a00e1db65e49edc9"), - "plugins/Zeitgeist/images/warning_small.png" => array("1083", "5ff491ccd2f32beb35d96fd79a4d7329"), - "plugins/Zeitgeist/images/zoom-out.png" => array("289", "b91cfbc280cfbf59fddc41348a78f2b6"), - "plugins/Zeitgeist/javascripts/ajaxHelper.js" => array("11827", "15e4b7bb70b671885e1597b7a61221bf"), - "plugins/Zeitgeist/javascripts/piwikHelper.js" => array("14986", "56e3acb719973a17f35316b74c155a3c"), - "plugins/Zeitgeist/plugin.json" => array("156", "90381b40a6f3b6e60f3d214bccdac5ab"), - "plugins/Zeitgeist/stylesheets/base.less" => array("555", "8bc4b5c0409a957679d70f19ab14a660"), - "plugins/Zeitgeist/stylesheets/general/_default.less" => array("2087", "47a06608a3652d1fd86254e314f81a5c"), - "plugins/Zeitgeist/stylesheets/general/_form.less" => array("2389", "6d43004e4fad124d15e45246e77bac1b"), - "plugins/Zeitgeist/stylesheets/general/_jqueryUI.less" => array("5147", "130ef76ab7dc45bbd50fcb4318aa5ada"), - "plugins/Zeitgeist/stylesheets/general/_misc.less" => array("576", "214e501d23f7e3f95f71644fbb50c886"), - "plugins/Zeitgeist/stylesheets/general/_utils.less" => array("391", "75d6698e72c26ffd6b562c3d738021e0"), - "plugins/Zeitgeist/stylesheets/ieonly.css" => array("506", "6a9862fb951ee731f968fee99d2480b3"), - "plugins/Zeitgeist/stylesheets/rtl.css" => array("53", "1be4a6f544e3ed971b966692b07d2c2d"), - "plugins/Zeitgeist/stylesheets/simple_structure.css" => array("1592", "e46a7d0ea302017e776fccc7b3db6060"), - "plugins/Zeitgeist/stylesheets/ui/_dataTable.less" => array("46", "29c2aa49f73efe255d4202c37a942ef5"), - "plugins/Zeitgeist/stylesheets/ui/_header.less" => array("739", "969c90051d99a050d9588f0bd02c4f82"), - "plugins/Zeitgeist/stylesheets/ui/_headerMessage.less" => array("1179", "16bf5e43fce5c2c39ff393c6c743a1aa"), - "plugins/Zeitgeist/stylesheets/ui/_languageSelect.less" => array("745", "6dd7f3399125f54809bda579420c1d6c"), - "plugins/Zeitgeist/stylesheets/ui/_loading.less" => array("360", "a48e9d0b63da915547a353ca0ace1656"), - "plugins/Zeitgeist/stylesheets/ui/_periodSelect.less" => array("1453", "07709490ca6206728d5274115a4cf71e"), - "plugins/Zeitgeist/stylesheets/ui/_siteSelect.less" => array("3503", "d0f0fec0d21d3013cc5e2540aff9f95c"), - "plugins/Zeitgeist/templates/admin.twig" => array("2040", "3801afc5349d0540bf749b5133d25ce0"), - "plugins/Zeitgeist/templates/ajaxMacros.twig" => array("632", "b6179aa5cdabd0f26b15ba6387f5af22"), - "plugins/Zeitgeist/templates/dashboard.twig" => array("2097", "0bb68e2e0d617932e57f3390d35aaccc"), - "plugins/Zeitgeist/templates/empty.twig" => array("35", "2c6a1dccfb394fef9ef03849c39a5bec"), - "plugins/Zeitgeist/templates/genericForm.twig" => array("991", "9e6a247b129a8b3932e2928273cc2b2f"), - "plugins/Zeitgeist/templates/_iframeBuster.twig" => array("385", "1c780792c51f5d2fecbf53c0ce0d547f"), - "plugins/Zeitgeist/templates/javascriptCode.tpl" => array("631", "8b239b1782acbb256597e965c49a7f92"), - "plugins/Zeitgeist/templates/_jsCssIncludes.twig" => array("278", "64e8a45c1d762222f4d3ac0fc072afbe"), - "plugins/Zeitgeist/templates/_jsGlobalVariables.twig" => array("1668", "4a163e13cc7cbf4bbf2723260f38765e"), - "plugins/Zeitgeist/templates/macros.twig" => array("891", "3343e6b0da8c13cd7f5d73bffcc86b49"), - "plugins/Zeitgeist/templates/_piwikTag.twig" => array("1217", "4d267c8a76a1c2212f160267095998ae"), - "plugins/Zeitgeist/templates/simpleLayoutFooter.tpl" => array("23", "c64111f363b9e1a0b0ea6d1c8ccbb9bd"), - "plugins/Zeitgeist/templates/simpleLayoutHeader.tpl" => array("511", "422d59273c966199cc15449891d697bb"), - "plugins/Zeitgeist/templates/_sparklineFooter.twig" => array("102", "a9e46848aaf5613b971827cf12ab6eaa"), - "README.md" => array("3813", "5ba34ead653bdb77ce52a3a4eeb221ba"), - "tests/README.md" => array("5681", "d9e5829add2c89dc0066f0299d82d7ed"), - "vendor/composer/autoload_classmap.php" => array("510", "91d089f53c9e426510a6f0292c655676"), - "vendor/composer/autoload_files.php" => array("255", "2cc6f4ba7d74bf4e09bff1ad7ed04816"), - "vendor/composer/autoload_namespaces.php" => array("341", "41fba06e8ccbd2eb2cf63e44a65345cd"), - "vendor/composer/autoload_psr4.php" => array("143", "dd3a00f0d13eb29781edd8c77d4c5100"), - "vendor/composer/ClassLoader.php" => array("11571", "3adcacc118804f98f1fd888e2575f00a"), - "vendor/composer/installed.json" => array("9348", "a1d1990a1a293648031fd49bc098b419"), - "vendor/leafo/lessphp/composer.json" => array("544", "e0d0ef78bbb2d2ea3db1be3ea7a34253"), - "vendor/leafo/lessphp/docs/docs.md" => array("38626", "b2eb5cf232f2d136fd4d14ba4f9c45f5"), - "vendor/leafo/lessphp/.gitignore" => array("71", "a51238606af0d1eb55ce05237e6778eb"), - "vendor/leafo/lessphp/lessc.inc.php" => array("91826", "b8ddd7795cbca49cad99e4a977d0b13d"), - "vendor/leafo/lessphp/lessify" => array("414", "e8e87d48dd91f4838219419395f1b996"), - "vendor/leafo/lessphp/lessify.inc.php" => array("9696", "e0c246fc5d113d6c42417b96f2e27b54"), - "vendor/leafo/lessphp/LICENSE" => array("33650", "2887747a1404ae4fb71a95d440d3778f"), - "vendor/leafo/lessphp/Makefile" => array("57", "bf733f8f889f8351526b07f50abf1dc1"), - "vendor/leafo/lessphp/package.sh" => array("758", "6bcb58ac88b574f30424245f65b2655d"), - "vendor/leafo/lessphp/plessc" => array("4911", "fb3df9045ff35eed6f8202c27a86ef14"), - "vendor/leafo/lessphp/README.md" => array("2817", "1fae9319ae33c7596e67d6adba644e13"), - "vendor/leafo/lessphp/.travis.yml" => array("57", "a37e9778c469a928db223337a03a9f04"), + "plugins/Widgetize/templates/iframe.twig" => array("791", "251a4b25bdab652b0bc6aaf41e4a8d70"), + "plugins/Widgetize/templates/index.twig" => array("3786", "c0964e648beb9609062d01fae7314f04"), + "plugins/Widgetize/Widgetize.php" => array("1823", "7fd3dd5cf9d2aa937c5807890fbd8d11"), + "PRIVACY.md" => array("4569", "641005cb30c18f6dc21dfd6e38a26e63"), + "README.md" => array("5355", "55c1ff24dade2aad1d0cf0607eeeeaee"), + "SECURITY.md" => array("1052", "5db5e0999e33a658dcdced04fe8d379d"), + "tests/README.md" => array("10421", "4a3f0037410bd36d024ce57839df7790"), + "vendor/composer/autoload_classmap.php" => array("253953", "09c7aadb018fb5b86f1a965fbd325d93"), + "vendor/composer/autoload_files.php" => array("325", "e6877f0ffe07bea4ee911abc9f9e95d1"), + "vendor/composer/autoload_namespaces.php" => array("1371", "70c4f68924ff8347a15eb07b7bd398e5"), + "vendor/composer/autoload_psr4.php" => array("885", "87c829bdd1cc35b90920cb50be6917f3"), + "vendor/composer/ClassLoader.php" => array("12466", "c67ebce5ff31e99311ceb750202adf2e"), + "vendor/composer/include_paths.php" => array("311", "9b2938881ce7597ff4f18807e52c4c05"), + "vendor/composer/installed.json" => array("46622", "bdec9a359fc85bc8ee0713b0232fb841"), + "vendor/composer/LICENSE" => array("1075", "084a034acbad39464e3df608c6dc064f"), + "vendor/container-interop/container-interop/composer.json" => array("304", "73b1b3bf2ddf4a1700c7acd74f8c3a2f"), + "vendor/container-interop/container-interop/docs/ContainerInterface.md" => array("5654", "a7329a38490dfa651289fb4500492c93"), + "vendor/container-interop/container-interop/docs/ContainerInterface-meta.md" => array("5342", "18278724c8f939ad8d74c2dfb577567f"), + "vendor/container-interop/container-interop/docs/Delegate-lookup.md" => array("2945", "893605b82872d606fa505f69dfb76245"), + "vendor/container-interop/container-interop/docs/Delegate-lookup-meta.md" => array("12499", "29f3e7bffa0896938e540d8919a8d0e8"), + "vendor/container-interop/container-interop/docs/images/interoperating_containers.png" => array("35971", "c8b901686c1055ce702cb43514655cbb"), + "vendor/container-interop/container-interop/docs/images/priority.png" => array("22949", "30083d1c148c9b9af6c2cdd64909bb57"), + "vendor/container-interop/container-interop/docs/images/side_by_side_containers.png" => array("22519", "9dd487bddd70c3ea19250cc2f10db192"), + "vendor/container-interop/container-interop/LICENSE" => array("1084", "a46a6933adb89c294f06f5420be849ea"), + "vendor/container-interop/container-interop/README.md" => array("3437", "6a46d69c1aa85e1b4e78a666f1d95d3f"), + "vendor/container-interop/container-interop/src/Interop/Container/ContainerInterface.php" => array("997", "45c3d55095602e2b5eadeb2f1b89d559"), + "vendor/container-interop/container-interop/src/Interop/Container/Exception/ContainerException.php" => array("253", "cc68c5023d35b24079a8966814310a1a"), + "vendor/container-interop/container-interop/src/Interop/Container/Exception/NotFoundException.php" => array("252", "bd2f95f06efcf84ae3cbca2ec4c875a0"), + "vendor/doctrine/annotations/composer.json" => array("976", "d54f54b6e09d8e243f1f50860a5432d7"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attribute.php" => array("1438", "2921761bd793c75cd5736f6886034042"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attributes.php" => array("1386", "0e523c927fc96fba49c0ff88f5fbd552"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Enum.php" => array("2599", "4e5dc8aab54f185d16858383e058bd30"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationException.php" => array("5828", "27c858a1fa6e2836d123230cf1ed6f53"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php" => array("1866", "01c531f662d3b74854dc3c4774ec0a29"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation.php" => array("2494", "d20195a2215b1ea1c80dc810ce153592"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationReader.php" => array("12321", "95cf187ece8e402e81e028883b9b1b5b"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationRegistry.php" => array("4742", "019b497f1119f9e9ef4b34fee33648bc"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Required.php" => array("1274", "061739a3e0a2b49d16bed325deadb8a9"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Target.php" => array("3294", "cac0c81c2a99b485d5e7d2cd7a35651a"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/CachedReader.php" => array("6406", "f7f402bc030b13cab6c762199a8560d3"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocLexer.php" => array("4059", "e773f401f139da60cd435f94c891e71d"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocParser.php" => array("38074", "31e63ae286df03e6c529b26b90f9f47c"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/FileCacheReader.php" => array("8847", "461fa02ad73cafba36e31bec21fedece"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/IndexedReader.php" => array("3297", "86216f4ba484e600c72789f9ae7a55fe"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/PhpParser.php" => array("2932", "d7bcd01f570e3e5a09b2e2ccfb911297"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Reader.php" => array("3527", "533c811e390904fcf708a92442bfcf1a"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/SimpleAnnotationReader.php" => array("3657", "1ba46f12b1838f7da056c397007249ee"), + "vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/TokenParser.php" => array("6119", "a60e19676ed026c65ef8929f4d86fed4"), + "vendor/doctrine/annotations/LICENSE" => array("1065", "2e75234cfca1e55b1cdce86615dccac9"), + "vendor/doctrine/annotations/README.md" => array("597", "82fa1351fe81ec873f57ad390ce69087"), + "vendor/doctrine/cache/build.properties" => array("143", "a5ab454bd1375b26122ae9489a049a65"), + "vendor/doctrine/cache/build.xml" => array("4132", "6c5ebac561ef7f795786ea6e7d178c61"), + "vendor/doctrine/cache/composer.json" => array("1085", "38d7ca768bc6ff2705611fd63cfce4e0"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/ApcCache.php" => array("3157", "03f8af6da8380c598d8329cadc1889b1"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/ArrayCache.php" => array("2459", "eb22acd84aa21dc269e8397f5cb82a97"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/Cache.php" => array("3812", "fb7942f78f96a07f94c32dfe8f018fdf"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/CacheProvider.php" => array("7715", "20659fab0c93218b3cec54780d6af6c6"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/ChainCache.php" => array("3786", "edaa0cfacbb1269da34784f33399ee11"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/ClearableCache.php" => array("1542", "4881af3c08654515587a962d6a113bc0"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/CouchbaseCache.php" => array("3176", "293e1d41557bf6b9ab5aef606c5e2b6a"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/FileCache.php" => array("6469", "ba3a8959403481cf9db9c1faa3337b51"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/FilesystemCache.php" => array("2914", "f0c1a19e62a4d973daa5b638b7bfef42"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/FlushableCache.php" => array("1391", "743326be176ade9f1a8dcc5cbebcf01e"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/MemcacheCache.php" => array("3303", "d298a5f9cf639cbc5acd12238d80a7a7"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/MemcachedCache.php" => array("3494", "5da2adb68cab71b74634bf2a0336ca39"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/MongoDBCache.php" => array("5894", "01a73549b122634ecd5accdd0158a8c0"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/MultiGetCache.php" => array("1603", "c97c479fcc9506c7474b114cba01a34d"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/PhpFileCache.php" => array("3354", "50838f97a46c320ac9881a11890093af"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/PredisCache.php" => array("2277", "d49230c8cc60b70f1e5a2605885fb899"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/RedisCache.php" => array("3639", "f9ce73e547a6a36c2fdb9e11d1ed2cc5"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/RiakCache.php" => array("6810", "ca83e1086997bee24b0ac1da228cea7d"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/SQLite3Cache.php" => array("5418", "27b869f86cc14381659397e56afc46d9"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/Version.php" => array("1074", "02171d579a106229195944e167af7559"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/VoidCache.php" => array("1937", "377ba38efc6aa8723cedd1b7d7b93b45"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/WinCacheCache.php" => array("2671", "98c21113bee715fc23c1010927bc0804"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/XcacheCache.php" => array("3161", "7bb6d247fd3010c037562b16195f0943"), + "vendor/doctrine/cache/lib/Doctrine/Common/Cache/ZendDataCache.php" => array("2212", "ea438b0409996d058d562326a737dade"), + "vendor/doctrine/cache/LICENSE" => array("1065", "c781539da0fb35b5c5ad0fb72172ded6"), + "vendor/doctrine/cache/phpunit.xml.dist" => array("709", "614bfe87dcae0543274c4cb8ad35e86a"), + "vendor/doctrine/cache/README.md" => array("695", "7b5db3fa29c926511a6c72395c22f9d9"), + "vendor/doctrine/cache/UPGRADE.md" => array("726", "125ec12b697e4354b91d6e773f736472"), + "vendor/doctrine/lexer/composer.json" => array("738", "1f7629cdf37c77c53466fd4b66953a3d"), + "vendor/doctrine/lexer/lib/Doctrine/Common/Lexer/AbstractLexer.php" => array("8084", "411100e0261c6a0de208a365fa1ede43"), + "vendor/doctrine/lexer/LICENSE" => array("1065", "2e75234cfca1e55b1cdce86615dccac9"), + "vendor/doctrine/lexer/README.md" => array("171", "3b9da77a5f3d874fcd05a0c9d5a51f91"), + "vendor/leafo/lessphp/composer.json" => array("546", "35dd24b2cac6551084bdf16744063267"), + "vendor/leafo/lessphp/docs/docs.md" => array("38664", "559d78269160f4dfed1bbe2e79233382"), + "vendor/leafo/lessphp/lessc.inc.php" => array("94979", "b91341fb580a2f2822e2868752e2b1a0"), + "vendor/leafo/lessphp/lessify" => array("413", "13835afac8dfcb883bb82827c40d6078"), + "vendor/leafo/lessphp/lessify.inc.php" => array("9686", "2fe52e278dac0f2b4c9a6169f7998b9f"), + "vendor/leafo/lessphp/LICENSE" => array("33647", "0eff5073f6de1624855d6de9ede3a60a"), + "vendor/leafo/lessphp/Makefile" => array("56", "11e5785c86764d87414ea98ba982c27a"), + "vendor/leafo/lessphp/package.sh" => array("797", "a27f7c79b86003d6c240e143dfa9a1fb"), + "vendor/leafo/lessphp/plessc" => array("4911", "be257c8e42dc4af90a3d1751ac9c85e1"), + "vendor/leafo/lessphp/README.md" => array("2823", "1542980726ea76f22c4d66979c0e5e0a"), + "vendor/mnapoli/phpdocreader/composer.json" => array("337", "5b20114af2f19f21e095340ad7d8fe9f"), + "vendor/mnapoli/phpdocreader/LICENSE" => array("1059", "7206d0fc5fca0b131ec0764753a95102"), + "vendor/mnapoli/phpdocreader/phpunit.xml.dist" => array("581", "75af35c6ca36f3406e4ceda2397a5741"), + "vendor/mnapoli/phpdocreader/README.md" => array("1539", "5bc299c03ded36dd6cefb1e9337cea8e"), + "vendor/mnapoli/phpdocreader/src/PhpDocReader/AnnotationException.php" => array("138", "97a7e0551660425c8a7f27cf04613d79"), + "vendor/mnapoli/phpdocreader/src/PhpDocReader/PhpDocReader.php" => array("8565", "94c5c1fcecc2b2af2d4d470ecf629917"), + "vendor/monolog/monolog/CHANGELOG.mdown" => array("14423", "cf86c859791758af76b7d27857b8b7e1"), + "vendor/monolog/monolog/composer.json" => array("2427", "d8432f4f488f0d8a246b62fd4bc6d296"), + "vendor/monolog/monolog/doc/01-usage.md" => array("8373", "87ca7ebcac15522f556963ed51c6fc09"), + "vendor/monolog/monolog/doc/02-handlers-formatters-processors.md" => array("8742", "f9da532c46e65aa49f3da5fc83fe138f"), + "vendor/monolog/monolog/doc/03-utilities.md" => array("798", "c4d3031fb9b82d8b5a0dbfa2de11ea80"), + "vendor/monolog/monolog/doc/04-extending.md" => array("2159", "22cf44916f0e58c64d69e9243cfd2a9a"), + "vendor/monolog/monolog/doc/sockets.md" => array("1023", "099d9fc2bd124a1d9f4e4f3d760428ad"), + "vendor/monolog/monolog/LICENSE" => array("1063", "b774b694b61e60534c6165cc6fea26c2"), + "vendor/monolog/monolog/phpunit.xml.dist" => array("460", "cd6f51f27a721b92f87c95c7fb6e2d32"), + "vendor/monolog/monolog/README.mdown" => array("4047", "b49184292c80bd09b7f6cade7952fe88"), + "vendor/monolog/monolog/src/Monolog/ErrorHandler.php" => array("7674", "39bb8b4fc9ef5cc91603e99e5b919940"), + "vendor/monolog/monolog/src/Monolog/Formatter/ChromePHPFormatter.php" => array("2071", "71556b69ade93eb7eb296611dcde581e"), + "vendor/monolog/monolog/src/Monolog/Formatter/ElasticaFormatter.php" => array("1743", "cdf84d2666304c10184eb4ee0787e4be"), + "vendor/monolog/monolog/src/Monolog/Formatter/FlowdockFormatter.php" => array("2548", "ba10a9093d8c9fe1a2b2acbbc52803fd"), + "vendor/monolog/monolog/src/Monolog/Formatter/FormatterInterface.php" => array("787", "6393c1db9899b0f9e3fc4ad3d6a898dd"), + "vendor/monolog/monolog/src/Monolog/Formatter/GelfMessageFormatter.php" => array("3304", "d440fde52d75340eb1a307cba0970e64"), + "vendor/monolog/monolog/src/Monolog/Formatter/HtmlFormatter.php" => array("4532", "b8f7335803da9715cfc9284f87ddeccd"), + "vendor/monolog/monolog/src/Monolog/Formatter/JsonFormatter.php" => array("2765", "e95ef9e43b17778829fc6fbceb7600a2"), + "vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php" => array("4679", "aa5d7736bc7e9c03bd4cb8a6de05c745"), + "vendor/monolog/monolog/src/Monolog/Formatter/LogglyFormatter.php" => array("1326", "17c423e50573a684d3f51539d6f27558"), + "vendor/monolog/monolog/src/Monolog/Formatter/LogstashFormatter.php" => array("5315", "e1a2a4f21d7d874af2a05855eb002a39"), + "vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php" => array("3260", "c9de870b2087e9881756ccddcb751987"), + "vendor/monolog/monolog/src/Monolog/Formatter/NormalizerFormatter.php" => array("5681", "4bdb115724509f1158372799fcdeb874"), + "vendor/monolog/monolog/src/Monolog/Formatter/ScalarFormatter.php" => array("1040", "9934bbe7fbf1a7536cbabf0bb31fac8b"), + "vendor/monolog/monolog/src/Monolog/Formatter/WildfireFormatter.php" => array("3234", "0a57bdcd42584e1b41e4e093631a22f6"), + "vendor/monolog/monolog/src/Monolog/Handler/AbstractHandler.php" => array("4048", "ef58ebfe96434d93ff4152662939652c"), + "vendor/monolog/monolog/src/Monolog/Handler/AbstractProcessingHandler.php" => array("1498", "cfdd846fac8ce3cb15afe9188575ed37"), + "vendor/monolog/monolog/src/Monolog/Handler/AbstractSyslogHandler.php" => array("2877", "551fa48711e5e1142432094b3af97727"), + "vendor/monolog/monolog/src/Monolog/Handler/AmqpHandler.php" => array("2750", "5123da7e55282796202c2bc224815c93"), + "vendor/monolog/monolog/src/Monolog/Handler/BrowserConsoleHandler.php" => array("6213", "951155eebf37067d40ecea01665ab451"), + "vendor/monolog/monolog/src/Monolog/Handler/BufferHandler.php" => array("3436", "f6c37a9f3618fb4f32f572262c81d9ca"), + "vendor/monolog/monolog/src/Monolog/Handler/ChromePHPHandler.php" => array("5339", "3c79764d7480846b0bd0c3bba5d7386c"), + "vendor/monolog/monolog/src/Monolog/Handler/CouchDBHandler.php" => array("1952", "d0457dc4ae523c5b1d6291ab46e4a62e"), + "vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php" => array("4543", "63b6f8364862bed65a29a8d95ec1dc76"), + "vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php" => array("1487", "4b4aafdac634c3314ad07ceb7d158a3f"), + "vendor/monolog/monolog/src/Monolog/Handler/DoctrineCouchDBHandler.php" => array("1000", "abc48686f395a089e55a2ec1a4fe4b78"), + "vendor/monolog/monolog/src/Monolog/Handler/DynamoDbHandler.php" => array("2159", "a7f97af3c1967d4ee7699059071681bd"), + "vendor/monolog/monolog/src/Monolog/Handler/ElasticSearchHandler.php" => array("3417", "303b76d4f8d8f6f7a92150e83894d2f1"), + "vendor/monolog/monolog/src/Monolog/Handler/ErrorLogHandler.php" => array("2380", "1854e5b1850705f60afdeb45cf80cd77"), + "vendor/monolog/monolog/src/Monolog/Handler/FilterHandler.php" => array("4383", "883312f37345d1a10bd6dfc5210ed999"), + "vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php" => array("651", "6bf84f27f4b7a0016afa63430cf6f8be"), + "vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ChannelLevelActivationStrategy.php" => array("1927", "6827ce52b4bc0d222300712248c1b40d"), + "vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ErrorLevelActivationStrategy.php" => array("759", "f31a9ad59022efe0de63808d275a0031"), + "vendor/monolog/monolog/src/Monolog/Handler/FingersCrossedHandler.php" => array("5504", "6132332bdf5482db41668e241dca2662"), + "vendor/monolog/monolog/src/Monolog/Handler/FirePHPHandler.php" => array("5466", "8b326519d23fccc55225f797f5f499fc"), + "vendor/monolog/monolog/src/Monolog/Handler/FleepHookHandler.php" => array("3362", "5e872dfb71c9c5e3dd53ac0035893a31"), + "vendor/monolog/monolog/src/Monolog/Handler/FlowdockHandler.php" => array("3357", "94c7a4507949751276a6fef75b92a529"), + "vendor/monolog/monolog/src/Monolog/Handler/GelfHandler.php" => array("2068", "780522085f93612bab462ff2538b4d6b"), + "vendor/monolog/monolog/src/Monolog/Handler/GroupHandler.php" => array("1889", "4bc9e4378c31a77b5e94e94b4b70ef9a"), + "vendor/monolog/monolog/src/Monolog/Handler/HandlerInterface.php" => array("2597", "ea71b279940a8c6b3e8bec2f8ab73ab5"), + "vendor/monolog/monolog/src/Monolog/Handler/HipChatHandler.php" => array("9737", "c3122d27dcebcca8f0945f8c5fac4fe8"), + "vendor/monolog/monolog/src/Monolog/Handler/IFTTTHandler.php" => array("2184", "1521d3a1b2b74166bb4fe3656b0dc969"), + "vendor/monolog/monolog/src/Monolog/Handler/LogEntriesHandler.php" => array("1605", "4f41355fbf6aca980dc7def2da449a14"), + "vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php" => array("2619", "beee53f1c85666d49978d0d8c0324a2c"), + "vendor/monolog/monolog/src/Monolog/Handler/MailHandler.php" => array("1295", "54cee36784ef08875244672c7002252c"), + "vendor/monolog/monolog/src/Monolog/Handler/MandrillHandler.php" => array("2156", "cd5054f63dc7b68f28f75ad6e115d5fa"), + "vendor/monolog/monolog/src/Monolog/Handler/MissingExtensionException.php" => array("450", "99ec7108652f07752a6e4585f781ee92"), + "vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.php" => array("1384", "2a8a13591a8be648d0558bbe942f85d8"), + "vendor/monolog/monolog/src/Monolog/Handler/NativeMailerHandler.php" => array("4920", "6fdae5a11f32fc764fcffe120fe2b25a"), + "vendor/monolog/monolog/src/Monolog/Handler/NewRelicHandler.php" => array("5735", "2b728877c085a706cdcc2685f841c4eb"), + "vendor/monolog/monolog/src/Monolog/Handler/NullHandler.php" => array("957", "d2f5ef3d84e4f42bcff3084a5d8fbb98"), + "vendor/monolog/monolog/src/Monolog/Handler/PHPConsoleHandler.php" => array("9954", "1bc4357a76a9411de757cf4d30402bfc"), + "vendor/monolog/monolog/src/Monolog/Handler/PsrHandler.php" => array("1433", "541ce4f4b61a8531b8fdf5eaa0328fe2"), + "vendor/monolog/monolog/src/Monolog/Handler/PushoverHandler.php" => array("6631", "bd6eb75321034f790d48d44b450e3f94"), + "vendor/monolog/monolog/src/Monolog/Handler/RavenHandler.php" => array("6334", "7be2e7dfa61880e81ed209478600ea0d"), + "vendor/monolog/monolog/src/Monolog/Handler/RedisHandler.php" => array("2893", "e636e693cb9556553ec382dec10e64c3"), + "vendor/monolog/monolog/src/Monolog/Handler/RollbarHandler.php" => array("2612", "8a1ace7cd474ff3fed86e56cd7d8ff6f"), + "vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php" => array("4531", "b7a31238c5d06f097e1f6541cba8c850"), + "vendor/monolog/monolog/src/Monolog/Handler/SamplingHandler.php" => array("2674", "85fc2f7a5cf4f34749aa70cbc8c732e6"), + "vendor/monolog/monolog/src/Monolog/Handler/SlackHandler.php" => array("8677", "a42c874dc0fa8adff4513a54c51b6700"), + "vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.php" => array("7348", "6aef6bf09be47775be2a208b9b49377d"), + "vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php" => array("4554", "841979a4004cbb61b984d1df390b54d8"), + "vendor/monolog/monolog/src/Monolog/Handler/SwiftMailerHandler.php" => array("2673", "4d572880045b536e0b05a371e76e0a82"), + "vendor/monolog/monolog/src/Monolog/Handler/SyslogHandler.php" => array("1848", "7a0bad8d1fb73a94adeb0d398eb42ad9"), + "vendor/monolog/monolog/src/Monolog/Handler/SyslogUdpHandler.php" => array("2036", "b08b66b2cad7d5b49ff5cfab85d0e5c4"), + "vendor/monolog/monolog/src/Monolog/Handler/SyslogUdp/UdpSocket.php" => array("1401", "3585d916f9c3280d9d2c61abc6aa8c0a"), + "vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php" => array("4657", "ee8545a99923fc654730f839cd9bafe5"), + "vendor/monolog/monolog/src/Monolog/Handler/WhatFailureGroupHandler.php" => array("1343", "cc7c8c6047b554c24a682575a53aa4bc"), + "vendor/monolog/monolog/src/Monolog/Handler/ZendMonitorHandler.php" => array("2240", "dc2ee6716c9dee2ef3d420c41ac0dc2b"), + "vendor/monolog/monolog/src/Monolog/Logger.php" => array("17796", "e423649d9768bc945e5d21d71c75133b"), + "vendor/monolog/monolog/src/Monolog/Processor/GitProcessor.php" => array("1406", "34832c01dc9cc6500d9cbc0ac648c276"), + "vendor/monolog/monolog/src/Monolog/Processor/IntrospectionProcessor.php" => array("3055", "1ef85be69eebdbad632f50bd1af4b880"), + "vendor/monolog/monolog/src/Monolog/Processor/MemoryPeakUsageProcessor.php" => array("790", "b753a93665221b4ec3bb9b7e673a94a6"), + "vendor/monolog/monolog/src/Monolog/Processor/MemoryProcessor.php" => array("1821", "85e6b9ede44b839f6224eb1d442aece5"), + "vendor/monolog/monolog/src/Monolog/Processor/MemoryUsageProcessor.php" => array("771", "6497ea95b20d9ae90826819cbc6d0676"), + "vendor/monolog/monolog/src/Monolog/Processor/ProcessIdProcessor.php" => array("574", "a8b0bbeab67f2bfcc0712d369357fefa"), + "vendor/monolog/monolog/src/Monolog/Processor/PsrLogMessageProcessor.php" => array("1272", "2511e6abb86e0eee16edcf818fa68d57"), + "vendor/monolog/monolog/src/Monolog/Processor/TagProcessor.php" => array("820", "f4abf7ee4c7b8fd5afa3b44c4e7e665c"), + "vendor/monolog/monolog/src/Monolog/Processor/UidProcessor.php" => array("944", "8872a7a39362441f4deea3ef866a0b9f"), + "vendor/monolog/monolog/src/Monolog/Processor/WebProcessor.php" => array("2876", "bbaa327b04a82135d5d93a5951e17fae"), + "vendor/monolog/monolog/src/Monolog/Registry.php" => array("4024", "efd8f92882e827054fd5d442fb8c06b4"), "vendor/mustangostang/spyc/composer.json" => array("588", "c3ed21823389befa4e8c8b81d0f3be63"), "vendor/mustangostang/spyc/COPYING" => array("1077", "b0987325db5fa8e52b7076860d8e2e0a"), "vendor/mustangostang/spyc/examples/yaml-dump.php" => array("974", "c18b833f0057beb6a5845bf4335c68e0"), @@ -2947,133 +6398,440 @@ class Manifest { "vendor/mustangostang/spyc/README" => array("5772", "d426a4028be21f70da24b9593df400a1"), "vendor/mustangostang/spyc/Spyc.php" => array("31690", "1ff392881801c81f27aa8ea2e2bd8f2a"), "vendor/mustangostang/spyc/spyc.yaml" => array("3609", "796845442758ffc5887b517580d960f1"), - "vendor/piwik/device-detector/composer.json" => array("958", "8c2246083e5b6d8752263c1074522890"), - "vendor/piwik/device-detector/DeviceDetector.php" => array("31360", "54677c9e65f2db309b04c0ef43d856b8"), - "vendor/piwik/device-detector/.gitignore" => array("30", "5a959e73a428396446cf2fc274e91c26"), - "vendor/piwik/device-detector/README.md" => array("391", "0a04395636c6a38d3f2b5fbaddaf054c"), - "vendor/piwik/device-detector/regexes/browsers.yml" => array("10214", "a745684b695696c1318751816b6ca365"), - "vendor/piwik/device-detector/regexes/mobiles.yml" => array("27043", "a9b5c80b5d331af8f1e8734265993ac0"), - "vendor/piwik/device-detector/regexes/oss.yml" => array("8511", "1b49d6cab78daeb3d7fe5fdca86a2ce8"), - "vendor/piwik/device-detector/regexes/televisions.yml" => array("3886", "6ed102a54f0be2e7413a5799b0f32a02"), - "vendor/piwik/device-detector/.travis.yml" => array("155", "f7214c64927a7b580f239487842ddc32"), - "vendor/symfony/console/Symfony/Component/Console/Application.php" => array("35538", "d7fa5d3e678f32b207a275d566bd226b"), - "vendor/symfony/console/Symfony/Component/Console/CHANGELOG.md" => array("1682", "e2b61d6b18ded64f0686599db5c7fb5b"), - "vendor/symfony/console/Symfony/Component/Console/Command/Command.php" => array("16439", "80d4d8e858f5a83230701aed15e6f9ff"), - "vendor/symfony/console/Symfony/Component/Console/Command/HelpCommand.php" => array("2655", "d10467979ea85e92951425fd351347ac"), - "vendor/symfony/console/Symfony/Component/Console/Command/ListCommand.php" => array("2752", "7f0bd50020e8b9c39d79692f3e32db8f"), - "vendor/symfony/console/Symfony/Component/Console/composer.json" => array("869", "9e146f5ae7085917809444efe2d868c6"), - "vendor/symfony/console/Symfony/Component/Console/ConsoleEvents.php" => array("1528", "10001de60b8e31092612e73dc02300d7"), - "vendor/symfony/console/Symfony/Component/Console/Descriptor/ApplicationDescription.php" => array("3513", "e5ffa840297bdf9232762c38218846dc"), + "vendor/pear/archive_tar/Archive/Tar.php" => array("82096", "89b230679f31da6f8dbdea25095f4ca9"), + "vendor/pear/archive_tar/composer.json" => array("1298", "b6aee05802817df0d650c33f0bcad5b9"), + "vendor/pear/archive_tar/docs/Archive_Tar.txt" => array("19110", "2fb90f0be7089a45c09a0d1182792419"), + "vendor/pear/archive_tar/package.xml" => array("13704", "93251d391179f24fca007a351c371a86"), + "vendor/pear/archive_tar/README.md" => array("787", "6d781414f77bf296d8cb8adb887280b6"), + "vendor/pear/archive_tar/scripts/phptar.in" => array("5086", "58e1e54d0bde1da8df7bc8714ca2405d"), + "vendor/pear/archive_tar/sync-php4" => array("180", "b8da2130b2b0f925d71f192f8d1d7fbb"), + "vendor/pear/console_getopt/composer.json" => array("885", "df3e25b43a37d97cb30ca6f388cc5434"), + "vendor/pear/console_getopt/Console/Getopt.php" => array("13412", "013530beb065fb8885f70af40b6c91fa"), + "vendor/pear/console_getopt/LICENSE" => array("1303", "ecc3a78fa3a689a22f34a11c5fbba675"), + "vendor/pear/console_getopt/package.xml" => array("6645", "8f9f4d05b41a964f02c88103291e0582"), + "vendor/pear/console_getopt/README.rst" => array("676", "d67e2a1237dc6ccdcb278aaabbef75f8"), + "vendor/pear/pear-core-minimal/composer.json" => array("799", "1da3ec0821b35b0caf98438e7d0bfb16"), + "vendor/pear/pear-core-minimal/copy-from-pear-core.sh" => array("223", "6f29ef0e0b810f9cd60092b0e984bb2d"), + "vendor/pear/pear-core-minimal/README.rst" => array("645", "2b962148ccdae13496feb9c6dcce0065"), + "vendor/pear/pear-core-minimal/src/OS/Guess.php" => array("10597", "39ae18d707e1aa6493a304cfc05d6e8b"), + "vendor/pear/pear-core-minimal/src/PEAR/Error.php" => array("323", "4139228d6b0e8de4bce672ffb0e48b2d"), + "vendor/pear/pear-core-minimal/src/PEAR/ErrorStack.php" => array("33807", "971bcf5708fcfab66a5e0761584a52a6"), + "vendor/pear/pear-core-minimal/src/PEAR.php" => array("35478", "dbcaa6e38da7f082d2004bcc030394e2"), + "vendor/pear/pear-core-minimal/src/System.php" => array("20299", "1729151097130d198ee7ac7755eb8586"), + "vendor/pear/pear_exception/composer.json" => array("962", "687020e9a73effe827e6ebc8e1b1bbdd"), + "vendor/pear/pear_exception/LICENSE" => array("1477", "45b44486d8090de17b2a8b4211fab247"), + "vendor/pear/pear_exception/package.xml" => array("3191", "cbf90547a920d34fab8018de219a14c4"), + "vendor/pear/pear_exception/PEAR/Exception.php" => array("15395", "9781b1d3656eb64a92430c97ee9a0c83"), + "vendor/php-di/invoker/composer.json" => array("658", "70958991141163ea5b8f5c121244e18d"), + "vendor/php-di/invoker/CONTRIBUTING.md" => array("834", "aa65bb191159fd722e2e7596bb1a8436"), + "vendor/php-di/invoker/doc/parameter-resolvers.md" => array("3474", "c5ff2cfdde5590e46b6319c787396bff"), + "vendor/php-di/invoker/LICENSE" => array("1077", "d1af6aad6f0f8f4e625c884b610e3c52"), + "vendor/php-di/invoker/README.md" => array("7746", "e72fea98e20f061b5ce241e43ba6ee42"), + "vendor/php-di/invoker/src/CallableResolver.php" => array("3946", "0022235457a99ab50eaa70e525de3bc7"), + "vendor/php-di/invoker/src/Exception/InvocationException.php" => array("184", "19cfae03502d258fbfe71463ac26898b"), + "vendor/php-di/invoker/src/Exception/NotCallableException.php" => array("204", "3c0ada74d8fbd10420393473eafa8a53"), + "vendor/php-di/invoker/src/Exception/NotEnoughParametersException.php" => array("231", "d4087c9580404276196e8e12d291f215"), + "vendor/php-di/invoker/src/InvokerInterface.php" => array("764", "3fdae5ae733c062d8079bfc88a4761e5"), + "vendor/php-di/invoker/src/Invoker.php" => array("3389", "8e07688dee37b43b24b41184d5cfc821"), + "vendor/php-di/invoker/src/ParameterResolver/AssociativeArrayResolver.php" => array("1128", "5af2f36e60a183ca9405bef1815ee096"), + "vendor/php-di/invoker/src/ParameterResolver/Container/ParameterNameContainerResolver.php" => array("1339", "d0e48cb8de070fa0e794092fdc7509cc"), + "vendor/php-di/invoker/src/ParameterResolver/Container/TypeHintContainerResolver.php" => array("1387", "d535f0145bdf7d310f42842d0a72bd84"), + "vendor/php-di/invoker/src/ParameterResolver/DefaultValueResolver.php" => array("1154", "b472d4c6750f78d0fb41ca4fbda44161"), + "vendor/php-di/invoker/src/ParameterResolver/NumericArrayResolver.php" => array("1089", "cea858228812ef19d33bed176cf48427"), + "vendor/php-di/invoker/src/ParameterResolver/ParameterResolver.php" => array("1006", "ef78d686a08ca6a4b8e7d92a3b678df8"), + "vendor/php-di/invoker/src/ParameterResolver/ResolverChain.php" => array("1756", "efd0937a455fb68686fe44fb16f4dff8"), + "vendor/php-di/invoker/src/Reflection/CallableReflection.php" => array("1596", "db85dc1d9338d816c1ca64f4504e167c"), + "vendor/php-di/php-di/404.md" => array("20", "53eebfedc3afd2e33a7816dd46c5d8c1"), + "vendor/php-di/php-di/change-log.md" => array("14539", "a4826d6c255b6fe0ff4b3b78f3f09664"), + "vendor/php-di/php-di/composer.json" => array("1248", "9e4e4bb2c1a9f107f7d5d7ae026dc5db"), + "vendor/php-di/php-di/CONTRIBUTING.md" => array("1424", "07edb8d01e3f9a43a81650bc6fff7cc2"), + "vendor/php-di/php-di/couscous.yml" => array("3390", "095d8175f02dc19339c8b830dca50af9"), + "vendor/php-di/php-di/LICENSE" => array("1089", "e9ca821bc26512de64035c2dff023766"), + "vendor/php-di/php-di/phpunit.xml.dist" => array("846", "68fd99ec416c05fdfbd14ebc01baf79e"), + "vendor/php-di/php-di/README.md" => array("1652", "aa6599e26e370dcca073059c8ac2e972"), + "vendor/php-di/php-di/src/DI/Annotation/Injectable.php" => array("1564", "a649507af04313d36219606ce8b8f8ac"), + "vendor/php-di/php-di/src/DI/Annotation/Inject.php" => array("2180", "16c5aa80f44a769eb120bc13723e60ae"), + "vendor/php-di/php-di/src/DI/ContainerBuilder.php" => array("7098", "33128814f59c0c89a9b00046dba87718"), + "vendor/php-di/php-di/src/DI/Container.php" => array("11467", "6f74af5cb5dd3be16c6feb9df2a0ce32"), + "vendor/php-di/php-di/src/DI/Debug.php" => array("766", "57998d756b251418d76b979ee11b9a2d"), + "vendor/php-di/php-di/src/DI/Definition/AbstractFunctionCallDefinition.php" => array("1758", "4e0197bff28c8e4b7594c6fc49ad8150"), + "vendor/php-di/php-di/src/DI/Definition/AliasDefinition.php" => array("1221", "2e207ee83335a8d1c4e0ef0e629f0515"), + "vendor/php-di/php-di/src/DI/Definition/ArrayDefinitionExtension.php" => array("1384", "85e1fc5fa91f66e88740f11615bc0273"), + "vendor/php-di/php-di/src/DI/Definition/ArrayDefinition.php" => array("1114", "adbba3c11308afac58777e09ec0b21da"), + "vendor/php-di/php-di/src/DI/Definition/CacheableDefinition.php" => array("367", "317f61260e34be82d3e0a19fd7cd4807"), + "vendor/php-di/php-di/src/DI/Definition/DecoratorDefinition.php" => array("934", "a823779ec18874db0f9e3700d505cb27"), + "vendor/php-di/php-di/src/DI/Definition/Definition.php" => array("575", "227cf34a88d987ef6dea42f1865f9959"), + "vendor/php-di/php-di/src/DI/Definition/Dumper/AliasDefinitionDumper.php" => array("1161", "e14829316ea2a98c5aa568460879155f"), + "vendor/php-di/php-di/src/DI/Definition/Dumper/ArrayDefinitionDumper.php" => array("1599", "ee762c38f7c54f976d6d26cddcf38420"), + "vendor/php-di/php-di/src/DI/Definition/Dumper/DecoratorDefinitionDumper.php" => array("934", "11795066b6be08d2676fa98299caeceb"), + "vendor/php-di/php-di/src/DI/Definition/Dumper/DefinitionDumperDispatcher.php" => array("1968", "7e7772483137360c7e1b59a8162c1186"), + "vendor/php-di/php-di/src/DI/Definition/Dumper/DefinitionDumper.php" => array("614", "1c996de13ce6788b4bbb21e53f386c49"), + "vendor/php-di/php-di/src/DI/Definition/Dumper/EnvironmentVariableDefinitionDumper.php" => array("1838", "1c0c55487ee6f943f0133967ec360856"), + "vendor/php-di/php-di/src/DI/Definition/Dumper/FactoryDefinitionDumper.php" => array("878", "fd18114ba4d1c7e1ceb70b154aee7942"), + "vendor/php-di/php-di/src/DI/Definition/Dumper/ObjectDefinitionDumper.php" => array("4920", "7c01645ecf385b75cd4423ddadd682ef"), + "vendor/php-di/php-di/src/DI/Definition/Dumper/StringDefinitionDumper.php" => array("892", "08f398896bc4fdf4ae9d9e8e9a2a82b9"), + "vendor/php-di/php-di/src/DI/Definition/Dumper/ValueDefinitionDumper.php" => array("1009", "7a937d6b1452dddf4be5356694ae2898"), + "vendor/php-di/php-di/src/DI/Definition/EntryReference.php" => array("1025", "9bb3949a05980d1afae22d18a9196ce7"), + "vendor/php-di/php-di/src/DI/Definition/EnvironmentVariableDefinition.php" => array("2702", "9d9be1f4ff32f6294524eba202bcf63d"), + "vendor/php-di/php-di/src/DI/Definition/Exception/AnnotationException.php" => array("408", "993733253f9090e36b63896a541ee115"), + "vendor/php-di/php-di/src/DI/Definition/Exception/DefinitionException.php" => array("654", "6f1f3f131dbc672365960fb2f97a29b9"), + "vendor/php-di/php-di/src/DI/Definition/FactoryDefinition.php" => array("1525", "2c560419808d1bb58e3d84d22d5dedc8"), + "vendor/php-di/php-di/src/DI/Definition/HasSubDefinition.php" => array("581", "333a29cbde70869767073170aaf78f00"), + "vendor/php-di/php-di/src/DI/Definition/Helper/ArrayDefinitionExtensionHelper.php" => array("1005", "c699cc76c5e3c8597aae6273a4fd783e"), + "vendor/php-di/php-di/src/DI/Definition/Helper/DefinitionHelper.php" => array("522", "6e62c3521c6e0e86dc7371c0b5796e3c"), + "vendor/php-di/php-di/src/DI/Definition/Helper/EnvironmentVariableDefinitionHelper.php" => array("1866", "e2a7f7c9c1248cd25bf2f9edfdcef7ec"), + "vendor/php-di/php-di/src/DI/Definition/Helper/FactoryDefinitionHelper.php" => array("1577", "fd405e53eb6595412d3f5532a7a7c10b"), + "vendor/php-di/php-di/src/DI/Definition/Helper/ObjectDefinitionHelper.php" => array("7966", "619d38517ca09368ee8f31e507a373a7"), + "vendor/php-di/php-di/src/DI/Definition/Helper/StringDefinitionHelper.php" => array("800", "6d4813e6a29f43853ab96f959e461715"), + "vendor/php-di/php-di/src/DI/Definition/Helper/ValueDefinitionHelper.php" => array("822", "ef79a8e4605949145459ff3f3cadb0d7"), + "vendor/php-di/php-di/src/DI/Definition/InstanceDefinition.php" => array("1432", "9b05df639543fcea340c3b78e2af13df"), + "vendor/php-di/php-di/src/DI/Definition/ObjectDefinition/MethodInjection.php" => array("1263", "8cbcee076348cf0fee058fc4e1df4678"), + "vendor/php-di/php-di/src/DI/Definition/ObjectDefinition.php" => array("7369", "6d87f62a5ad8c98f1982a5d2b4867072"), + "vendor/php-di/php-di/src/DI/Definition/ObjectDefinition/PropertyInjection.php" => array("1160", "a39f0832b01ba023d1c115bd9b4a2cea"), + "vendor/php-di/php-di/src/DI/Definition/Resolver/AliasResolver.php" => array("2004", "ddb9ff616e356a98d55103bd04a3bc56"), + "vendor/php-di/php-di/src/DI/Definition/Resolver/ArrayResolver.php" => array("2874", "f2f1a9e357438dbb622619813ee92c11"), + "vendor/php-di/php-di/src/DI/Definition/Resolver/DecoratorResolver.php" => array("3196", "6b9b3339281035c33a0d8f57cf602fd4"), + "vendor/php-di/php-di/src/DI/Definition/Resolver/DefinitionResolver.php" => array("1301", "f6346d70e76ed669ddea8a22fef7f7e3"), + "vendor/php-di/php-di/src/DI/Definition/Resolver/EnvironmentVariableResolver.php" => array("2757", "7cbd3677eff8a427b26cfa06f9e52fda"), + "vendor/php-di/php-di/src/DI/Definition/Resolver/FactoryResolver.php" => array("2273", "1d228640232beae237033e31e35cdf8c"), + "vendor/php-di/php-di/src/DI/Definition/Resolver/InstanceInjector.php" => array("1894", "5ab05bfca01ed507dc51304321b034f1"), + "vendor/php-di/php-di/src/DI/Definition/Resolver/ObjectCreator.php" => array("8786", "958834666773c79ada983384697f94c0"), + "vendor/php-di/php-di/src/DI/Definition/Resolver/ParameterResolver.php" => array("4840", "9eed801d2d4fbd35582a97f168fe604d"), + "vendor/php-di/php-di/src/DI/Definition/Resolver/ResolverDispatcher.php" => array("5022", "b7491e8572f1f51773b76b68eaf6f0b5"), + "vendor/php-di/php-di/src/DI/Definition/Resolver/StringResolver.php" => array("2773", "5a8629f9870aa106d85582ac40ba86fa"), + "vendor/php-di/php-di/src/DI/Definition/Resolver/ValueResolver.php" => array("1492", "fcef9edb8985c574fb7246e056ab0a2e"), + "vendor/php-di/php-di/src/DI/Definition/Source/AnnotationReader.php" => array("9399", "5417670e190f1e454477f846eb50f330"), + "vendor/php-di/php-di/src/DI/Definition/Source/Autowiring.php" => array("1770", "06f0314c312c478397ce1f77ac78c862"), + "vendor/php-di/php-di/src/DI/Definition/Source/CachedDefinitionSource.php" => array("2338", "2bd54552181ff0cc376fa5c0b917b252"), + "vendor/php-di/php-di/src/DI/Definition/Source/DefinitionArray.php" => array("4162", "511d81cbd4bdb16edd659c583756d22f"), + "vendor/php-di/php-di/src/DI/Definition/Source/DefinitionFile.php" => array("1626", "1143fe16f6e091941f619deaea65654e"), + "vendor/php-di/php-di/src/DI/Definition/Source/DefinitionSource.php" => array("714", "f12a05e8a7c4167568747e1b3fc9c215"), + "vendor/php-di/php-di/src/DI/Definition/Source/MutableDefinitionSource.php" => array("323", "13a13f4eb78b57d6e44a10efe2df977c"), + "vendor/php-di/php-di/src/DI/Definition/Source/SourceChain.php" => array("2992", "0e8fd49a4d7e8e6602372d34490524f6"), + "vendor/php-di/php-di/src/DI/Definition/StringDefinition.php" => array("1137", "0140f6c08739a8f9036e149473645ec0"), + "vendor/php-di/php-di/src/DI/Definition/ValueDefinition.php" => array("1194", "58aee00fbb192bf5d5ed60d29532bd48"), + "vendor/php-di/php-di/src/DI/DependencyException.php" => array("390", "1e33f301d9c0dd5987e28e82341f7f6b"), + "vendor/php-di/php-di/src/DI/FactoryInterface.php" => array("1167", "9ec67d26328639027ff0a752b927c241"), + "vendor/php-di/php-di/src/DI/functions.php" => array("4807", "0eae8278e0966ea9405e4c1f88d9ee43"), + "vendor/php-di/php-di/src/DI/Invoker/DefinitionParameterResolver.php" => array("1133", "6300f6cdfaf3a51665ce801a633251dc"), + "vendor/php-di/php-di/src/DI/InvokerInterface.php" => array("366", "40a97309af2e2d56892c7cd0e4cff272"), + "vendor/php-di/php-di/src/DI/NotFoundException.php" => array("458", "580d4be2332c070f76b5af2db1845b5a"), + "vendor/php-di/php-di/src/DI/Proxy/ProxyFactory.php" => array("2827", "a87b6cc9165a3bf2c07532364114a27c"), + "vendor/php-di/php-di/src/DI/Reflection/CallableReflectionFactory.php" => array("1451", "b2f87d9ec62f8d6c93b100b1dfbbcbc9"), + "vendor/php-di/php-di/src/DI/Scope.php" => array("1144", "33a4763935b7ea01d952465f92a9d2c1"), + "vendor/piwik/cache/composer.json" => array("717", "2b18598296132f7f95a154dc35a61386"), + "vendor/piwik/cache/phpunit.xml" => array("550", "003027148995b036415ae0647b810ee8"), + "vendor/piwik/cache/README.md" => array("6517", "fd4c05f9ea47a3bcd51729b3761c3e0b"), + "vendor/piwik/cache/src/Backend/ArrayCache.php" => array("875", "64ddb89e65e0df769276de14f1498c8e"), + "vendor/piwik/cache/src/Backend/Chained.php" => array("2412", "c45449fa8ef3d2fca6f8c8bab8fbf4dd"), + "vendor/piwik/cache/src/Backend/Factory/BackendNotFoundException.php" => array("268", "a5f04138c2c789de2fb730f5b20c056c"), + "vendor/piwik/cache/src/Backend/Factory.php" => array("2789", "aafe84e187fff1228b9b26920413f6fb"), + "vendor/piwik/cache/src/Backend/File.php" => array("4118", "8ccf9b65576f59432907befe7cbac4ba"), + "vendor/piwik/cache/src/Backend/NullCache.php" => array("698", "49ee0dbf7afb707c16b4251a499d9b17"), + "vendor/piwik/cache/src/Backend.php" => array("1589", "2e298daefdf9d01869cdbeb7e9915b0d"), + "vendor/piwik/cache/src/Backend/Redis.php" => array("763", "65fba9eb867ef95b81c23521a0ffd2e6"), + "vendor/piwik/cache/src/Cache.php" => array("1547", "2b6476b6f29c5ecdf904f38440dfec4b"), + "vendor/piwik/cache/src/Eager.php" => array("3720", "9a170a6070a7461c1b93eb47dc48c834"), + "vendor/piwik/cache/src/Lazy.php" => array("3282", "a05d654dd3e12ae3d9781c0d4f00db47"), + "vendor/piwik/cache/src/Transient.php" => array("1817", "1d148ba926ddf1423f61fb673f4d0a1a"), + "vendor/piwik/decompress/composer.json" => array("483", "05300623a40c5013c688fc5dee95c021"), + "vendor/piwik/decompress/libs/PclZip/gnu-lgpl.txt" => array("26934", "f14599a2f089f6ff8c97e2baa4e3d575"), + "vendor/piwik/decompress/libs/PclZip/pclzip.lib.php" => array("191558", "2b8146f5bdef4dc695aae6214f26b30b"), + "vendor/piwik/decompress/libs/PclZip/readme.txt" => array("22011", "0d82536577908a1f78e1b5c6220f5810"), + "vendor/piwik/decompress/libs/README.md" => array("519", "ec1c1d348903b869537db50ba8a06f0c"), + "vendor/piwik/decompress/phpunit.xml" => array("748", "ff1c22662a32f9cbff16d4871e35091c"), + "vendor/piwik/decompress/README.md" => array("1503", "fa831c7578e414bded48acff72dad814"), + "vendor/piwik/decompress/src/DecompressInterface.php" => array("834", "b851f409181f672b91e5226c830abb6f"), + "vendor/piwik/decompress/src/Gzip.php" => array("1593", "80069c36d39643d408115e07fd953091"), + "vendor/piwik/decompress/src/PclZip.php" => array("2120", "d1bcb655c3199852124209dab0edc2e2"), + "vendor/piwik/decompress/src/Tar.php" => array("1900", "73250e2a65b9fcbf7e8d292d97947e31"), + "vendor/piwik/decompress/src/ZipArchive.php" => array("4479", "9e49a7fdbc3f4893f762ec659dcb4096"), + "vendor/piwik/device-detector/Cache/Cache.php" => array("437", "eec9f30425cf26ba1d8041f6809abf9f"), + "vendor/piwik/device-detector/Cache/StaticCache.php" => array("1137", "ff8384ab56487fc7f0fc437ecbaf999a"), + "vendor/piwik/device-detector/composer.json" => array("1226", "ebdbab53b79d86e3324bf9dee6dc97c0"), + "vendor/piwik/device-detector/DeviceDetector.php" => array("22355", "9bc6e15dba074c3c34974c31b32f3721"), + "vendor/piwik/device-detector/Parser/Bot.php" => array("1893", "f247ddac0519f237b153f238511692cb"), + "vendor/piwik/device-detector/Parser/Client/Browser/Engine.php" => array("1900", "71ac08bbe06236c1d6243ab3ddf8f491"), + "vendor/piwik/device-detector/Parser/Client/Browser.php" => array("8064", "59eb45133a212a31de70a075bdd00b5b"), + "vendor/piwik/device-detector/Parser/Client/ClientParserAbstract.php" => array("2164", "7c3b7230ea579204c3198fc87a811a46"), + "vendor/piwik/device-detector/Parser/Client/FeedReader.php" => array("510", "23cdf73457e711c0715c88b3495f0c89"), + "vendor/piwik/device-detector/Parser/Client/Library.php" => array("501", "01fea395b442c264240bce59c4e5e052"), + "vendor/piwik/device-detector/Parser/Client/MediaPlayer.php" => array("512", "8f18f6ba20b9bc0277d20caf0c378e2b"), + "vendor/piwik/device-detector/Parser/Client/MobileApp.php" => array("505", "ce24881b79f45cd29b8cd8d19eb25a39"), + "vendor/piwik/device-detector/Parser/Client/PIM.php" => array("502", "624de8fdf2d3651f625d97d371a781fa"), + "vendor/piwik/device-detector/Parser/Device/Camera.php" => array("639", "2d75efc730ad7b1dbdfa526abb26a50d"), + "vendor/piwik/device-detector/Parser/Device/CarBrowser.php" => array("662", "19c0a0f23049caee76ec6702b1b6fe21"), + "vendor/piwik/device-detector/Parser/Device/Console.php" => array("644", "a1f0f07e4c4e907406bad5e66e6d9d4e"), + "vendor/piwik/device-detector/Parser/Device/DeviceParserAbstract.php" => array("13570", "46aa1d592b94d621facfa929deb936df"), + "vendor/piwik/device-detector/Parser/Device/HbbTv.php" => array("1292", "f6deedef0e099c1087d809afa5c44fa5"), + "vendor/piwik/device-detector/Parser/Device/Mobile.php" => array("488", "b786e48000d4181635a8f37c5ae99977"), + "vendor/piwik/device-detector/Parser/Device/PortableMediaPlayer.php" => array("707", "fab6e15110c98f4be460d5d648a9b431"), + "vendor/piwik/device-detector/Parser/OperatingSystem.php" => array("7415", "c84ba83c516f9371469ed9619cd10dac"), + "vendor/piwik/device-detector/Parser/ParserAbstract.php" => array("7947", "0d9449011522ef41658fd64a8d1c1d5a"), + "vendor/piwik/device-detector/Parser/VendorFragment.php" => array("1101", "339d091036b4b79ee78f10b5506cf1c2"), + "vendor/piwik/device-detector/README.md" => array("10665", "803aa5cade40980b86b10350dadf28f9"), + "vendor/piwik/device-detector/regexes/bots.yml" => array("26699", "536855d5eee9e1eb1e06f64f1e546843"), + "vendor/piwik/device-detector/regexes/client/browser_engine.yml" => array("511", "0ec16431362040744f39b37acff85514"), + "vendor/piwik/device-detector/regexes/client/browsers.yml" => array("15839", "1d52696b74bef999dafe94a6f83350e1"), + "vendor/piwik/device-detector/regexes/client/feed_readers.yml" => array("2735", "538eadcf87556fa24ec5c4452a5f1f00"), + "vendor/piwik/device-detector/regexes/client/libraries.yml" => array("764", "b453f519f3c00edcc991b57d6128b232"), + "vendor/piwik/device-detector/regexes/client/mediaplayers.yml" => array("1579", "fadb1da67c146d6ab9829163fa73c4f1"), + "vendor/piwik/device-detector/regexes/client/mobile_apps.yml" => array("1079", "14229579b2655fcd20f8dd7e2731175d"), + "vendor/piwik/device-detector/regexes/client/pim.yml" => array("887", "64677339838f0c66a4aaacf662987aaa"), + "vendor/piwik/device-detector/regexes/device/cameras.yml" => array("651", "9be6bf6a9a52a23d4ceed4910ff8de22"), + "vendor/piwik/device-detector/regexes/device/car_browsers.yml" => array("299", "5e1268cea5085840117e29791ee533c3"), + "vendor/piwik/device-detector/regexes/device/consoles.yml" => array("754", "8df4d3b748f9c08fdc807c029082c48f"), + "vendor/piwik/device-detector/regexes/device/mobiles.yml" => array("116747", "f302406bf965dd1371e94f7324ec9d01"), + "vendor/piwik/device-detector/regexes/device/portable_media_player.yml" => array("1547", "b8c2bf3e673d81c057959f344b52c255"), + "vendor/piwik/device-detector/regexes/device/televisions.yml" => array("4717", "7172420aadfb4b118f00f01bbdb485cb"), + "vendor/piwik/device-detector/regexes/oss.yml" => array("10703", "fe4ac9986e1febe3bd166758510eec1d"), + "vendor/piwik/device-detector/regexes/vendorfragments.yml" => array("870", "c0c55dce0e8b49145dc8b4df89aac9cc"), + "vendor/piwik/ini/composer.json" => array("418", "24d3ce247f96f7a69de1b20ce3dcabc9"), + "vendor/piwik/ini/phpunit.xml" => array("550", "003027148995b036415ae0647b810ee8"), + "vendor/piwik/ini/README.md" => array("2131", "b691656f2c2006359dba19e76b76ae69"), + "vendor/piwik/ini/src/IniReader.php" => array("10920", "f7dcad9c3d95f8527200c47145f72d68"), + "vendor/piwik/ini/src/IniReadingException.php" => array("280", "c4bc3c466314e91bd910fc28bb6d0564"), + "vendor/piwik/ini/src/IniWriter.php" => array("3138", "57246595dfb934634692eb9767f7fbc0"), + "vendor/piwik/ini/src/IniWritingException.php" => array("280", "2f0377f8c02b1fd71017ccdf02d38cf9"), + "vendor/piwik/network/composer.json" => array("392", "41b50bd22c70ad245bd095191a560e2e"), + "vendor/piwik/network/phpunit.xml" => array("587", "4bf37072775ecb24b3a27216c4dab4d4"), + "vendor/piwik/network/README.md" => array("1764", "1752a4a860b758599dcaa7c318a73073"), + "vendor/piwik/network/src/IP.php" => array("5576", "13a89aaa67d90828d5cdc6ce1ffee4ab"), + "vendor/piwik/network/src/IPUtils.php" => array("5828", "dfbfeca489cddcffe690715f9505f243"), + "vendor/piwik/network/src/IPv4.php" => array("850", "58d5ed5212bcf801fcb1b12cf9e686c8"), + "vendor/piwik/network/src/IPv6.php" => array("1820", "49b818a413746ce19c0ce3cd09d78b84"), + "vendor/piwik/piwik-php-tracker/composer.json" => array("658", "47974c3d4bd1134b8f977f04030ac5ae"), + "vendor/piwik/piwik-php-tracker/LICENSE" => array("1503", "ea8df6f6cdb84d038a21520c73be939b"), + "vendor/piwik/piwik-php-tracker/PiwikTracker.php" => array("64003", "b7a27650db6279e0614c575d70294a70"), + "vendor/piwik/piwik-php-tracker/README.md" => array("705", "445bbdb60454851043b3c4801897ebd6"), + "vendor/piwik/referrer-spam-blacklist/composer.json" => array("150", "3c4ddde1c99338292d3b052154aaa19f"), + "vendor/piwik/referrer-spam-blacklist/CONTRIBUTING.md" => array("865", "0ccaa1b51b12b0bba85c158a24debd5d"), + "vendor/piwik/referrer-spam-blacklist/README.md" => array("4111", "123aa46dc6c0cdc79cbc36d95fa7dd7c"), + "vendor/piwik/referrer-spam-blacklist/spammers.txt" => array("5378", "2319d199710ad61b6ff5c59d15b8aec3"), + "vendor/piwik/searchengine-and-social-list/composer.json" => array("159", "6912ff808dee09bd8b1a065e9e77eea1"), + "vendor/piwik/searchengine-and-social-list/README.md" => array("4787", "3971f2f0bc9cb731b7238664eccfafd6"), + "vendor/piwik/searchengine-and-social-list/SearchEngines.yml" => array("36892", "06aec8f4a7419888ccf060c7887f0ba8"), + "vendor/piwik/searchengine-and-social-list/Socials.yml" => array("2088", "2c156f1e654854d8893520c6bca4bea3"), + "vendor/psr/log/composer.json" => array("358", "de1539d2aa7830d13a968d33d1039f1a"), + "vendor/psr/log/LICENSE" => array("1085", "1a74629072fd794937be394ab689327e"), + "vendor/psr/log/Psr/Log/AbstractLogger.php" => array("3024", "a57c1be541193b72d09307bb0dfb9ed2"), + "vendor/psr/log/Psr/Log/InvalidArgumentException.php" => array("96", "7d2f0bd1583524d739fff12f0507de65"), + "vendor/psr/log/Psr/Log/LoggerAwareInterface.php" => array("288", "3ba5ffac8108e1da7657b1fad8651900"), + "vendor/psr/log/Psr/Log/LoggerAwareTrait.php" => array("351", "5b3adf6c4f09c61d7488b0f9ac2c696a"), + "vendor/psr/log/Psr/Log/LoggerInterface.php" => array("2965", "023885df6a26d8137d5a13da51f066d2"), + "vendor/psr/log/Psr/Log/LoggerTrait.php" => array("3288", "1cb8db6d0b81cf85f81b6c7c09db7a9a"), + "vendor/psr/log/Psr/Log/LogLevel.php" => array("312", "19ab55cc711ed2f3ab2ec72e7f0600cb"), + "vendor/psr/log/Psr/Log/NullLogger.php" => array("641", "e71559fea0239b7441d221f8c7beae5b"), + "vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.php" => array("3874", "867f36a94c35322470458f4eba246458"), + "vendor/psr/log/README.md" => array("1088", "144a71a4e1f9c67ac79751acc37614c4"), + "vendor/symfony/console/Symfony/Component/Console/Application.php" => array("37790", "e975b9d1b85d9acff879cb363e565da8"), + "vendor/symfony/console/Symfony/Component/Console/CHANGELOG.md" => array("2099", "19e8a451cd9f8adbc4b1c1c499e8a4a6"), + "vendor/symfony/console/Symfony/Component/Console/Command/Command.php" => array("17872", "7379e2a29cd8ebe710f31247a1950233"), + "vendor/symfony/console/Symfony/Component/Console/Command/HelpCommand.php" => array("2658", "5a868d08465065ebc2d1e85d64dd6cf7"), + "vendor/symfony/console/Symfony/Component/Console/Command/ListCommand.php" => array("2748", "56678782b084fd8ff7b73248d6463ada"), + "vendor/symfony/console/Symfony/Component/Console/composer.json" => array("1057", "d8d71cb83b8f287e4df6564cb9026dfa"), + "vendor/symfony/console/Symfony/Component/Console/ConsoleEvents.php" => array("1591", "0a76aea6561a0b6c62971aa2cbd2b058"), + "vendor/symfony/console/Symfony/Component/Console/Descriptor/ApplicationDescription.php" => array("3611", "3849ebf225805d613401ac9160d92c89"), "vendor/symfony/console/Symfony/Component/Console/Descriptor/DescriptorInterface.php" => array("737", "d5b9abe215ed28ac8ca7fe9d72fc9ddd"), - "vendor/symfony/console/Symfony/Component/Console/Descriptor/Descriptor.php" => array("3516", "e08a534aa99b2e90d1538a0c166973e5"), - "vendor/symfony/console/Symfony/Component/Console/Descriptor/JsonDescriptor.php" => array("4981", "a997223049fcf34294263e3a6c37e230"), - "vendor/symfony/console/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php" => array("4910", "bec9d13cc9cb6e778c4f4083199137b6"), - "vendor/symfony/console/Symfony/Component/Console/Descriptor/TextDescriptor.php" => array("8184", "97b571928164645af31610ea97bc43fb"), - "vendor/symfony/console/Symfony/Component/Console/Descriptor/XmlDescriptor.php" => array("9712", "a9214dd6cf39faa121dc4de62996071a"), - "vendor/symfony/console/Symfony/Component/Console/Event/ConsoleCommandEvent.php" => array("448", "2832db2aee4207de5d53d012b314436d"), + "vendor/symfony/console/Symfony/Component/Console/Descriptor/Descriptor.php" => array("3530", "23c50f3de98246e9b48913f74c2bdb4c"), + "vendor/symfony/console/Symfony/Component/Console/Descriptor/JsonDescriptor.php" => array("4909", "9f49ee47be6e7370625caaa7bcf00836"), + "vendor/symfony/console/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php" => array("4911", "5f5217660c4680db12a74ece15ae743d"), + "vendor/symfony/console/Symfony/Component/Console/Descriptor/TextDescriptor.php" => array("9127", "a59f225a378c9940570228ae64ac2b40"), + "vendor/symfony/console/Symfony/Component/Console/Descriptor/XmlDescriptor.php" => array("9728", "cc0c1389ddab8272f805ed40ef008a33"), + "vendor/symfony/console/Symfony/Component/Console/Event/ConsoleCommandEvent.php" => array("1336", "9334cbd4f82765230bb6f5a1e985a167"), "vendor/symfony/console/Symfony/Component/Console/Event/ConsoleEvent.php" => array("1464", "aaaa8c390cd99e01d7bb2649a735e469"), - "vendor/symfony/console/Symfony/Component/Console/Event/ConsoleExceptionEvent.php" => array("1601", "22e5c027468f33b0bd20540b41af7a74"), - "vendor/symfony/console/Symfony/Component/Console/Event/ConsoleTerminateEvent.php" => array("1318", "82fbfa586f39a859d66a45221cb773c1"), - "vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterInterface.php" => array("1763", "75e7364468e427f080f86354f9bab9b9"), - "vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatter.php" => array("6341", "304b90bba47ad3ed5534a37ec1647980"), + "vendor/symfony/console/Symfony/Component/Console/Event/ConsoleExceptionEvent.php" => array("1597", "2940244a154aeb8bfbcd9cf8d805702a"), + "vendor/symfony/console/Symfony/Component/Console/Event/ConsoleTerminateEvent.php" => array("1306", "44e02b0b4064120d43fcf762a80d7331"), + "vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterInterface.php" => array("1754", "7daf3d13402d56dd1e3e20435e5414fb"), + "vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatter.php" => array("6457", "b8fcb83352e271ebf4a24ec52174b48e"), "vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyleInterface.php" => array("1439", "091bf3ec019e14a48b1f61ef7cb6871a"), - "vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyle.php" => array("5926", "4d0b8e5a84926a6013be77aa123e61eb"), - "vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php" => array("2788", "27952fd939968e3615ce7ada4c761e0e"), - "vendor/symfony/console/Symfony/Component/Console/.gitignore" => array("34", "a1155c508134e9bda943ae266aee1819"), - "vendor/symfony/console/Symfony/Component/Console/Helper/DescriptorHelper.php" => array("2561", "d5bb73eb73014de416365c07cd81cac5"), - "vendor/symfony/console/Symfony/Component/Console/Helper/DialogHelper.php" => array("16446", "ae0bb3641414453024a3d71275abd1c1"), - "vendor/symfony/console/Symfony/Component/Console/Helper/FormatterHelper.php" => array("2236", "86ebeb32b3353bc7ba4363bf7a05fa2f"), + "vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyle.php" => array("6876", "1fbf2f24197148dafcab33a47a01e1fa"), + "vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php" => array("2789", "f4e0c65c42142834b564e341a63d6a7e"), + "vendor/symfony/console/Symfony/Component/Console/Helper/DebugFormatterHelper.php" => array("4179", "1f30fb180be772d344aba52ea74abb60"), + "vendor/symfony/console/Symfony/Component/Console/Helper/DescriptorHelper.php" => array("2592", "622aab20b7a9c4b69f581c3a5d67e650"), + "vendor/symfony/console/Symfony/Component/Console/Helper/DialogHelper.php" => array("16529", "4b35ff40c568a40763e3fcb707e545f9"), + "vendor/symfony/console/Symfony/Component/Console/Helper/FormatterHelper.php" => array("2325", "81f2ef763d845157091ac229f0a4c35e"), "vendor/symfony/console/Symfony/Component/Console/Helper/HelperInterface.php" => array("1011", "e9012e6f3559129d513da4ba75ebf4e9"), - "vendor/symfony/console/Symfony/Component/Console/Helper/Helper.php" => array("1451", "426298ce1ccd85ab527a5cef78f9a33b"), - "vendor/symfony/console/Symfony/Component/Console/Helper/HelperSet.php" => array("2526", "9c0badab04f9b052cec6a06c3f11fb07"), - "vendor/symfony/console/Symfony/Component/Console/Helper/InputAwareHelper.php" => array("747", "c75592e1f3b10c1f35961c50d55ae58d"), - "vendor/symfony/console/Symfony/Component/Console/Helper/ProgressHelper.php" => array("11887", "c6c85dbb2f08fa52bc6739ba6896bdc9"), - "vendor/symfony/console/Symfony/Component/Console/Helper/TableHelper.php" => array("12820", "1671e0f028279a642e6c0f7dcaa474c4"), - "vendor/symfony/console/Symfony/Component/Console/Input/ArgvInput.php" => array("10680", "e95b97c2b870d6223e84a995c93f16ce"), - "vendor/symfony/console/Symfony/Component/Console/Input/ArrayInput.php" => array("5925", "1e51135fb1c605381452c4d0cae3f1c4"), - "vendor/symfony/console/Symfony/Component/Console/Input/InputArgument.php" => array("3287", "c2f88afd2a0f1601dcfd3cce07092197"), + "vendor/symfony/console/Symfony/Component/Console/Helper/Helper.php" => array("3148", "3a0c5d5521d3a6d8800346547253ed7b"), + "vendor/symfony/console/Symfony/Component/Console/Helper/HelperSet.php" => array("2523", "d4bcdcc2056214dc911aa3a95b3a707b"), + "vendor/symfony/console/Symfony/Component/Console/Helper/InputAwareHelper.php" => array("747", "46d23e6342ebb9cf17e7363b2295f5ba"), + "vendor/symfony/console/Symfony/Component/Console/Helper/ProcessHelper.php" => array("4825", "f8014e14d56132a3324e8fa4f9a46a86"), + "vendor/symfony/console/Symfony/Component/Console/Helper/ProgressBar.php" => array("17141", "3646dc3f3ade7c1d51a233177ccd3ac3"), + "vendor/symfony/console/Symfony/Component/Console/Helper/ProgressHelper.php" => array("12079", "ecde8c07f3709cfa3350a34b45ad7eb6"), + "vendor/symfony/console/Symfony/Component/Console/Helper/QuestionHelper.php" => array("12608", "ccd04b6cf3e259fa277cc0eb6ed37023"), + "vendor/symfony/console/Symfony/Component/Console/Helper/TableHelper.php" => array("5895", "ad3438727bab52219452d8a08f6e5b93"), + "vendor/symfony/console/Symfony/Component/Console/Helper/Table.php" => array("10349", "965b29a7911faef3b468c6003fd7dd7e"), + "vendor/symfony/console/Symfony/Component/Console/Helper/TableSeparator.php" => array("405", "531d3585fbc7e64c7e5f11f259dab7eb"), + "vendor/symfony/console/Symfony/Component/Console/Helper/TableStyle.php" => array("4943", "814b0ebf864228fc5fabdb0cee6858f2"), + "vendor/symfony/console/Symfony/Component/Console/Input/ArgvInput.php" => array("10696", "b322c20a800cf1f6563e8fb787094fa6"), + "vendor/symfony/console/Symfony/Component/Console/Input/ArrayInput.php" => array("5963", "0709a96fc72a94d72e22f1da7e1fc126"), + "vendor/symfony/console/Symfony/Component/Console/Input/InputArgument.php" => array("3263", "9b4a500384e6bbbbb456730f5f45173a"), "vendor/symfony/console/Symfony/Component/Console/Input/InputAwareInterface.php" => array("606", "a67cb5e626983b20e2c03b618d7ab012"), - "vendor/symfony/console/Symfony/Component/Console/Input/InputDefinition.php" => array("12328", "74e92dce0c01082eb3d96959f3268509"), - "vendor/symfony/console/Symfony/Component/Console/Input/InputInterface.php" => array("4150", "9a40ecfaca79fa6bed0e459c035fdb5c"), - "vendor/symfony/console/Symfony/Component/Console/Input/InputOption.php" => array("5974", "2712657bb89ede768dfb9d0eb1f1d256"), - "vendor/symfony/console/Symfony/Component/Console/Input/Input.php" => array("6144", "87ba519b169574bdcaabaef68740989a"), - "vendor/symfony/console/Symfony/Component/Console/Input/StringInput.php" => array("2811", "eaf496e1188492448d9cfe8dfbcede6a"), - "vendor/symfony/console/Symfony/Component/Console/LICENSE" => array("1065", "09ce405e925cdeb923da1789121864c7"), + "vendor/symfony/console/Symfony/Component/Console/Input/InputDefinition.php" => array("12149", "cda35eaf829825f8c3753fa291d866b5"), + "vendor/symfony/console/Symfony/Component/Console/Input/InputInterface.php" => array("4125", "a9f0876e2ef074464cde27a9c5120981"), + "vendor/symfony/console/Symfony/Component/Console/Input/InputOption.php" => array("5946", "2206c5212d4030b5f74dc6b15cc69225"), + "vendor/symfony/console/Symfony/Component/Console/Input/Input.php" => array("6120", "e99cc00559eda36a99ccfa518d6b0518"), + "vendor/symfony/console/Symfony/Component/Console/Input/StringInput.php" => array("2725", "19e11d9e2720769350efc35a403b658c"), + "vendor/symfony/console/Symfony/Component/Console/LICENSE" => array("1065", "56dedd4bd25ecd034ac4e1c17ebba0cc"), + "vendor/symfony/console/Symfony/Component/Console/Logger/ConsoleLogger.php" => array("3813", "ff6421408fcb0ee16bc7be84d66cfe8a"), "vendor/symfony/console/Symfony/Component/Console/Output/BufferedOutput.php" => array("872", "07bca2bc77753b0b0cc4502f109c4a8e"), "vendor/symfony/console/Symfony/Component/Console/Output/ConsoleOutputInterface.php" => array("843", "9cf400160437ff90febb665841a968fe"), - "vendor/symfony/console/Symfony/Component/Console/Output/ConsoleOutput.php" => array("2941", "64e00ce49a3a47e8ea7b8885f10c1ee6"), - "vendor/symfony/console/Symfony/Component/Console/Output/NullOutput.php" => array("1748", "561f460b93be0c27aa9be546af49e9ec"), - "vendor/symfony/console/Symfony/Component/Console/Output/OutputInterface.php" => array("2893", "ac6d776cf82c8b887395c54482f17bd7"), - "vendor/symfony/console/Symfony/Component/Console/Output/Output.php" => array("4155", "15c209a7581a632e6979d5441a02f241"), - "vendor/symfony/console/Symfony/Component/Console/Output/StreamOutput.php" => array("3149", "dc57630f625779ad86f6f3d8240ad0c3"), - "vendor/symfony/console/Symfony/Component/Console/phpunit.xml.dist" => array("873", "c94fc23909e8f1d27908cd0486653611"), - "vendor/symfony/console/Symfony/Component/Console/README.md" => array("1959", "70a92aecafcdc1f689a2a798acccefe4"), - "vendor/symfony/console/Symfony/Component/Console/Shell.php" => array("6395", "fedc847f6c992a242e45a167b0cf1866"), - "vendor/symfony/console/Symfony/Component/Console/Tester/ApplicationTester.php" => array("3430", "f2c7b88716f13cb54825a933832a74a8"), - "vendor/symfony/console/Symfony/Component/Console/Tester/CommandTester.php" => array("3612", "9b7bf197b61b1b3c23772c57a2ec3ee5"), + "vendor/symfony/console/Symfony/Component/Console/Output/ConsoleOutput.php" => array("3678", "3d93df750561afef312f476bd7a8de16"), + "vendor/symfony/console/Symfony/Component/Console/Output/NullOutput.php" => array("2015", "82d9a6c1fb3894504d7ee349d0bc77f8"), + "vendor/symfony/console/Symfony/Component/Console/Output/OutputInterface.php" => array("2849", "836c4843f80a794acc7aa0e1e2d42b15"), + "vendor/symfony/console/Symfony/Component/Console/Output/Output.php" => array("4153", "f44fc2d4776c81356966aedef42dce28"), + "vendor/symfony/console/Symfony/Component/Console/Output/StreamOutput.php" => array("2999", "8152ff1f16ba69cea7677ed6f5a65d08"), + "vendor/symfony/console/Symfony/Component/Console/phpunit.xml.dist" => array("826", "8ccc6b24534ec4ac815e779ca43c079a"), + "vendor/symfony/console/Symfony/Component/Console/Question/ChoiceQuestion.php" => array("3876", "80aec3f68cab6e7b27983d10f1ee5a11"), + "vendor/symfony/console/Symfony/Component/Console/Question/ConfirmationQuestion.php" => array("1316", "20d79db7cf694c34ddd252715756e9c3"), + "vendor/symfony/console/Symfony/Component/Console/Question/Question.php" => array("5484", "dc78751c1dfe8d6453816296a4868e5e"), + "vendor/symfony/console/Symfony/Component/Console/README.md" => array("1906", "81b8ef7ae615346a92585a2db6b26ec5"), + "vendor/symfony/console/Symfony/Component/Console/Shell.php" => array("6392", "f5705c7c034ea72937ab718bed705229"), + "vendor/symfony/console/Symfony/Component/Console/Tester/ApplicationTester.php" => array("3419", "f9f7325bd54a1986ceb41f929986915b"), + "vendor/symfony/console/Symfony/Component/Console/Tester/CommandTester.php" => array("3659", "d0dd63b3455ec5862200c13c308d25cc"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/CHANGELOG.md" => array("696", "745dd467bb8d944ee82b38034a3b71d2"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/composer.json" => array("1142", "f2ff5d79977cd6bc0d3b60cf1b1894bf"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/ContainerAwareEventDispatcher.php" => array("6573", "0bcadf91749e82c0e6f3ad64c89be078"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcherInterface.php" => array("803", "aa09cc5a555cfad4239045906e2850b0"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php" => array("10273", "c4e84cc92b835e7acc5d4a5df9b25aef"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/Debug/WrappedListener.php" => array("1749", "b25eea7b4663606e7ee568151f63f246"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php" => array("4299", "737fe94715322e3c7d2b1a06da312f3f"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/EventDispatcherInterface.php" => array("3029", "571cc8d5ac702f06eacaa849e3774226"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/EventDispatcher.php" => array("5588", "726413ae13dfec061f23b8fff4235d25"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/Event.php" => array("3207", "ec4077bd7b6bf237bec7136cbef589e0"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/EventSubscriberInterface.php" => array("1581", "c14793051c0916ae555a170db42d635c"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/GenericEvent.php" => array("3903", "57592b7539db9e18dc64528de170050a"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/ImmutableEventDispatcher.php" => array("2240", "2aa90b7e3856053d0d6100d6e13832f8"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/LICENSE" => array("1065", "56dedd4bd25ecd034ac4e1c17ebba0cc"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/phpunit.xml.dist" => array("834", "80e32ada6de2dd8b634ed666650fc87e"), + "vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/README.md" => array("629", "ce7f9b787b9c507c07d1e6e36254a7e7"), + "vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/CHANGELOG.md" => array("219", "094d6ae95193dc93574972855a7d4832"), + "vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/composer.json" => array("1373", "65c151dab71c7917546af9ce2dc17642"), + "vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php" => array("1611", "770271a4d444a5edd938fde8d3329f95"), + "vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php" => array("1866", "802bc632e18f892ac6149df81b785064"), + "vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php" => array("5877", "71bc89a0d486b815c92a6552599e9652"), + "vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/Handler/DebugHandler.php" => array("1446", "450ee4aa4f8ff115dc3299b5dcdb52d2"), + "vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php" => array("1584", "1e0758ae14cad212d3c83378934145d6"), + "vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php" => array("1939", "7fc6f9e4753cf67533a0ca3ca0615508"), + "vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php" => array("2235", "65d9b7f8bd3838197c58f97476af73a8"), + "vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/LICENSE" => array("1065", "56dedd4bd25ecd034ac4e1c17ebba0cc"), + "vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/Logger.php" => array("2446", "27c5f10435a78e290ec8ac3cfc248318"), + "vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/phpunit.xml.dist" => array("775", "fdf89201b4cf373b7a34e2c47857fa95"), + "vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/Processor/WebProcessor.php" => array("967", "fdbdb9d4ee923c4c54375369f6cbd68f"), + "vendor/symfony/monolog-bridge/Symfony/Bridge/Monolog/README.md" => array("253", "6ed8293be5da72ba7c55961f646c4fd8"), + "vendor/tecnickcom/tcpdf/CHANGELOG.TXT" => array("116016", "e819e4201ca72583101a5419252fb5d4"), + "vendor/tecnickcom/tcpdf/composer.json" => array("944", "0eedf4e9c9c3e3fc74587f2ecb50684b"), + "vendor/tecnickcom/tcpdf/config/tcpdf_config.php" => array("5371", "34c21a79ab0bc8332ea5c142e4b8d636"), + "vendor/tecnickcom/tcpdf/fonts/aealarabiya.ctg.z" => array("1849", "0eac1ff3b999791227dadfb340ddd83f"), + "vendor/tecnickcom/tcpdf/fonts/aealarabiya.php" => array("35225", "c49883919b7912057b1ffad674a9c7c2"), + "vendor/tecnickcom/tcpdf/fonts/aealarabiya.z" => array("56189", "18ca47dea418152b135e93b582f84d3b"), + "vendor/tecnickcom/tcpdf/fonts/dejavusans.ctg.z" => array("10454", "4856e25c5027ef93e512646becf3eda5"), + "vendor/tecnickcom/tcpdf/fonts/dejavusans.php" => array("202079", "8b75ae7921f26b4f4b11d18ed921248e"), + "vendor/tecnickcom/tcpdf/fonts/dejavusans.z" => array("375806", "f910decf31ee5f189c5397ee0937794a"), + "vendor/tecnickcom/tcpdf/fonts/freesans.ctg.z" => array("8661", "be9c85808d55cbef8619a67495a43fbc"), + "vendor/tecnickcom/tcpdf/fonts/freesans.php" => array("165789", "4fd59032d7c3a59fd45028bafa245721"), + "vendor/tecnickcom/tcpdf/fonts/freesans.z" => array("807705", "bc1ca4370455ca07e2c22b8c5c5d3cce"), + "vendor/tecnickcom/tcpdf/fonts/freeserif.ctg.z" => array("12610", "0daa9bbabdda3c85e5060086916447d5"), + "vendor/tecnickcom/tcpdf/fonts/freeserif.php" => array("242895", "02aac38356af65808e08e21f1ddd225d"), + "vendor/tecnickcom/tcpdf/fonts/freeserif.z" => array("1835770", "084dca967854129a0aaec1713a46733e"), + "vendor/tecnickcom/tcpdf/fonts/helveticabi.php" => array("2589", "c22fdc8941f2956e0930b20105870468"), + "vendor/tecnickcom/tcpdf/fonts/helveticab.php" => array("2580", "3daad3713df02c15beebd09ceecacacd"), + "vendor/tecnickcom/tcpdf/fonts/helveticai.php" => array("2584", "e0a7f23376f50de631db93814aff2e35"), + "vendor/tecnickcom/tcpdf/fonts/helvetica.php" => array("2575", "2a315fa2593161154c319788f0ef2127"), + "vendor/tecnickcom/tcpdf/fonts/hysmyeongjostdmedium.php" => array("1829", "51f6fe162641de3714866950d5eff4e8"), + "vendor/tecnickcom/tcpdf/fonts/kozgopromedium.php" => array("3577", "2c5e8a67d1a805aae9842bbad59a873f"), + "vendor/tecnickcom/tcpdf/fonts/kozminproregular.php" => array("3454", "78fdf805f1cea6cd01912192821ec734"), + "vendor/tecnickcom/tcpdf/fonts/msungstdlight.php" => array("1550", "c940b153fb6c5b3498efa181881b5b6c"), + "vendor/tecnickcom/tcpdf/fonts/stsongstdlight.php" => array("1627", "eb85dc872664c0769e9fab1b7540b4d5"), + "vendor/tecnickcom/tcpdf/fonts/symbol.php" => array("2555", "20e28c8b386ddbb38ead777f717d7c44"), + "vendor/tecnickcom/tcpdf/fonts/zapfdingbats.php" => array("2552", "191b3c2e856e750c06c0ba7987f902fb"), + "vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php" => array("42800", "9a29da1e201fb23de4f499adbb9f6a71"), + "vendor/tecnickcom/tcpdf/include/barcodes/pdf417.php" => array("53882", "3abe66ba8da6b6bf9cf1c6b0e907d51d"), + "vendor/tecnickcom/tcpdf/include/barcodes/qrcode.php" => array("80083", "f214c50485dc7301cdd70b23e46193a0"), + "vendor/tecnickcom/tcpdf/include/sRGB.icc" => array("3048", "060e79448f1454582be37b3de490da2f"), + "vendor/tecnickcom/tcpdf/include/tcpdf_colors.php" => array("14699", "cacdbe68a428ae36151a3d1152b2b77b"), + "vendor/tecnickcom/tcpdf/include/tcpdf_filters.php" => array("14682", "fb794db6e06fa3cf7479fc889894caf3"), + "vendor/tecnickcom/tcpdf/include/tcpdf_font_data.php" => array("313432", "8f83bbc144d70505672f82679546c72d"), + "vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php" => array("95920", "a70cd0b384c720e631792fb309443ac7"), + "vendor/tecnickcom/tcpdf/include/tcpdf_images.php" => array("11509", "72ef804a6aa7becfeda43428ec55489b"), + "vendor/tecnickcom/tcpdf/include/tcpdf_static.php" => array("107704", "ff9d445fef5a526e78949020d4c21b7e"), + "vendor/tecnickcom/tcpdf/LICENSE.TXT" => array("43636", "5c87b66a5358ebcc495b03e0afcd342c"), + "vendor/tecnickcom/tcpdf/README.TXT" => array("5631", "aa8e457a1186b5cd5937806123e6411d"), + "vendor/tecnickcom/tcpdf/tcpdf_autoconfig.php" => array("7155", "bcb93bbeb8cf2831e49ff5541d277a1f"), + "vendor/tecnickcom/tcpdf/tcpdf_barcodes_1d.php" => array("73406", "d306e9ad7b8b67464493c3281417afdc"), + "vendor/tecnickcom/tcpdf/tcpdf_barcodes_2d.php" => array("14683", "17bfd10e3232de9145f5b74a6ef6afac"), + "vendor/tecnickcom/tcpdf/tcpdf_import.php" => array("3327", "6bb88a8a3d69511d1bf9e7af12ab5f47"), + "vendor/tecnickcom/tcpdf/tcpdf_parser.php" => array("27636", "2018d958a0c5723149e988ee5d2290d1"), + "vendor/tecnickcom/tcpdf/tcpdf.php" => array("897169", "7d6a2e3735c4aaf11e08a35c5ce31405"), + "vendor/tecnickcom/tcpdf/tools/convert_fonts_examples.txt" => array("2003", "01d1bb3c8c8bdb35f3837e2715dbe681"), + "vendor/tecnickcom/tcpdf/tools/.htaccess" => array("14", "183e8e4abc660eaba3c3da4bb82b0bcf"), + "vendor/tecnickcom/tcpdf/tools/tcpdf_addfont.php" => array("7449", "8a55d83a4002cf045b586982b64c8356"), "vendor/tedivm/jshrink/composer.json" => array("418", "b7603f6ba06352f101cc74dc6b72c5b1"), - "vendor/tedivm/jshrink/.gitignore" => array("70", "416a3b6d772480ee1f7833d8a790a6a2"), "vendor/tedivm/jshrink/LICENSE" => array("1493", "0a8fd596034db85a8ae22c1d2330edfb"), - "vendor/tedivm/jshrink/README.md" => array("745", "57a8f87308e7b475d9b34689d8c1f381"), + "vendor/tedivm/jshrink/package.xml" => array("7686", "7f1efac19edc2eb5be7b21f9457e1ab8"), + "vendor/tedivm/jshrink/phpunit.xml.dist" => array("414", "77a32cc6e305e2c1a2ac61b3262b2e46"), + "vendor/tedivm/jshrink/README.md" => array("1893", "eff0027af64c410aa1243fb370088565"), "vendor/tedivm/jshrink/src/JShrink/Minifier.php" => array("11416", "d245eafc47812bd47bf18fb4ede5508d"), - "vendor/tedivm/jshrink/.travis.yml" => array("59", "cbac6ff01d60d6cf2045eb78150dabe3"), - "vendor/twig/twig/CHANGELOG" => array("32353", "03b21b00ace91062257dd2855ee196cf"), - "vendor/twig/twig/composer.json" => array("1102", "b2d4cf29abf14c07d3eace63816c58c3"), - "vendor/twig/twig/.editorconfig" => array("224", "c1be7a30bae43bc6616f3f266e199030"), + "vendor/twig/twig/CHANGELOG" => array("37100", "fa3a21578ca0fd4228094277c7390bae"), + "vendor/twig/twig/composer.json" => array("1193", "14ec2cf5c9e4c889948dd5bca4068781"), "vendor/twig/twig/ext/twig/config.m4" => array("221", "20ad1d1005402766ddd16b7110aaf4ca"), "vendor/twig/twig/ext/twig/config.w32" => array("149", "2b1bdfd1a2b8c54966b04f7d143c6632"), - "vendor/twig/twig/ext/twig/.gitignore" => array("328", "7567b95d7259dea2d26ea326730c357d"), - "vendor/twig/twig/ext/twig/LICENSE" => array("1527", "c74e1ac7caf1b0a49bd267b04f9205de"), - "vendor/twig/twig/ext/twig/php_twig.h" => array("1113", "dfc045ec57553713c0db3688a1e69500"), - "vendor/twig/twig/ext/twig/twig.c" => array("30216", "0c2a8fd5aabd9417c98c271b625ab36d"), - "vendor/twig/twig/.gitignore" => array("27", "e8e8b05dcada28af47c4a7f92a2106c0"), - "vendor/twig/twig/lib/Twig/Autoloader.php" => array("1161", "636fed90535196cf757198769e7f26bd"), - "vendor/twig/twig/lib/Twig/CompilerInterface.php" => array("779", "5c6bd37854846a6b1df5e948f2129d53"), - "vendor/twig/twig/lib/Twig/Compiler.php" => array("6906", "61f080c47c7a7f8cd7b9b19a0b418aaf"), - "vendor/twig/twig/lib/Twig/Environment.php" => array("36964", "1a9da7fe09974a4423d8a844fc9de7bb"), + "vendor/twig/twig/ext/twig/php_twig.h" => array("1205", "d6c87ec218f9f3f888c49d2caeb2e3b3"), + "vendor/twig/twig/ext/twig/twig.c" => array("31861", "f9d3c4a10bdf7e014e7ce1182d742b2a"), + "vendor/twig/twig/lib/Twig/Autoloader.php" => array("1440", "ed056f0873067ac38e7f929f09393860"), + "vendor/twig/twig/lib/Twig/BaseNodeVisitor.php" => array("1794", "7000bc1312328eb8d35ebb2c926e1fa0"), + "vendor/twig/twig/lib/Twig/Cache/Filesystem.php" => array("2461", "28874b26b33726bcffed71b2a4660eb3"), + "vendor/twig/twig/lib/Twig/CacheInterface.php" => array("1391", "3032bd05a36c6e5f5f1ffa105fd071b4"), + "vendor/twig/twig/lib/Twig/Cache/Null.php" => array("758", "f755e8a4987054919cd07b518e0b6f3d"), + "vendor/twig/twig/lib/Twig/CompilerInterface.php" => array("782", "3cda46652dfae43db0ba6edad95d0d86"), + "vendor/twig/twig/lib/Twig/Compiler.php" => array("7069", "05cd6f7a7672f9039e4f326fc07f68da"), + "vendor/twig/twig/lib/Twig/Environment.php" => array("42395", "219dd14ce6b053e7c34f1b114e752b81"), "vendor/twig/twig/lib/Twig/Error/Loader.php" => array("946", "0297a085a7341500baa1706db2917a59"), - "vendor/twig/twig/lib/Twig/Error.php" => array("7496", "08acca21a4a57fc7bdecf80411e6d00e"), + "vendor/twig/twig/lib/Twig/Error.php" => array("7541", "d11a146c72d5131bdf14627be19c8dbc"), "vendor/twig/twig/lib/Twig/Error/Runtime.php" => array("395", "3a4c02d5a2b37fb343b6e9fb7056e3e7"), "vendor/twig/twig/lib/Twig/Error/Syntax.php" => array("428", "41d4a9282175957c316b0ffb313d03e0"), - "vendor/twig/twig/lib/Twig/ExistsLoaderInterface.php" => array("692", "2dcb498891d0bea0a55754869618e032"), - "vendor/twig/twig/lib/Twig/ExpressionParser.php" => array("23710", "3fa6eab1d6d8c63b5014f7cadaeb3b58"), - "vendor/twig/twig/lib/Twig/Extension/Core.php" => array("49755", "479a2f77269f5b255b7896096aa8f2d8"), - "vendor/twig/twig/lib/Twig/Extension/Debug.php" => array("2009", "01bfdc7bef74ae207b06874d839acb43"), - "vendor/twig/twig/lib/Twig/Extension/Escaper.php" => array("2788", "1f2be1edc3e5c9fba1aa58976638d8ff"), - "vendor/twig/twig/lib/Twig/ExtensionInterface.php" => array("2091", "8bed99e211ba66f478342efc9faa4013"), + "vendor/twig/twig/lib/Twig/ExistsLoaderInterface.php" => array("692", "7d81a10f4354e32d79d3e376f2318725"), + "vendor/twig/twig/lib/Twig/ExpressionParser.php" => array("25995", "d541aa69c5bcf4ad0967b71bd863c99a"), + "vendor/twig/twig/lib/Twig/Extension/Core.php" => array("53684", "bccaef36e110b05573a5488b16f071ae"), + "vendor/twig/twig/lib/Twig/Extension/Debug.php" => array("2009", "fd47103168ab6df5e91eeea3b5429aa6"), + "vendor/twig/twig/lib/Twig/Extension/Escaper.php" => array("3251", "95bae54d6f7c535e8051d9dd83067068"), + "vendor/twig/twig/lib/Twig/ExtensionInterface.php" => array("2012", "9733ec61d5bfd0d75cd800a9f382e0ab"), "vendor/twig/twig/lib/Twig/Extension/Optimizer.php" => array("664", "90d559bc3f34fd2952905ddd72f906b3"), - "vendor/twig/twig/lib/Twig/Extension.php" => array("2132", "4a3d6c03eb002ca4e15bb4bd87f33679"), - "vendor/twig/twig/lib/Twig/Extension/Sandbox.php" => array("2636", "1cdfeb7346b86fa17eddfd8717d57667"), - "vendor/twig/twig/lib/Twig/Extension/Staging.php" => array("2096", "6fc289bff03eacfd4a2136ba9a503633"), - "vendor/twig/twig/lib/Twig/Extension/StringLoader.php" => array("1474", "15f299f6e4c3ce7acf21f0fda6d63e37"), - "vendor/twig/twig/lib/Twig/FilterCallableInterface.php" => array("473", "557ea6fcaa5b7b2c503a497a3b931267"), - "vendor/twig/twig/lib/Twig/Filter/Function.php" => array("748", "260ca9d38bdc329f4a82405dc11bb1a4"), - "vendor/twig/twig/lib/Twig/FilterInterface.php" => array("846", "6e43136a50b94a80951dd34da661ccc8"), - "vendor/twig/twig/lib/Twig/Filter/Method.php" => array("930", "98d91cb41f5a5a9a4d67f7e967698141"), - "vendor/twig/twig/lib/Twig/Filter/Node.php" => array("731", "6e30432a47d6bff1ddea8e428c2b3276"), - "vendor/twig/twig/lib/Twig/Filter.php" => array("1869", "ffcbc10fbc3456d605d3f8510c60210d"), - "vendor/twig/twig/lib/Twig/FunctionCallableInterface.php" => array("479", "2f8d67c0e76b701a5e4c76f8bf685797"), - "vendor/twig/twig/lib/Twig/Function/Function.php" => array("784", "3aff76b4acc885f1f0f34f915c718daf"), - "vendor/twig/twig/lib/Twig/FunctionInterface.php" => array("804", "1a989db9f04110dff68441ab95849661"), - "vendor/twig/twig/lib/Twig/Function/Method.php" => array("966", "9f5db96bdcddf2cdecf4ac03cffe5072"), - "vendor/twig/twig/lib/Twig/Function/Node.php" => array("739", "e429ee124350f3a9b95f9d6d461d1ca3"), - "vendor/twig/twig/lib/Twig/Function.php" => array("1628", "cdf7c64b75e20ebe85739da5b308addc"), - "vendor/twig/twig/lib/Twig/LexerInterface.php" => array("761", "cd9cf96833fea0cc3e1657a1abb62e83"), - "vendor/twig/twig/lib/Twig/Lexer.php" => array("16213", "f3ebd10d22c844d1ba4f73326b5b77ba"), - "vendor/twig/twig/lib/Twig/Loader/Array.php" => array("2364", "47e8b8acf9d123975c3336f739911375"), - "vendor/twig/twig/lib/Twig/Loader/Chain.php" => array("3636", "82fc84ebb25255ca44b0451ea00a8e03"), - "vendor/twig/twig/lib/Twig/Loader/Filesystem.php" => array("6040", "38c92a7d80cae890eed1bb48fede872a"), - "vendor/twig/twig/lib/Twig/LoaderInterface.php" => array("1334", "72f395318a031756c876a56cc37a5260"), - "vendor/twig/twig/lib/Twig/Loader/String.php" => array("1334", "d60a08d13f3b0183be2602df9fdfcf6a"), + "vendor/twig/twig/lib/Twig/Extension.php" => array("1148", "cb89750141475a8da0f5cbd54c801e6a"), + "vendor/twig/twig/lib/Twig/Extension/Profiler.php" => array("1083", "3eb9d6c19a4852e3261e88a7b26b7bb7"), + "vendor/twig/twig/lib/Twig/Extension/Sandbox.php" => array("2649", "3cf44ff9868c7bb151386bbbe72a281c"), + "vendor/twig/twig/lib/Twig/Extension/Staging.php" => array("2112", "7370912826a6907da8c8e2fc6cdc5456"), + "vendor/twig/twig/lib/Twig/Extension/StringLoader.php" => array("1090", "f1c63b3d9dff53f28ed3e8c1e0cd25dd"), + "vendor/twig/twig/lib/Twig/FileExtensionEscapingStrategy.php" => array("1456", "4b03368c8d273e6e343b4308194d26ba"), + "vendor/twig/twig/lib/Twig/FilterCallableInterface.php" => array("476", "2249cea150fbb3fbc9d8410938df4c1c"), + "vendor/twig/twig/lib/Twig/Filter/Function.php" => array("913", "e1370773a69ab83e6d38948a53c2d903"), + "vendor/twig/twig/lib/Twig/FilterInterface.php" => array("849", "f918648cbc5e14f4432e474e294158ae"), + "vendor/twig/twig/lib/Twig/Filter/Method.php" => array("1093", "e9a7acdf358c7a99c39f6a87aa865f0e"), + "vendor/twig/twig/lib/Twig/Filter/Node.php" => array("892", "1bb35e563eb3f0f964cb60229d21b83a"), + "vendor/twig/twig/lib/Twig/Filter.php" => array("2004", "0f368adbba33e48dde2386318534d2e3"), + "vendor/twig/twig/lib/Twig/FunctionCallableInterface.php" => array("482", "3117a9ec03f6951ffe11252e56efce55"), + "vendor/twig/twig/lib/Twig/Function/Function.php" => array("953", "c5f288891c97519e984b28537cebbf24"), + "vendor/twig/twig/lib/Twig/FunctionInterface.php" => array("807", "343da1424a370c83629cad6b32f81d69"), + "vendor/twig/twig/lib/Twig/Function/Method.php" => array("1133", "0df07b30b73880c422d6a9c4d48a0e77"), + "vendor/twig/twig/lib/Twig/Function/Node.php" => array("904", "e92534d2821ef62bb2386bfdd6e59880"), + "vendor/twig/twig/lib/Twig/Function.php" => array("1775", "08b10c651f2722d7736f4b7c301e870f"), + "vendor/twig/twig/lib/Twig/LexerInterface.php" => array("764", "23c121f90dd026acd6681a8b55ba14c2"), + "vendor/twig/twig/lib/Twig/Lexer.php" => array("16251", "66279d3d814bc08c8028af5933e626ee"), + "vendor/twig/twig/lib/Twig/Loader/Array.php" => array("2389", "a272ca0f665eb402ca464ebea719db98"), + "vendor/twig/twig/lib/Twig/Loader/Chain.php" => array("3713", "18752a499b5ca69311869087efcb3712"), + "vendor/twig/twig/lib/Twig/Loader/Filesystem.php" => array("6966", "796a73abba9983282efd0c4b89a2cc07"), + "vendor/twig/twig/lib/Twig/LoaderInterface.php" => array("1365", "3e997f5d76dd381d63d22480233124d4"), + "vendor/twig/twig/lib/Twig/Loader/String.php" => array("1511", "a3f6aa9f268c2c05659366d221ab2dc0"), "vendor/twig/twig/lib/Twig/Markup.php" => array("764", "7ebff11af28eeedb1fdfe31d92132d20"), - "vendor/twig/twig/lib/Twig/Node/AutoEscape.php" => array("950", "25ae0b7054bb57ea1228774e9e64910a"), - "vendor/twig/twig/lib/Twig/Node/Block.php" => array("1080", "756f2c4dc092046f77c40c0a18e15308"), - "vendor/twig/twig/lib/Twig/Node/BlockReference.php" => array("915", "602786b6a5c05060cc5d55b384cb3f43"), + "vendor/twig/twig/lib/Twig/Node/AutoEscape.php" => array("960", "c12a2f1a4b99560b89b9e9fdc5c3cda7"), + "vendor/twig/twig/lib/Twig/Node/Block.php" => array("1090", "3f09071711cbe03a25b0473633372f5b"), + "vendor/twig/twig/lib/Twig/Node/BlockReference.php" => array("925", "66d4e1224ffbc78e7e4c730f21457d4e"), "vendor/twig/twig/lib/Twig/Node/Body.php" => array("337", "3768ca70d202fd3b29e02f0049ae5432"), - "vendor/twig/twig/lib/Twig/Node/Do.php" => array("839", "033e1b69c2ba4b06b81a00f9a6b34964"), - "vendor/twig/twig/lib/Twig/Node/Embed.php" => array("1181", "b155f10a68234505e1b9da02e2521116"), - "vendor/twig/twig/lib/Twig/Node/Expression/Array.php" => array("2350", "00d9b12693a06114f73ec464d9ef6099"), - "vendor/twig/twig/lib/Twig/Node/Expression/AssignName.php" => array("616", "42e94b8e2a7a849e517ba1f4762feb6a"), + "vendor/twig/twig/lib/Twig/Node/CheckSecurity.php" => array("3002", "cb5bed1e9f5dc05e2a296f2f356bba99"), + "vendor/twig/twig/lib/Twig/Node/Do.php" => array("849", "f8f840206cf6baf3296ab3aaff5fc7a1"), + "vendor/twig/twig/lib/Twig/Node/Embed.php" => array("1305", "23f161e670e5c6e1f6026eeba0699076"), + "vendor/twig/twig/lib/Twig/Node/Expression/Array.php" => array("2360", "4171ae568bdaba3311a26af08d1a53f1"), + "vendor/twig/twig/lib/Twig/Node/Expression/AssignName.php" => array("626", "a90452f1d9bbfd90e668c5629fb284d7"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Add.php" => array("413", "5d16cd53c83be9b5d332f2e6a80d124f"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/And.php" => array("414", "308e349bc7dae1d6e8ffe563a0e059fc"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/BitwiseAnd.php" => array("420", "621c82ab6fafa611309b08ce5f060f0c"), @@ -3081,120 +6839,130 @@ class Manifest { "vendor/twig/twig/lib/Twig/Node/Expression/Binary/BitwiseXor.php" => array("420", "ff650ff1dcad63ac4dbc9dd57df5eef2"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Concat.php" => array("416", "702c3a6603b0138b5a6b3d82e80e2a05"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Div.php" => array("413", "5b28cc3d88ddfae12b04f22a4f2860c2"), - "vendor/twig/twig/lib/Twig/Node/Expression/Binary/EndsWith.php" => array("758", "0147cde001fa855a79e976f5110745fc"), + "vendor/twig/twig/lib/Twig/Node/Expression/Binary/EndsWith.php" => array("892", "656c53f7693e079734394b3745044b2d"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Equal.php" => array("389", "3d26fb48b35b0f5ad315d296299a4c20"), - "vendor/twig/twig/lib/Twig/Node/Expression/Binary/FloorDiv.php" => array("673", "0910da99e3e01329fec591b7950dfb4f"), + "vendor/twig/twig/lib/Twig/Node/Expression/Binary/FloorDiv.php" => array("683", "df9f4cfa7582ee392095a5235e0f4928"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/GreaterEqual.php" => array("396", "eecadb39bce3c07af85c8a03e735a326"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Greater.php" => array("390", "6209c99dc1bce32340da0bebb5895bf3"), - "vendor/twig/twig/lib/Twig/Node/Expression/Binary/In.php" => array("772", "1c6b54c6128846b57bab85686a34bc73"), + "vendor/twig/twig/lib/Twig/Node/Expression/Binary/In.php" => array("782", "c7510af3489b87ec2154f25041eedd5e"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/LessEqual.php" => array("393", "e4fd68cfbded83d91cfafafc81e80311"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Less.php" => array("387", "d2df3932fc9d04720399302098258a18"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Matches.php" => array("662", "ab9d3177aa110a6de020e466a6db65b3"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Mod.php" => array("413", "4e61aee836424a723f1c0314f32b3ec3"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Mul.php" => array("413", "a8d6077b84b988aef093159e4521969f"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/NotEqual.php" => array("392", "e5cf8bb39c1bce092fb84cbe5dbcf1f6"), - "vendor/twig/twig/lib/Twig/Node/Expression/Binary/NotIn.php" => array("780", "99281fa54772d040df9301b9018875e6"), + "vendor/twig/twig/lib/Twig/Node/Expression/Binary/NotIn.php" => array("790", "162e482008c8f13c44b39a361982455e"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Or.php" => array("413", "835aa4edcbe9290749ca3e6f56c1fcd7"), - "vendor/twig/twig/lib/Twig/Node/Expression/Binary.php" => array("1028", "5753c84484d2fd6b4ca0920fb30513da"), - "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Power.php" => array("764", "c960e3c92ae6311d280f491658355c63"), - "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Range.php" => array("766", "3362365c37d9cf6d3e56c1eb0dbd104f"), - "vendor/twig/twig/lib/Twig/Node/Expression/Binary/StartsWith.php" => array("669", "8d5900c492c79a667e2739a36e354d56"), + "vendor/twig/twig/lib/Twig/Node/Expression/Binary.php" => array("1038", "e94cfe28739090c3a4d0c02d62b11fa4"), + "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Power.php" => array("774", "e8b1beacdce801928fcf5a3f41770db2"), + "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Range.php" => array("776", "a89823fdc86c5d92240961148ba5d4a0"), + "vendor/twig/twig/lib/Twig/Node/Expression/Binary/StartsWith.php" => array("881", "d415d9719a3f33f9bcbb3cbd71d1ca3a"), "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Sub.php" => array("413", "47650251c8b7e7c2b82df3a699d10cd6"), - "vendor/twig/twig/lib/Twig/Node/Expression/BlockReference.php" => array("1390", "b5cdd67c38367d62735ea829a4be6c53"), - "vendor/twig/twig/lib/Twig/Node/Expression/Call.php" => array("6426", "145a35b399a10d2d893802028d4d236f"), + "vendor/twig/twig/lib/Twig/Node/Expression/BlockReference.php" => array("1396", "039c443e43555e2400f5d0512068d3dc"), + "vendor/twig/twig/lib/Twig/Node/Expression/Call.php" => array("9542", "4f379f0639b7b20662f0a33d49559e73"), "vendor/twig/twig/lib/Twig/Node/Expression/Conditional.php" => array("902", "94611e1b6bbc76388011855f0348fc2e"), "vendor/twig/twig/lib/Twig/Node/Expression/Constant.php" => array("557", "b3703810a66fd8d2fe792ed95e9949ea"), - "vendor/twig/twig/lib/Twig/Node/Expression/ExtensionReference.php" => array("806", "11d53bdac885a7604336297228ac7091"), + "vendor/twig/twig/lib/Twig/Node/Expression/ExtensionReference.php" => array("816", "629b4b904799751bf0f9b860fff3c2cf"), "vendor/twig/twig/lib/Twig/Node/Expression/Filter/Default.php" => array("1583", "81eb255cf59793aa37f7ab73df5b8f47"), - "vendor/twig/twig/lib/Twig/Node/Expression/Filter.php" => array("1378", "859a893a66e17778142c059668f5ffd2"), - "vendor/twig/twig/lib/Twig/Node/Expression/Function.php" => array("1249", "f5dce5675c390ae1d35497c7f76cf22a"), - "vendor/twig/twig/lib/Twig/Node/Expression/GetAttr.php" => array("2240", "bf8ac92138c4f5cdab11de9a7aadff46"), + "vendor/twig/twig/lib/Twig/Node/Expression/Filter.php" => array("1511", "460bd0c8461592d59f5652034c2b592c"), + "vendor/twig/twig/lib/Twig/Node/Expression/Function.php" => array("1388", "166e90837f902588071783d187027d4c"), + "vendor/twig/twig/lib/Twig/Node/Expression/GetAttr.php" => array("2314", "652744703da073f2139df43702efefb4"), "vendor/twig/twig/lib/Twig/Node/Expression/MethodCall.php" => array("1203", "ec007ddb0878bb9bc7017a12c6e4c080"), - "vendor/twig/twig/lib/Twig/Node/Expression/Name.php" => array("2872", "09b1ba6d503efb4d48a166fcf5c74a6f"), - "vendor/twig/twig/lib/Twig/Node/Expression/Parent.php" => array("1230", "c6628a0c055557b1ac412590ce76e4af"), + "vendor/twig/twig/lib/Twig/Node/Expression/Name.php" => array("3284", "fb2a40e9cfbe28602786e6d650b74045"), + "vendor/twig/twig/lib/Twig/Node/Expression/Parent.php" => array("1236", "1c6002bf3cb8ffff85c4db193663294a"), "vendor/twig/twig/lib/Twig/Node/Expression.php" => array("415", "d2fcc744c0d3ece6f32c398ae50e2cb5"), "vendor/twig/twig/lib/Twig/Node/Expression/TempName.php" => array("594", "312be4fbbf9f33caec6b64733e8f7c35"), "vendor/twig/twig/lib/Twig/Node/Expression/Test/Constant.php" => array("1130", "cea24b966c64017afa46413e189956b7"), "vendor/twig/twig/lib/Twig/Node/Expression/Test/Defined.php" => array("1607", "28345503b665a4d6edbbd7b06428718a"), - "vendor/twig/twig/lib/Twig/Node/Expression/Test/Divisibleby.php" => array("747", "cf1043b7f1479d71150e6202be527bef"), + "vendor/twig/twig/lib/Twig/Node/Expression/Test/Divisibleby.php" => array("748", "0d047f7788b0e83479551206536c3662"), "vendor/twig/twig/lib/Twig/Node/Expression/Test/Even.php" => array("636", "b67fd353edb6c3804d8ab2d41c6f8cbb"), "vendor/twig/twig/lib/Twig/Node/Expression/Test/Null.php" => array("618", "d0fe20539e14edd2085ec6b0e139b415"), "vendor/twig/twig/lib/Twig/Node/Expression/Test/Odd.php" => array("633", "5bb79634c28ab1e54b7ca7ded7ee9f28"), - "vendor/twig/twig/lib/Twig/Node/Expression/Test.php" => array("1036", "a0b2bd4d99f906c4857f057c7bb4864e"), + "vendor/twig/twig/lib/Twig/Node/Expression/Test.php" => array("1163", "179eae0507a747cebddd73eab56f6d22"), "vendor/twig/twig/lib/Twig/Node/Expression/Test/Sameas.php" => array("690", "0126ce6f962cf906f8f2370133c3ddff"), "vendor/twig/twig/lib/Twig/Node/Expression/Unary/Neg.php" => array("404", "08e8112e6fabf7eba63b3a19672706c9"), "vendor/twig/twig/lib/Twig/Node/Expression/Unary/Not.php" => array("404", "0d3a4717baffc2ec33b373bb265a6413"), - "vendor/twig/twig/lib/Twig/Node/Expression/Unary.php" => array("754", "b712815036f4e0cbca846e3088cb3fc6"), + "vendor/twig/twig/lib/Twig/Node/Expression/Unary.php" => array("709", "2b004cc15d141f618a5e8f2c22e2305b"), "vendor/twig/twig/lib/Twig/Node/Expression/Unary/Pos.php" => array("404", "e35ab856982e8b07351a893609751c26"), - "vendor/twig/twig/lib/Twig/Node/Flush.php" => array("731", "4fd771f76093c1b60f1ddc1535295c19"), - "vendor/twig/twig/lib/Twig/Node/ForLoop.php" => array("1623", "f2fabfada4aac6f3358d99e39170508d"), - "vendor/twig/twig/lib/Twig/Node/For.php" => array("4367", "687d9e44460eaa3b47e2848b79e5011a"), - "vendor/twig/twig/lib/Twig/Node/If.php" => array("1723", "2263178b12b0fa6609b90cd5ee7dba8d"), - "vendor/twig/twig/lib/Twig/Node/Import.php" => array("1289", "479ef20c51f0e633c7044adead4d0456"), - "vendor/twig/twig/lib/Twig/Node/Include.php" => array("2897", "37a4f90abba8755171ada5732c4bda64"), - "vendor/twig/twig/lib/Twig/NodeInterface.php" => array("649", "2105584cf35b89563c3f86b647ce1f43"), - "vendor/twig/twig/lib/Twig/Node/Macro.php" => array("2655", "8cbf723f3f7518f120f1c17fc033b62e"), - "vendor/twig/twig/lib/Twig/Node/Module.php" => array("12182", "1702dccd13b5a0bfc63a7096327eea0b"), + "vendor/twig/twig/lib/Twig/Node/Flush.php" => array("741", "03eb18a04ac1322f21ac8091feb85dcb"), + "vendor/twig/twig/lib/Twig/Node/ForLoop.php" => array("1633", "8b5c430f031903f112c44e0546b4c592"), + "vendor/twig/twig/lib/Twig/Node/For.php" => array("4312", "53b1e061b662f69da0a3a84a10297b75"), + "vendor/twig/twig/lib/Twig/Node/If.php" => array("1733", "83fd51054492861e13430c3ce2f10cfa"), + "vendor/twig/twig/lib/Twig/Node/Import.php" => array("1439", "b978e1b7870956095be615c9172bec56"), + "vendor/twig/twig/lib/Twig/Node/Include.php" => array("2538", "6ce31c0fff8adff66d0b3ee38d4dd9a7"), + "vendor/twig/twig/lib/Twig/NodeInterface.php" => array("662", "18e4ff5e65c12efabb1b04cbe1a158e1"), + "vendor/twig/twig/lib/Twig/Node/Macro.php" => array("3509", "81cd380d11eed35ce31702ef3a0b2eb0"), + "vendor/twig/twig/lib/Twig/Node/Module.php" => array("13122", "252f9162f4fa9026e15569209903e84a"), "vendor/twig/twig/lib/Twig/NodeOutputInterface.php" => array("351", "2125948f894bcba40e4dd4bc906d47e2"), - "vendor/twig/twig/lib/Twig/Node.php" => array("5801", "e24ef0947fe4eb47966813c035b21058"), - "vendor/twig/twig/lib/Twig/Node/Print.php" => array("934", "fe93dc5534fd3c96eccfc4b6f3d5f6b9"), - "vendor/twig/twig/lib/Twig/Node/SandboxedModule.php" => array("2042", "c0d63404cf94209400098023f29d35c4"), - "vendor/twig/twig/lib/Twig/Node/SandboxedPrint.php" => array("1597", "b465d85ba2b5016df5c4f78a14bb184c"), - "vendor/twig/twig/lib/Twig/Node/Sandbox.php" => array("1259", "3dae5d4756cd400f5f26b18d744212cd"), - "vendor/twig/twig/lib/Twig/Node/Set.php" => array("3224", "d0e25c6c5329877865cc4d18cd53c473"), + "vendor/twig/twig/lib/Twig/Node.php" => array("5782", "b86d9604c81d34c5ac7176098291dbfb"), + "vendor/twig/twig/lib/Twig/Node/Print.php" => array("944", "304c80e1a81f270fba5200c30b919f1c"), + "vendor/twig/twig/lib/Twig/Node/SandboxedPrint.php" => array("1639", "0189a34f2472a321646d5797902de1b0"), + "vendor/twig/twig/lib/Twig/Node/Sandbox.php" => array("1269", "129cb6cd4c57c500622b15233009a925"), + "vendor/twig/twig/lib/Twig/Node/Set.php" => array("3234", "a8fdcf7f0924d16b20cf337c7b0a8a4b"), "vendor/twig/twig/lib/Twig/Node/SetTemp.php" => array("840", "5c93ae290cfe346e9a2d155494f28626"), - "vendor/twig/twig/lib/Twig/Node/Spaceless.php" => array("972", "6b38e3713a5ffda4e7197602f31122e7"), - "vendor/twig/twig/lib/Twig/Node/Text.php" => array("872", "a4dc26dca8d39676f578401e19a05e12"), - "vendor/twig/twig/lib/Twig/NodeTraverser.php" => array("2354", "2a1673a9e56b086c2773d07ca47b9d02"), - "vendor/twig/twig/lib/Twig/NodeVisitor/Escaper.php" => array("5366", "9fe8e3ad07965534c4724211c33daafc"), - "vendor/twig/twig/lib/Twig/NodeVisitorInterface.php" => array("1328", "5efd03cd67a21bf09a6e09be86b570a8"), - "vendor/twig/twig/lib/Twig/NodeVisitor/Optimizer.php" => array("7982", "b57d389aa29b600141e2e3e94327c9c6"), - "vendor/twig/twig/lib/Twig/NodeVisitor/SafeAnalysis.php" => array("4594", "cba1963c52ef6763d47fff8cd7ee6d05"), - "vendor/twig/twig/lib/Twig/NodeVisitor/Sandbox.php" => array("2600", "c823c1317891b559dae63ee717a1b2fe"), - "vendor/twig/twig/lib/Twig/ParserInterface.php" => array("733", "eca931dfd8457bbbb98720da37ae683b"), - "vendor/twig/twig/lib/Twig/Parser.php" => array("11763", "b0d037510f563ebbef2fd4e87eb470ec"), + "vendor/twig/twig/lib/Twig/Node/Spaceless.php" => array("982", "0b2160f8c47033d0a6756333636d5bca"), + "vendor/twig/twig/lib/Twig/Node/Text.php" => array("882", "3bd0ee146116e0aa21e1822a02242cb0"), + "vendor/twig/twig/lib/Twig/NodeTraverser.php" => array("2365", "6bbfc50054836007c8cf42c073d729b3"), + "vendor/twig/twig/lib/Twig/NodeVisitor/Escaper.php" => array("4917", "d32e9f3460e315752b1aa8addbad0307"), + "vendor/twig/twig/lib/Twig/NodeVisitorInterface.php" => array("1324", "b82cb0e406b2cf95947c705cfd7b226f"), + "vendor/twig/twig/lib/Twig/NodeVisitor/Optimizer.php" => array("9058", "dac3694eed8cb5006fa3531f836642ca"), + "vendor/twig/twig/lib/Twig/NodeVisitor/SafeAnalysis.php" => array("4847", "430600aeeec9772b821e55ceb278105c"), + "vendor/twig/twig/lib/Twig/NodeVisitor/Sandbox.php" => array("2372", "d9e126ead14400900d08bf5352f79927"), + "vendor/twig/twig/lib/Twig/ParserInterface.php" => array("736", "7b01b047dc4e9e7c63bdf2cad6cca089"), + "vendor/twig/twig/lib/Twig/Parser.php" => array("12087", "8f63128e7c06ee432e8c766abe6d2e50"), + "vendor/twig/twig/lib/Twig/Profiler/Dumper/Blackfire.php" => array("1960", "64f4301ca251c43d1a2bc52045f486c4"), + "vendor/twig/twig/lib/Twig/Profiler/Dumper/Html.php" => array("1439", "4f8115d6c38bf2a805a3d05234baaecf"), + "vendor/twig/twig/lib/Twig/Profiler/Dumper/Text.php" => array("1986", "ac5796497aa637098c9fb1beeefe7626"), + "vendor/twig/twig/lib/Twig/Profiler/Node/EnterProfile.php" => array("1223", "4a259d3710d066c0340272c164d9ac48"), + "vendor/twig/twig/lib/Twig/Profiler/Node/LeaveProfile.php" => array("772", "039b88bfb04233fb5fee0ab5e42fd817"), + "vendor/twig/twig/lib/Twig/Profiler/NodeVisitor/Profiler.php" => array("2334", "2a75334fe100fdd54fe3d62f6c6dc701"), + "vendor/twig/twig/lib/Twig/Profiler/Profile.php" => array("3676", "66def9e5042d51b2a4ea7e27b1784e34"), "vendor/twig/twig/lib/Twig/Sandbox/SecurityError.php" => array("384", "2038673c16bf93db76dd4e8085bb1199"), + "vendor/twig/twig/lib/Twig/Sandbox/SecurityNotAllowedFilterError.php" => array("776", "a1dc41649daba29cd350f17f19584db7"), + "vendor/twig/twig/lib/Twig/Sandbox/SecurityNotAllowedFunctionError.php" => array("788", "836916750b2284010b903ca967b0b411"), + "vendor/twig/twig/lib/Twig/Sandbox/SecurityNotAllowedTagError.php" => array("748", "c7b865fb29c6e585a4b5442eed42db23"), "vendor/twig/twig/lib/Twig/Sandbox/SecurityPolicyInterface.php" => array("560", "d556afa8416c83b79752040a10ece3d4"), - "vendor/twig/twig/lib/Twig/Sandbox/SecurityPolicy.php" => array("3708", "28281244b087a7de31fde6203829fd83"), - "vendor/twig/twig/lib/Twig/SimpleFilter.php" => array("2117", "f539a1cf697d6df8dbe0a1ab20fa60c4"), - "vendor/twig/twig/lib/Twig/SimpleFunction.php" => array("1872", "3288da22d048babb201c4c8b21383672"), - "vendor/twig/twig/lib/Twig/SimpleTest.php" => array("920", "67c237535f937d19f9acbabe4bee2b7c"), - "vendor/twig/twig/lib/Twig/TemplateInterface.php" => array("1244", "58ada114eb5a42306e3b510af39dfeae"), - "vendor/twig/twig/lib/Twig/Template.php" => array("15939", "259e9b0fa9da7dc774a9eaf5c8a5667f"), - "vendor/twig/twig/lib/Twig/TestCallableInterface.php" => array("432", "680324d83f36b80a71cc3e18ce6da3bf"), - "vendor/twig/twig/lib/Twig/Test/Function.php" => array("705", "c5256cfda774aad3796e83c5ccc76df1"), - "vendor/twig/twig/lib/Twig/Test/IntegrationTestCase.php" => array("5645", "4e4581eabad75b9cbeb41c4f716e62eb"), - "vendor/twig/twig/lib/Twig/TestInterface.php" => array("506", "c64d3221a3e0ac3a1a27f629529ca22f"), - "vendor/twig/twig/lib/Twig/Test/Method.php" => array("887", "b05886b7c3bb0d5a4b4fd871a0de612e"), - "vendor/twig/twig/lib/Twig/Test/Node.php" => array("688", "41a3b3e4657168e15d1b1613e5ae689a"), - "vendor/twig/twig/lib/Twig/Test/NodeTestCase.php" => array("1593", "fcf420a35fd3b0a9117bff01ec7df9d2"), - "vendor/twig/twig/lib/Twig/Test.php" => array("753", "907df6428c547e9a5af3a07ea99c31a1"), - "vendor/twig/twig/lib/Twig/TokenParser/AutoEscape.php" => array("2611", "999df38e7f5a2dc1a0d255515e35f6d5"), - "vendor/twig/twig/lib/Twig/TokenParser/Block.php" => array("2622", "91f80e924e22f40739703995e18d6bcb"), - "vendor/twig/twig/lib/Twig/TokenParserBrokerInterface.php" => array("1270", "0a586d723c9c4a0eee1fbb8e39bad88e"), - "vendor/twig/twig/lib/Twig/TokenParserBroker.php" => array("3879", "29a8948f912da59f4bcab2a5d718bb59"), + "vendor/twig/twig/lib/Twig/Sandbox/SecurityPolicy.php" => array("3781", "8777cac171b4e8f1c182e9d3fbdee403"), + "vendor/twig/twig/lib/Twig/SimpleFilter.php" => array("2474", "eb763eed98fb37876ae1289cf2afa96b"), + "vendor/twig/twig/lib/Twig/SimpleFunction.php" => array("2237", "bc68c2d38574020ccbbd8f103ccce6b2"), + "vendor/twig/twig/lib/Twig/SimpleTest.php" => array("1307", "ce84b1599e58d33b4fde25badae5bdc5"), + "vendor/twig/twig/lib/Twig/TemplateInterface.php" => array("1243", "3391f37e99f475da27edaaf3ce8ffe86"), + "vendor/twig/twig/lib/Twig/Template.php" => array("20363", "3fab800221b474a9491909b22e133df8"), + "vendor/twig/twig/lib/Twig/TestCallableInterface.php" => array("435", "6c282931db6133a729a713d246d6fc6c"), + "vendor/twig/twig/lib/Twig/Test/Function.php" => array("866", "e7358794a78cc2b7b858abccbc5c6d74"), + "vendor/twig/twig/lib/Twig/Test/IntegrationTestCase.php" => array("7639", "d59404075c2598718e9aa390a2514e42"), + "vendor/twig/twig/lib/Twig/TestInterface.php" => array("509", "dc9086f80f2e7bd7a552b76c080c9f33"), + "vendor/twig/twig/lib/Twig/Test/Method.php" => array("1046", "5c078658129d554b298c110468174d0a"), + "vendor/twig/twig/lib/Twig/Test/Node.php" => array("816", "575c0dba0cc29e772ebad48895c96566"), + "vendor/twig/twig/lib/Twig/Test/NodeTestCase.php" => array("1700", "76dbdba1e96927a3c7ae6654ae4bb81d"), + "vendor/twig/twig/lib/Twig/Test.php" => array("905", "3fe6d271859891bc6feef9eaa4fa0fde"), + "vendor/twig/twig/lib/Twig/TokenParser/AutoEscape.php" => array("2754", "ead0b2dea144a4ff2465f5438c895a29"), + "vendor/twig/twig/lib/Twig/TokenParser/Block.php" => array("2628", "f85e7a6f2f30a61704b010eb164c032b"), + "vendor/twig/twig/lib/Twig/TokenParserBrokerInterface.php" => array("1273", "d628516ee447f244512ec26d2c745bc2"), + "vendor/twig/twig/lib/Twig/TokenParserBroker.php" => array("4101", "3ada35cdc18bdb38b7cdfbe54540a767"), "vendor/twig/twig/lib/Twig/TokenParser/Do.php" => array("980", "87cfd1e3e9c357fa6707126c4708f5e0"), "vendor/twig/twig/lib/Twig/TokenParser/Embed.php" => array("1947", "f59d933b6e44590ed33aa6dff014fcc1"), "vendor/twig/twig/lib/Twig/TokenParser/Extends.php" => array("1355", "83f589057c3b2ed4b0ee6fcd9c7140ce"), "vendor/twig/twig/lib/Twig/TokenParser/Filter.php" => array("1702", "947049e09760ed46c9195e435fe4bc52"), "vendor/twig/twig/lib/Twig/TokenParser/Flush.php" => array("905", "b6f0a3ec7581d0414899fc50bdaad814"), "vendor/twig/twig/lib/Twig/TokenParser/For.php" => array("4799", "c4ae472d2b3821f8ae56f86464cc295e"), - "vendor/twig/twig/lib/Twig/TokenParser/From.php" => array("1793", "e180cccf0c62b7cad73c63edb4005baa"), + "vendor/twig/twig/lib/Twig/TokenParser/From.php" => array("2037", "fc8ff0c6ad54f030f67b3611b2446e97"), "vendor/twig/twig/lib/Twig/TokenParser/If.php" => array("2709", "35fdb85b56353849c394a9e4da7fe715"), "vendor/twig/twig/lib/Twig/TokenParser/Import.php" => array("1296", "3e7397da9c81999b32a1b637871db37c"), "vendor/twig/twig/lib/Twig/TokenParser/Include.php" => array("1846", "aafd1e3b2b6e6b4223ddc3c0eff599a0"), - "vendor/twig/twig/lib/Twig/TokenParserInterface.php" => array("953", "62c183068343c7dbcad42a1cc4b27581"), - "vendor/twig/twig/lib/Twig/TokenParser/Macro.php" => array("2023", "5f0a590691c64a79a1031a7ebe1da557"), - "vendor/twig/twig/lib/Twig/TokenParser.php" => array("662", "83224f13e3f3e14ed5e7c0d1c567a9c6"), + "vendor/twig/twig/lib/Twig/TokenParserInterface.php" => array("966", "6aae321543a7847609cedfc3396d18ee"), + "vendor/twig/twig/lib/Twig/TokenParser/Macro.php" => array("2029", "c84e6bfb37cf2b3f06eed5b21c55e177"), + "vendor/twig/twig/lib/Twig/TokenParser.php" => array("675", "1cdcd32c05c0b10558619580f63532d2"), "vendor/twig/twig/lib/Twig/TokenParser/Sandbox.php" => array("1950", "4e7a32b6b2436967b85ac5bee63f99da"), - "vendor/twig/twig/lib/Twig/TokenParser/Set.php" => array("2286", "9bb035bb1ad955d96257a8a5e0e6b496"), + "vendor/twig/twig/lib/Twig/TokenParser/Set.php" => array("2286", "fb3639fc5ff4871428008527cbdf8b10"), "vendor/twig/twig/lib/Twig/TokenParser/Spaceless.php" => array("1392", "ab7d1d53dd9b4e7628837e7af93b3db3"), "vendor/twig/twig/lib/Twig/TokenParser/Use.php" => array("2135", "7bd09009867e76e0b006586fa3fc6275"), - "vendor/twig/twig/lib/Twig/Token.php" => array("6211", "4b3f25c7d4929c29712d25711bc87768"), - "vendor/twig/twig/lib/Twig/TokenStream.php" => array("3989", "8eb8e69d38ec4f6ee0b73d716d064957"), + "vendor/twig/twig/lib/Twig/Token.php" => array("6038", "0840e8a6e9c1cf6603fb0a3f1e771bf2"), + "vendor/twig/twig/lib/Twig/TokenStream.php" => array("3950", "5ce8f2e90ee93b6772509ad0dcfe2f67"), + "vendor/twig/twig/lib/Twig/Util/DeprecationCollector.php" => array("2123", "26540ffa664d8f81cc308da12f6add77"), + "vendor/twig/twig/lib/Twig/Util/TemplateDirIterator.php" => array("497", "958ac185752b7b3e1fdd7c18138ff173"), "vendor/twig/twig/LICENSE" => array("1497", "1886505263500ef827db124cf26c2408"), - "vendor/twig/twig/phpunit.xml.dist" => array("651", "64f59fc76504c822331e5e5eccc3e1cf"), + "vendor/twig/twig/phpunit.xml.dist" => array("652", "d665ebeeddfb06ed1efbdda2f1baa54f"), "vendor/twig/twig/README.rst" => array("486", "32d5a3ca77dace5b9255842b15c55699"), - "vendor/twig/twig/.travis.yml" => array("440", "c60b7a77686a5d1a4201f9fbc4c88dbb"), ); } diff --git a/www/analytics/console b/www/analytics/console index 0e3dc3ff..47b8ba59 100755 --- a/www/analytics/console +++ b/www/analytics/console @@ -3,26 +3,25 @@ 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); } -if (!defined('PIWIK_USER_PATH')) { - define('PIWIK_USER_PATH', PIWIK_DOCUMENT_ROOT); -} -require_once PIWIK_INCLUDE_PATH . '/core/testMinimumPhpVersion.php'; -require_once file_exists(PIWIK_INCLUDE_PATH . '/vendor/autoload.php') - ? PIWIK_INCLUDE_PATH . '/vendor/autoload.php' // Piwik is the main project - : PIWIK_INCLUDE_PATH . '/../../autoload.php'; // Piwik is installed as a dependency -require_once PIWIK_INCLUDE_PATH . '/core/Loader.php'; -require_once PIWIK_INCLUDE_PATH . '/libs/upgradephp/upgrade.php'; - -Piwik\Translate::loadEnglishTranslation(); +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->init(); -$console->run(); \ No newline at end of file +$console->run(); diff --git a/www/analytics/core/.htaccess b/www/analytics/core/.htaccess index 6cd2e134..f4e970ee 100644 --- a/www/analytics/core/.htaccess +++ b/www/analytics/core/.htaccess @@ -1,13 +1,8 @@ - -Deny from all - - - -Deny from all - - - -Deny from all - + + Deny from all + + = 2.4> + Require all denied + diff --git a/www/analytics/core/API/ApiRenderer.php b/www/analytics/core/API/ApiRenderer.php new file mode 100644 index 00000000..36f84e63 --- /dev/null +++ b/www/analytics/core/API/ApiRenderer.php @@ -0,0 +1,131 @@ +request = $request; + $this->init(); + } + + protected function init() + { + } + + abstract public function sendHeader(); + + public function renderSuccess($message) + { + return 'Success:' . $message; + } + + public function renderException($message, \Exception $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'; + } + + $renderer = Renderer::factory($format); + $renderer->setTable($dataTable); + $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)); + } +} diff --git a/www/analytics/core/API/CORSHandler.php b/www/analytics/core/API/CORSHandler.php new file mode 100644 index 00000000..779794df --- /dev/null +++ b/www/analytics/core/API/CORSHandler.php @@ -0,0 +1,41 @@ +domains = Url::getCorsHostsFromConfig(); + } + + public function handle() + { + // allow Piwik to serve data to all domains + if (in_array("*", $this->domains)) { + header('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)) { + header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']); + } + } + } +} diff --git a/www/analytics/core/API/DataTableGenericFilter.php b/www/analytics/core/API/DataTableGenericFilter.php index 965e4fde..5d4ba494 100644 --- a/www/analytics/core/API/DataTableGenericFilter.php +++ b/www/analytics/core/API/DataTableGenericFilter.php @@ -1,6 +1,6 @@ request = $request; + $this->report = $report; } /** @@ -37,6 +54,16 @@ class DataTableGenericFilter $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. @@ -51,43 +78,54 @@ class DataTableGenericFilter */ public static function getGenericFiltersInformation() { - if (is_null(self::$genericFiltersInfo)) { - self::$genericFiltersInfo = array( - 'Pattern' => array( - 'filter_column' => array('string', 'label'), - 'filter_pattern' => array('string'), - ), - 'PatternRecursive' => array( - 'filter_column_recursive' => array('string', 'label'), - 'filter_pattern_recursive' => array('string'), - ), - 'ExcludeLowPopulation' => array( - 'filter_excludelowpop' => array('string'), - 'filter_excludelowpop_value' => array('float', '0'), - ), - 'AddColumnsProcessedMetrics' => array( - 'filter_add_columns_when_show_all_columns' => array('integer') - ), - 'AddColumnsProcessedMetricsGoal' => array( - 'filter_update_columns_when_show_all_goals' => array('integer'), - 'idGoal' => array('string', AddColumnsProcessedMetricsGoal::GOALS_OVERVIEW), - ), - 'Sort' => array( - 'filter_sort_column' => array('string'), - 'filter_sort_order' => array('string', 'desc'), - ), - 'Truncate' => array( - 'filter_truncate' => array('integer'), - ), - 'Limit' => array( - 'filter_offset' => array('integer', '0'), - 'filter_limit' => array('integer'), - 'keep_summary_row' => array('integer', '0'), - ), - ); + 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'), + )), + 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()); + } + } } - return self::$genericFiltersInfo; + return $filters; } /** @@ -107,13 +145,20 @@ class DataTableGenericFilter return; } - $genericFilters = self::getGenericFiltersInformation(); + $genericFilters = $this->getGenericFiltersHavingDefaultValues(); $filterApplied = false; - foreach ($genericFilters as $filterName => $parameters) { + foreach ($genericFilters as $filterMeta) { + $filterName = $filterMeta[0]; + $filterParams = $filterMeta[1]; $filterParameters = array(); $exceptionRaised = false; - foreach ($parameters as $name => $info) { + + if (in_array($filterName, $this->disabledFilters)) { + continue; + } + + foreach ($filterParams as $name => $info) { // parameter type to cast to $type = $info[0]; @@ -123,12 +168,6 @@ class DataTableGenericFilter $defaultValue = $info[1]; } - // third element in the array, if it exists, overrides the name of the request variable - $varName = $name; - if (isset($info[2])) { - $varName = $info[2]; - } - try { $value = Common::getRequestVar($name, $defaultValue, $type, $this->request); settype($value, $type); @@ -144,6 +183,45 @@ class DataTableGenericFilter $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; + } } diff --git a/www/analytics/core/API/DataTableManipulator.php b/www/analytics/core/API/DataTableManipulator.php index 5ebdd2fb..d084d6d1 100644 --- a/www/analytics/core/API/DataTableManipulator.php +++ b/www/analytics/core/API/DataTableManipulator.php @@ -1,6 +1,6 @@ manipulateDataTableMap($dataTable); - } else if ($dataTable instanceof DataTable) { + } elseif ($dataTable instanceof DataTable) { return $this->manipulateDataTable($dataTable); } else { return $dataTable; @@ -90,7 +89,7 @@ abstract class DataTableManipulator * Manipulates a single DataTable instance. Derived classes must define * this function. */ - protected abstract function manipulateDataTable($dataTable); + abstract protected function manipulateDataTable($dataTable); /** * Load the subtable for a row. @@ -124,7 +123,7 @@ abstract class DataTableManipulator } } - $method = $this->getApiMethodForSubtable(); + $method = $this->getApiMethodForSubtable($request); return $this->callApiAndReturnDataTable($this->apiModule, $method, $request); } @@ -136,19 +135,34 @@ abstract class DataTableManipulator * @param $request * @return */ - protected abstract function manipulateSubtableRequest($request); + abstract protected function manipulateSubtableRequest($request); /** * Extract the API method for loading subtables from the meta data * + * @throws Exception * @return string */ - private function getApiMethodForSubtable() + private function getApiMethodForSubtable($request) { if (!$this->apiMethodForSubtable) { - $meta = API::getInstance()->getMetadata('all', $this->apiModule, $this->apiMethod); + if (!empty($request['idSite'])) { + $idSite = $request['idSite']; + } else { + $idSite = 'all'; + } - if(empty($meta)) { + $apiParameters = array(); + if (!empty($request['idDimension'])) { + $apiParameters['idDimension'] = $request['idDimension']; + } + if (!empty($request['idGoal'])) { + $apiParameters['idGoal'] = $request['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: http://developer.piwik.org/api-reference/events#apigetreportmetadata", $this->apiModule, $this->apiMethod @@ -171,6 +185,8 @@ abstract class DataTableManipulator $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 @@ -179,14 +195,8 @@ abstract class DataTableManipulator $dataTable = Proxy::getInstance()->call($class, $method, $request); $response = new ResponseBuilder($format = 'original', $request); - $dataTable = $response->getResponse($dataTable); - - if (Common::getRequestVar('disable_queued_filters', 0, 'int', $request) == 0) { - if (method_exists($dataTable, 'applyQueuedFilters')) { - $dataTable->applyQueuedFilters(); - } - } - + $response->disableSendHeader(); + $dataTable = $response->getResponse($dataTable, $apiModule, $method); return $dataTable; } } diff --git a/www/analytics/core/API/DataTableManipulator/Flattener.php b/www/analytics/core/API/DataTableManipulator/Flattener.php index 20fe2c02..a976b7a6 100644 --- a/www/analytics/core/API/DataTableManipulator/Flattener.php +++ b/www/analytics/core/API/DataTableManipulator/Flattener.php @@ -1,6 +1,6 @@ apiModule == 'Actions' || $this->apiMethod == 'getWebsites') { - $this->recursiveLabelSeparator = '/'; - } + $this->recursiveLabelSeparator = $recursiveLabelSeparator; return $this->manipulate($dataTable); } @@ -72,9 +71,10 @@ class Flattener extends DataTableManipulator } $newDataTable = $dataTable->getEmptyClone($keepFilters); - foreach ($dataTable->getRows() as $row) { - $this->flattenRow($row, $newDataTable); + foreach ($dataTable->getRows() as $rowId => $row) { + $this->flattenRow($row, $rowId, $newDataTable); } + return $newDataTable; } @@ -84,15 +84,21 @@ class Flattener extends DataTableManipulator * @param string $labelPrefix * @param bool $parentLogo */ - private function flattenRow(Row $row, DataTable $dataTable, + private function flattenRow(Row $row, $rowId, DataTable $dataTable, $labelPrefix = '', $parentLogo = false) { $label = $row->getColumn('label'); if ($label !== false) { $label = trim($label); - if (substr($label, 0, 1) == '/' && $this->recursiveLabelSeparator == '/') { - $label = substr($label, 1); + + if ($this->recursiveLabelSeparator == '/') { + if (substr($label, 0, 1) == '/') { + $label = substr($label, 1); + } elseif ($rowId === DataTable::ID_SUMMARY_ROW && $labelPrefix && $label != DataTable::LABEL_SUMMARY_ROW) { + $label = ' - ' . $label; + } } + $label = $labelPrefix . $label; $row->setColumn('label', $label); } @@ -103,7 +109,16 @@ class Flattener extends DataTableManipulator $row->setMetadata('logo', $logo); } - $subTable = $this->loadSubtable($dataTable, $row); + /** @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) { @@ -117,8 +132,8 @@ class Flattener extends DataTableManipulator $dataTable->addRow($row); } $prefix = $label . $this->recursiveLabelSeparator; - foreach ($subTable->getRows() as $row) { - $this->flattenRow($row, $dataTable, $prefix, $logo); + foreach ($subTable->getRows() as $rowId => $row) { + $this->flattenRow($row, $rowId, $dataTable, $prefix, $logo); } } } @@ -127,6 +142,7 @@ class Flattener extends DataTableManipulator * Remove the flat parameter from the subtable request * * @param array $request + * @return array */ protected function manipulateSubtableRequest($request) { diff --git a/www/analytics/core/API/DataTableManipulator/LabelFilter.php b/www/analytics/core/API/DataTableManipulator/LabelFilter.php index 05c594b3..c691398a 100644 --- a/www/analytics/core/API/DataTableManipulator/LabelFilter.php +++ b/www/analytics/core/API/DataTableManipulator/LabelFilter.php @@ -1,6 +1,6 @@ '; + const TERMINAL_OPERATOR = '@'; private $labels; private $addLabelIndex; @@ -63,6 +64,10 @@ class LabelFilter extends DataTableManipulator */ 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); @@ -101,6 +106,9 @@ class LabelFilter extends DataTableManipulator 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; } @@ -111,16 +119,22 @@ class LabelFilter extends DataTableManipulator * Note: The HTML Encoded version must be tried first, since in ResponseBuilder the $label is unsanitized * via Common::unsanitizeLabelParameter. * - * @param string $label + * @param string $originalLabel * @return array */ - private function getLabelVariations($label) + 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 = urldecode($label); - $label = trim($label); + $label = trim(urldecode($originalLabel)); $sanitizedLabel = Common::sanitizeInputValue($label); $variations[] = $sanitizedLabel; @@ -128,13 +142,20 @@ class LabelFilter extends DataTableManipulator if ($this->apiModule == 'Actions' && in_array($this->apiMethod, $pageTitleReports) ) { - // 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; + 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; } diff --git a/www/analytics/core/API/DataTableManipulator/ReportTotalsCalculator.php b/www/analytics/core/API/DataTableManipulator/ReportTotalsCalculator.php index ee289d8b..83fef53a 100644 --- a/www/analytics/core/API/DataTableManipulator/ReportTotalsCalculator.php +++ b/www/analytics/core/API/DataTableManipulator/ReportTotalsCalculator.php @@ -1,6 +1,6 @@ [summed value] * @var array */ - private static $reportMetadata = array(); + private $totals = array(); + + /** + * @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 @@ -46,7 +61,7 @@ class ReportTotalsCalculator extends DataTableManipulator try { return $this->manipulate($table); - } catch(\Exception $e) { + } 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 @@ -62,75 +77,32 @@ class ReportTotalsCalculator extends DataTableManipulator */ protected function manipulateDataTable($dataTable) { - $report = $this->findCurrentReport(); - - if (!empty($report) && empty($report['dimension'])) { + if (!empty($this->report) && !$this->report->getDimension() && !$this->isAllMetricsReport()) { // we currently do not calculate the total value for reports having no dimension return $dataTable; } - // Array [readableMetric] => [summed value] - $totalValues = array(); - + $this->totals = array(); $firstLevelTable = $this->makeSureToWorkOnFirstLevelDataTable($dataTable); $metricsToCalculate = Metrics::getMetricIdsToProcessReportTotal(); + $metricNames = array(); foreach ($metricsToCalculate as $metricId) { - if (!$this->hasDataTableMetric($firstLevelTable, $metricId)) { - continue; - } + $metricNames[$metricId] = Metrics::getReadableColumnName($metricId); + } - foreach ($firstLevelTable->getRows() as $row) { - $totalValues = $this->sumColumnValueToTotal($row, $metricId, $totalValues); + foreach ($firstLevelTable->getRows() as $row) { + $columns = $row->getColumns(); + foreach ($metricNames as $metricId => $metricName) { + $this->sumColumnValueToTotal($columns, $metricId, $metricName); } } - $dataTable->setMetadata('totals', $totalValues); + $dataTable->setMetadata('totals', $this->totals); return $dataTable; } - private function hasDataTableMetric(DataTable $dataTable, $metricId) - { - $firstRow = $dataTable->getFirstRow(); - - if (empty($firstRow)) { - return false; - } - - if (false === $this->getColumn($firstRow, $metricId)) { - return false; - } - - return true; - } - - /** - * Returns column from a given row. - * Will work with 2 types of datatable - * - raw datatables coming from the archive DB, which columns are int indexed - * - datatables processed resulting of API calls, which columns have human readable english names - * - * @param Row|array $row - * @param int $columnIdRaw see consts in Metrics:: - * @return mixed Value of column, false if not found - */ - private function getColumn($row, $columnIdRaw) - { - $columnIdReadable = Metrics::getReadableColumnName($columnIdRaw); - - if ($row instanceof Row) { - $raw = $row->getColumn($columnIdRaw); - if ($raw !== false) { - return $raw; - } - - return $row->getColumn($columnIdReadable); - } - - return false; - } - private function makeSureToWorkOnFirstLevelDataTable($table) { if (!array_key_exists('idSubtable', $this->request)) { @@ -144,8 +116,8 @@ class ReportTotalsCalculator extends DataTableManipulator $module = $this->apiModule; $action = $this->apiMethod; } else { - $module = $firstLevelReport['module']; - $action = $firstLevelReport['action']; + $module = $firstLevelReport->getModule(); + $action = $firstLevelReport->getAction(); } $request = $this->request; @@ -164,33 +136,56 @@ class ReportTotalsCalculator extends DataTableManipulator } } - return $this->callApiAndReturnDataTable($module, $action, $request); + $table = $this->callApiAndReturnDataTable($module, $action, $request); + + if ($table instanceof DataTable\Map) { + $table = $table->mergeChildren(); + } + + return $table; } - private function sumColumnValueToTotal(Row $row, $metricId, $totalValues) + private function sumColumnValueToTotal($columns, $metricId, $metricName) { - $value = $this->getColumn($row, $metricId); - - if (false === $value) { - - return $totalValues; + $value = false; + if (array_key_exists($metricId, $columns)) { + $value = $columns[$metricId]; } - $metricName = Metrics::getReadableColumnName($metricId); + if ($value === false) { + // we do not add $metricId to $possibleMetricNames for a small performance improvement since in most cases + // $metricId should be present in $columns so we avoid this foreach loop + $possibleMetricNames = array( + $metricName, + // TODO: this and below is a hack to get report totals to work correctly w/ MultiSites.getAll. can be corrected + // when all metrics are described by Metadata classes & internal naming quirks are handled by core system. + 'Goal_' . $metricName, + 'Actions_' . $metricName + ); + foreach ($possibleMetricNames as $possibleMetricName) { + if (array_key_exists($possibleMetricName, $columns)) { + $value = $columns[$possibleMetricName]; + break; + } + } - if (array_key_exists($metricName, $totalValues)) { - $totalValues[$metricName] += $value; + if ($value === false) { + return; + } + } + + if (array_key_exists($metricName, $this->totals)) { + $this->totals[$metricName] += $value; } else { - $totalValues[$metricName] = $value; + $this->totals[$metricName] = $value; } - - return $totalValues; } /** * Make sure to get all rows of the first level table. * * @param array $request + * @return array */ protected function manipulateSubtableRequest($request) { @@ -198,6 +193,7 @@ class ReportTotalsCalculator extends DataTableManipulator $request['expanded'] = 0; $request['filter_limit'] = -1; $request['filter_offset'] = 0; + $request['filter_sort_column'] = ''; $parametersToRemove = array('flat'); @@ -213,38 +209,21 @@ class ReportTotalsCalculator extends DataTableManipulator return $request; } - private function getReportMetadata() - { - if (!empty(static::$reportMetadata)) { - return static::$reportMetadata; - } - - static::$reportMetadata = API::getInstance()->getReportMetadata(); - - return static::$reportMetadata; - } - - private function findCurrentReport() - { - foreach ($this->getReportMetadata() as $report) { - if ($this->apiMethod == $report['action'] - && $this->apiModule == $report['module']) { - - return $report; - } - } - } - private function findFirstLevelReport() { - foreach ($this->getReportMetadata() as $report) { - if (!empty($report['actionToLoadSubTables']) - && $this->apiMethod == $report['actionToLoadSubTables'] - && $this->apiModule == $report['module'] + foreach (Report::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'; } } diff --git a/www/analytics/core/API/DataTablePostProcessor.php b/www/analytics/core/API/DataTablePostProcessor.php new file mode 100644 index 00000000..9a673116 --- /dev/null +++ b/www/analytics/core/API/DataTablePostProcessor.php @@ -0,0 +1,436 @@ +apiModule = $apiModule; + $this->apiMethod = $apiMethod; + $this->setRequest($request); + + $this->report = Report::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); + + $reportId = $this->apiModule . '.' . $this->apiMethod; + $pivotByColumn = Common::getRequestVar('pivotByColumn', false, 'string', $this->request); + $pivotByColumnLimit = Common::getRequestVar('pivotByColumnLimit', false, 'int', $this->request); + + $dataTable->filter('ColumnCallbackDeleteMetadata', array('segmentValue')); + $dataTable->filter('ColumnCallbackDeleteMetadata', array('segment')); + $dataTable->filter('PivotByDimension', array($reportId, $pivotBy, $pivotByColumn, $pivotByColumnLimit, + PivotByDimension::isSegmentFetchingEnabledInConfig())); + } + return $dataTable; + } + + /** + * @param DataTableInterface $dataTable + * @return DataTable|DataTableInterface|DataTable\Map + */ + public function applyFlattener($dataTable) + { + if (Common::getRequestVar('flat', '0', 'string', $this->request) == '1') { + $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(); + } + + $dataTable->filter(array($this->formatter, 'formatMetrics'), array($this->report, $metricsToFormat)); + 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 > and we need to undo that here. + $label = str_replace( htmlentities('>'), '>', $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 + $computedValue = $processedMetric->compute($row); + if ($computedValue !== false) { + $row->addColumn($name, $computedValue); + } + + $subtable = $row->getSubtable(); + if (!empty($subtable)) { + $this->computeProcessedMetrics($subtable); + } + } + } + } + } + + public function applyComputeProcessedMetrics(DataTableInterface $dataTable) + { + $dataTable->filter(array($this, 'computeProcessedMetrics')); + } +} diff --git a/www/analytics/core/API/DocumentationGenerator.php b/www/analytics/core/API/DocumentationGenerator.php index ecfece70..60807267 100644 --- a/www/analytics/core/API/DocumentationGenerator.php +++ b/www/analytics/core/API/DocumentationGenerator.php @@ -1,6 +1,6 @@ Common::getRequestVar('idSite', 1, 'int'), - 'period' => Common::getRequestVar('period', 'day', 'string'), - 'date' => Common::getRequestVar('date', 'today', 'string') - ); foreach (Proxy::getInstance()->getMetadata() as $class => $info) { $moduleName = Proxy::getInstance()->getModuleNameFromClassName($class); - if (in_array($moduleName, $this->modulesToHide)) { + $rClass = new ReflectionClass($class); + + if (!Piwik::hasUserSuperUserAccess() && $this->checkIfClassCommentContainsHideAnnotation($rClass)) { continue; } - $toc .= "$moduleName
"; - $str .= "\n

Module " . $moduleName . "

"; - $str .= "
" . $info['__documentation'] . "
"; - foreach ($info as $methodName => $infoMethod) { - if ($methodName == '__documentation') { - continue; - } - $params = $this->getParametersString($class, $methodName); - $str .= "\n
- $moduleName.$methodName " . $params . ""; - $str .= ''; - if ($outputExampleUrls) { - // we prefix all URLs with $prefixUrls - // used when we include this output in the Piwik official documentation for example - $str .= ""; - $exampleUrl = $this->getExampleUrl($class, $methodName, $parametersToSet); - if ($exampleUrl !== false) { - $lastNUrls = ''; - if (preg_match('/(&period)|(&date)/', $exampleUrl)) { - $exampleUrlRss1 = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last10', 'period' => 'day') + $parametersToSet); - $exampleUrlRss2 = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last5', 'period' => 'week',) + $parametersToSet); - $lastNUrls = ", RSS of the last 10 days"; - } - $exampleUrl = $prefixUrls . $exampleUrl; - $str .= " [ Example in - XML, - Json, - Tsv (Excel) - $lastNUrls - ]"; - } else { - $str .= " [ No example available ]"; - } - $str .= ""; - } - $str .= ''; - $str .= "
\n"; + $toDisplay = $this->prepareModulesAndMethods($info, $moduleName); + foreach ($toDisplay as $moduleName => $methods) { + $toc .= $this->prepareModuleToDisplay($moduleName); + $str .= $this->prepareMethodToDisplay($moduleName, $info, $methods, $class, $outputExampleUrls, $prefixUrls); } - $str .= '
↑ Back to top
'; } $str = "

Quick access to APIs

$toc $str"; + + return $str; + } + + public function prepareModuleToDisplay($moduleName) + { + return "$moduleName
"; + } + + public function prepareMethodToDisplay($moduleName, $info, $methods, $class, $outputExampleUrls, $prefixUrls) + { + $str = ''; + $str .= "\n

Module " . $moduleName . "

"; + $info['__documentation'] = $this->checkDocumentation($info['__documentation']); + $str .= "
" . $info['__documentation'] . "
"; + foreach ($methods as $methodName) { + if (Proxy::getInstance()->isDeprecatedMethod($class, $methodName)) { + continue; + } + + $params = $this->getParametersString($class, $methodName); + + $str .= "\n
- $moduleName.$methodName " . $params . ""; + $str .= ''; + if ($outputExampleUrls) { + $str .= $this->addExamples($class, $methodName, $prefixUrls); + } + $str .= ''; + $str .= "
\n"; + } + + return $str; + } + + public function prepareModulesAndMethods($info, $moduleName) + { + $toDisplay = array(); + + foreach ($info as $methodName => $infoMethod) { + if ($methodName == '__documentation') { + continue; + } + $toDisplay[$moduleName][] = $methodName; + } + + return $toDisplay; + } + + public 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 = ''; +// used when we include this output in the Piwik official documentation for example + $str .= ""; + $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 10 days"; + } + $exampleUrl = $prefixUrls . $exampleUrl; + $str .= " [ Example in + XML, + Json, + Tsv (Excel) + $lastNUrls + ]"; + } else { + $str .= " [ No example available ]"; + } + $str .= ""; + 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 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; + } + + private function getInterfaceString($moduleName, $class, $info, $parametersToSet, $outputExampleUrls, $prefixUrls) + { + $str = ''; + + $str .= "\n

Module " . $moduleName . "

"; + $str .= "
" . $info['__documentation'] . "
"; + foreach ($info as $methodName => $infoMethod) { + if ($methodName == '__documentation') { + continue; + } + + if (Proxy::getInstance()->isDeprecatedMethod($class, $methodName)) { + continue; + } + + $str .= $this->getMethodString($moduleName, $class, $parametersToSet, $outputExampleUrls, $prefixUrls, $methodName, $str); + } + + $str .= '
↑ Back to top
'; + return $str; } @@ -136,6 +225,7 @@ class DocumentationGenerator 'ip' => '194.57.91.215', 'idSites' => '1,2', 'idAlert' => '1', + 'seconds' => '3600', // 'segmentName' => 'browserCode', ); @@ -169,8 +259,8 @@ class DocumentationGenerator $aParameters = Proxy::getInstance()->getParametersList($class, $methodName); // Kindly force some known generic parameters to appear in the final list // the parameter 'format' can be set to all API methods (used in tests) - // the parameter 'hideIdSubDatable' is used for integration tests only - // the parameter 'serialize' sets php outputs human readable, used in integration tests and debug + // the parameter 'hideIdSubDatable' is used for system tests only + // the parameter 'serialize' sets php outputs human readable, used in system tests and debug // the parameter 'language' sets the language for the response (eg. country names) // the parameter 'flat' reduces a hierarchical table to a single level by concatenating labels // the parameter 'include_aggregate_rows' can be set to include inner nodes in flat reports @@ -183,12 +273,25 @@ class DocumentationGenerator $aParameters['label'] = false; $aParameters['flat'] = false; $aParameters['include_aggregate_rows'] = false; - $aParameters['filter_limit'] = false; //@review without adding this, I can not set filter_limit in $otherRequestParameters integration tests - $aParameters['filter_sort_column'] = false; //@review without adding this, I can not set filter_sort_column in $otherRequestParameters integration tests + $aParameters['filter_offset'] = false; //@review without adding this, I can not set filter_offset in $otherRequestParameters system tests + $aParameters['filter_limit'] = false; //@review without adding this, I can not set filter_limit in $otherRequestParameters system tests + $aParameters['filter_sort_column'] = false; //@review without adding this, I can not set filter_sort_column in $otherRequestParameters system tests + $aParameters['filter_sort_order'] = false; //@review without adding this, I can not set filter_sort_order in $otherRequestParameters system tests + $aParameters['filter_excludelowpop'] = false; //@review without adding this, I can not set filter_sort_order in $otherRequestParameters system tests + $aParameters['filter_excludelowpop_value'] = false; //@review without adding this, I can not set filter_sort_order in $otherRequestParameters system tests + $aParameters['filter_column_recursive'] = false; //@review without adding this, I can not set filter_sort_order in $otherRequestParameters system tests + $aParameters['filter_pattern_recursive'] = false; //@review without adding this, I can not set filter_sort_order in $otherRequestParameters system tests $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; $moduleName = Proxy::getInstance()->getModuleNameFromClassName($class); $aParameters = array_merge(array('module' => 'API', 'method' => $moduleName . '.' . $methodName), $aParameters); @@ -235,4 +338,43 @@ class DocumentationGenerator $sParameters = implode(", ", $asParameters); return "($sParameters)"; } + + private function getMethodString($moduleName, $class, $parametersToSet, $outputExampleUrls, $prefixUrls, $methodName) + { + $str = ''; + $token_auth = "&token_auth=" . Piwik::getCurrentUserTokenAuth(); + + $params = $this->getParametersString($class, $methodName); + $str .= "\n
- $moduleName.$methodName " . $params . ""; + $str .= ''; + + if ($outputExampleUrls) { + // we prefix all URLs with $prefixUrls + // used when we include this output in the Piwik official documentation for example + $str .= ""; + $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 10 days"; + } + $exampleUrl = $prefixUrls . $exampleUrl; + $str .= " [ Example in + XML, + Json, + Tsv (Excel) + $lastNUrls + ]"; + } else { + $str .= " [ No example available ]"; + } + $str .= ""; + } + + $str .= ''; + $str .= "
\n"; + + return $str; + } } diff --git a/www/analytics/core/API/Inconsistencies.php b/www/analytics/core/API/Inconsistencies.php new file mode 100644 index 00000000..36c85bb9 --- /dev/null +++ b/www/analytics/core/API/Inconsistencies.php @@ -0,0 +1,42 @@ +noDefaultValue = new NoDefaultValue(); } @@ -78,12 +75,14 @@ class Proxy extends Singleton $this->checkClassIsSingleton($className); $rClass = new ReflectionClass($className); - foreach ($rClass->getMethods() as $method) { - $this->loadMethodMetadata($className, $method); - } + if (!$this->shouldHideAPIMethod($rClass->getDocComment())) { + foreach ($rClass->getMethods() as $method) { + $this->loadMethodMetadata($className, $method); + } - $this->setDocumentation($rClass, $className); - $this->alreadyRegistered[$className] = true; + $this->setDocumentation($rClass, $className); + $this->alreadyRegistered[$className] = true; + } } /** @@ -164,11 +163,11 @@ class Proxy extends Singleton /** * 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') { @@ -178,7 +177,7 @@ class Proxy extends Singleton * } * } * }); - * + * * @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. @@ -187,20 +186,20 @@ class Proxy extends Singleton /** * 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)); @@ -218,16 +217,16 @@ class Proxy extends Singleton /** * 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) { @@ -238,13 +237,13 @@ class Proxy extends Singleton * } * }, 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 @@ -257,20 +256,20 @@ class Proxy extends Singleton /** * 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', function (&$returnValue, $info)) { + * 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)"; @@ -279,12 +278,12 @@ class Proxy extends Singleton * } * }, 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 @@ -323,6 +322,14 @@ class Proxy extends Singleton 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' * @@ -378,7 +385,6 @@ class Proxy extends Singleton $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']); @@ -405,7 +411,7 @@ class Proxy extends Singleton } /** - * Includes the class API by looking up plugins/UserSettings/API.php + * Includes the class API by looking up plugins/xxx/API.php * * @param string $fileName api class name eg. "API" * @throws Exception @@ -428,29 +434,27 @@ class Proxy extends Singleton */ private function loadMethodMetadata($class, $method) { - if ($method->isPublic() - && !$method->isConstructor() - && $method->getName() != 'getInstance' - && false === strstr($method->getDocComment(), '@deprecated') - && (!$this->hideIgnoredFunctions || false === strstr($method->getDocComment(), '@ignore')) - ) { - $name = $method->getName(); - $parameters = $method->getParameters(); - - $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(); + 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'); } /** @@ -468,15 +472,56 @@ class Proxy extends Singleton } /** - * Returns the number of required parameters (parameters without default values). - * - * @param string $class The class name - * @param string $name The method name - * @return int The number of required parameters + * @param $docComment + * @return bool */ - private function getNumberOfRequiredParameters($class, $name) + public function shouldHideAPIMethod($docComment) { - return $this->metadataArray[$class][$name]['numberOfRequiredParameters']; + $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; } /** @@ -500,7 +545,7 @@ class Proxy extends Singleton private function checkClassIsSingleton($className) { if (!method_exists($className, "getInstance")) { - throw new Exception("$className that provide an API must be Singleton and have a 'static public function getInstance()' method."); + throw new Exception("$className that provide an API must be Singleton and have a 'public static function getInstance()' method."); } } } diff --git a/www/analytics/core/API/Request.php b/www/analytics/core/API/Request.php index 68cd3fff..6ddb8333 100644 --- a/www/analytics/core/API/Request.php +++ b/www/analytics/core/API/Request.php @@ -1,6 +1,6 @@ process(); * echo $result; - * + * * **Getting a unrendered DataTable** - * + * * // use the convenience method 'processRequest' - * $dataTable = Request::processRequest('UserSettings.getWideScreen', array( + * $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."; @@ -69,41 +69,46 @@ use Piwik\UrlHelper; */ class Request { - protected $request = null; + 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 $request The base request string or array, eg, - * `'module=UserSettings&action=getWidescreen'`. + * @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 */ - static public function getRequestArrayFromString($request) + public static function getRequestArrayFromString($request, $defaultRequest = null) { - $defaultRequest = $_GET + $_POST; + if ($defaultRequest === null) { + $defaultRequest = self::getDefaultRequest(); - $requestRaw = self::getRequestParametersGET(); - if (!empty($requestRaw['segment'])) { - $defaultRequest['segment'] = $requestRaw['segment']; + $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)) { - $url = array(); - foreach ($request as $key => $value) { - $url[] = $key . "=" . $value; - } - $request = implode("&", $url); + $requestParsed = $request; + } else { + $request = trim($request); + $request = str_replace(array("\n", "\t"), '', $request); + + $requestParsed = UrlHelper::getArrayFromQueryString($request); } - $request = trim($request); - $request = str_replace(array("\n", "\t"), '', $request); - - $requestParsed = UrlHelper::getArrayFromQueryString($request); $requestArray = $requestParsed + $defaultRequest; } @@ -119,14 +124,17 @@ class Request * Constructor. * * @param string|array $request Query string that defines the API call (must at least contain a **method** parameter), - * eg, `'method=UserSettings.getWideScreen&idSite=1&date=yesterday&period=week&format=xml'` + * 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) + public function __construct($request = null, $defaultRequest = null) { - $this->request = self::getRequestArrayFromString($request); + $this->request = self::getRequestArrayFromString($request, $defaultRequest); $this->sanitizeRequest(); + $this->renameModuleAndActionInRequest(); } /** @@ -134,19 +142,23 @@ class Request * we rewrite to correct renamed plugin: Referrers * * @param $module - * @return string + * @param $action + * @return array( $module, $action ) * @ignore */ - public static function renameModule($module) + public static function getRenamedModuleAndAction($module, $action) { - $moduleToRedirect = array( - 'Referers' => 'Referrers', - 'PDFReports' => 'ScheduledReports', - ); - if (isset($moduleToRedirect[$module])) { - return $moduleToRedirect[$module]; - } - return $module; + /** + * 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); } /** @@ -168,9 +180,9 @@ class Request /** * 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 @@ -178,10 +190,10 @@ class Request * - 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** @@ -196,42 +208,90 @@ class Request // create the response $response = new ResponseBuilder($outputFormat, $this->request); + $corsHandler = new CORSHandler(); + $corsHandler->handle(); + + $tokenAuth = Common::getRequestVar('token_auth', '', 'string', $this->request); + $shouldReloadAuth = false; + try { // 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); - $module = $this->renameModule($module); + $apiClassName = self::getClassNameAPI($module); - if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated($module)) { - throw new PluginDeactivatedException($module); + if ($shouldReloadAuth = self::shouldReloadAuthUsingTokenAuth($this->request)) { + $access = Access::getInstance(); + $tokenAuthToRestore = $access->getTokenAuth(); + $hadSuperUserAccess = $access->hasSuperUserAccess(); + self::forceReloadAuthUsingTokenAuth($tokenAuth); } - $apiClassName = $this->getClassNameAPI($module); - - self::reloadAuthUsingTokenAuth($this->request); // call the method $returnedValue = Proxy::getInstance()->call($apiClassName, $method, $this->request); $toReturn = $response->getResponse($returnedValue, $module, $method); } catch (Exception $e) { + Log::debug($e); + $toReturn = $response->getResponseException($e); } + + 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'`. */ - static public function getClassNameAPI($plugin) + public static function getClassNameAPI($plugin) { return sprintf('\Piwik\Plugins\%s\API', $plugin); } + /** + * Detect if request is an API request. Meaning the module is 'API' and an API method having a valid format was + * specified. + * + * @param array $request eg array('module' => 'API', 'method' => 'Test.getMethod') + * @return bool + * @throws Exception + */ + public static function isApiRequest($request) + { + $module = Common::getRequestVar('module', '', 'string', $request); + $method = Common::getRequestVar('method', '', 'string', $request); + + return $module === 'API' && !empty($method) && (count(explode('.', $method)) === 2); + } + /** * If the token_auth is found in the $request parameter, * the current session will be authenticated using this token_auth. @@ -241,28 +301,60 @@ class Request * @return void * @ignore */ - static public function reloadAuthUsingTokenAuth($request = null) + 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 ($token_auth) { - /** - * 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 `Registry::get('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($token_auth)); - Access::getInstance()->reloadAccess(); - SettingsServer::raiseMemoryLimitIfNecessary(); + 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)); + Access::getInstance()->reloadAccess(); + 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 * @@ -286,18 +378,23 @@ class Request * @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()) + 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); + $request = new Request($params, $defaultRequest); return $request->process(); } @@ -305,7 +402,7 @@ class Request * 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() @@ -343,7 +440,7 @@ class Request // unless the filter param was in $queryParams $genericFiltersInfo = DataTableGenericFilter::getGenericFiltersInformation(); foreach ($genericFiltersInfo as $filter) { - foreach ($filter as $queryParamName => $queryParamInfo) { + foreach ($filter[1] as $queryParamName => $queryParamInfo) { if (!isset($params[$queryParamName])) { $params[$queryParamName] = null; } @@ -379,10 +476,10 @@ class Request /** * Returns the segment query parameter from the original request, without modifications. - * + * * @return array|bool */ - static public function getRawSegmentFromRequest() + 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; @@ -395,4 +492,23 @@ class Request } 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; + } } diff --git a/www/analytics/core/API/ResponseBuilder.php b/www/analytics/core/API/ResponseBuilder.php index 692a2ceb..3fe13f96 100644 --- a/www/analytics/core/API/ResponseBuilder.php +++ b/www/analytics/core/API/ResponseBuilder.php @@ -1,6 +1,6 @@ request = $request; $this->outputFormat = $outputFormat; + $this->request = $request; + $this->apiRenderer = ApiRenderer::factory($outputFormat, $request); + } + + public function disableSendHeader() + { + $this->sendHeader = false; + } + + public function disableDataTablePostProcessor() + { + $this->postProcessDataTable = false; } /** @@ -70,61 +82,21 @@ class ResponseBuilder $this->apiModule = $apiModule; $this->apiMethod = $apiMethod; - if($this->outputFormat == 'original') { - @header('Content-Type: text/plain; charset=utf-8'); - } - return $this->renderValue($value); - } + $this->sendHeaderIfEnabled(); - /** - * Returns an error $message in the requested $format - * - * @param Exception $e - * @throws Exception - * @return string - */ - public function getResponseException(Exception $e) - { - $format = strtolower($this->outputFormat); - - if ($format == 'original') { - throw $e; - } - - try { - $renderer = Renderer::factory($format); - } catch (Exception $exceptionRenderer) { - return "Error: " . $e->getMessage() . " and: " . $exceptionRenderer->getMessage(); - } - - $e = $this->decorateExceptionWithDebugTrace($e); - - $renderer->setException($e); - - if ($format == 'php') { - $renderer->setSerialize($this->caseRendererPHPSerialize()); - } - - return $renderer->renderException(); - } - - /** - * @param $value - * @return string - */ - protected function renderValue($value) - { // when null or void is returned from the api call, we handle it as a successful operation if (!isset($value)) { - return $this->handleSuccess(); + 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 DataTable - || $value instanceof DataTable\Map - ) { + if ($value instanceof DataTableInterface) { return $this->handleDataTable($value); } @@ -137,26 +109,39 @@ class ResponseBuilder return $this->handleArray($value); } - // original data structure requested, we return without process - if ($this->outputFormat == 'original') { - return $value; + if (is_object($value)) { + return $this->apiRenderer->renderObject($value); } - if (is_object($value) - || is_resource($value) - ) { - return $this->getResponseException(new Exception('The API cannot handle this data structure.')); + if (is_resource($value)) { + return $this->apiRenderer->renderResource($value); } - // bool // integer // float // serialized object - return $this->handleScalar($value); + return $this->apiRenderer->renderScalar($value); + } + + /** + * Returns an error $message in the requested $format + * + * @param Exception $e + * @throws Exception + * @return string + */ + public function getResponseException(Exception $e) + { + $e = $this->decorateExceptionWithDebugTrace($e); + $message = $this->formatExceptionMessage($e); + + $this->sendHeaderIfEnabled(); + + return $this->apiRenderer->renderException($message, $e); } /** * @param Exception $e * @return Exception */ - protected function decorateExceptionWithDebugTrace(Exception $e) + private function decorateExceptionWithDebugTrace(Exception $e) { // If we are in tests, show full backtrace if (defined('PIWIK_PATH_TEST_TO_ROOT')) { @@ -165,314 +150,109 @@ class ResponseBuilder } 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; } - /** - * Returns true if the user requested to serialize the output data (&serialize=1 in the request) - * - * @param mixed $defaultSerializeValue Default value in case the user hasn't specified a value - * @return bool - */ - protected function caseRendererPHPSerialize($defaultSerializeValue = 1) + private function formatExceptionMessage(Exception $exception) { - $serialize = Common::getRequestVar('serialize', $defaultSerializeValue, 'int', $this->request); - if ($serialize) { + $message = $exception->getMessage(); + if (\Piwik_ShouldPrintBackTraceWithMessage()) { + $message .= "\n" . $exception->getTraceAsString(); + } + + 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 ($this->shouldApplyLimitOnArray($limit, $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 shouldApplyLimitOnArray($limit, $offset) + { + if ($limit === -1) { + // all fields are requested + return false; + } + + if ($offset > 0) { + // an offset is specified, we have to apply the limit return true; } - return false; + + // "api_datatable_default_limit" is set by API\Controller if no filter_limit is specified by the user. + // it holds the number of the configured default limit. + $limitSetBySystem = Common::getRequestVar('api_datatable_default_limit', -2, 'integer', $this->request); + + // we ignore the limit if the datatable_default_limit was set by the system as this default filter_limit is + // only meant for dataTables but not for arrays. This way we stay BC as filter_limit was not applied pre + // Piwik 2.6 and some fixes were made in Piwik 2.13. + $wasFilterLimitSetBySystem = $limitSetBySystem !== -2; + + // we check for "$limitSetBySystem === $limit" as an API method could request another API method with + // another limit. In this case we need to apply it again. + $isLimitStillDefaultLimit = $limitSetBySystem === $limit; + + if ($wasFilterLimitSetBySystem && $isLimitStillDefaultLimit) { + return false; + } + + return true; } - /** - * Apply the specified renderer to the DataTable - * - * @param DataTable|array $dataTable - * @return string - */ - protected function getRenderedDataTable($dataTable) + private function sendHeaderIfEnabled() { - $format = strtolower($this->outputFormat); - - // if asked for original dataStructure - if ($format == 'original') { - // by default "original" data is not serialized - if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) { - $dataTable = serialize($dataTable); - } - return $dataTable; + if ($this->sendHeader) { + $this->apiRenderer->sendHeader(); } - - $method = Common::getRequestVar('method', '', 'string', $this->request); - - $renderer = Renderer::factory($format); - $renderer->setTable($dataTable); - $renderer->setRenderSubTables(Common::getRequestVar('expanded', false, 'int', $this->request)); - $renderer->setHideIdSubDatableFromResponse(Common::getRequestVar('hideIdSubDatable', false, 'int', $this->request)); - - if ($format == 'php') { - $renderer->setSerialize($this->caseRendererPHPSerialize()); - $renderer->setPrettyDisplay(Common::getRequestVar('prettyDisplay', false, 'int', $this->request)); - } else if ($format == 'html') { - $renderer->setTableId($this->request['method']); - } else if ($format == 'csv' || $format == 'tsv') { - $renderer->setConvertToUnicode(Common::getRequestVar('convertToUnicode', true, 'int', $this->request)); - } - - // prepare translation of column names - if ($format == 'html' || $format == 'csv' || $format == 'tsv' || $format = 'rss') { - $renderer->setApiMethod($method); - $renderer->setIdSite(Common::getRequestVar('idSite', false, 'int', $this->request)); - $renderer->setTranslateColumnNames(Common::getRequestVar('translateColumnNames', false, 'int', $this->request)); - } - - return $renderer->render(); - } - - /** - * Returns a success $message in the requested $format - * - * @param string $message - * @return string - */ - protected function handleSuccess($message = 'ok') - { - // return a success message only if no content has already been buffered, useful when APIs return raw text or html content to the browser - if (!ob_get_contents()) { - switch ($this->outputFormat) { - case 'xml': - @header("Content-Type: text/xml;charset=utf-8"); - $return = - "\n" . - "\n" . - "\t\n" . - ""; - break; - case 'json': - @header("Content-Type: application/json"); - $return = '{"result":"success", "message":"' . $message . '"}'; - break; - case 'php': - $return = array('result' => 'success', 'message' => $message); - if ($this->caseRendererPHPSerialize()) { - $return = serialize($return); - } - break; - - case 'csv': - @header("Content-Type: application/vnd.ms-excel"); - @header("Content-Disposition: attachment; filename=piwik-report-export.csv"); - $return = "message\n" . $message; - break; - - default: - $return = 'Success:' . $message; - break; - } - return $return; - } - } - - /** - * Converts the given scalar to an data table - * - * @param mixed $scalar - * @return string - */ - protected function handleScalar($scalar) - { - $dataTable = new Simple(); - $dataTable->addRowsFromArray(array($scalar)); - return $this->getRenderedDataTable($dataTable); - } - - /** - * Handles the given data table - * - * @param DataTable $datatable - * @return string - */ - protected function handleDataTable($datatable) - { - // if requested, flatten nested tables - if (Common::getRequestVar('flat', '0', 'string', $this->request) == '1') { - $flattener = new Flattener($this->apiModule, $this->apiMethod, $this->request); - if (Common::getRequestVar('include_aggregate_rows', '0', 'string', $this->request) == '1') { - $flattener->includeAggregateRows(); - } - $datatable = $flattener->flatten($datatable); - } - - if (1 == Common::getRequestVar('totals', '1', 'integer', $this->request)) { - $genericFilter = new ReportTotalsCalculator($this->apiModule, $this->apiMethod, $this->request); - $datatable = $genericFilter->calculate($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)) { - $genericFilter = new DataTableGenericFilter($this->request); - $genericFilter->filter($datatable); - } - - // we automatically safe decode all datatable labels (against xss) - $datatable->queueFilter('SafeDecodeLabel'); - - // 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(); - } - - // 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); - if ($hideColumns !== '' || $showColumns !== '') { - $datatable->filter('ColumnDelete', array($hideColumns, $showColumns)); - } - - // apply label filter: only return rows matching the label parameter (more than one if more than one label) - $label = $this->getLabelFromRequest($this->request); - 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 $this->getRenderedDataTable($datatable); - } - - /** - * Converts the given simple array to a data table - * - * @param array $array - * @return string - */ - protected function handleArray($array) - { - if ($this->outputFormat == 'original') { - // we handle the serialization. Because some php array have a very special structure that - // couldn't be converted with the automatic DataTable->addRowsFromSimpleArray - // the user may want to request the original PHP data structure serialized by the API - // in case he has to setup serialize=1 in the URL - if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) { - return serialize($array); - } - return $array; - } - - $multiDimensional = $this->handleMultiDimensionalArray($array); - if ($multiDimensional !== false) { - return $multiDimensional; - } - - return $this->getRenderedDataTable($array); - } - - /** - * Is this a multi dimensional array? - * Multi dim arrays are not supported by the Datatable renderer. - * We manually render these. - * - * array( - * array( - * 1, - * 2 => array( 1, - * 2 - * ) - * ), - * array( 2, - * 3 - * ) - * ); - * - * @param array $array - * @return string|bool false if it isn't a multidim array - */ - protected function handleMultiDimensionalArray($array) - { - $first = reset($array); - foreach ($array as $first) { - if (is_array($first)) { - foreach ($first as $key => $value) { - // Yes, this is a multi dim array - if (is_array($value)) { - switch ($this->outputFormat) { - case 'json': - @header("Content-Type: application/json"); - return self::convertMultiDimensionalArrayToJson($array); - break; - - case 'php': - if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) { - return serialize($array); - } - return $array; - - case 'xml': - @header("Content-Type: text/xml;charset=utf-8"); - return $this->getRenderedDataTable($array); - default: - break; - } - } - } - } - } - return false; - } - - /** - * Render a multidimensional array to Json - * Handle DataTable|Set elements in the first dimension only, following case does not work: - * array( - * array( - * DataTable, - * 2 => array( - * 1, - * 2 - * ), - * ), - * ); - * - * @param array $array can contain scalar, arrays, DataTable and Set - * @return string - */ - public static function convertMultiDimensionalArrayToJson($array) - { - $jsonRenderer = new Json(); - $jsonRenderer->setTable($array); - $renderedReport = $jsonRenderer->render(); - return $renderedReport; - } - - /** - * 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 - */ - static public 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; - } - - static public 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 > and we need to undo that here. - $label = Common::unsanitizeInputValues($label); - return $label; } } diff --git a/www/analytics/core/Access.php b/www/analytics/core/Access.php index 71a5dd5f..881810bf 100644 --- a/www/analytics/core/Access.php +++ b/www/analytics/core/Access.php @@ -1,6 +1,6 @@ resetSites(); + } + + private function resetSites() { $this->idsitesByAccess = array( 'view' => array(), @@ -138,15 +130,22 @@ class Access */ public function reloadAccess(Auth $auth = null) { - if (!is_null($auth)) { + $this->resetSites(); + + if (isset($auth)) { $this->auth = $auth; } - // if the Auth wasn't set, we may be in the special case of setSuperUser(), otherwise we fail - if (is_null($this->auth)) { - if ($this->hasSuperUserAccess()) { - return $this->reloadAccessSuperUser(); - } + 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 ($this->auth === null) { return false; } @@ -156,28 +155,23 @@ class Access if (!$result->wasAuthenticationSuccessful()) { return false; } + $this->login = $result->getIdentity(); $this->token_auth = $result->getTokenAuth(); // case the superUser is logged in if ($result->hasSuperUserAccess()) { - return $this->reloadAccessSuperUser(); + $this->setSuperUserAccess(true); } - // in case multiple calls to API using different tokens, we ensure we reset it as not SU - $this->setSuperUserAccess(false); - // 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) { - $this->idsitesByAccess[$access['access']][] = $access['idsite']; - } return true; } public function getRawSitesWithSomeViewAccess($login) { - return Db::fetchAll(self::getSqlAccessSite("access, t2.idsite"), $login); + $sql = self::getSqlAccessSite("access, t2.idsite"); + + return Db::fetchAll($sql, $login); } /** @@ -188,29 +182,50 @@ class Access */ public static function getSqlAccessSite($select) { - return "SELECT " . $select . " - FROM " . Common::prefixTable('access') . " as t1 - JOIN " . Common::prefixTable('site') . " as t2 USING (idsite) " . - " WHERE login = ?"; + $access = Common::prefixTable('access'); + $siteTable = Common::prefixTable('site'); + + return "SELECT " . $select . " FROM " . $access . " as t1 + JOIN " . $siteTable . " as t2 USING (idsite) WHERE login = ?"; } /** - * Reload Super User access + * Make sure a login name is set * - * @return bool + * @return true */ - protected function reloadAccessSuperUser() + protected function makeSureLoginNameIsSet() { - $this->hasSuperUserAccess = true; - - try { - $allSitesId = Plugins\SitesManager\API::getInstance()->getAllSitesId(); - } catch (\Exception $e) { - $allSitesId = array(); + if (empty($this->login)) { + // flag to force non empty login so Super User is not mistaken for anonymous + $this->login = 'super user was set'; } - $this->idsitesByAccess['superuser'] = $allSitesId; + } - return true; + protected function loadSitesIfNeeded() + { + if ($this->hasSuperUserAccess) { + if (empty($this->idsitesByAccess['superuser'])) { + try { + $allSitesId = Plugins\SitesManager\API::getInstance()->getAllSitesId(); + } catch (\Exception $e) { + $allSitesId = array(); + } + $this->idsitesByAccess['superuser'] = $allSitesId; + } + } elseif (isset($this->login)) { + if (empty($this->idsitesByAccess['view']) + && 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) { + $this->idsitesByAccess[$access['access']][] = $access['idsite']; + } + } + } } /** @@ -221,12 +236,12 @@ class Access */ public function setSuperUserAccess($bool = true) { - if ($bool) { - $this->reloadAccessSuperUser(); - } else { - $this->hasSuperUserAccess = false; - $this->idsitesByAccess['superuser'] = array(); + $this->hasSuperUserAccess = (bool) $bool; + if ($bool) { + $this->makeSureLoginNameIsSet(); + } else { + $this->resetSites(); } } @@ -269,6 +284,8 @@ class Access */ public function getSitesIdWithAtLeastViewAccess() { + $this->loadSitesIfNeeded(); + return array_unique(array_merge( $this->idsitesByAccess['view'], $this->idsitesByAccess['admin'], @@ -284,13 +301,14 @@ class Access */ 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. * @@ -300,6 +318,8 @@ class Access */ public function getSitesIdWithViewAccess() { + $this->loadSitesIfNeeded(); + return $this->idsitesByAccess['view']; } @@ -315,6 +335,22 @@ class Access } } + /** + * 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 ADMIN access for at least one website, throws an exception * @@ -322,11 +358,7 @@ class Access */ public function checkUserHasSomeAdminAccess() { - if ($this->hasSuperUserAccess()) { - return; - } - $idSitesAccessible = $this->getSitesIdWithAdminAccess(); - if (count($idSitesAccessible) == 0) { + if (!$this->isUserHasSomeAdminAccess()) { throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('admin'))); } } @@ -341,7 +373,9 @@ class Access if ($this->hasSuperUserAccess()) { return; } + $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess(); + if (count($idSitesAccessible) == 0) { throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('view'))); } @@ -359,8 +393,10 @@ class Access 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))); @@ -380,8 +416,10 @@ class Access 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))); @@ -401,11 +439,41 @@ class Access } $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(); + + self::getInstance()->setSuperUserAccess(true); + + try { + $result = $function(); + } catch (Exception $ex) { + self::getInstance()->setSuperUserAccess($isSuperUser); + + throw $ex; + } + + self::getInstance()->setSuperUserAccess($isSuperUser); + + return $result; + } } /** diff --git a/www/analytics/core/Application/Environment.php b/www/analytics/core/Application/Environment.php new file mode 100644 index 00000000..74388358 --- /dev/null +++ b/www/analytics/core/Application/Environment.php @@ -0,0 +1,246 @@ +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; + } + } +} diff --git a/www/analytics/core/Application/EnvironmentManipulator.php b/www/analytics/core/Application/EnvironmentManipulator.php new file mode 100644 index 00000000..15be5ac0 --- /dev/null +++ b/www/analytics/core/Application/EnvironmentManipulator.php @@ -0,0 +1,59 @@ +settingsProvider = $settingsProvider; + $this->translator = $translator; + } + + public function validate() + { + $inTrackerRequest = SettingsServer::isTrackerApiRequest(); + $inConsole = Common::isPhpCliMode(); + + $this->checkConfigFileExists($this->settingsProvider->getPathGlobal()); + $this->checkConfigFileExists($this->settingsProvider->getPathLocal(), $startInstaller = !$inTrackerRequest && !$inConsole); + } + + /** + * @param $path + * @param bool $startInstaller + * @throws \Exception + */ + private function checkConfigFileExists($path, $startInstaller = false) + { + if (is_readable($path)) { + return; + } + + $message = $this->translator->translate('General_ExceptionConfigurationFileNotFound', array($path)); + if (Common::isPhpCliMode()) { + $message .= "\n" . $this->translator->translate('General_ExceptionConfigurationFileNotFound2', array($path, get_current_user())); + } + + $exception = new \Exception($message); + + if ($startInstaller) { + /** + * 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); + } else { + throw $exception; + } + } +} diff --git a/www/analytics/core/Application/Kernel/GlobalSettingsProvider.php b/www/analytics/core/Application/Kernel/GlobalSettingsProvider.php new file mode 100644 index 00000000..f459e793 --- /dev/null +++ b/www/analytics/core/Application/Kernel/GlobalSettingsProvider.php @@ -0,0 +1,111 @@ + 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; + } +} diff --git a/www/analytics/core/Application/Kernel/PluginList.php b/www/analytics/core/Application/Kernel/PluginList.php new file mode 100644 index 00000000..5d263a6e --- /dev/null +++ b/www/analytics/core/Application/Kernel/PluginList.php @@ -0,0 +1,120 @@ +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. + * + * @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 + sort($otherPluginsToLoadAfterDefaultPlugins); + + $sorted = array_merge($defaultPluginsLoadedFirst, $otherPluginsToLoadAfterDefaultPlugins); + + return $sorted; + } +} diff --git a/www/analytics/core/Archive.php b/www/analytics/core/Archive.php index e5884aea..2efe2303 100644 --- a/www/analytics/core/Archive.php +++ b/www/analytics/core/Archive.php @@ -1,6 +1,6 @@ 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']; * @@ -68,41 +69,40 @@ use Piwik\Period\Range; * // 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::getDataTableFromArchive('MyPlugin_MyReport', $idSite, $period, $date, $segment, $expanded); * $dataTable->queueFilter('ReplaceColumnNames'); * 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')); - * + * * * [1]: The archiving process will not be launched if browser archiving is disabled - * and the current request came from a browser (and not the **archive.php** cron - * script). + * and the current request came from a browser. * * * @api @@ -162,6 +162,16 @@ class Archive */ 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. @@ -173,6 +183,8 @@ class Archive $this->params = $params; $this->forceIndexedBySite = $forceIndexedBySite; $this->forceIndexedByDate = $forceIndexedByDate; + + $this->invalidator = StaticContainer::get('Piwik\Archive\ArchiveInvalidator'); } /** @@ -192,37 +204,42 @@ class Archive * 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. - * @param bool $skipAggregationOfSubTables Whether the archive, when it is processed, should also aggregate all sub-tables - * @return Archive + * @return static */ - public static function build($idSites, $period, $strDate, $segment = false, $_restrictSitesToLogin = false, $skipAggregationOfSubTables = false) + public static function build($idSites, $period, $strDate, $segment = false, $_restrictSitesToLogin = false) { $websiteIds = Site::getIdSitesFromIdSitesString($idSites, $_restrictSitesToLogin); + $timezone = false; + if (count($websiteIds) == 1) { + $timezone = Site::getTimezoneFor($websiteIds[0]); + } + if (Period::isMultiplePeriod($strDate, $period)) { - $oPeriod = new Range($period, $strDate); + $oPeriod = PeriodFactory::build($period, $strDate, $timezone); $allPeriods = $oPeriod->getSubperiods(); } else { - $timezone = count($websiteIds) == 1 ? Site::getTimezoneFor($websiteIds[0]) : false; - $oPeriod = Period::makePeriodFromQueryParams($timezone, $period, $strDate); + $oPeriod = PeriodFactory::makePeriodFromQueryParams($timezone, $period, $strDate); $allPeriods = array($oPeriod); } - $segment = new Segment($segment, $websiteIds); - $idSiteIsAll = $idSites == self::REQUEST_ALL_WEBSITES_FLAG; + + $segment = new Segment($segment, $websiteIds); + $idSiteIsAll = $idSites == self::REQUEST_ALL_WEBSITES_FLAG; $isMultipleDate = Period::isMultiplePeriod($strDate, $period); - return Archive::factory($segment, $allPeriods, $websiteIds, $idSiteIsAll, $isMultipleDate, $skipAggregationOfSubTables); + + return static::factory($segment, $allPeriods, $websiteIds, $idSiteIsAll, $isMultipleDate); } /** * 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)`). @@ -232,41 +249,42 @@ class Archive * @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`. - * @param bool $skipAggregationOfSubTables Whether the archive should skip aggregation of all sub-tables * * @return Archive */ - public static function factory(Segment $segment, array $periods, array $idSites, $idSiteIsAll = false, $isMultipleDate = false, $skipAggregationOfSubTables = false) + public static 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, $skipAggregationOfSubTables); + $params = new Parameters($idSites, $periods, $segment); - return new Archive($params, $forceIndexedBySite, $forceIndexedByDate); + return new static($params, $forceIndexedBySite, $forceIndexedByDate); } /** * 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|numeric|array `false` if there is no data to return, a single numeric value if we're not querying + * @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. */ @@ -288,50 +306,22 @@ class Archive return $result; } - /** - * Queries and returns blob data in an array. - * - * Reports are stored in blobs as serialized arrays of {@link DataTable\Row} instances, but this - * data can technically be anything. In other words, you can store whatever you want - * as archive data blobs. - * - * 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, `'Referrers_keywordBySearchEngine'`. - * @param null|string $idSubtable If we're returning serialized DataTable data, then this refers - * to the subtable ID to return. If set to 'all', all subtables - * of each requested report are returned. - * @return array An array of appropriately indexed blob data. - */ - public function getBlob($names, $idSubtable = null) - { - $data = $this->get($names, 'blob', $idSubtable); - return $data->getIndexedArray($this->getResultIndices()); - } - /** * 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. @@ -343,22 +333,40 @@ class Archive 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. @@ -372,13 +380,13 @@ class Archive /** * 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. @@ -400,16 +408,18 @@ class Archive /** * 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); } @@ -427,7 +437,7 @@ class Archive /** * 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 $name The name of the report to return. * @param int|string|array $idSite @see {@link build()} * @param string $period @see {@link build()} @@ -435,21 +445,19 @@ class Archive * @param string $segment @see {@link build()} * @param bool $expanded If true, loads all subtables. See {@link getDataTableExpanded()} * @param int|null $idSubtable See {@link getDataTableExpanded()} - * @param bool $skipAggregationOfSubTables Whether or not we should skip the aggregation of all sub-tables and only aggregate parent DataTable. * @param int|null $depth See {@link getDataTableExpanded()} + * @throws \Exception * @return DataTable|DataTable\Map See {@link getDataTable()} and * {@link getDataTableExpanded()} for more * information + * @deprecated Since Piwik 2.12.0 Use Archive::createDataTableFromArchive() instead */ public static function getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded, - $idSubtable = null, $skipAggregationOfSubTables = false, $depth = null) + $idSubtable = null, $depth = null) { Piwik::checkUserHasViewAccess($idSite); - if($skipAggregationOfSubTables && ($expanded || $idSubtable)) { - throw new \Exception("Not expected to skipAggregationOfSubTables when expanded=1 or idSubtable is set."); - } - $archive = Archive::build($idSite, $period, $date, $segment, $_restrictSitesToLogin = false, $skipAggregationOfSubTables); + $archive = Archive::build($idSite, $period, $date, $segment, $_restrictSitesToLogin = false); if ($idSubtable === false) { $idSubtable = null; } @@ -465,9 +473,102 @@ class Archive return $dataTable; } - private function appendIdSubtable($recordName, $id) + /** + * 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) { - return $recordName . "_" . $id; + if ($flat && !$idSubtable) { + $expanded = true; + } + + $dataTable = self::getDataTableFromArchive($recordName, $idSite, $period, $date, $segment, $expanded, $idSubtable, $depth); + + $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(); } /** @@ -477,7 +578,7 @@ class Archive * @param null|int $idSubtable * @return Archive\DataCollection */ - private function get($archiveNames, $archiveDataType, $idSubtable = null) + protected function get($archiveNames, $archiveDataType, $idSubtable = null) { if (!is_array($archiveNames)) { $archiveNames = array($archiveNames); @@ -487,35 +588,41 @@ class Archive if ($idSubtable !== null && $idSubtable != self::ID_SUBTABLE_LOAD_ALL_SUBTABLES ) { - foreach ($archiveNames as &$name) { - $name = $this->appendIdsubtable($name, $idSubtable); + // 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( - $archiveNames, $archiveDataType, $this->params->getIdSites(), $this->params->getPeriods(), $defaultRow = null); + $dataNames, $archiveDataType, $this->params->getIdSites(), $this->params->getPeriods(), $defaultRow = null); $archiveIds = $this->getArchiveIds($archiveNames); + if (empty($archiveIds)) { return $result; } - $loadAllSubtables = $idSubtable == self::ID_SUBTABLE_LOAD_ALL_SUBTABLES; - $archiveData = ArchiveSelector::getArchiveData($archiveIds, $archiveNames, $archiveDataType, $loadAllSubtables); + $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) - $idSite = $row['idsite']; - $periodStr = $row['date1'] . "," . $row['date2']; + $periodStr = $row['date1'] . ',' . $row['date2']; - if ($archiveDataType == 'numeric') { - $value = $this->formatNumericValue($row['value']); + if ($isNumeric) { + $row['value'] = $this->formatNumericValue($row['value']); } else { - $value = $this->uncompress($row['value']); - $result->addMetadata($idSite, $periodStr, 'ts_archived', $row['ts_archived']); + $result->addMetadata($row['idsite'], $periodStr, DataTable::ARCHIVED_DATE_METADATA_NAME, $row['ts_archived']); } - $resultRow = & $result->get($idSite, $periodStr); - $resultRow[$row['name']] = $value; + $result->set($row['idsite'], $periodStr, $row['name'], $row['value']); } return $result; @@ -526,6 +633,9 @@ class Archive * 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) { @@ -533,7 +643,7 @@ class Archive // 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(); + $doneFlags = array(); $archiveGroups = array(); foreach ($plugins as $plugin) { $doneFlag = $this->getDoneStringForPlugin($plugin); @@ -542,7 +652,7 @@ class Archive if (!isset($this->idarchives[$doneFlag])) { $archiveGroup = $this->getArchiveGroupOfPlugin($plugin); - if($archiveGroup == self::ARCHIVE_ALL_PLUGINS_FLAG) { + if ($archiveGroup == self::ARCHIVE_ALL_PLUGINS_FLAG) { $archiveGroup = reset($plugins); } $archiveGroups[] = $archiveGroup; @@ -560,19 +670,7 @@ class Archive } } - // 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; - } - } - } + $idArchivesByMonth = $this->getIdArchivesByMonth($doneFlags); return $idArchivesByMonth; } @@ -587,6 +685,8 @@ class Archive */ private function cacheArchiveIdsAfterLaunching($archiveGroups, $plugins) { + $this->invalidatedReportsIfNeeded(); + $today = Date::today(); foreach ($this->params->getPeriods() as $period) { @@ -600,14 +700,14 @@ class Archive // 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::verbose("Archive site %s, %s (%s) skipped, archive is before the website was created.", + 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::verbose("Archive site %s, %s (%s) skipped, archive is after today.", + Log::debug("Archive site %s, %s (%s) skipped, archive is after today.", $idSite, $period->getLabel(), $period->getPrettyString()); continue; } @@ -627,7 +727,7 @@ class Archive private function cacheArchiveIdsWithoutLaunching($plugins) { $idarchivesByReport = ArchiveSelector::getArchiveIds( - $this->params->getIdSites(), $this->params->getPeriods(), $this->params->getSegment(), $plugins, $this->params->isSkipAggregationOfSubTables()); + $this->params->getIdSites(), $this->params->getPeriods(), $this->params->getSegment(), $plugins); // initialize archive ID cache for each report foreach ($plugins as $plugin) { @@ -655,8 +755,7 @@ class Archive $this->params->getIdSites(), $this->params->getSegment(), $this->getPeriodLabel(), - $plugin, - $this->params->isSkipAggregationOfSubTables() + $plugin ); } @@ -707,11 +806,6 @@ class Archive return round((float)$value, 2); } - private function uncompress($data) - { - return @gzuncompress($data); - } - /** * Initializes the archive ID cache ($this->idarchives) for a particular 'done' flag. * @@ -723,6 +817,8 @@ class Archive * 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) { @@ -745,7 +841,10 @@ class Archive */ private function getArchiveGroupOfPlugin($plugin) { - if ($this->getPeriodLabel() != 'range') { + $periods = $this->params->getPeriods(); + $periodLabel = reset($periods)->getLabel(); + + if (Rules::shouldProcessReportsAllPlugins($this->params->getIdSites(), $this->params->getSegment(), $periodLabel)) { return self::ARCHIVE_ALL_PLUGINS_FLAG; } @@ -755,7 +854,7 @@ class Archive /** * Returns the name of the plugin that archives a given report. * - * @param string $report Archive data name, eg, `'nb_visits'`, `'UserSettings_...'`, etc. + * @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. @@ -766,9 +865,9 @@ class Archive if (in_array($report, Metrics::getVisitsMetricNames())) { $report = 'VisitsSummary_CoreMetrics'; } // Goal_* metrics are processed by the Goals plugin (HACK) - else if (strpos($report, 'Goal_') === 0) { + elseif (strpos($report, 'Goal_') === 0) { $report = 'Goals_Metrics'; - } else if (strrpos($report, '_returning') === strlen($report) - strlen('_returning')) { // HACK + } elseif (strrpos($report, '_returning') === strlen($report) - strlen('_returning')) { // HACK $report = 'VisitFrequency_Metrics'; } @@ -789,7 +888,7 @@ class Archive */ private function prepareArchive(array $archiveGroups, Site $site, Period $period) { - $parameters = new ArchiveProcessor\Parameters($site, $period, $this->params->getSegment(), $this->params->isSkipAggregationOfSubTables()); + $parameters = new ArchiveProcessor\Parameters($site, $period, $this->params->getSegment()); $archiveLoader = new ArchiveProcessor\Loader($parameters); $periodString = $period->getRangeString(); @@ -801,9 +900,37 @@ class Archive $idArchive = $archiveLoader->prepareArchive($plugin); - if($idArchive) { + 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; + } } diff --git a/www/analytics/core/Archive/ArchiveInvalidator.php b/www/analytics/core/Archive/ArchiveInvalidator.php new file mode 100644 index 00000000..6ecc5b87 --- /dev/null +++ b/www/analytics/core/Archive/ArchiveInvalidator.php @@ -0,0 +1,317 @@ +model = $model; + } + + public function rememberToInvalidateArchivedReportsLater($idSite, Date $date) + { + $key = $this->buildRememberArchivedReportId($idSite, $date->toString()); + $value = Option::get($key); + + // 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) + + if (false === $value) { + Option::set($key, '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 buildRememberArchivedReportId($idSite, $date) + { + $id = $this->buildRememberArchivedReportIdForSite($idSite); + $id .= '_' . trim($date); + + return $id; + } + + private function buildRememberArchivedReportIdForSite($idSite) + { + return $this->rememberArchivedReportIdStart . (int) $idSite; + } + + public function forgetRememberedArchivedReportsToInvalidateForSite($idSite) + { + $id = $this->buildRememberArchivedReportIdForSite($idSite) . '_%'; + Option::deleteLike($id); + } + + /** + * @internal + */ + public function forgetRememberedArchivedReportsToInvalidate($idSite, Date $date) + { + $id = $this->buildRememberArchivedReportId($idSite, $date->toString()); + + Option::delete($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(); + + $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); + } +} diff --git a/www/analytics/core/Archive/ArchiveInvalidator/InvalidationResult.php b/www/analytics/core/Archive/ArchiveInvalidator/InvalidationResult.php new file mode 100644 index 00000000..517e1138 --- /dev/null +++ b/www/analytics/core/Archive/ArchiveInvalidator/InvalidationResult.php @@ -0,0 +1,56 @@ +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; + } +} \ No newline at end of file diff --git a/www/analytics/core/Archive/ArchivePurger.php b/www/analytics/core/Archive/ArchivePurger.php new file mode 100644 index 00000000..078203cb --- /dev/null +++ b/www/analytics/core/Archive/ArchivePurger.php @@ -0,0 +1,272 @@ +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; + } +} diff --git a/www/analytics/core/Archive/Chunk.php b/www/analytics/core/Archive/Chunk.php new file mode 100644 index 00000000..70afec44 --- /dev/null +++ b/www/analytics/core/Archive/Chunk.php @@ -0,0 +1,144 @@ +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()); + } +} diff --git a/www/analytics/core/Archive/DataCollection.php b/www/analytics/core/Archive/DataCollection.php index 5f57e6c3..36ba553f 100644 --- a/www/analytics/core/Archive/DataCollection.php +++ b/www/analytics/core/Archive/DataCollection.php @@ -1,6 +1,6 @@ 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. @@ -188,6 +203,7 @@ class DataCollection $this->putRowInIndex($result, $indexKeys, $row, $idSite, $period); } } + return $result; } @@ -208,9 +224,27 @@ class DataCollection $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 @@ -249,6 +283,7 @@ class DataCollection $dataTableFactory->useSubtable($idSubTable); $index = $this->getIndexedArray($resultIndices); + return $dataTableFactory->make($index, $resultIndices); } @@ -296,12 +331,16 @@ class DataCollection if ($metadataName == DataTableFactory::TABLE_METADATA_SITE_INDEX) { $indexKeyValues = array_values($this->sitesId); - } else if ($metadataName == DataTableFactory::TABLE_METADATA_PERIOD_INDEX) { + } elseif ($metadataName == DataTableFactory::TABLE_METADATA_PERIOD_INDEX) { $indexKeyValues = array_keys($this->periods); } - foreach ($indexKeyValues as $key) { - $result[$key] = $this->createOrderedIndex($metadataNamesToIndexBy); + if (empty($metadataNamesToIndexBy)) { + $result = array_fill_keys($indexKeyValues, array()); + } else { + foreach ($indexKeyValues as $key) { + $result[$key] = $this->createOrderedIndex($metadataNamesToIndexBy); + } } } @@ -318,7 +357,7 @@ class DataCollection foreach ($metadataNamesToIndexBy as $metadataName) { if ($metadataName == DataTableFactory::TABLE_METADATA_SITE_INDEX) { $key = $idSite; - } else if ($metadataName == DataTableFactory::TABLE_METADATA_PERIOD_INDEX) { + } elseif ($metadataName == DataTableFactory::TABLE_METADATA_PERIOD_INDEX) { $key = $period; } else { $key = $row[self::METADATA_CONTAINER_ROW_KEY][$metadataName]; diff --git a/www/analytics/core/Archive/DataTableFactory.php b/www/analytics/core/Archive/DataTableFactory.php index 44beba02..e8b17b67 100644 --- a/www/analytics/core/Archive/DataTableFactory.php +++ b/www/analytics/core/Archive/DataTableFactory.php @@ -1,6 +1,6 @@ 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. @@ -128,6 +145,11 @@ class DataTableFactory $this->idSubtable = $idSubtable; } + private function isNumericDataType() + { + return $this->dataType == 'numeric'; + } + /** * Creates a DataTable|Set instance using an index of * archive data. @@ -139,21 +161,63 @@ class DataTableFactory */ 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->dataType == 'numeric' + && $this->isNumericDataType() ) { $index = $this->defaultRow; } - $dataTable = $this->createDataTable($index, $keyMetadata = array()); + $dataTable = $this->createDataTable($index, $keyMetadata); } else { - $dataTable = $this->createDataTableMapFromIndex($index, $resultIndices, $keyMetadata = array()); + $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); } - $this->transformMetadata($dataTable); return $dataTable; } @@ -171,16 +235,16 @@ class DataTableFactory * @param array $blobRow * @return DataTable|DataTable\Map */ - private function makeFromBlobRow($blobRow) + private function makeFromBlobRow($blobRow, $keyMetadata) { if ($blobRow === false) { return new DataTable(); } if (count($this->dataNames) === 1) { - return $this->makeDataTableFromSingleBlob($blobRow); + return $this->makeDataTableFromSingleBlob($blobRow, $keyMetadata); } else { - return $this->makeIndexedByRecordNameDataTable($blobRow); + return $this->makeIndexedByRecordNameDataTable($blobRow, $keyMetadata); } } @@ -192,7 +256,7 @@ class DataTableFactory * @param array $blobRow * @return DataTable */ - private function makeDataTableFromSingleBlob($blobRow) + private function makeDataTableFromSingleBlob($blobRow, $keyMetadata) { $recordName = reset($this->dataNames); if ($this->idSubtable !== null) { @@ -206,7 +270,7 @@ class DataTableFactory } // set table metadata - $table->setMetadataValues(DataCollection::getDataRowMetadata($blobRow)); + $table->setAllTableMetadata(array_merge(DataCollection::getDataRowMetadata($blobRow), $keyMetadata)); if ($this->expandDataTable) { $table->enableRecursiveFilters(); @@ -223,12 +287,12 @@ class DataTableFactory * @param array $blobRow * @return DataTable\Map */ - private function makeIndexedByRecordNameDataTable($blobRow) + private function makeIndexedByRecordNameDataTable($blobRow, $keyMetadata) { $table = new DataTable\Map(); $table->setKeyName('recordName'); - $tableMetadata = DataCollection::getDataRowMetadata($blobRow); + $tableMetadata = array_merge(DataCollection::getDataRowMetadata($blobRow), $keyMetadata); foreach ($blobRow as $name => $blob) { $newTable = DataTable::fromSerializedArray($blob); @@ -248,23 +312,23 @@ class DataTableFactory * @param array $keyMetadata The metadata to add to the table when it's created. * @return DataTable\Map */ - private function createDataTableMapFromIndex($index, $resultIndices, $keyMetadata = array()) + private function createDataTableMapFromIndex($index, $resultIndices, $keyMetadata) { - $resultIndexLabel = reset($resultIndices); + $result = new DataTable\Map(); + $result->setKeyName(reset($resultIndices)); $resultIndex = key($resultIndices); array_shift($resultIndices); - $result = new DataTable\Map(); - $result->setKeyName($resultIndexLabel); + $hasIndices = !empty($resultIndices); foreach ($index as $label => $value) { - $keyMetadata[$resultIndex] = $label; + $keyMetadata[$resultIndex] = $this->createTableIndexMetadata($resultIndex, $label); - if (empty($resultIndices)) { - $newTable = $this->createDataTable($value, $keyMetadata); - } else { + if ($hasIndices) { $newTable = $this->createDataTableMapFromIndex($value, $resultIndices, $keyMetadata); + } else { + $newTable = $this->createDataTable($value, $keyMetadata); } $result->addTable($newTable, $this->prettifyIndexLabel($resultIndex, $label)); @@ -273,6 +337,15 @@ class DataTableFactory 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. * @@ -283,11 +356,11 @@ class DataTableFactory private function createDataTable($data, $keyMetadata) { if ($this->dataType == 'blob') { - $result = $this->makeFromBlobRow($data); + $result = $this->makeFromBlobRow($data, $keyMetadata); } else { - $result = $this->makeFromMetricsArray($data); + $result = $this->makeFromMetricsArray($data, $keyMetadata); } - $this->setTableMetadata($keyMetadata, $result); + return $result; } @@ -307,7 +380,7 @@ class DataTableFactory && $treeLevel >= $this->maxSubtableDepth ) { // unset the subtables so DataTableManager doesn't throw - foreach ($dataTable->getRows() as $row) { + foreach ($dataTable->getRowsWithoutSummaryRow() as $row) { $row->removeSubtable(); } @@ -316,7 +389,7 @@ class DataTableFactory $dataName = reset($this->dataNames); - foreach ($dataTable->getRows() as $row) { + foreach ($dataTable->getRowsWithoutSummaryRow() as $row) { $sid = $row->getIdSubDataTable(); if ($sid === null) { continue; @@ -340,17 +413,12 @@ class DataTableFactory } } - /** - * Converts site IDs and period string ranges into Site instances and - * Period instances in DataTable metadata. - */ - private function transformMetadata($table) + private function getDefaultMetadata() { - $periods = $this->periods; - $table->filter(function ($table) use ($periods) { - $table->setMetadata(DataTableFactory::TABLE_METADATA_SITE_INDEX, new Site($table->getMetadata(DataTableFactory::TABLE_METADATA_SITE_INDEX))); - $table->setMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX, $periods[$table->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)]); - }); + return array( + DataTableFactory::TABLE_METADATA_SITE_INDEX => new Site(reset($this->sitesId)), + DataTableFactory::TABLE_METADATA_PERIOD_INDEX => reset($this->periods), + ); } /** @@ -368,39 +436,16 @@ class DataTableFactory return $label; } - /** - * @param $keyMetadata - * @param $result - */ - private function setTableMetadata($keyMetadata, $result) - { - if (!isset($keyMetadata[DataTableFactory::TABLE_METADATA_SITE_INDEX])) { - $keyMetadata[DataTableFactory::TABLE_METADATA_SITE_INDEX] = reset($this->sitesId); - } - - if (!isset($keyMetadata[DataTableFactory::TABLE_METADATA_PERIOD_INDEX])) { - reset($this->periods); - $keyMetadata[DataTableFactory::TABLE_METADATA_PERIOD_INDEX] = key($this->periods); - } - - // Note: $result can be a DataTable\Map - $result->filter(function ($table) use ($keyMetadata) { - foreach ($keyMetadata as $name => $value) { - $table->setMetadata($name, $value); - } - }); - } - /** * @param $data * @return DataTable\Simple */ - private function makeFromMetricsArray($data) + private function makeFromMetricsArray($data, $keyMetadata) { $table = new DataTable\Simple(); if (!empty($data)) { - $table->setAllTableMetadata(DataCollection::getDataRowMetadata($data)); + $table->setAllTableMetadata(array_merge(DataCollection::getDataRowMetadata($data), $keyMetadata)); DataCollection::removeMetadataFromDataRow($data); @@ -412,15 +457,89 @@ class DataTableFactory // w/o this code, an empty array would be created, and other parts of Piwik // would break. if (count($this->dataNames) == 1 - && $this->dataType == 'numeric' + && $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; + } +} diff --git a/www/analytics/core/Archive/Parameters.php b/www/analytics/core/Archive/Parameters.php index 1f700819..905166de 100644 --- a/www/analytics/core/Archive/Parameters.php +++ b/www/analytics/core/Archive/Parameters.php @@ -1,6 +1,6 @@ segment; } - public function __construct($idSites, $periods, Segment $segment, $skipAggregationOfSubTables) + public function __construct($idSites, $periods, Segment $segment) { $this->idSites = $idSites; $this->periods = $periods; $this->segment = $segment; - $this->skipAggregationOfSubTables = $skipAggregationOfSubTables; } public function getPeriods() @@ -63,11 +56,4 @@ class Parameters { return $this->idSites; } - - public function isSkipAggregationOfSubTables() - { - return $this->skipAggregationOfSubTables; - } - } - diff --git a/www/analytics/core/ArchiveProcessor.php b/www/analytics/core/ArchiveProcessor.php index 20c4c2df..f24b4993 100644 --- a/www/analytics/core/ArchiveProcessor.php +++ b/www/analytics/core/ArchiveProcessor.php @@ -1,6 +1,6 @@ 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 + * + * // aggregate a report * $archiveProcessor->aggregateDataTableRecords('MyPlugin_myFancyReport'); * } - * + * */ class ArchiveProcessor { /** * @var \Piwik\DataAccess\ArchiveWriter */ - protected $archiveWriter; + private $archiveWriter; /** * @var \Piwik\DataAccess\LogAggregator */ - protected $logAggregator; + private $logAggregator; /** * @var Archive @@ -94,28 +92,41 @@ class ArchiveProcessor /** * @var Parameters */ - protected $params; + private $params; /** * @var int */ - protected $numberOfVisits = false; - protected $numberOfVisitsConverted = false; + private $numberOfVisits = false; - public function __construct(Parameters $params, ArchiveWriter $archiveWriter) + 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 = new LogAggregator($params); + $this->logAggregator = $logAggregator; $this->archiveWriter = $archiveWriter; + + $this->skipUniqueVisitorsCalculationForMultipleSites = Rules::shouldSkipUniqueVisitorsCalculationForMultipleSites(); } protected function getArchive() { - if(empty($this->archive)) { + if (empty($this->archive)) { $subPeriods = $this->params->getSubPeriods(); - $idSites = $this->params->getIdSites(); + $idSites = $this->params->getIdSites(); $this->archive = Archive::factory($this->params->getSegment(), $subPeriods, $idSites); } + return $this->archive; } @@ -155,7 +166,8 @@ class ArchiveProcessor * @var array */ protected static $columnsToRenameAfterAggregation = array( - Metrics::INDEX_NB_UNIQ_VISITORS => Metrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS + Metrics::INDEX_NB_UNIQ_VISITORS => Metrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS, + Metrics::INDEX_NB_USERS => Metrics::INDEX_SUM_DAILY_NB_USERS, ); /** @@ -172,8 +184,11 @@ class ArchiveProcessor * @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()), @@ -188,25 +203,23 @@ class ArchiveProcessor $maximumRowsInSubDataTable = null, $columnToSortByBeforeTruncation = null, &$columnsAggregationOperation = null, - $columnsToRenameAfterAggregation = 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); - $rowsCount = $table->getRowsCount(); - $nameToCount[$recordName]['level0'] = $rowsCount; - - $rowsCountRecursive = $rowsCount; - if($this->isAggregateSubTables()) { - $rowsCountRecursive = $table->getRowsCountRecursive(); + $nameToCount[$recordName]['level0'] = $table->getRowsCount(); + if ($countRowsRecursive === true || (is_array($countRowsRecursive) && in_array($recordName, $countRowsRecursive))) { + $nameToCount[$recordName]['recursive'] = $table->getRowsCountRecursive(); } - $nameToCount[$recordName]['recursive'] = $rowsCountRecursive; $blob = $table->getSerialized($maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation); Common::destroy($table); @@ -228,12 +241,12 @@ class ArchiveProcessor * @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 @@ -242,7 +255,8 @@ class ArchiveProcessor { $metrics = $this->getAggregatedNumericMetrics($columns, $operationToApply); - foreach($metrics as $column => $value) { + foreach ($metrics as $column => $value) { + $value = Common::forceDotAsSeparatorForDecimalPoint($value); $this->archiveWriter->insertRecord($column, $value); } // if asked for only one field to sum @@ -256,7 +270,7 @@ class ArchiveProcessor public function getNumberOfVisits() { - if($this->numberOfVisits === false) { + if ($this->numberOfVisits === false) { throw new Exception("visits should have been set here"); } return $this->numberOfVisits; @@ -273,7 +287,7 @@ class ArchiveProcessor * * @param array $numericRecords A name-value mapping of numeric values that should be * archived, eg, - * + * * array('Referrers_distinctKeywords' => 23, 'Referrers_distinctCampaigns' => 234) * @api */ @@ -297,6 +311,8 @@ class ArchiveProcessor public function insertNumericRecord($name, $value) { $value = round($value, 2); + $value = Common::forceDotAsSeparatorForDecimalPoint($value); + $this->archiveWriter->insertRecord($name, $value); } @@ -328,20 +344,53 @@ class ArchiveProcessor */ protected function aggregateDataTableRecord($name, $columnsAggregationOperation = null, $columnsToRenameAfterAggregation = null) { - if($this->isAggregateSubTables()) { - // By default we shall aggregate all sub-tables. - $dataTable = $this->getArchive()->getDataTableExpanded($name, $idSubTable = null, $depth = null, $addMetadataSubtableId = false); - } else { - // In some cases (eg. Actions plugin when period=range), - // for better performance we will only aggregate the parent table - $dataTable = $this->getArchive()->getDataTable($name, $idSubTable = null); + // 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); - $this->renameColumnsAfterAggregation($dataTable, $columnsToRenameAfterAggregation); + + if (!$columnsRenamed) { + $this->renameColumnsAfterAggregation($dataTable, $columnsToRenameAfterAggregation); + } + 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(); @@ -357,18 +406,54 @@ class ArchiveProcessor protected function enrichWithUniqueVisitorsMetric(Row $row) { - if(!$this->getParams()->isSingleSite() ) { - // we only compute unique visitors for a single site + // 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) { - if (SettingsPiwik::isUniqueVisitorsEnabled($this->getParams()->getPeriod()->getLabel())) { - $uniqueVisitors = (float)$this->computeNbUniqVisitors(); - $row->setColumn('nb_uniq_visitors', $uniqueVisitors); - } else { - $row->deleteColumn('nb_uniq_visitors'); - } + + 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) @@ -388,14 +473,15 @@ class ArchiveProcessor * 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. * - * @return int + * @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 computeNbUniqVisitors() + protected function computeNbUniques($metrics) { $logAggregator = $this->getLogAggregator(); - $query = $logAggregator->queryVisitsByDimension(array(), false, array(), array(Metrics::INDEX_NB_UNIQ_VISITORS)); + $query = $logAggregator->queryVisitsByDimension(array(), false, array(), $metrics); $data = $query->fetch(); - return $data[Metrics::INDEX_NB_UNIQ_VISITORS]; + return $data; } /** @@ -409,15 +495,18 @@ class ArchiveProcessor 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, $this->isAggregateSubTables()); + $table->addDataTable($data); } + return $table; } @@ -429,22 +518,33 @@ class ArchiveProcessor protected function aggregatedDataTableMapsAsOne(Map $map, DataTable $aggregated) { foreach ($map->getDataTables() as $tableToAggregate) { - if($tableToAggregate instanceof Map) { + if ($tableToAggregate instanceof Map) { $this->aggregatedDataTableMapsAsOne($tableToAggregate, $aggregated); } else { - $aggregated->addDataTable($tableToAggregate, $this->isAggregateSubTables()); + $aggregated->addDataTable($tableToAggregate); } } } - protected function renameColumnsAfterAggregation(DataTable $table, $columnsToRenameAfterAggregation = null) + /** + * 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; } - foreach ($columnsToRenameAfterAggregation as $oldName => $newName) { - $table->renameColumn($oldName, $newName, $this->isAggregateSubTables()); + + foreach ($table->getRows() as $row) { + foreach ($columnsToRenameAfterAggregation as $oldName => $newName) { + $row->renameColumn($oldName, $newName); + } + + $subTable = $row->getSubtable(); + if ($subTable) { + $this->renameColumnsAfterAggregation($subTable, $columnsToRenameAfterAggregation); + } } } @@ -453,6 +553,7 @@ class ArchiveProcessor if (!is_array($columns)) { $columns = array($columns); } + $operationForColumn = $this->getOperationForColumns($columns, $operationToApply); $dataTable = $this->getArchive()->getDataTableFromNumeric($columns); @@ -463,11 +564,11 @@ class ArchiveProcessor } $rowMetrics = $results->getFirstRow(); - if($rowMetrics === false) { + if ($rowMetrics === false) { $rowMetrics = new Row; } $this->enrichWithUniqueVisitorsMetric($rowMetrics); - $this->renameColumnsAfterAggregation($results); + $this->renameColumnsAfterAggregation($results, self::$columnsToRenameAfterAggregation); $metrics = $rowMetrics->getColumns(); @@ -476,14 +577,7 @@ class ArchiveProcessor $metrics[$name] = 0; } } + return $metrics; } - - /** - * @return bool - */ - protected function isAggregateSubTables() - { - return !$this->getParams()->isSkipAggregationOfSubTables(); - } } diff --git a/www/analytics/core/ArchiveProcessor/Loader.php b/www/analytics/core/ArchiveProcessor/Loader.php index 03a054a6..c08bf2c5 100644 --- a/www/analytics/core/ArchiveProcessor/Loader.php +++ b/www/analytics/core/ArchiveProcessor/Loader.php @@ -1,18 +1,20 @@ params, $this->isArchiveTemporary()); + if ($this->mustProcessVisitCount($visits) || $this->doesRequestedPluginIncludeVisitsSummary() ) { @@ -114,14 +119,14 @@ class Loader $visits = $metrics['nb_visits']; $visitsConverted = $metrics['nb_visits_converted']; } - if ($this->isThereSomeVisits($visits)) { + + if ($this->isThereSomeVisits($visits) + || $this->shouldArchiveForSiteEvenWhenNoVisits() + ) { $pluginsArchiver->callAggregateAllPlugins($visits, $visitsConverted); } - $idArchive = $pluginsArchiver->finalizeArchive(); - if (!$this->params->isSingleSiteDayArchive() && $visits) { - ArchiveSelector::purgeOutdatedArchives($this->params->getPeriod()->getDateStart()); - } + $idArchive = $pluginsArchiver->finalizeArchive(); return array($idArchive, $visits); } @@ -139,11 +144,13 @@ class Loader { $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]; } @@ -165,9 +172,11 @@ class Loader } $idAndVisits = ArchiveSelector::getArchiveIdAndVisits($this->params, $minDatetimeArchiveProcessedUTC); + if (!$idAndVisits) { return $noArchiveFound; } + return $idAndVisits; } @@ -186,19 +195,27 @@ class Loader // 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($this->params->getDateStart(), $this->params->getPeriod(), $this->params->getSegment(), $this->params->getSite()); + 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; } @@ -207,8 +224,30 @@ class Loader 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); + } +} diff --git a/www/analytics/core/ArchiveProcessor/Parameters.php b/www/analytics/core/ArchiveProcessor/Parameters.php index 9d9a9088..4edd1f4b 100644 --- a/www/analytics/core/ArchiveProcessor/Parameters.php +++ b/www/analytics/core/ArchiveProcessor/Parameters.php @@ -1,6 +1,6 @@ site = $site; $this->period = $period; $this->segment = $segment; - $this->skipAggregationOfSubTables = $skipAggregationOfSubTables; } /** @@ -91,7 +90,7 @@ class Parameters */ public function getSubPeriods() { - if($this->getPeriod()->getLabel() == 'day') { + if ($this->getPeriod()->getLabel() == 'day') { return array( $this->getPeriod() ); } return $this->getPeriod()->getSubperiods(); @@ -169,18 +168,13 @@ class Parameters return count($this->getIdSites()) == 1; } - public function isSkipAggregationOfSubTables() - { - return $this->skipAggregationOfSubTables; - } - public function logStatusDebug($isTemporary) { $temporary = 'definitive archive'; if ($isTemporary) { $temporary = 'temporary archive'; } - Log::verbose( + Log::debug( "%s archive, idSite = %d (%s), segment '%s', report = '%s', UTC datetime [%s -> %s]", $this->getPeriod()->getLabel(), $this->getSite()->getId(), diff --git a/www/analytics/core/ArchiveProcessor/PluginsArchiver.php b/www/analytics/core/ArchiveProcessor/PluginsArchiver.php index 82f361f7..28c90cb1 100644 --- a/www/analytics/core/ArchiveProcessor/PluginsArchiver.php +++ b/www/analytics/core/ArchiveProcessor/PluginsArchiver.php @@ -1,6 +1,6 @@ archiveWriter = new ArchiveWriter($this->params, $isTemporaryArchive); $this->archiveWriter->initNewArchive(); - $this->archiveProcessor = new ArchiveProcessor($this->params, $this->archiveWriter); + $this->logAggregator = new LogAggregator($params); + + $this->archiveProcessor = new ArchiveProcessor($this->params, $this->archiveWriter, $this->logAggregator); $this->isSingleSiteDayArchive = $this->params->isSingleSiteDayArchive(); } @@ -57,7 +68,9 @@ class PluginsArchiver */ public function callAggregateCoreMetrics() { - if($this->isSingleSiteDayArchive) { + $this->logAggregator->setQueryOriginHint('Core'); + + if ($this->isSingleSiteDayArchive) { $metrics = $this->aggregateDayVisitsMetrics(); } else { $metrics = $this->aggregateMultipleVisitsMetrics(); @@ -81,27 +94,57 @@ class PluginsArchiver */ public function callAggregateAllPlugins($visits, $visitsConverted) { + Log::debug("PluginsArchiver::%s: Initializing archiving process for all plugins [visits = %s, visits converted = %s]", + __FUNCTION__, $visits, $visitsConverted); + $this->archiveProcessor->setNumberOfVisits($visits, $visitsConverted); $archivers = $this->getPluginArchivers(); - foreach($archivers as $pluginName => $archiverClass) { - + 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 = new $archiverClass($this->archiveProcessor); - if(!$archiver->isEnabled()) { + if (!$archiver->isEnabled()) { + Log::debug("PluginsArchiver::%s: Skipping archiving for plugin '%s'.", __FUNCTION__, $pluginName); continue; } - if($this->shouldProcessReportsForPlugin($pluginName)) { - if($this->isSingleSiteDayArchive) { - $archiver->aggregateDayReport(); - } else { - $archiver->aggregateMultipleReports(); + + if ($this->shouldProcessReportsForPlugin($pluginName)) { + + $this->logAggregator->setQueryOriginHint($pluginName); + + try { + $timer = new Timer(); + if ($this->isSingleSiteDayArchive) { + Log::debug("PluginsArchiver::%s: Archiving day reports for plugin '%s'.", __FUNCTION__, $pluginName); + + $archiver->aggregateDayReport(); + } else { + Log::debug("PluginsArchiver::%s: Archiving period reports for plugin '%s'.", __FUNCTION__, $pluginName); + + $archiver->aggregateMultipleReports(); + } + + $this->logAggregator->setQueryOriginHint(''); + + Log::debug("PluginsArchiver::%s: %s while archiving %s reports for plugin '%s'.", + __FUNCTION__, + $timer->getMemoryLeak(), + $this->params->getPeriod()->getLabel(), + $pluginName + ); + } catch (Exception $e) { + $className = get_class($e); + $exception = new $className($e->getMessage() . " - caused by plugin $pluginName", $e->getCode(), $e); + + throw $exception; } + } else { + Log::debug("PluginsArchiver::%s: Not archiving reports for plugin '%s'.", __FUNCTION__, $pluginName); } Manager::getInstance()->deleteAll($latestUsedTableId); @@ -111,7 +154,7 @@ class PluginsArchiver public function finalizeArchive() { - $this->params->logStatusDebug( $this->archiveWriter->isArchiveTemporary ); + $this->params->logStatusDebug($this->archiveWriter->isArchiveTemporary); $this->archiveWriter->finalizeArchive(); return $this->archiveWriter->getIdArchive(); } @@ -193,5 +236,4 @@ class PluginsArchiver $metrics = $this->archiveProcessor->aggregateNumericMetrics($toSum); return $metrics; } - } diff --git a/www/analytics/core/ArchiveProcessor/Rules.php b/www/analytics/core/ArchiveProcessor/Rules.php index 6bcd5fbd..0ec92f75 100644 --- a/www/analytics/core/ArchiveProcessor/Rules.php +++ b/www/analytics/core/ArchiveProcessor/Rules.php @@ -1,6 +1,6 @@ getHash() . '.' . $plugin . $partial ; + return 'done' . $segment->getHash() . '.' . $plugin ; } - private static function getDoneFlagArchiveContainsAllPlugins(Segment $segment) + public static function getDoneFlagArchiveContainsAllPlugins(Segment $segment) { return 'done' . $segment->getHash(); } /** - * @param $plugin - * @param $isSkipAggregationOfSubTables - * @return string - */ - private static function isFlagArchivePartial($plugin, $isSkipAggregationOfSubTables) - { - $partialArchive = ''; - if ($plugin != "VisitsSummary" // VisitsSummary is always called when segmenting and should not have its own .partial archive - && $isSkipAggregationOfSubTables - ) { - $partialArchive = '.partial'; - } - return $partialArchive; - } - - /** + * 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, $isSkipAggregationOfSubTables) + public static function getDoneFlags(array $plugins, Segment $segment) { $doneFlags = array(); $doneAllPlugins = self::getDoneFlagArchiveContainsAllPlugins($segment); @@ -124,58 +106,12 @@ class Rules $plugins = array_unique($plugins); foreach ($plugins as $plugin) { - $doneOnePlugin = self::getDoneFlagArchiveContainsOnePlugin($segment, $plugin, $isSkipAggregationOfSubTables); + $doneOnePlugin = self::getDoneFlagArchiveContainsOnePlugin($segment, $plugin); $doneFlags[$plugin] = $doneOnePlugin; } return $doneFlags; } - /** - * Given a monthly archive table, will delete all reports that are now outdated, - * or reports that ended with an error - * - * @param \Piwik\Date $date - * @return int|bool False, or timestamp indicating which archives to delete - */ - public static function shouldPurgeOutdatedArchives(Date $date) - { - if (self::$purgeDisabledByTests) { - return false; - } - $key = self::FLAG_TABLE_PURGED . "blob_" . $date->toString('Y_m'); - $timestamp = Option::get($key); - - // we shall purge temporary archives after their timeout is finished, plus an extra 6 hours - // in case archiving is disabled or run once a day, we give it this extra time to run - // and re-process more recent records... - $temporaryArchivingTimeout = self::getTodayArchiveTimeToLive(); - $hoursBetweenPurge = 6; - $purgeEveryNSeconds = max($temporaryArchivingTimeout, $hoursBetweenPurge * 3600); - - // we only delete archives if we are able to process them, otherwise, the browser might process reports - // when &segment= is specified (or custom date range) and would below, delete temporary archives that the - // browser is not able to process until next cron run (which could be more than 1 hour away) - if (self::isRequestAuthorizedToArchive() - && (!$timestamp - || $timestamp < time() - $purgeEveryNSeconds) - ) { - Option::set($key, time()); - - if (self::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 - $purgeArchivesOlderThan = Date::factory(time() - 2 * $temporaryArchivingTimeout)->getDateTime(); - } else { - // If archive.php via Cron is building the reports, we should keep all temporary reports from today - $purgeArchivesOlderThan = Date::factory('today')->getDateTime(); - } - return $purgeArchivesOlderThan; - } - - Log::info("Purging temporary archives: skipped."); - return false; - } - public static function getMinTimeProcessedForTemporaryArchive( Date $dateStart, \Piwik\Period $period, Segment $segment, Site $site) { @@ -213,30 +149,45 @@ class Rules { $uiSettingIsEnabled = Controller::isGeneralSettingsAdminEnabled(); - if($uiSettingIsEnabled) { + if ($uiSettingIsEnabled) { $timeToLive = Option::get(self::OPTION_TODAY_ARCHIVE_TTL); if ($timeToLive !== false) { return $timeToLive; } } + return self::getTodayArchiveTimeToLiveDefault(); + } + + public static function getTodayArchiveTimeToLiveDefault() + { return Config::getInstance()->General['time_before_today_archive_considered_outdated']; } public static function isArchivingDisabledFor(array $idSites, Segment $segment, $periodLabel) { + $generalConfig = Config::getInstance()->General; + if ($periodLabel == 'range') { - return false; + if (!isset($generalConfig['archiving_range_force_on_browser_request']) + || $generalConfig['archiving_range_force_on_browser_request'] != false + ) { + return false; + } else { + Log::debug("Not forcing archiving for range period."); + } } + $processOneReportOnly = !self::shouldProcessReportsAllPlugins($idSites, $segment, $periodLabel); $isArchivingDisabled = !self::isRequestAuthorizedToArchive() || self::$archivingDisabledByTests; - if ($processOneReportOnly) { - + if ($processOneReportOnly + && $periodLabel != 'range' + ) { // When there is a segment, we disable archiving when browser_archiving_disabled_enforce applies if (!$segment->isEmpty() && $isArchivingDisabled - && Config::getInstance()->General['browser_archiving_disabled_enforce'] - && !SettingsServer::isArchivePhpTriggered() // Only applies when we are not running archive.php + && $generalConfig['browser_archiving_disabled_enforce'] + && !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; @@ -248,7 +199,7 @@ class Rules return $isArchivingDisabled; } - protected static function isRequestAuthorizedToArchive() + public static function isRequestAuthorizedToArchive() { return Rules::isBrowserTriggerEnabled() || SettingsServer::isArchivePhpTriggered(); } @@ -257,7 +208,7 @@ class Rules { $uiSettingIsEnabled = Controller::isGeneralSettingsAdminEnabled(); - if($uiSettingIsEnabled) { + if ($uiSettingIsEnabled) { $browserArchivingEnabled = Option::get(self::OPTION_BROWSER_TRIGGER_ARCHIVING); if ($browserArchivingEnabled !== false) { return (bool)$browserArchivingEnabled; @@ -275,6 +226,18 @@ class Rules 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 @@ -294,11 +257,24 @@ class Rules // Turns out the getString() above returns the URL decoded segment string $segmentsToProcessUrlDecoded = array_map('urldecode', $segmentsToProcess); - if (in_array($segment, $segmentsToProcess) - || in_array($segment, $segmentsToProcessUrlDecoded) - ) { - return true; + 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 false; + + return $possibleValues; } } diff --git a/www/analytics/core/Archiver/Request.php b/www/analytics/core/Archiver/Request.php new file mode 100644 index 00000000..e1d1909d --- /dev/null +++ b/www/analytics/core/Archiver/Request.php @@ -0,0 +1,48 @@ +url = $url; + } + + public function before($callable) + { + $this->before = $callable; + } + + public function start() + { + if ($this->before) { + $callable = $this->before; + $callable(); + } + } + + public function __toString() + { + return $this->url; + } +} diff --git a/www/analytics/core/AssetManager.php b/www/analytics/core/AssetManager.php index 972c84c3..407a2058 100644 --- a/www/analytics/core/AssetManager.php +++ b/www/analytics/core/AssetManager.php @@ -1,6 +1,6 @@ cacheBuster = UIAssetCacheBuster::getInstance(); - $this->minimalStylesheetFetcher = new StaticUIAssetFetcher(array('plugins/Zeitgeist/stylesheets/base.less'), array(), $this->theme); + $this->minimalStylesheetFetcher = new StaticUIAssetFetcher(array('plugins/Morpheus/stylesheets/base.less', 'plugins/Morpheus/stylesheets/general/_forms.less'), array(), $this->theme); $theme = Manager::getInstance()->getThemeEnabled(); - if(!empty($theme)) { + if (!empty($theme)) { $this->theme = new Theme(); } } @@ -121,14 +120,11 @@ class AssetManager extends Singleton $result = ""; if ($this->isMergedAssetsDisabled()) { - $this->getMergedCoreJSAsset()->delete(); $this->getMergedNonCoreJSAsset()->delete(); $result .= $this->getIndividualJsIncludes(); - } 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); } @@ -201,13 +197,13 @@ class AssetManager extends Singleton { $loadedPlugins = array(); - foreach(Manager::getInstance()->getPluginsLoadedAndActivated() as $plugin) { - + foreach (Manager::getInstance()->getPluginsLoadedAndActivated() as $plugin) { $pluginName = $plugin->getPluginName(); $pluginIsCore = Manager::getInstance()->isPluginBundledWithCore($pluginName); - if(($pluginIsCore && $core) || (!$pluginIsCore && !$core)) + if (($pluginIsCore && $core) || (!$pluginIsCore && !$core)) { $loadedPlugins[] = $pluginName; + } } return $loadedPlugins; @@ -220,23 +216,15 @@ class AssetManager extends Singleton { $assetsToRemove = array($this->getMergedStylesheetAsset()); - if($pluginName) { - - if($this->pluginContainsJScriptAssets($pluginName)) { - - PiwikConfig::getInstance()->init(); - if(Manager::getInstance()->isPluginBundledWithCore($pluginName)) { - + 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(); } @@ -252,8 +240,7 @@ class AssetManager extends Singleton */ public function getAssetDirectory() { - $mergedFileDirectory = PIWIK_USER_PATH . "/tmp/assets"; - $mergedFileDirectory = SettingsPiwik::rewriteTmpPathWithHostname($mergedFileDirectory); + $mergedFileDirectory = StaticContainer::get('path.tmp') . '/assets'; if (!is_dir($mergedFileDirectory)) { Filesystem::mkdir($mergedFileDirectory); @@ -273,7 +260,15 @@ class AssetManager extends Singleton */ public function isMergedAssetsDisabled() { - return Config::getInstance()->Debug['disable_merged_assets']; + 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; } /** @@ -311,7 +306,6 @@ class AssetManager extends Singleton $jsIncludeString = ''; foreach ($assetFetcher->getCatalog()->getAssets() as $jsFile) { - $jsFile->validateFile(); $jsIncludeString = $jsIncludeString . sprintf(self::JS_IMPORT_DIRECTIVE, $jsFile->getRelativeLocation()); } @@ -339,7 +333,7 @@ class AssetManager extends Singleton try { $assets = $fetcher->getCatalog()->getAssets(); - } catch(\Exception $e) { + } 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; @@ -347,14 +341,14 @@ class AssetManager extends Singleton $plugin = Manager::getInstance()->getLoadedPlugin($pluginName); - if($plugin->isTheme()) { - + if ($plugin->isTheme()) { $theme = Manager::getInstance()->getTheme($pluginName); $javaScriptFiles = $theme->getJavaScriptFiles(); - if(!empty($javaScriptFiles)) + if (!empty($javaScriptFiles)) { $assets = array_merge($assets, $javaScriptFiles); + } } return !empty($assets); @@ -363,9 +357,9 @@ class AssetManager extends Singleton /** * @param UIAsset[] $uiAssets */ - private function removeAssets($uiAssets) + public function removeAssets($uiAssets) { - foreach($uiAssets as $uiAsset) { + foreach ($uiAssets as $uiAsset) { $uiAsset->delete(); } } @@ -373,7 +367,7 @@ class AssetManager extends Singleton /** * @return UIAsset */ - private function getMergedStylesheetAsset() + public function getMergedStylesheetAsset() { return $this->getMergedUIAsset(self::MERGED_CSS_FILE); } diff --git a/www/analytics/core/AssetManager/UIAsset.php b/www/analytics/core/AssetManager/UIAsset.php index 4361812e..c380ed4a 100644 --- a/www/analytics/core/AssetManager/UIAsset.php +++ b/www/analytics/core/AssetManager/UIAsset.php @@ -1,6 +1,6 @@ content = $content; diff --git a/www/analytics/core/AssetManager/UIAsset/OnDiskUIAsset.php b/www/analytics/core/AssetManager/UIAsset/OnDiskUIAsset.php index f008f8aa..cbd702a6 100644 --- a/www/analytics/core/AssetManager/UIAsset/OnDiskUIAsset.php +++ b/www/analytics/core/AssetManager/UIAsset/OnDiskUIAsset.php @@ -1,6 +1,6 @@ baseDirectory = $baseDirectory; $this->relativeLocation = $fileLocation; @@ -50,20 +51,23 @@ class OnDiskUIAsset extends UIAsset public function validateFile() { - if (!$this->assetIsReadable()) + if (!$this->assetIsReadable()) { throw new Exception("The ui asset with 'href' = " . $this->getAbsoluteLocation() . " is not readable"); + } } public function delete() { if ($this->exists()) { - - if (!unlink($this->getAbsoluteLocation())) + 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. - @unlink($this->getAbsoluteLocation() . ".deflate"); - @unlink($this->getAbsoluteLocation() . ".gz"); + Filesystem::remove($this->getAbsoluteLocation() . ".deflate", true); + Filesystem::remove($this->getAbsoluteLocation() . ".gz", true); } } @@ -77,8 +81,9 @@ class OnDiskUIAsset extends UIAsset $newFile = @fopen($this->getAbsoluteLocation(), "w"); - if (!$newFile) - throw new Exception ("The file : " . $newFile . " can not be opened in write mode."); + if (!$newFile) { + throw new Exception("The file : " . $newFile . " can not be opened in write mode."); + } fwrite($newFile, $content); diff --git a/www/analytics/core/AssetManager/UIAssetCacheBuster.php b/www/analytics/core/AssetManager/UIAssetCacheBuster.php index 800de15d..fcbd0720 100644 --- a/www/analytics/core/AssetManager/UIAssetCacheBuster.php +++ b/www/analytics/core/AssetManager/UIAssetCacheBuster.php @@ -1,6 +1,6 @@ catalogSorter = $catalogSorter; } @@ -33,8 +38,10 @@ class UIAssetCatalog */ public function addUIAsset($uiAsset) { - if(!$this->assetAlreadyInCatalog($uiAsset)) { + $location = $uiAsset->getAbsoluteLocation(); + if (!$this->assetAlreadyInCatalog($location)) { + $this->existingAssetLocations[] = $location; $this->uiAssets[] = $uiAsset; } } @@ -59,12 +66,8 @@ class UIAssetCatalog * @param UIAsset $uiAsset * @return boolean */ - private function assetAlreadyInCatalog($uiAsset) + private function assetAlreadyInCatalog($location) { - foreach($this->uiAssets as $existingAsset) - if($uiAsset->getAbsoluteLocation() == $existingAsset->getAbsoluteLocation()) - return true; - - return false; + return in_array($location, $this->existingAssetLocations); } } diff --git a/www/analytics/core/AssetManager/UIAssetCatalogSorter.php b/www/analytics/core/AssetManager/UIAssetCatalogSorter.php index 65882744..fece3da0 100644 --- a/www/analytics/core/AssetManager/UIAssetCatalogSorter.php +++ b/www/analytics/core/AssetManager/UIAssetCatalogSorter.php @@ -1,6 +1,6 @@ priorityOrder = $priorityOrder; } @@ -31,12 +31,11 @@ class UIAssetCatalogSorter { $sortedCatalog = new UIAssetCatalog($this); foreach ($this->priorityOrder as $filePattern) { - - $assetsMatchingPattern = array_filter($uiAssetCatalog->getAssets(), function($uiAsset) use ($filePattern) { + $assetsMatchingPattern = array_filter($uiAssetCatalog->getAssets(), function ($uiAsset) use ($filePattern) { return preg_match('~^' . $filePattern . '~', $uiAsset->getRelativeLocation()); }); - foreach($assetsMatchingPattern as $assetMatchingPattern) { + foreach ($assetsMatchingPattern as $assetMatchingPattern) { $sortedCatalog->addUIAsset($assetMatchingPattern); } } diff --git a/www/analytics/core/AssetManager/UIAssetFetcher.php b/www/analytics/core/AssetManager/UIAssetFetcher.php index 6a51018d..6d710e13 100644 --- a/www/analytics/core/AssetManager/UIAssetFetcher.php +++ b/www/analytics/core/AssetManager/UIAssetFetcher.php @@ -1,6 +1,6 @@ plugins = $plugins; $this->theme = $theme; @@ -56,8 +56,9 @@ abstract class UIAssetFetcher */ public function getCatalog() { - if($this->catalog == null) + if ($this->catalog == null) { $this->createCatalog(); + } return $this->catalog; } @@ -89,7 +90,6 @@ abstract class UIAssetFetcher private function populateCatalog() { foreach ($this->fileLocations as $fileLocation) { - $newUIAsset = new OnDiskUIAsset($this->getBaseDirectory(), $fileLocation); $this->catalog->addUIAsset($newUIAsset); } diff --git a/www/analytics/core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php b/www/analytics/core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php index 700e2432..aed31925 100644 --- a/www/analytics/core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php +++ b/www/analytics/core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php @@ -1,6 +1,6 @@ plugins)) { + if (!empty($this->plugins)) { /** * Triggered when gathering the list of all JavaScript files needed by Piwik @@ -31,7 +29,7 @@ class JScriptUIAssetFetcher extends UIAssetFetcher * plugin's root directory. * * _Note: While you are developing your plugin you should enable the config setting - * `[Debug] disable_merged_assets` so JavaScript files will be reloaded immediately + * `[Development] disable_merged_assets` so JavaScript files will be reloaded immediately * after every change._ * * **Example** @@ -53,17 +51,14 @@ class JScriptUIAssetFetcher extends UIAssetFetcher protected function addThemeFiles() { $theme = $this->getTheme(); - if(!$theme) { + if (!$theme) { return; } - if(in_array($theme->getThemeName(), $this->plugins)) { - + if (in_array($theme->getThemeName(), $this->plugins)) { $jsInThemes = $this->getTheme()->getJavaScriptFiles(); - if(!empty($jsInThemes)) { - - foreach($jsInThemes as $jsFile) { - + if (!empty($jsInThemes)) { + foreach ($jsInThemes as $jsFile) { $this->fileLocations[] = $jsFile; } } @@ -73,13 +68,17 @@ class JScriptUIAssetFetcher extends UIAssetFetcher protected function getPriorityOrder() { return array( - 'libs/jquery/jquery.js', - 'libs/jquery/jquery-ui.js', + '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', 'plugins/CoreHome/javascripts/require.js', - 'plugins/Zeitgeist/javascripts/piwikHelper.js', - 'plugins/Zeitgeist/javascripts/', + 'plugins/Morpheus/javascripts/piwikHelper.js', + 'plugins/Morpheus/javascripts/jquery.icheck.min.js', + 'plugins/Morpheus/javascripts/morpheus.js', + 'plugins/Morpheus/javascripts/', 'plugins/CoreHome/javascripts/uiControl.js', 'plugins/CoreHome/javascripts/broadcast.js', 'plugins/CoreHome/javascripts/', // load CoreHome JS before other plugins diff --git a/www/analytics/core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php b/www/analytics/core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php index 0d9027f1..cdebbb1e 100644 --- a/www/analytics/core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php +++ b/www/analytics/core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php @@ -1,6 +1,6 @@ getTheme(); + $themeName = $theme->getThemeName(); + + $order = array( 'libs/', 'plugins/CoreHome/stylesheets/color_manager.css', // must be before other Piwik stylesheets - 'plugins/Zeitgeist/stylesheets/base.less', - 'plugins/Zeitgeist/stylesheets/', - 'plugins/', - 'plugins/Dashboard/stylesheets/dashboard.less', - 'tests/', + '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() @@ -55,9 +70,13 @@ class StylesheetUIAssetFetcher extends UIAssetFetcher protected function addThemeFiles() { + $theme = $this->getTheme(); + if (!$theme) { + return; + } $themeStylesheet = $this->getTheme()->getStylesheet(); - if($themeStylesheet) { + if ($themeStylesheet) { $this->fileLocations[] = $themeStylesheet; } } diff --git a/www/analytics/core/AssetManager/UIAssetMerger.php b/www/analytics/core/AssetManager/UIAssetMerger.php index fd98da90..645f003d 100644 --- a/www/analytics/core/AssetManager/UIAssetMerger.php +++ b/www/analytics/core/AssetManager/UIAssetMerger.php @@ -1,6 +1,6 @@ mergedAsset = $mergedAsset; $this->assetFetcher = $assetFetcher; @@ -48,8 +45,9 @@ abstract class UIAssetMerger public function generateFile() { - if(!$this->shouldGenerate()) + if (!$this->shouldGenerate()) { return; + } $this->mergedContent = $this->getMergedAssets(); @@ -95,8 +93,9 @@ abstract class UIAssetMerger protected function getConcatenatedAssets() { - if(empty($this->mergedContent)) + if (empty($this->mergedContent)) { $this->concatenateAssets(); + } return $this->mergedContent; } @@ -106,7 +105,6 @@ abstract class UIAssetMerger $mergedContent = ''; foreach ($this->getAssetCatalog()->getAssets() as $uiAsset) { - $uiAsset->validateFile(); $content = $this->processFileContent($uiAsset); @@ -137,8 +135,9 @@ abstract class UIAssetMerger */ private function shouldGenerate() { - if(!$this->mergedAsset->exists()) + if (!$this->mergedAsset->exists()) { return true; + } return !$this->isFileUpToDate(); } @@ -161,19 +160,11 @@ abstract class UIAssetMerger return false; } - /** - * @return boolean - */ - private function isMergedAssetsDisabled() - { - return AssetManager::getInstance()->isMergedAssetsDisabled(); - } - private function adjustPaths() { $theme = $this->assetFetcher->getTheme(); // During installation theme is not yet ready - if($theme) { + if ($theme) { $this->mergedContent = $this->assetFetcher->getTheme()->rewriteAssetsPathToTheme($this->mergedContent); } } @@ -188,8 +179,9 @@ abstract class UIAssetMerger */ protected function getCacheBusterValue() { - if(empty($this->cacheBusterValue)) + if (empty($this->cacheBusterValue)) { $this->cacheBusterValue = $this->generateCacheBuster(); + } return $this->cacheBusterValue; } @@ -198,12 +190,4 @@ abstract class UIAssetMerger { $this->mergedContent = $this->getPreamble() . $this->mergedContent; } - - /** - * @return boolean - */ - private function shouldCompareExistingVersion() - { - return $this->isMergedAssetsDisabled(); - } } diff --git a/www/analytics/core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php b/www/analytics/core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php index 93b7d7bf..8dd25017 100644 --- a/www/analytics/core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php +++ b/www/analytics/core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php @@ -1,6 +1,6 @@ getConcatenatedAssets(); - - return str_replace("\n", "\r\n", $concatenatedAssets); + return $this->getConcatenatedAssets(); } protected function generateCacheBuster() { $cacheBuster = $this->cacheBuster->piwikVersionBasedCacheBuster($this->getPlugins()); - return "/* Piwik Javascript - cb=" . $cacheBuster . "*/\r\n"; + return "/* Piwik Javascript - cb=" . $cacheBuster . "*/\n"; } protected function getPreamble() @@ -57,7 +55,7 @@ class JScriptUIAssetMerger extends UIAssetMerger { $plugins = $this->getPlugins(); - if(!empty($plugins)) { + if (!empty($plugins)) { /** * Triggered after all the JavaScript files Piwik uses are minified and merged into a @@ -74,15 +72,16 @@ class JScriptUIAssetMerger extends UIAssetMerger public function getFileSeparator() { - return PHP_EOL; + return "\n"; } protected function processFileContent($uiAsset) { $content = $uiAsset->getContent(); - if (!$this->assetMinifier->isMinifiedJs($content)) + if (!$this->assetMinifier->isMinifiedJs($content)) { $content = $this->assetMinifier->minifyJs($content); + } return $content; } diff --git a/www/analytics/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php b/www/analytics/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php index 29e487df..5161b2f1 100644 --- a/www/analytics/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php +++ b/www/analytics/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php @@ -1,6 +1,6 @@ getAssetCatalog()->getAssets() as $uiAsset) { - - $content = $uiAsset->getContent(); - if (false !== strpos($content, '@import')) { - $this->lessCompiler->addImportDir(dirname($uiAsset->getAbsoluteLocation())); - } - - } - - return $this->lessCompiler->compile($this->getConcatenatedAssets()); + // note: we're using setImportDir on purpose (not addImportDir) + $this->lessCompiler->setImportDir(PIWIK_USER_PATH); + $concatenatedAssets = $this->getConcatenatedAssets(); + return $this->lessCompiler->compile($concatenatedAssets); } /** @@ -87,46 +81,71 @@ class StylesheetUIAssetMerger extends UIAssetMerger protected function processFileContent($uiAsset) { - return $this->rewriteCssPathsDirectives($uiAsset); + $pathsRewriter = $this->getCssPathsRewriter($uiAsset); + $content = $uiAsset->getContent(); + $content = $this->rewriteCssImagePaths($content, $pathsRewriter); + $content = $this->rewriteCssImportPaths($content, $pathsRewriter); + return $content; } /** - * Rewrite css url directives + * 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 string + * @return \Closure */ - private function rewriteCssPathsDirectives($uiAsset) + private function getCssPathsRewriter($uiAsset) { - static $rootDirectoryLength = null; - if (is_null($rootDirectoryLength)) { - $rootDirectoryLength = self::countDirectoriesInPathToRoot($uiAsset); - } - $baseDirectory = dirname($uiAsset->getRelativeLocation()); - $content = preg_replace_callback( - "/(url\(['\"]?)([^'\")]*)/", - function ($matches) use ($rootDirectoryLength, $baseDirectory) { - $absolutePath = realpath(PIWIK_USER_PATH . "/$baseDirectory/" . $matches[2]); + return function ($matches) use ($baseDirectory) { + $absolutePath = PIWIK_USER_PATH . "/$baseDirectory/" . $matches[2]; - if($absolutePath) { + // Allow to import extension less file + if (strpos($matches[2], '.') === false) { + $absolutePath .= '.less'; + } - $relativePath = substr($absolutePath, $rootDirectoryLength); + // Prevent from rewriting full path + $absolutePath = realpath($absolutePath); + if ($absolutePath) { + $relativePath = $baseDirectory . "/" . $matches[2]; + $relativePath = str_replace('\\', '/', $relativePath); + $publicPath = $matches[1] . $relativePath; + } else { + $publicPath = $matches[1] . $matches[2]; + } - $relativePath = str_replace('\\', '/', $relativePath); - - return $matches[1] . $relativePath; - - } else { - return $matches[1] . $matches[2]; - } - }, - $uiAsset->getContent() - ); - return $content; + return $publicPath; + }; } /** @@ -138,7 +157,7 @@ class StylesheetUIAssetMerger extends UIAssetMerger $rootDirectory = realpath($uiAsset->getBaseDirectory()); if ($rootDirectory != PATH_SEPARATOR - && substr_compare($rootDirectory, PATH_SEPARATOR, -1)) { + && substr($rootDirectory, -strlen(PATH_SEPARATOR)) !== PATH_SEPARATOR) { $rootDirectory .= PATH_SEPARATOR; } $rootDirectoryLen = strlen($rootDirectory); diff --git a/www/analytics/core/AssetManager/UIAssetMinifier.php b/www/analytics/core/AssetManager/UIAssetMinifier.php index 34006f5a..99eebe0a 100644 --- a/www/analytics/core/AssetManager/UIAssetMinifier.php +++ b/www/analytics/core/AssetManager/UIAssetMinifier.php @@ -1,6 +1,6 @@ php composer.phar update"); + if (!class_exists("JShrink\\Minifier")) { + throw new Exception("JShrink could not be found, maybe you are using Piwik from git and need to update Composer. $ php composer.phar update"); + } } - } diff --git a/www/analytics/core/Auth.php b/www/analytics/core/Auth.php index b86af5d1..0ea9503a 100644 --- a/www/analytics/core/Auth.php +++ b/www/analytics/core/Auth.php @@ -1,6 +1,6 @@ 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 { /** - * Authentication module's name, e.g., "Login" + * Must return the Authentication module's name, e.g., `"Login"`. * * @return string */ public function getName(); /** - * Authenticates user - * - * @return AuthResult - */ - public function authenticate(); - - /** - * Authenticates the user and initializes the session. - */ - public function initSession($login, $md5Password, $rememberMe); - - /** - * Accessor to set authentication token. If set, you can authenticate the tokenAuth by calling the authenticate() - * method afterwards. + * Sets the authentication token to authenticate with. * * @param string $token_auth authentication token */ public function setTokenAuth($token_auth); /** - * Accessor to set login name + * Returns the login of the user being authenticated. * - * @param string $login user login + * @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 + * Authentication result. This is what is returned by authentication attempts using {@link Auth} + * implementations. * + * @api */ class AuthResult { diff --git a/www/analytics/core/BaseFactory.php b/www/analytics/core/BaseFactory.php new file mode 100644 index 00000000..24425c8f --- /dev/null +++ b/www/analytics/core/BaseFactory.php @@ -0,0 +1,60 @@ +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; + } +} diff --git a/www/analytics/core/CacheFile.php b/www/analytics/core/CacheFile.php deleted file mode 100644 index 043770b3..00000000 --- a/www/analytics/core/CacheFile.php +++ /dev/null @@ -1,206 +0,0 @@ -cachePath = SettingsPiwik::rewriteTmpPathWithHostname($cachePath); - - if ($timeToLiveInSeconds < self::MINIMUM_TTL) { - $timeToLiveInSeconds = self::MINIMUM_TTL; - } - $this->ttl = $timeToLiveInSeconds; - } - - /** - * Function to fetch a cache entry - * - * @param string $id The cache entry ID - * @return array|bool False on error, or array the cache content - */ - public function get($id) - { - if (empty($id)) { - return false; - } - $id = $this->cleanupId($id); - - $cache_complete = false; - $content = ''; - $expires_on = false; - - // We are assuming that most of the time cache will exists - $cacheFilePath = $this->cachePath . $id . '.php'; - if (self::$invalidateOpCacheBeforeRead) { - $this->opCacheInvalidate($cacheFilePath); - } - - $ok = @include($cacheFilePath); - - if ($ok && $cache_complete == true) { - - if (empty($expires_on) - || $expires_on < time() - ) { - return false; - } - return $content; - } - - return false; - } - - private function getExpiresTime() - { - return time() + $this->ttl; - } - - protected function cleanupId($id) - { - if (!Filesystem::isValidFilename($id)) { - throw new Exception("Invalid cache ID request $id"); - } - return $id; - } - - /** - * A function to store content a cache entry. - * - * @param string $id The cache entry ID - * @param array $content The cache content - * @throws \Exception - * @return bool True if the entry was succesfully stored - */ - public function set($id, $content) - { - if (empty($id)) { - return false; - } - if (!is_dir($this->cachePath)) { - Filesystem::mkdir($this->cachePath); - } - if (!is_writable($this->cachePath)) { - return false; - } - $id = $this->cleanupId($id); - - $id = $this->cachePath . $id . '.php'; - - if (is_object($content)) { - throw new \Exception('You cannot use the CacheFile to cache an object, only arrays, strings and numbers.'); - } - - $cache_literal = "<" . "?php\n"; - $cache_literal .= "$" . "content = " . var_export($content, true) . ";\n"; - $cache_literal .= "$" . "expires_on = " . $this->getExpiresTime() . ";\n"; - $cache_literal .= "$" . "cache_complete = true;\n"; - $cache_literal .= "?" . ">"; - - // Write cache to a temp file, then rename it, overwriting the old cache - // On *nix systems this should guarantee atomicity - $tmp_filename = tempnam($this->cachePath, 'tmp_'); - @chmod($tmp_filename, 0640); - if ($fp = @fopen($tmp_filename, 'wb')) { - @fwrite($fp, $cache_literal, strlen($cache_literal)); - @fclose($fp); - - if (!@rename($tmp_filename, $id)) { - // On some systems rename() doesn't overwrite destination - @unlink($id); - if (!@rename($tmp_filename, $id)) { - // Make sure that no temporary file is left over - // if the destination is not writable - @unlink($tmp_filename); - } - } - - $this->opCacheInvalidate($id); - - return true; - } - return false; - } - - /** - * A function to delete a single cache entry - * - * @param string $id The cache entry ID - * @return bool True if the entry was succesfully deleted - */ - public function delete($id) - { - if (empty($id)) { - return false; - } - $id = $this->cleanupId($id); - - $filename = $this->cachePath . $id . '.php'; - if (file_exists($filename)) { - $this->opCacheInvalidate($filename); - @unlink($filename); - return true; - } - return false; - } - - /** - * A function to delete all cache entries in the directory - */ - public function deleteAll() - { - $self = $this; - $beforeUnlink = function ($path) use ($self) { - $self->opCacheInvalidate($path); - }; - - Filesystem::unlinkRecursive($this->cachePath, $deleteRootToo = false, $beforeUnlink); - } - - public function opCacheInvalidate($filepath) - { - if (function_exists('opcache_invalidate') - && is_file($filepath) - ) { - opcache_invalidate($filepath, $force = true); - } - } -} diff --git a/www/analytics/core/CacheId.php b/www/analytics/core/CacheId.php new file mode 100644 index 00000000..cc346bee --- /dev/null +++ b/www/analytics/core/CacheId.php @@ -0,0 +1,29 @@ +getLoadedPluginsName(); + $cacheId = $cacheId . '-' . md5(implode('', $pluginNames)); + $cacheId = self::languageAware($cacheId); + + return $cacheId; + } +} diff --git a/www/analytics/core/CliMulti.php b/www/analytics/core/CliMulti.php index 653ceccf..ef360f02 100644 --- a/www/analytics/core/CliMulti.php +++ b/www/analytics/core/CliMulti.php @@ -1,19 +1,23 @@ supportsAsync = $this->supportsAsync(); @@ -44,26 +68,38 @@ class CliMulti { * 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...') + * + * `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) { - $this->start($piwikUrls); + $chunks = array($piwikUrls); + if ($this->concurrentProcessesLimit) { + $chunks = array_chunk($piwikUrls, $this->concurrentProcessesLimit); + } - do { - usleep(100000); // 100 * 1000 = 100ms - } while (!$this->hasFinished()); - - $results = $this->getResponse($piwikUrls); - $this->cleanup(); - - self::cleanupNotRemovedFiles(); + $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. @@ -74,28 +110,51 @@ class CliMulti { $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) { - $cmdId = $this->generateCommandId($url) . $index; - $output = new Output($cmdId); - - if ($this->supportsAsync) { - $this->executeAsyncCli($url, $output, $cmdId); - } else { - $this->executeNotAsyncHttp($url, $output); + if ($url instanceof Request) { + $url->start(); } - $this->outputs[] = $output; + $cmdId = $this->generateCommandId($url) . $index; + $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) { $bin = $this->findPhpBinary(); + $superuserCommand = $this->runAsSuperUser ? "--superuser" : ""; - return sprintf('%s -q %s/console climulti:request --piwik-domain=%s %s > %s 2>&1 &', - $bin, PIWIK_INCLUDE_PATH, escapeshellarg($hostname), escapeshellarg($query), $outputFile); + return sprintf('%s %s %s/console climulti:request -q --piwik-domain=%s %s %s > %s 2>&1 &', + $bin, $this->phpCliOptions, PIWIK_INCLUDE_PATH, escapeshellarg($hostname), $superuserCommand, escapeshellarg($query), $outputFile); } private function getResponse() @@ -119,7 +178,6 @@ class CliMulti { // ==> declare the process as finished $process->finishProcess(); continue; - } elseif (!$hasStarted) { return false; } @@ -128,6 +186,14 @@ class CliMulti { 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]); @@ -146,11 +212,15 @@ class CliMulti { * 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() */ - private function supportsAsync() + public function supportsAsync() { - return !SettingsServer::isWindows() - && Process::isSupported() - && $this->findPhpBinary(); + return Process::isSupported() && !Common::isPhpCgiType() && $this->findPhpBinary(); + } + + private function findPhpBinary() + { + $cliPhp = new CliPhp(); + return $cliPhp->findPhpBinary(); } private function cleanup() @@ -176,61 +246,33 @@ class CliMulti { $timeOneWeekAgo = strtotime('-1 week'); $files = _glob(self::getTmpPath() . '/*'); - if(empty($files)) { + if (empty($files)) { return; } foreach ($files as $file) { - $timeLastModified = filemtime($file); + if (file_exists($file)) { + $timeLastModified = filemtime($file); - if ($timeOneWeekAgo > $timeLastModified) { - unlink($file); + if ($timeLastModified !== false && $timeOneWeekAgo > $timeLastModified) { + unlink($file); + } } } } public static function getTmpPath() { - $dir = PIWIK_INCLUDE_PATH . '/tmp/climulti'; - return SettingsPiwik::rewriteTmpPathWithHostname($dir); - } - - private function findPhpBinary() - { - if (defined('PHP_BINARY') && false === strpos(PHP_BINARY, 'fpm')) { - return PHP_BINARY; - } - - $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 (empty($bin)) { - $bin = shell_exec('which php'); - } - - if (empty($bin)) { - $bin = shell_exec('which php5'); - } - - if (!empty($bin)) { - return trim($bin); - } + return StaticContainer::get('path.tmp') . '/climulti'; } private function executeAsyncCli($url, Output $output, $cmdId) { $this->processes[] = new Process($cmdId); - $url = $this->appendTestmodeParamToUrlIfNeeded($url); - $query = UrlHelper::getQueryFromUrl($url, array('pid' => $cmdId)); - $hostname = UrlHelper::getHostFromUrl($url); + $url = $this->appendTestmodeParamToUrlIfNeeded($url); + $query = UrlHelper::getQueryFromUrl($url, array('pid' => $cmdId)); + $hostname = Url::getHost($checkIfTrusted = false); $command = $this->buildCommand($hostname, $query, $output->getPathToFile()); Log::debug($command); @@ -239,6 +281,29 @@ class CliMulti { 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); @@ -246,19 +311,21 @@ class CliMulti { } catch (\Exception $e) { $message = "Got invalid response from API request: $url. "; - if (empty($response)) { + 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 = $url && false !== strpos($url, 'tests/PHPUnit/proxy'); + $isTestMode = defined('PIWIK_TEST_MODE'); if ($isTestMode && false === strpos($url, '?')) { $url .= "?testmode=1"; @@ -269,12 +336,42 @@ class CliMulti { return $url; } - private function getPhpCommandIfValid($path) + /** + * @param array $piwikUrls + * @return array + */ + private function requestUrls(array $piwikUrls) { - if (!empty($path) && is_executable($path)) { - if (0 === strpos($path, PHP_BINDIR) && false === strpos($path, 'phpunit')) { - return $path; - } - } + $this->start($piwikUrls); + + do { + usleep(100000); // 100 * 1000 = 100ms + } while (!$this->hasFinished()); + + $results = $this->getResponse($piwikUrls); + $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; } } diff --git a/www/analytics/core/CliMulti/CliPhp.php b/www/analytics/core/CliMulti/CliPhp.php new file mode 100644 index 00000000..c405d66a --- /dev/null +++ b/www/analytics/core/CliMulti/CliPhp.php @@ -0,0 +1,101 @@ +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; + } +} diff --git a/www/analytics/core/CliMulti/Output.php b/www/analytics/core/CliMulti/Output.php index e060bc49..b97df746 100644 --- a/www/analytics/core/CliMulti/Output.php +++ b/www/analytics/core/CliMulti/Output.php @@ -1,6 +1,6 @@ tmpFile = $dir . '/' . $outputId . '.output'; + Filesystem::mkdir($dir); + + $this->tmpFile = $dir . '/' . $outputId . '.output'; + $this->outputId = $outputId; + } + + public function getOutputId() + { + return $this->outputId; } public function write($content) @@ -35,6 +44,13 @@ class Output { 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); @@ -49,5 +65,4 @@ class Output { { Filesystem::deleteFileIfExists($this->tmpFile); } - } diff --git a/www/analytics/core/CliMulti/Process.php b/www/analytics/core/CliMulti/Process.php index 1f0059ea..e1d702fd 100644 --- a/www/analytics/core/CliMulti/Process.php +++ b/www/analytics/core/CliMulti/Process.php @@ -1,6 +1,6 @@ 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(); @@ -97,6 +104,11 @@ class Process return false; } + if (!$this->pidFileSizeIsNormal()) { + $this->finishProcess(); + return false; + } + if ($this->isProcessStillRunning($content)) { return true; } @@ -108,6 +120,13 @@ class Process return false; } + private function pidFileSizeIsNormal() + { + $size = Filesystem::getFileSize($this->pidFile); + + return $size !== null && $size < 500; + } + public function finishProcess() { Filesystem::deleteFileIfExists($this->pidFile); @@ -125,7 +144,7 @@ class Process } $lockedPID = trim($content); - $runningPIDs = explode("\n", trim( `ps -e | awk '{print $1}'` )); + $runningPIDs = self::getRunningProcesses(); return !empty($lockedPID) && in_array($lockedPID, $runningPIDs); } @@ -154,17 +173,30 @@ class Process return false; } - if (static::commandExists('ps') && self::returnsSuccessCode('ps') && self::commandExists('awk')) { + if (!self::commandExists('ps') || !self::returnsSuccessCode('ps') || !self::commandExists('awk')) { + return false; + } + + if (count(self::getRunningProcesses()) > 0) { return true; } - return false; + if (!self::isProcFSMounted()) { + return false; + } + + return true; } private static function isSystemNotSupported() { - $uname = shell_exec('uname -a'); - if(strpos($uname, 'synology') !== false) { + $uname = @shell_exec('uname -a 2> /dev/null'); + + if (empty($uname)) { + $uname = php_uname(); + } + + if (strpos($uname, 'synology') !== false) { return true; } return false; @@ -175,12 +207,12 @@ class Process $command = 'shell_exec'; $disabled = explode(',', ini_get('disable_functions')); $disabled = array_map('trim', $disabled); - return in_array($command, $disabled); + return in_array($command, $disabled) || !function_exists($command); } private static function returnsSuccessCode($command) { - $exec = $command . ' > /dev/null 2>&1 & echo $?'; + $exec = $command . ' > /dev/null 2>&1; echo $?'; $returnCode = shell_exec($exec); $returnCode = trim($returnCode); return 0 == (int) $returnCode; @@ -188,8 +220,38 @@ class Process private static function commandExists($command) { - $result = shell_exec('which ' . escapeshellarg($command)); + $result = @shell_exec('which ' . escapeshellarg($command) . ' 2> /dev/null'); return !empty($result); } -} \ No newline at end of file + + /** + * 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 existance 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; + } + + /** + * @return int[] The ids of the currently running processes + */ + public static function getRunningProcesses() + { + $ids = explode("\n", trim(`ps ex 2>/dev/null | awk '{print $1}' 2>/dev/null`)); + + $ids = array_map('intval', $ids); + $ids = array_filter($ids, function ($id) { + return $id > 0; + }); + + return $ids; + } +} diff --git a/www/analytics/core/CliMulti/RequestCommand.php b/www/analytics/core/CliMulti/RequestCommand.php index f6ccfd34..7ae2fc44 100644 --- a/www/analytics/core/CliMulti/RequestCommand.php +++ b/www/analytics/core/CliMulti/RequestCommand.php @@ -1,6 +1,6 @@ setName('climulti:request'); $this->setDescription('Parses and executes the given query. See Piwik\CliMulti. Intended only for system usage.'); - $this->addArgument('url-query', null, InputOption::VALUE_REQUIRED, 'Piwik URL query string, for instance: "module=API&method=API.getPiwikVersion&token_auth=123456789"'); + $this->addArgument('url-query', InputArgument::REQUIRED, 'Piwik 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()) { - Config::getInstance()->setTestEnvironment(); - $indexFile = '/tests/PHPUnit/proxy/index.php'; + $indexFile = '/tests/PHPUnit/proxy/'; + + $this->resetDatabase(); } else { - $indexFile = '/index.php'; + $indexFile = '/'; } + $indexFile .= 'index.php'; + if (!empty($_GET['pid'])) { $process = new Process($_GET['pid']); @@ -52,6 +66,16 @@ class RequestCommand extends ConsoleCommand $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)) { @@ -75,10 +99,30 @@ class RequestCommand extends ConsoleCommand Url::setHost($hostname); $query = $input->getArgument('url-query'); - $query = UrlHelper::getArrayFromQueryString($query); + $query = UrlHelper::getArrayFromQueryString($query); // NOTE: this method can create the StaticContainer now foreach ($query as $name => $value) { $_GET[$name] = $value; } } -} \ No newline at end of file + /** + * 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(); + } +} diff --git a/www/analytics/core/Columns/Dimension.php b/www/analytics/core/Columns/Dimension.php new file mode 100644 index 00000000..c89ae637 --- /dev/null +++ b/www/analytics/core/Columns/Dimension.php @@ -0,0 +1,242 @@ +setSegment('exitPageUrl'); + * $segment->setName('Actions_ColumnExitPageURL'); + * $segment->setCategory('General_Visit'); + * $this->addSegment($segment); + * ``` + */ + protected function configureSegments() + { + } + + /** + * 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. The segment type will be set to 'dimension' automatically if not already set. + * @param Segment $segment + * @api + */ + protected function addSegment(Segment $segment) + { + $type = $segment->getType(); + + if (empty($type)) { + $segment->setType(Segment::TYPE_DIMENSION); + } + + $this->segments[] = $segment; + } + + /** + * Get the list of configured segments. + * @return Segment[] + * @ignore + */ + public function getSegments() + { + if (empty($this->segments)) { + $this->configureSegments(); + } + + return $this->segments; + } + + /** + * Get the name of the dimension column. + * @return string + * @ignore + */ + public function getColumnName() + { + return $this->columnName; + } + + /** + * Check whether the dimension has a column type configured + * @return bool + * @ignore + */ + public function hasColumnType() + { + return !empty($this->columnType); + } + + /** + * Get the translated name of the dimension. Defaults to an empty string. + * @return string + * @api + */ + public function getName() + { + return ''; + } + + /** + * 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); + + // 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() + { + $dimensions = array(); + + foreach (VisitDimension::getAllDimensions() as $dimension) { + $dimensions[] = $dimension; + } + + foreach (ActionDimension::getAllDimensions() as $dimension) { + $dimensions[] = $dimension; + } + + foreach (ConversionDimension::getAllDimensions() as $dimension) { + $dimensions[] = $dimension; + } + + return $dimensions; + } + + public static function getDimensions(Plugin $plugin) + { + $dimensions = array(); + + foreach (VisitDimension::getDimensions($plugin) as $dimension) { + $dimensions[] = $dimension; + } + + foreach (ActionDimension::getDimensions($plugin) as $dimension) { + $dimensions[] = $dimension; + } + + foreach (ConversionDimension::getDimensions($plugin) as $dimension) { + $dimensions[] = $dimension; + } + + return $dimensions; + } + + /** + * 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 + */ + 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); + } +} diff --git a/www/analytics/core/Columns/Updater.php b/www/analytics/core/Columns/Updater.php new file mode 100644 index 00000000..8dea10de --- /dev/null +++ b/www/analytics/core/Columns/Updater.php @@ -0,0 +1,372 @@ +visitDimensions = $visitDimensions; + $this->actionDimensions = $actionDimensions; + $this->conversionDimensions = $conversionDimensions; + } + + public function getMigrationQueries(PiwikUpdater $updater) + { + $sqls = array(); + + $changingColumns = $this->getUpdates($updater); + + foreach ($changingColumns as $table => $columns) { + if (empty($columns) || !is_array($columns)) { + continue; + } + + $sqls["ALTER TABLE `" . Common::prefixTable($table) . "` " . implode(', ', $columns)] = array('1091', '1060'); + } + + return $sqls; + } + + public function doUpdate(PiwikUpdater $updater) + { + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); + } + + private function getVisitDimensions() + { + // see eg https://github.com/piwik/piwik/issues/8399 we fetch them only on demand to improve performance + if (!isset($this->visitDimensions)) { + $this->visitDimensions = VisitDimension::getAllDimensions(); + } + + return $this->visitDimensions; + } + + private function getActionDimensions() + { + // see eg https://github.com/piwik/piwik/issues/8399 we fetch them only on demand to improve performance + if (!isset($this->actionDimensions)) { + $this->actionDimensions = ActionDimension::getAllDimensions(); + } + + return $this->actionDimensions; + } + + private function getConversionDimensions() + { + // see eg https://github.com/piwik/piwik/issues/8399 we fetch them only on demand to improve performance + if (!isset($this->conversionDimensions)) { + $this->conversionDimensions = ConversionDimension::getAllDimensions(); + } + + return $this->conversionDimensions; + } + + private function getUpdates(PiwikUpdater $updater) + { + $visitColumns = DbHelper::getTableColumns(Common::prefixTable('log_visit')); + $actionColumns = DbHelper::getTableColumns(Common::prefixTable('log_link_visit_action')); + $conversionColumns = DbHelper::getTableColumns(Common::prefixTable('log_conversion')); + + $allUpdatesToRun = array(); + + foreach ($this->getVisitDimensions() as $dimension) { + $updates = $this->getUpdatesForDimension($updater, $dimension, 'log_visit.', $visitColumns, $conversionColumns); + $allUpdatesToRun = $this->mixinUpdates($allUpdatesToRun, $updates); + } + + foreach ($this->getActionDimensions() as $dimension) { + $updates = $this->getUpdatesForDimension($updater, $dimension, 'log_link_visit_action.', $actionColumns); + $allUpdatesToRun = $this->mixinUpdates($allUpdatesToRun, $updates); + } + + foreach ($this->getConversionDimensions() as $dimension) { + $updates = $this->getUpdatesForDimension($updater, $dimension, 'log_conversion.', $conversionColumns); + $allUpdatesToRun = $this->mixinUpdates($allUpdatesToRun, $updates); + } + + return $allUpdatesToRun; + } + + /** + * @param ActionDimension|ConversionDimension|VisitDimension $dimension + * @param string $componentPrefix + * @param array $existingColumnsInDb + * @param array $conversionColumns + * @return array + */ + private function getUpdatesForDimension(PiwikUpdater $updater, $dimension, $componentPrefix, $existingColumnsInDb, $conversionColumns = array()) + { + $column = $dimension->getColumnName(); + $componentName = $componentPrefix . $column; + + if (!$updater->hasNewVersion($componentName)) { + return array(); + } + + if (array_key_exists($column, $existingColumnsInDb)) { + if ($dimension instanceof VisitDimension) { + $sqlUpdates = $dimension->update($conversionColumns); + } else { + $sqlUpdates = $dimension->update(); + } + } else { + $sqlUpdates = $dimension->install(); + } + + return $sqlUpdates; + } + + private function mixinUpdates($allUpdatesToRun, $updatesFromDimension) + { + if (!empty($updatesFromDimension)) { + foreach ($updatesFromDimension as $table => $col) { + if (empty($allUpdatesToRun[$table])) { + $allUpdatesToRun[$table] = $col; + } else { + $allUpdatesToRun[$table] = array_merge($allUpdatesToRun[$table], $col); + } + } + } + + return $allUpdatesToRun; + } + + public function getAllVersions(PiwikUpdater $updater) + { + // to avoid having to load all dimensions on each request we check if there were any changes on the file system + // can easily save > 100ms for each request + $cachedTimes = self::getCachedDimensionFileChanges(); + $currentTimes = self::getCurrentDimensionFileChanges(); + $diff = array_diff_assoc($currentTimes, $cachedTimes); + + if (empty($diff)) { + return array(); + } + + $versions = array(); + + $visitColumns = DbHelper::getTableColumns(Common::prefixTable('log_visit')); + $actionColumns = DbHelper::getTableColumns(Common::prefixTable('log_link_visit_action')); + $conversionColumns = DbHelper::getTableColumns(Common::prefixTable('log_conversion')); + + foreach ($this->getVisitDimensions() as $dimension) { + $versions = $this->mixinVersions($updater, $dimension, VisitDimension::INSTALLER_PREFIX, $visitColumns, $versions); + } + + foreach ($this->getActionDimensions() as $dimension) { + $versions = $this->mixinVersions($updater, $dimension, ActionDimension::INSTALLER_PREFIX, $actionColumns, $versions); + } + + foreach ($this->getConversionDimensions() as $dimension) { + $versions = $this->mixinVersions($updater, $dimension, ConversionDimension::INSTALLER_PREFIX, $conversionColumns, $versions); + } + + return $versions; + } + + /** + * @param ActionDimension|ConversionDimension|VisitDimension $dimension + * @param string $componentPrefix + * @param array $columns + * @param array $versions + * @return array The modified versions array + */ + private function mixinVersions(PiwikUpdater $updater, $dimension, $componentPrefix, $columns, $versions) + { + $columnName = $dimension->getColumnName(); + + // dimensions w/o columns do not need DB updates + if (!$columnName || !$dimension->hasColumnType()) { + return $versions; + } + + $component = $componentPrefix . $columnName; + $version = $dimension->getVersion(); + + // if the column exists in the table, but has no associated version, and was one of the core columns + // that was moved when the dimension refactor took place, then: + // - set the installed version in the DB to the current code version + // - and do not check for updates since we just set the version to the latest + if (array_key_exists($columnName, $columns) + && false === $updater->getCurrentComponentVersion($component) + && self::wasDimensionMovedFromCoreToPlugin($component, $version) + ) { + $updater->markComponentSuccessfullyUpdated($component, $version); + return $versions; + } + + $versions[$component] = $version; + + return $versions; + } + + public static function isDimensionComponent($name) + { + return 0 === strpos($name, 'log_visit.') + || 0 === strpos($name, 'log_conversion.') + || 0 === strpos($name, 'log_conversion_item.') + || 0 === strpos($name, 'log_link_visit_action.'); + } + + public static function wasDimensionMovedFromCoreToPlugin($name, $version) + { + // maps names of core dimension columns that were part of the original dimension refactor with their + // initial "version" strings. The '1' that is sometimes appended to the end of the string (sometimes seen as + // NULL1) is from individual dimension "versioning" logic (eg, see VisitDimension::getVersion()) + $initialCoreDimensionVersions = array( + 'log_visit.config_resolution' => 'VARCHAR(9) NOT NULL', + 'log_visit.config_device_brand' => 'VARCHAR( 100 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL', + 'log_visit.config_device_model' => 'VARCHAR( 100 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL', + 'log_visit.config_windowsmedia' => 'TINYINT(1) NOT NULL', + 'log_visit.config_silverlight' => 'TINYINT(1) NOT NULL', + 'log_visit.config_java' => 'TINYINT(1) NOT NULL', + 'log_visit.config_gears' => 'TINYINT(1) NOT NULL', + 'log_visit.config_pdf' => 'TINYINT(1) NOT NULL', + 'log_visit.config_quicktime' => 'TINYINT(1) NOT NULL', + 'log_visit.config_realplayer' => 'TINYINT(1) NOT NULL', + 'log_visit.config_device_type' => 'TINYINT( 100 ) NULL DEFAULT NULL', + 'log_visit.visitor_localtime' => 'TIME NOT NULL', + 'log_visit.location_region' => 'char(2) DEFAULT NULL1', + 'log_visit.visitor_days_since_last' => 'SMALLINT(5) UNSIGNED NOT NULL', + 'log_visit.location_longitude' => 'float(10, 6) DEFAULT NULL1', + 'log_visit.visit_total_events' => 'SMALLINT(5) UNSIGNED NOT NULL', + 'log_visit.config_os_version' => 'VARCHAR( 100 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL', + 'log_visit.location_city' => 'varchar(255) DEFAULT NULL1', + 'log_visit.location_country' => 'CHAR(3) NOT NULL1', + 'log_visit.location_latitude' => 'float(10, 6) DEFAULT NULL1', + 'log_visit.config_flash' => 'TINYINT(1) NOT NULL', + 'log_visit.config_director' => 'TINYINT(1) NOT NULL', + 'log_visit.visit_total_time' => 'SMALLINT(5) UNSIGNED NOT NULL', + 'log_visit.visitor_count_visits' => 'SMALLINT(5) UNSIGNED NOT NULL1', + 'log_visit.visit_entry_idaction_name' => 'INTEGER(11) UNSIGNED NOT NULL', + 'log_visit.visit_entry_idaction_url' => 'INTEGER(11) UNSIGNED NOT NULL', + 'log_visit.visitor_returning' => 'TINYINT(1) NOT NULL1', + 'log_visit.visitor_days_since_order' => 'SMALLINT(5) UNSIGNED NOT NULL1', + 'log_visit.visit_goal_buyer' => 'TINYINT(1) NOT NULL', + 'log_visit.visit_first_action_time' => 'DATETIME NOT NULL', + 'log_visit.visit_goal_converted' => 'TINYINT(1) NOT NULL', + 'log_visit.visitor_days_since_first' => 'SMALLINT(5) UNSIGNED NOT NULL1', + 'log_visit.visit_exit_idaction_name' => 'INTEGER(11) UNSIGNED NOT NULL', + 'log_visit.visit_exit_idaction_url' => 'INTEGER(11) UNSIGNED NULL DEFAULT 0', + 'log_visit.config_browser_version' => 'VARCHAR(20) NOT NULL', + 'log_visit.config_browser_name' => 'VARCHAR(10) NOT NULL', + 'log_visit.config_browser_engine' => 'VARCHAR(10) NOT NULL', + 'log_visit.location_browser_lang' => 'VARCHAR(20) NOT NULL', + 'log_visit.config_os' => 'CHAR(3) NOT NULL', + 'log_visit.config_cookie' => 'TINYINT(1) NOT NULL', + 'log_visit.referer_url' => 'TEXT NOT NULL', + 'log_visit.visit_total_searches' => 'SMALLINT(5) UNSIGNED NOT NULL', + 'log_visit.visit_total_actions' => 'SMALLINT(5) UNSIGNED NOT NULL', + 'log_visit.referer_keyword' => 'VARCHAR(255) NULL1', + 'log_visit.referer_name' => 'VARCHAR(70) NULL1', + 'log_visit.referer_type' => 'TINYINT(1) UNSIGNED NULL1', + 'log_visit.user_id' => 'VARCHAR(200) NULL', + 'log_link_visit_action.idaction_name' => 'INTEGER(10) UNSIGNED', + 'log_link_visit_action.idaction_url' => 'INTEGER(10) UNSIGNED DEFAULT NULL', + 'log_link_visit_action.server_time' => 'DATETIME NOT NULL', + 'log_link_visit_action.time_spent_ref_action' => 'INTEGER(10) UNSIGNED NOT NULL', + 'log_link_visit_action.idaction_event_action' => 'INTEGER(10) UNSIGNED DEFAULT NULL', + 'log_link_visit_action.idaction_event_category' => 'INTEGER(10) UNSIGNED DEFAULT NULL', + 'log_conversion.revenue_discount' => 'float default NULL', + 'log_conversion.revenue' => 'float default NULL', + 'log_conversion.revenue_shipping' => 'float default NULL', + 'log_conversion.revenue_subtotal' => 'float default NULL', + 'log_conversion.revenue_tax' => 'float default NULL', + ); + + if (!array_key_exists($name, $initialCoreDimensionVersions)) { + return false; + } + + return strtolower($initialCoreDimensionVersions[$name]) === strtolower($version); + } + + public function onNoUpdateAvailable($versionsThatWereChecked) + { + if (!empty($versionsThatWereChecked)) { + // invalidate cache only if there were actually file changes before, otherwise we write the cache on each + // request. There were versions checked only if there was a file change but no update, meaning we can + // set the cache and declare this state as "no update available". + self::cacheCurrentDimensionFileChanges(); + } + } + + private static function getCurrentDimensionFileChanges() + { + $files = Filesystem::globr(PIWIK_INCLUDE_PATH . '/plugins/*/Columns', '*.php'); + + $times = array(); + foreach ($files as $file) { + $times[$file] = filemtime($file); + } + + return $times; + } + + private static function cacheCurrentDimensionFileChanges() + { + $changes = self::getCurrentDimensionFileChanges(); + + $cache = self::buildCache(); + $cache->save(self::$cacheId, $changes); + } + + private static function buildCache() + { + return PiwikCache::getEagerCache(); + } + + private static function getCachedDimensionFileChanges() + { + $cache = self::buildCache(); + + if ($cache->contains(self::$cacheId)) { + return $cache->fetch(self::$cacheId); + } + + return array(); + } +} diff --git a/www/analytics/core/Common.php b/www/analytics/core/Common.php index 876f6ff8..7992275a 100644 --- a/www/analytics/core/Common.php +++ b/www/analytics/core/Common.php @@ -1,6 +1,6 @@ isPluginActivated('Goals'); + return Plugin\Manager::getInstance()->isPluginActivated('Goals'); } public static function isActionsPluginEnabled() { - return \Piwik\Plugin\Manager::getInstance()->isPluginActivated('Actions'); + return Plugin\Manager::getInstance()->isPluginActivated('Actions'); } /** @@ -127,21 +130,44 @@ class Common return self::$isCliMode; } - $remoteAddr = @$_SERVER['REMOTE_ADDR']; - return PHP_SAPI == 'cli' || - (!strncmp(PHP_SAPI, 'cgi', 3) && empty($remoteAddr)); + if(PHP_SAPI == 'cli'){ + return true; + } + + if(self::isPhpCgiType() && (!isset($_SERVER['REMOTE_ADDR']) || empty($_SERVER['REMOTE_ADDR']))){ + return true; + } + + return false; } /** - * Returns true if the current request is a console command, eg. ./console xx:yy + * Returns true if PHP is executed as CGI type. + * + * @since added in 0.4.4 + * @return bool true if PHP invoked as a CGI + */ + public static function isPhpCgiType() + { + $sapiType = php_sapi_name(); + + return substr($sapiType, 0, 3) === 'cgi'; + } + + /** + * Returns true if the current request is a console command, eg. + * ./console xx:yy + * or + * php console xx:yy + * * @return bool */ public static function isRunningConsoleCommand() { - $searched = '/console'; + $searched = 'console'; $consolePos = strpos($_SERVER['SCRIPT_NAME'], $searched); $expectedConsolePos = strlen($_SERVER['SCRIPT_NAME']) - strlen($searched); - $isScriptIsConsole = $consolePos == $expectedConsolePos; + $isScriptIsConsole = ($consolePos === $expectedConsolePos); return self::isPhpCliMode() && $isScriptIsConsole; } @@ -151,7 +177,7 @@ class Common /** * Multi-byte substr() - works with UTF-8. - * + * * Calls `mb_substr` if available and falls back to `substr` if it's not. * * @param string $string @@ -175,7 +201,7 @@ class Common /** * Multi-byte strlen() - works with UTF-8 - * + * * Calls `mb_substr` if available and falls back to `substr` if not. * * @param string $string @@ -193,9 +219,9 @@ class Common /** * Multi-byte strtolower() - works with UTF-8. - * + * * Calls `mb_strtolower` if available and falls back to `strtolower` if not. - * + * * @param string $string * @return string * @api @@ -215,18 +241,18 @@ class Common /** * Sanitizes a string to help avoid XSS vulnerabilities. - * + * * This function is automatically called when {@link getRequestVar()} is called, * so you should not normally have to use it. - * + * * This function should be used when outputting data that isn't escaped and was * obtained from the user (for example when using the `|raw` twig filter on goal names). - * + * * _NOTE: Sanitized input should not be used directly in an SQL query; SQL placeholders * should still be used._ - * + * * **Implementation Details** - * + * * - [htmlspecialchars](http://php.net/manual/en/function.htmlspecialchars.php) is used to escape text. * - Single quotes are not escaped so **Piwik's amazing community** will still be * **Piwik's amazing community**. @@ -246,10 +272,11 @@ class Common if (is_numeric($value)) { return $value; } elseif (is_string($value)) { - $value = self::sanitizeInputValue($value); + $value = self::sanitizeString($value); + + if (!$alreadyStripslashed) { + // a JSON array was already stripslashed, don't do it again for each value - if (!$alreadyStripslashed) // a JSON array was already stripslashed, don't do it again for each value - { $value = self::undoMagicQuotes($value); } } elseif (is_array($value)) { @@ -272,20 +299,32 @@ class Common } /** - * Sanitize a single input value + * Sanitize a single input value and removes line breaks, tabs and null characters. * * @param string $value * @return string sanitized input */ public static function sanitizeInputValue($value) + { + $value = self::sanitizeLineBreaks($value); + $value = self::sanitizeString($value); + return $value; + } + + /** + * Sanitize a single input value + * + * @param $value + * @return string + */ + private static function sanitizeString($value) { // $_GET and $_REQUEST already urldecode()'d // decode // note: before php 5.2.7, htmlspecialchars() double encodes &#x hex items $value = html_entity_decode($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8'); - // filter - $value = self::sanitizeLineBreaks($value); + $value = self::sanitizeNullBytes($value); // escape $tmp = @htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8'); @@ -295,15 +334,17 @@ class Common // convert and escape $value = utf8_encode($value); $tmp = htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8'); + return $tmp; } return $tmp; } /** * Unsanitizes a single input value and returns the result. - * + * * @param string $value * @return string unsanitized input + * @api */ public static function unsanitizeInputValue($value) { @@ -315,10 +356,10 @@ class Common * * This method should be used when you need to unescape data that was obtained from * the user. - * + * * Some data in Piwik is stored sanitized (such as site name). In this case you may * have to use this method to unsanitize it in order to, for example, output it in JSON. - * + * * @param string|array $value The data to unsanitize. If an array is passed, the * array is sanitized recursively. Key values are not unsanitized. * @return string|array The unsanitized data. @@ -345,28 +386,42 @@ class Common */ private static function undoMagicQuotes($value) { - return version_compare(PHP_VERSION, '5.4', '<') - && get_magic_quotes_gpc() - ? stripslashes($value) - : $value; - } + static $shouldUndo; + + if (!isset($shouldUndo)) { + $shouldUndo = version_compare(PHP_VERSION, '5.4', '<') && get_magic_quotes_gpc(); + } + + if ($shouldUndo) { + $value = stripslashes($value); + } - /** - * - * @param string - * @return string Line breaks and line carriage removed - */ - public static function sanitizeLineBreaks($value) - { - $value = str_replace(array("\n", "\r", "\0"), '', $value); return $value; } + /** + * @param string $value + * @return string Line breaks and line carriage removed + */ + public static function sanitizeLineBreaks($value) + { + return str_replace(array("\n", "\r"), '', $value); + } + + /** + * @param string $value + * @return string Null bytes removed + */ + public static function sanitizeNullBytes($value) + { + return str_replace(array("\0"), '', $value); + } + /** * Gets a sanitized request parameter by name from the `$_GET` and `$_POST` superglobals. - * + * * Use this function to get request parameter values. **_NEVER use `$_GET` and `$_POST` directly._** - * + * * If the variable cannot be found, and a default value was not provided, an exception is raised. * * _See {@link sanitizeInputValues()} to learn more about sanitization._ @@ -376,7 +431,7 @@ class Common * @param string|null $varDefault The value to return if the request parameter cannot be found or has an empty value. * @param string|null $varType Expected type of the request variable. This parameters value must be one of the following: * `'array'`, `'int'`, `'integer'`, `'string'`, `'json'`. - * + * * If `'json'`, the string value will be `json_decode`-d and then sanitized. * @param array|null $requestArrayToUse The array to use instead of `$_GET` and `$_POST`. * @throws Exception If the request parameter doesn't exist and there is no default value, or if the request parameter @@ -389,6 +444,7 @@ class Common if (is_null($requestArrayToUse)) { $requestArrayToUse = $_GET + $_POST; } + $varDefault = self::sanitizeInputValues($varDefault); if ($varType === 'int') { // settype accepts only integer @@ -420,22 +476,36 @@ class Common // we deal w/ json differently if ($varType == 'json') { $value = self::undoMagicQuotes($requestArrayToUse[$varName]); - $value = self::json_decode($value, $assoc = true); + $value = json_decode($value, $assoc = true); return self::sanitizeInputValues($value, $alreadyStripslashed = true); } $value = self::sanitizeInputValues($requestArrayToUse[$varName]); - if (!is_null($varType)) { + if (isset($varType)) { $ok = false; if ($varType === 'string') { - if (is_string($value)) $ok = true; + if (is_string($value) || is_int($value)) { + $ok = true; + } elseif (is_float($value)) { + $value = Common::forceDotAsSeparatorForDecimalPoint($value); + $ok = true; + } } elseif ($varType === 'integer') { - if ($value == (string)(int)$value) $ok = true; + if ($value == (string)(int)$value) { + $ok = true; + } } elseif ($varType === 'float') { - if ($value == (string)(float)$value) $ok = true; + $valueToCompare = (string)(float)$value; + $valueToCompare = Common::forceDotAsSeparatorForDecimalPoint($valueToCompare); + + if ($value == $valueToCompare) { + $ok = true; + } } elseif ($varType === 'array') { - if (is_array($value)) $ok = true; + if (is_array($value)) { + $ok = true; + } } else { throw new Exception("\$varType specified is not known. It should be one of the following: array, int, integer, float, string"); } @@ -452,6 +522,7 @@ class Common } settype($value, $varType); } + return $value; } @@ -466,11 +537,17 @@ class Common */ public static function generateUniqId() { - return md5(uniqid(rand(), true)); + if (function_exists('mt_rand')) { + $rand = mt_rand(); + } else { + $rand = rand(); + } + + return md5(uniqid($rand, true)); } /** - * Configureable hash() algorithm (defaults to md5) + * Configurable hash() algorithm (defaults to md5) * * @param string $str String to be hashed * @param bool $raw_output @@ -479,14 +556,16 @@ class Common public static function hash($str, $raw_output = false) { static $hashAlgorithm = null; + if (is_null($hashAlgorithm)) { $hashAlgorithm = @Config::getInstance()->General['hash_algorithm']; } if ($hashAlgorithm) { $hash = @hash($hashAlgorithm, $str, $raw_output); - if ($hash !== false) + if ($hash !== false) { return $hash; + } } return md5($str, $raw_output); @@ -503,16 +582,13 @@ class Common public static function getRandomString($length = 16, $alphabet = "abcdefghijklmnoprstuvwxyz0123456789") { $chars = $alphabet; - $str = ''; - - list($usec, $sec) = explode(" ", microtime()); - $seed = ((float)$sec + (float)$usec) * 100000; - mt_srand($seed); + $str = ''; for ($i = 0; $i < $length; $i++) { $rand_key = mt_rand(0, strlen($chars) - 1); $str .= substr($chars, $rand_key, 1); } + return str_shuffle($str); } @@ -554,27 +630,22 @@ class Common ) { throw new Exception("visitorId is expected to be a " . Tracker::LENGTH_HEX_ID_STRING . " hex char string"); } + return self::hex2bin($id); } /** - * Convert IP address (in network address format) to presentation format. - * This is a backward compatibility function for code that only expects - * IPv4 addresses (i.e., doesn't support IPv6). + * Converts a User ID string to the Visitor ID Binary representation. * - * @see IP::N2P() - * - * This function does not support the long (or its string representation) - * returned by the built-in ip2long() function, from Piwik 1.3 and earlier. - * - * @deprecated 1.4 - * - * @param string $ip IP address in network address format + * @param $userId * @return string */ - public static function long2ip($ip) + public static function convertUserIdToVisitorIdBin($userId) { - return IP::long2ip($ip); + require_once PIWIK_INCLUDE_PATH . '/libs/PiwikTracker/PiwikTracker.php'; + $userIdHashed = \PiwikTracker::getUserIdHashed($userId); + + return self::convertVisitorIdToBin($userIdHashed); } /** @@ -653,14 +724,14 @@ class Common /** * Returns the list of parent classes for the given class. * - * @param string $klass A class name. + * @param string $class A class name. * @return string[] The list of parent classes in order from highest ancestor to the descended class. */ - public static function getClassLineage($klass) + public static function getClassLineage($class) { - $klasses = array_merge(array($klass), array_values(class_parents($klass, $autoload = false))); + $classes = array_merge(array($class), array_values(class_parents($class, $autoload = false))); - return array_reverse($klasses); + return array_reverse($classes); } /* @@ -672,14 +743,16 @@ class Common * * @see core/DataFiles/Countries.php * - * @return array Array of 3 letter continent codes + * @return array Array of 3 letter continent codes + * + * @deprecated Use Piwik\Intl\Data\Provider\RegionDataProvider instead. + * @see \Piwik\Intl\Data\Provider\RegionDataProvider::getContinentList() */ public static function getContinentsList() { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Countries.php'; - - $continentsList = $GLOBALS['Piwik_ContinentList']; - return $continentsList; + /** @var RegionDataProvider $dataProvider */ + $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider'); + return $dataProvider->getContinentList(); } /** @@ -688,19 +761,16 @@ class Common * @see core/DataFiles/Countries.php * * @param bool $includeInternalCodes - * @return array Array of (2 letter ISO codes => 3 letter continent code) + * @return array Array of (2 letter ISO codes => 3 letter continent code) + * + * @deprecated Use Piwik\Intl\Data\Provider\RegionDataProvider instead. + * @see \Piwik\Intl\Data\Provider\RegionDataProvider::getCountryList() */ public static function getCountriesList($includeInternalCodes = false) { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Countries.php'; - - $countriesList = $GLOBALS['Piwik_CountryList']; - $extras = $GLOBALS['Piwik_CountryList_Extras']; - - if ($includeInternalCodes) { - return array_merge($countriesList, $extras); - } - return $countriesList; + /** @var RegionDataProvider $dataProvider */ + $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider'); + return $dataProvider->getCountryList($includeInternalCodes); } /** @@ -711,13 +781,15 @@ class Common * @return array Array of two letter ISO codes mapped with their associated language names (in English). E.g. * `array('en' => 'English', 'ja' => 'Japanese')`. * @api + * + * @deprecated Use Piwik\Intl\Data\Provider\LanguageDataProvider instead. + * @see \Piwik\Intl\Data\Provider\LanguageDataProvider::getLanguageList() */ public static function getLanguagesList() { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Languages.php'; - - $languagesList = $GLOBALS['Piwik_LanguageList']; - return $languagesList; + /** @var LanguageDataProvider $dataProvider */ + $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider'); + return $dataProvider->getLanguageList(); } /** @@ -728,70 +800,15 @@ class Common * @return array Array of two letter ISO language codes mapped with two letter ISO country codes: * `array('fr' => 'fr') // French => France` * @api + * + * @deprecated Use Piwik\Intl\Data\Provider\LanguageDataProvider instead. + * @see \Piwik\Intl\Data\Provider\LanguageDataProvider::getLanguageToCountryList() */ public static function getLanguageToCountryList() { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/LanguageToCountry.php'; - - $languagesList = $GLOBALS['Piwik_LanguageToCountry']; - return $languagesList; - } - - /** - * Returns list of search engines by URL - * - * @see core/DataFiles/SearchEngines.php - * - * @return array Array of ( URL => array( searchEngineName, keywordParameter, path, charset ) ) - */ - public static function getSearchEngineUrls() - { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/SearchEngines.php'; - - $searchEngines = $GLOBALS['Piwik_SearchEngines']; - - Piwik::postEvent('Referrer.addSearchEngineUrls', array(&$searchEngines)); - - return $searchEngines; - } - - /** - * Returns list of search engines by name - * - * @see core/DataFiles/SearchEngines.php - * - * @return array Array of ( searchEngineName => URL ) - */ - public static function getSearchEngineNames() - { - $searchEngines = self::getSearchEngineUrls(); - - $nameToUrl = array(); - foreach ($searchEngines as $url => $info) { - if (!isset($nameToUrl[$info[0]])) { - $nameToUrl[$info[0]] = $url; - } - } - - return $nameToUrl; - } - - /** - * Returns list of social networks by URL - * - * @see core/DataFiles/Socials.php - * - * @return array Array of ( URL => Social Network Name ) - */ - public static function getSocialUrls() - { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Socials.php'; - - $socialUrls = $GLOBALS['Piwik_socialUrl']; - - Piwik::postEvent('Referrer.addSocialUrls', array(&$socialUrls)); - - return $socialUrls; + /** @var LanguageDataProvider $dataProvider */ + $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider'); + return $dataProvider->getLanguageToCountryList(); } /** @@ -840,7 +857,7 @@ class Common } } - if (is_null($browserLang)) { + if (empty($browserLang)) { // a fallback might be to infer the language in HTTP_USER_AGENT (i.e., localized build) $browserLang = ""; } else { @@ -872,11 +889,15 @@ class Common */ public static function getCountry($lang, $enableLanguageToCountryGuess, $ip) { - if (empty($lang) || strlen($lang) < 2 || $lang == 'xx') { - return 'xx'; + if (empty($lang) || strlen($lang) < 2 || $lang == self::LANGUAGE_CODE_INVALID) { + return self::LANGUAGE_CODE_INVALID; } - $validCountries = self::getCountriesList(); + /** @var RegionDataProvider $dataProvider */ + $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider'); + + $validCountries = $dataProvider->getCountryList(); + return self::extractCountryCodeFromBrowserLanguage($lang, $validCountries, $enableLanguageToCountryGuess); } @@ -890,7 +911,10 @@ class Common */ public static function extractCountryCodeFromBrowserLanguage($browserLanguage, $validCountries, $enableLanguageToCountryGuess) { - $langToCountry = self::getLanguageToCountryList(); + /** @var LanguageDataProvider $dataProvider */ + $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider'); + + $langToCountry = $dataProvider->getLanguageToCountryList(); if ($enableLanguageToCountryGuess) { if (preg_match('/^([a-z]{2,3})(?:,|;|$)/', $browserLanguage, $matches)) { @@ -909,51 +933,90 @@ class Common } } } - return 'xx'; + return self::LANGUAGE_CODE_INVALID; } /** - * Returns the visitor language based only on the Browser 'accepted language' information + * Returns the language and region string, based only on the Browser 'accepted language' information. + * * The language tag is defined by ISO 639-1 * * @param string $browserLanguage Browser's accepted langauge header * @param array $validLanguages array of valid language codes - * @return string 2 letter ISO 639 code + * @return string 2 letter ISO 639 code 'es' (Spanish) */ - public static function extractLanguageCodeFromBrowserLanguage($browserLanguage, $validLanguages) + public static function extractLanguageCodeFromBrowserLanguage($browserLanguage, $validLanguages = array()) { - // assumes language preference is sorted; - // does not handle language-script-region tags or language range (*) - if (!empty($validLanguages) && preg_match_all('/(?:^|,)([a-z]{2,3})([-][a-z]{2})?/', $browserLanguage, $matches, PREG_SET_ORDER)) { - foreach ($matches as $parts) { - if (count($parts) == 3) { - // match locale (language and location) - if (in_array($parts[1] . $parts[2], $validLanguages)) { - return $parts[1] . $parts[2]; - } + $validLanguages = self::checkValidLanguagesIsSet($validLanguages); + $languageRegionCode = self::extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages); + + if (strlen($languageRegionCode) == 2) { + $languageCode = $languageRegionCode; + } else { + $languageCode = substr($languageRegionCode, 0, 2); + } + if (in_array($languageCode, $validLanguages)) { + return $languageCode; + } + return self::LANGUAGE_CODE_INVALID; + } + + /** + * Returns the language and region string, based only on the Browser 'accepted language' information. + * * The language tag is defined by ISO 639-1 + * * The region tag is defined by ISO 3166-1 + * + * @param string $browserLanguage Browser's accepted langauge header + * @param array $validLanguages array of valid language codes. Note that if the array includes "fr" then it will consider all regional variants of this language valid, such as "fr-ca" etc. + * @return string 2 letter ISO 639 code 'es' (Spanish) or if found, includes the region as well: 'es-ar' + */ + public static function extractLanguageAndRegionCodeFromBrowserLanguage($browserLanguage, $validLanguages = array()) + { + $validLanguages = self::checkValidLanguagesIsSet($validLanguages); + + if (!preg_match_all('/(?:^|,)([a-z]{2,3})([-][a-z]{2})?/', $browserLanguage, $matches, PREG_SET_ORDER)) { + return self::LANGUAGE_CODE_INVALID; + } + foreach ($matches as $parts) { + $langIso639 = $parts[1]; + if (empty($langIso639)) { + continue; + } + + // If a region tag is found eg. "fr-ca" + if (count($parts) == 3) { + $regionIso3166 = $parts[2]; // eg. "-ca" + + if (in_array($langIso639 . $regionIso3166, $validLanguages)) { + return $langIso639 . $regionIso3166; } - // match language only (where no region provided) - if (in_array($parts[1], $validLanguages)) { - return $parts[1]; + + if (in_array($langIso639, $validLanguages)) { + return $langIso639 . $regionIso3166; } } + // eg. "fr" or "es" + if (in_array($langIso639, $validLanguages)) { + return $langIso639; + } } - return 'xx'; + return self::LANGUAGE_CODE_INVALID; } /** * Returns the continent of a given country * - * @param string $country 2 letters isocode + * @param string $country 2 letters iso code * * @return string Continent (3 letters code : afr, asi, eur, amn, ams, oce) */ public static function getContinent($country) { - $countryList = self::getCountriesList(); - if (isset($countryList[$country])) { - return $countryList[$country]; - } - return 'unk'; + /** @var RegionDataProvider $dataProvider */ + $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider'); + + $countryList = $dataProvider->getCountryList(); + + return isset($countryList[$country]) ? $countryList[$country] : 'unk'; } /* @@ -995,10 +1058,10 @@ class Common /** * Returns a string with a comma separated list of placeholders for use in an SQL query. Used mainly * to fill the `IN (...)` part of a query. - * + * * @param array|string $fields The names of the mysql table fields to bind, e.g. * `array(fieldName1, fieldName2, fieldName3)`. - * + * * _Note: The content of the array isn't important, just its length._ * @return string The placeholder string, e.g. `"?, ?, ?"`. * @api @@ -1015,6 +1078,22 @@ class Common return '?' . str_repeat(',?', $count - 1); } + /** + * Force the separator for decimal point to be a dot. See https://github.com/piwik/piwik/issues/6435 + * If for instance a German locale is used it would be a comma otherwise. + * + * @param float|string $value + * @return string + */ + public static function forceDotAsSeparatorForDecimalPoint($value) + { + if (null === $value || false === $value) { + return $value; + } + + return str_replace(',', '.', $value); + } + /** * Sets outgoing header. * @@ -1024,23 +1103,62 @@ class Common public static function sendHeader($header, $replace = true) { // don't send header in CLI mode - if(Common::isPhpCliMode()) { - return; - } - if (isset($GLOBALS['PIWIK_TRACKER_LOCAL_TRACKING']) && $GLOBALS['PIWIK_TRACKER_LOCAL_TRACKING']) { - @header($header, $replace); - } else { + if (!Common::isPhpCliMode() and !headers_sent()) { header($header, $replace); } } + /** + * Sends the given response code if supported. + * + * @param int $code Eg 204 + * + * @throws Exception + */ + public static function sendResponseCode($code) + { + $messages = array( + 200 => 'Ok', + 204 => 'No Response', + 301 => 'Moved Permanently', + 302 => 'Found', + 304 => 'Not Modified', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 403 => 'Forbidden', + 404 => 'Not Found', + 500 => 'Internal Server Error', + 503 => 'Service Unavailable', + ); + + if (!array_key_exists($code, $messages)) { + throw new Exception('Response code not supported: ' . $code); + } + + if (strpos(PHP_SAPI, '-fcgi') === false) { + $key = 'HTTP/1.1'; + + if (array_key_exists('SERVER_PROTOCOL', $_SERVER) + && strlen($_SERVER['SERVER_PROTOCOL']) < 15 + && strlen($_SERVER['SERVER_PROTOCOL']) > 1) { + $key = $_SERVER['SERVER_PROTOCOL']; + } + } else { + // FastCGI + $key = 'Status:'; + } + + $message = $messages[$code]; + Common::sendHeader($key . ' ' . $code . ' ' . $message); + } + /** * Returns the ID of the current LocationProvider (see UserCountry plugin code) from * the Tracker cache. */ public static function getCurrentLocationProviderId() { - $cache = Cache::getCacheGeneral(); + $cache = TrackerCache::getCacheGeneral(); return empty($cache['currentLocationProviderId']) ? DefaultProvider::ID : $cache['currentLocationProviderId']; @@ -1049,11 +1167,11 @@ class Common /** * Marks an orphaned object for garbage collection. * - * For more information: {@link http://dev.piwik.org/trac/ticket/374} - * @param $var The object to destroy. + * For more information: {@link https://github.com/piwik/piwik/issues/374} + * @param mixed $var The object to destroy. * @api */ - static public function destroy(&$var) + public static function destroy(&$var) { if (is_object($var) && method_exists($var, '__destruct')) { $var->__destruct(); @@ -1062,27 +1180,62 @@ class Common $var = null; } - static public function printDebug($info = '') + /** + * @todo This method is weird, it's debugging statements but seem to only work for the tracker, maybe it + * should be moved elsewhere + */ + public static function printDebug($info = '') { if (isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) { + if (!headers_sent()) { + // prevent XSS in tracker debug output + header('Content-type: text/plain'); + } if (is_object($info)) { $info = var_export($info, true); } - Log::getInstance()->setLogLevel(Log::DEBUG); + $logger = StaticContainer::get('Psr\Log\LoggerInterface'); if (is_array($info) || is_object($info)) { $info = Common::sanitizeInputValues($info); $out = var_export($info, true); foreach (explode("\n", $out) as $line) { - Log::debug($line); + $logger->debug($line); } } else { foreach (explode("\n", $info) as $line) { - Log::debug(htmlspecialchars($line, ENT_QUOTES)); + $logger->debug($line); } } } } + + /** + * Returns true if the request is an AJAX request. + * + * @return bool + */ + public static function isXmlHttpRequest() + { + return isset($_SERVER['HTTP_X_REQUESTED_WITH']) + && (strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'); + } + + /** + * @param $validLanguages + * @return array + */ + protected static function checkValidLanguagesIsSet($validLanguages) + { + /** @var LanguageDataProvider $dataProvider */ + $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider'); + + if (empty($validLanguages)) { + $validLanguages = array_keys($dataProvider->getLanguageList()); + return $validLanguages; + } + return $validLanguages; + } } diff --git a/www/analytics/core/Composer/ScriptHandler.php b/www/analytics/core/Composer/ScriptHandler.php new file mode 100644 index 00000000..0595473f --- /dev/null +++ b/www/analytics/core/Composer/ScriptHandler.php @@ -0,0 +1,47 @@ +optionName = $optionName; + $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface'); + } + + /** + * Queries the option table and returns all items in this list. + * + * @return array + */ + public function getAll() + { + $result = $this->getListOptionValue(); + + foreach ($result as $key => $item) { + // remove non-array items (unexpected state, though can happen when upgrading from an old Piwik) + if (is_array($item)) { + $this->logger->info("Found array item in DistributedList option value '{name}': {data}", array( + 'name' => $this->optionName, + 'data' => var_export($result, true) + )); + + unset($result[$key]); + } + } + + return $result; + } + + /** + * Sets the contents of the list in the option table. + * + * @param string[] $items + */ + public function setAll($items) + { + foreach ($items as $key => &$item) { + if (is_array($item)) { + throw new \InvalidArgumentException("Array item encountered in DistributedList::setAll() [ key = $key ]."); + } else { + $item = (string)$item; + } + } + + Option::set($this->optionName, serialize($items)); + } + + /** + * Adds one or more items to the list in the option table. + * + * @param string|array $item + */ + public function add($item) + { + $allItems = $this->getAll(); + if (is_array($item)) { + $allItems = array_merge($allItems, $item); + } else { + $allItems[] = $item; + } + + $this->setAll($allItems); + } + + /** + * Removes one or more items by value from the list in the option table. + * + * Does not preserve array keys. + * + * @param string|array $items + */ + public function remove($items) + { + if (!is_array($items)) { + $items = array($items); + } + + $allItems = $this->getAll(); + + foreach ($items as $item) { + $existingIndex = array_search($item, $allItems); + if ($existingIndex === false) { + return; + } + + unset($allItems[$existingIndex]); + } + + $this->setAll(array_values($allItems)); + } + + /** + * Removes one or more items by index from the list in the option table. + * + * Does not preserve array keys. + * + * @param int[]|int $indices + */ + public function removeByIndex($indices) + { + if (!is_array($indices)) { + $indices = array($indices); + } + + $indices = array_unique($indices); + + $allItems = $this->getAll(); + foreach ($indices as $index) { + unset($allItems[$index]); + } + + $this->setAll(array_values($allItems)); + } + + protected function getListOptionValue() + { + Option::clearCachedOption($this->optionName); + $array = Option::get($this->optionName); + + $result = array(); + if ($array + && ($array = unserialize($array)) + && count($array) + ) { + $result = $array; + } + return $result; + } +} diff --git a/www/analytics/core/Config.php b/www/analytics/core/Config.php index 6fe19d2e..adc3ba94 100644 --- a/www/analytics/core/Config.php +++ b/www/analytics/core/Config.php @@ -1,6 +1,6 @@ General['minimum_memory_limit'] = 256; * Config::getInstance()->forceSave(); - * + * * **Setting an entire section:** - * + * * Config::getInstance()->MySection = array('myoption' => 1); * Config::getInstance()->forceSave(); - * - * @method static \Piwik\Config getInstance() */ -class Config extends Singleton +class Config { const DEFAULT_LOCAL_CONFIG_PATH = '/config/config.ini.php'; const DEFAULT_COMMON_CONFIG_PATH = '/config/common.config.ini.php'; const DEFAULT_GLOBAL_CONFIG_PATH = '/config/global.ini.php'; - /** - * Contains configuration files values - * - * @var array - */ - protected $initialized = false; - protected $configGlobal = array(); - protected $configLocal = array(); - protected $configCommon = array(); - protected $configCache = array(); - protected $pathGlobal = null; - protected $pathCommon = null; - protected $pathLocal = null; - /** * @var boolean */ - protected $isTest = false; + protected $doNotWriteConfigInTests = false; /** - * Constructor + * @var GlobalSettingsProvider */ - public function __construct($pathGlobal = null, $pathLocal = null, $pathCommon = null) + protected $settings; + + /** + * @return Config + */ + public static function getInstance() { - $this->pathGlobal = $pathGlobal ?: self::getGlobalConfigPath(); - $this->pathCommon = $pathCommon ?: self::getCommonConfigPath(); - $this->pathLocal = $pathLocal ?: self::getLocalConfigPath(); + return StaticContainer::get('Piwik\Config'); + } + + public function __construct(GlobalSettingsProvider $settings) + { + $this->settings = $settings; } /** @@ -80,7 +74,7 @@ class Config extends Singleton */ public function getLocalPath() { - return $this->pathLocal; + return $this->settings->getPathLocal(); } /** @@ -90,7 +84,7 @@ class Config extends Singleton */ public function getGlobalPath() { - return $this->pathGlobal; + return $this->settings->getPathGlobal(); } /** @@ -100,72 +94,7 @@ class Config extends Singleton */ public function getCommonPath() { - return $this->pathCommon; - } - - /** - * Enable test environment - * - * @param string $pathLocal - * @param string $pathGlobal - * @param string $pathCommon - */ - public function setTestEnvironment($pathLocal = null, $pathGlobal = null, $pathCommon = null, $allowSaving = false) - { - if (!$allowSaving) { - $this->isTest = true; - } - - $this->clear(); - - $this->pathLocal = $pathLocal ?: Config::getLocalConfigPath(); - $this->pathGlobal = $pathGlobal ?: Config::getGlobalConfigPath(); - $this->pathCommon = $pathCommon ?: Config::getCommonConfigPath(); - - $this->init(); - - // this proxy will not record any data in the production database. - // this provides security for Piwik installs and tests were setup. - if (isset($this->configGlobal['database_tests']) - || isset($this->configLocal['database_tests']) - ) { - $this->__get('database_tests'); - $this->configCache['database'] = $this->configCache['database_tests']; - } - - // Ensure local mods do not affect tests - if (empty($pathGlobal)) { - $this->configCache['log'] = $this->configGlobal['log']; - $this->configCache['Debug'] = $this->configGlobal['Debug']; - $this->configCache['mail'] = $this->configGlobal['mail']; - $this->configCache['General'] = $this->configGlobal['General']; - $this->configCache['Segments'] = $this->configGlobal['Segments']; - $this->configCache['Tracker'] = $this->configGlobal['Tracker']; - $this->configCache['Deletelogs'] = $this->configGlobal['Deletelogs']; - $this->configCache['Deletereports'] = $this->configGlobal['Deletereports']; - } - - // for unit tests, we set that no plugin is installed. This will force - // the test initialization to create the plugins tables, execute ALTER queries, etc. - $this->configCache['PluginsInstalled'] = array('PluginsInstalled' => array()); - - // DevicesDetection plugin is not yet enabled by default - if (isset($configGlobal['Plugins'])) { - $this->configCache['Plugins'] = $this->configGlobal['Plugins']; - $this->configCache['Plugins']['Plugins'][] = 'DevicesDetection'; - } - if (isset($configGlobal['Plugins_Tracker'])) { - $this->configCache['Plugins_Tracker'] = $this->configGlobal['Plugins_Tracker']; - $this->configCache['Plugins_Tracker']['Plugins_Tracker'][] = 'DevicesDetection'; - } - - // to avoid weird session error in travis - if (empty($pathGlobal)) { - $configArray = &$this->configCache; - } else { - $configArray = &$this->configLocal; - } - $configArray['General']['session_save_handler'] = 'dbtables'; + return $this->settings->getPathCommon(); } /** @@ -173,7 +102,7 @@ class Config extends Singleton * * @return string */ - protected static function getGlobalConfigPath() + public static function getGlobalConfigPath() { return PIWIK_USER_PATH . self::DEFAULT_GLOBAL_CONFIG_PATH; } @@ -204,6 +133,8 @@ class Config extends Singleton private static function getLocalConfigInfoForHostname($hostname) { + // Remove any port number to get actual hostname + $hostname = Url::getHostSanitized($hostname); $perHostFilename = $hostname . '.config.ini.php'; $pathDomainConfig = PIWIK_USER_PATH . '/config/' . $perHostFilename; @@ -225,11 +156,25 @@ class Config extends Singleton return array( 'action_url_category_delimiter' => $general['action_url_category_delimiter'], 'autocomplete_min_sites' => $general['autocomplete_min_sites'], - 'datatable_export_range_as_day' => $general['datatable_export_range_as_day'] + 'datatable_export_range_as_day' => $general['datatable_export_range_as_day'], + 'datatable_row_limits' => $this->getDatatableRowLimits(), + 'are_ads_enabled' => $general['piwik_pro_ads_enabled'] ); } - protected static function getByDomainConfigPath() + /** + * @param $general + * @return mixed + */ + private function getDatatableRowLimits() + { + $limits = $this->General['datatable_row_limits']; + $limits = explode(",", $limits); + $limits = array_map('trim', $limits); + return $limits; + } + + public static function getByDomainConfigPath() { $host = self::getHostname(); $hostConfig = self::getLocalConfigInfoForHostname($host); @@ -242,9 +187,19 @@ class Config extends Singleton return false; } - protected static function getHostname() + /** + * Returns the hostname of the current request (without port number) + * + * @return string + */ + public static function getHostname() { - $host = Url::getHost($checkIfTrusted = false); // Check trusted requires config file which is not ready yet + // Check trusted requires config file which is not ready yet + $host = Url::getHost($checkIfTrusted = false); + + // Remove any port number to get actual hostname + $host = Url::getHostSanitized($host); + return $host; } @@ -259,19 +214,26 @@ class Config extends Singleton * @param string $hostname eg piwik.example.com * @return string * @throws \Exception In case the domain contains not allowed characters + * @internal */ public function forceUsageOfLocalHostnameConfig($hostname) { - $hostConfig = static::getLocalConfigInfoForHostname($hostname); + $hostConfig = self::getLocalConfigInfoForHostname($hostname); - if (!Filesystem::isValidFilename($hostConfig['file'])) { - throw new Exception('Hostname is not valid'); + $filename = $hostConfig['file']; + if (!Filesystem::isValidFilename($filename)) { + throw new Exception('Piwik domain is not a valid looking hostname (' . $filename . ').'); } - $this->pathLocal = $hostConfig['path']; - $this->configLocal = array(); - $this->initialized = false; - return $this->pathLocal; + $pathLocal = $hostConfig['path']; + + try { + $this->reload($pathLocal); + } catch (Exception $ex) { + // pass (not required for local file to exist at this point) + } + + return $pathLocal; } /** @@ -281,99 +243,57 @@ class Config extends Singleton */ public function isFileWritable() { - return is_writable($this->pathLocal); + return is_writable($this->settings->getPathLocal()); } /** * Clear in-memory configuration so it can be reloaded + * @deprecated since v2.12.0 */ public function clear() { - $this->configGlobal = array(); - $this->configLocal = array(); - $this->configCache = array(); - $this->initialized = false; + $this->reload(); } /** * Read configuration from files into memory * * @throws Exception if local config file is not readable; exits for other errors + * @deprecated since v2.12.0 */ public function init() { - $this->initialized = true; - $reportError = SettingsServer::isTrackerApiRequest(); - - // read defaults from global.ini.php - if (!is_readable($this->pathGlobal) && $reportError) { - Piwik_ExitWithMessage(Piwik::translate('General_ExceptionConfigurationFileNotFound', array($this->pathGlobal))); - } - - $this->configGlobal = _parse_ini_file($this->pathGlobal, true); - - if (empty($this->configGlobal) && $reportError) { - Piwik_ExitWithMessage(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathGlobal, "parse_ini_file()"))); - } - - $this->configCommon = _parse_ini_file($this->pathCommon, true); - - // Check config.ini.php last - $this->checkLocalConfigFound(); - - $this->configLocal = _parse_ini_file($this->pathLocal, true); - if (empty($this->configLocal) && $reportError) { - Piwik_ExitWithMessage(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathLocal, "parse_ini_file()"))); - } + $this->reload(); } + /** + * Reloads config data from disk. + * + * @throws \Exception if the global config file is not found and this is a tracker request, or + * if the local config file is not found and this is NOT a tracker request. + */ + protected function reload($pathLocal = null, $pathGlobal = null, $pathCommon = null) + { + $this->settings->reload($pathGlobal, $pathLocal, $pathCommon); + } + + /** + * @deprecated + */ public function existsLocalConfig() { - return is_readable($this->pathLocal); + return is_readable($this->getLocalPath()); } - public function checkLocalConfigFound() + public function deleteLocalConfig() { - if (!$this->existsLocalConfig()) { - throw new Exception(Piwik::translate('General_ExceptionConfigurationFileNotFound', array($this->pathLocal))); + $configLocal = $this->getLocalPath(); + + if(file_exists($configLocal)){ + @unlink($configLocal); } } - /** - * Decode HTML entities - * - * @param mixed $values - * @return mixed - */ - protected function decodeValues($values) - { - if (is_array($values)) { - foreach ($values as &$value) { - $value = $this->decodeValues($value); - } - return $values; - } - return html_entity_decode($values, ENT_COMPAT, 'UTF-8'); - } - - /** - * Encode HTML entities - * - * @param mixed $values - * @return mixed - */ - protected function encodeValues($values) - { - if (is_array($values)) { - foreach ($values as &$value) { - $value = $this->encodeValues($value); - } - } else { - $values = htmlentities($values, ENT_COMPAT, 'UTF-8'); - } - return $values; - } - /** * Returns a configuration value or section by name. * @@ -385,85 +305,32 @@ class Config extends Singleton */ public function &__get($name) { - if (!$this->initialized) { - $this->init(); - - // must be called here, not in init(), since setTestEnvironment() calls init(). (this avoids - // infinite recursion) - Piwik::postTestEvent('Config.createConfigSingleton', - array($this, &$this->configCache, &$this->configLocal)); - } - - // check cache for merged section - if (isset($this->configCache[$name])) { - $tmp =& $this->configCache[$name]; - return $tmp; - } - - $section = $this->getFromGlobalConfig($name); - $sectionCommon = $this->getFromCommonConfig($name); - if(empty($section) && !empty($sectionCommon)) { - $section = $sectionCommon; - } elseif(!empty($section) && !empty($sectionCommon)) { - $section = $this->array_merge_recursive_distinct($section, $sectionCommon); - } - - if (isset($this->configLocal[$name])) { - // local settings override the global defaults - $section = $section - ? array_merge($section, $this->configLocal[$name]) - : $this->configLocal[$name]; - } - - if ($section === null && $name = 'superuser') { - $user = $this->getConfigSuperUserForBackwardCompatibility(); - return $user; - } else if ($section === null) { - throw new Exception("Error while trying to read a specific config file entry '$name' from your configuration files.If you just completed a Piwik upgrade, please check that the file config/global.ini.php was overwritten by the latest Piwik version."); - } - - // cache merged section for later - $this->configCache[$name] = $this->decodeValues($section); - $tmp =& $this->configCache[$name]; - - return $tmp; + $section =& $this->settings->getIniFileChain()->get($name); + return $section; } /** - * @deprecated since version 2.0.4 + * @api */ - public function getConfigSuperUserForBackwardCompatibility() - { - try { - $db = Db::get(); - $user = $db->fetchRow("SELECT login, email, password - FROM " . Common::prefixTable("user") . " - WHERE superuser_access = 1 - ORDER BY date_registered ASC LIMIT 1"); - - if (!empty($user)) { - $user['bridge'] = 1; - return $user; - } - } catch (Exception $e) {} - - return array(); - } - public function getFromGlobalConfig($name) { - if (isset($this->configGlobal[$name])) { - return $this->configGlobal[$name]; - } - return null; + return $this->settings->getIniFileChain()->getFrom($this->getGlobalPath(), $name); } + /** + * @api + */ public function getFromCommonConfig($name) { - if (isset($this->configCommon[$name])) { - return $this->configCommon[$name]; - } - return null; + return $this->settings->getIniFileChain()->getFrom($this->getCommonPath(), $name); + } + + /** + * @api + */ + public function getFromLocalConfig($name) + { + return $this->settings->getIniFileChain()->getFrom($this->getLocalPath(), $name); } /** @@ -475,154 +342,24 @@ class Config extends Singleton */ public function __set($name, $value) { - $this->configCache[$name] = $value; - } - - /** - * Comparison function - * - * @param mixed $elem1 - * @param mixed $elem2 - * @return int; - */ - public static function compareElements($elem1, $elem2) - { - if (is_array($elem1)) { - if (is_array($elem2)) { - return strcmp(serialize($elem1), serialize($elem2)); - } - - return 1; - } - - if (is_array($elem2)) { - return -1; - } - - if ((string)$elem1 === (string)$elem2) { - return 0; - } - - return ((string)$elem1 > (string)$elem2) ? 1 : -1; - } - - /** - * Compare arrays and return difference, such that: - * - * $modified = array_merge($original, $difference); - * - * @param array $original original array - * @param array $modified modified array - * @return array differences between original and modified - */ - public function array_unmerge($original, $modified) - { - // return key/value pairs for keys in $modified but not in $original - // return key/value pairs for keys in both $modified and $original, but values differ - // ignore keys that are in $original but not in $modified - - return array_udiff_assoc($modified, $original, array(__CLASS__, 'compareElements')); + $this->settings->getIniFileChain()->set($name, $value); } /** * Dump config * - * @param array $configLocal - * @param array $configGlobal - * @param array $configCommon - * @param array $configCache - * @return string + * @return string|null + * @throws \Exception */ - public function dumpConfig($configLocal, $configGlobal, $configCommon, $configCache) + public function dumpConfig() { - $dirty = false; + $chain = $this->settings->getIniFileChain(); - $output = "; DO NOT REMOVE THIS LINE\n"; - $output .= "; file automatically generated or modified by Piwik; you can manually override the default values in global.ini.php by redefining them in this file.\n"; - - if (!$configCache) { - return false; - } - - // If there is a common.config.ini.php, this will ensure config.ini.php does not duplicate its values - if(!empty($configCommon)) { - $configGlobal = $this->array_merge_recursive_distinct($configGlobal, $configCommon); - } - - if ($configLocal) { - foreach ($configLocal as $name => $section) { - if (!isset($configCache[$name])) { - $configCache[$name] = $this->decodeValues($section); - } - } - } - - $sectionNames = array_unique(array_merge(array_keys($configGlobal), array_keys($configCache))); - - foreach ($sectionNames as $section) { - if (!isset($configCache[$section])) { - continue; - } - - // Only merge if the section exists in global.ini.php (in case a section only lives in config.ini.php) - - // get local and cached config - $local = isset($configLocal[$section]) ? $configLocal[$section] : array(); - $config = $configCache[$section]; - - // remove default values from both (they should not get written to local) - if (isset($configGlobal[$section])) { - $config = $this->array_unmerge($configGlobal[$section], $configCache[$section]); - $local = $this->array_unmerge($configGlobal[$section], $local); - } - - // if either local/config have non-default values and the other doesn't, - // OR both have values, but different values, we must write to config.ini.php - if (empty($local) xor empty($config) - || (!empty($local) - && !empty($config) - && self::compareElements($config, $configLocal[$section])) - ) { - $dirty = true; - } - - // no point in writing empty sections, so skip if the cached section is empty - if (empty($config)) { - continue; - } - - $output .= "[$section]\n"; - - foreach ($config as $name => $value) { - $value = $this->encodeValues($value); - - if (is_numeric($name)) { - $name = $section; - $value = array($value); - } - - if (is_array($value)) { - foreach ($value as $currentValue) { - $output .= $name . "[] = \"$currentValue\"\n"; - } - } else { - if (!is_numeric($value)) { - $value = "\"$value\""; - } - $output .= $name . ' = ' . $value . "\n"; - } - } - - $output .= "\n"; - } - - if ($dirty) { - return $output; - } - return false; + $header = "; DO NOT REMOVE THIS LINE\n"; + $header .= "; file automatically generated or modified by Piwik; you can manually override the default values in global.ini.php by redefining them in this file.\n"; + return $chain->dumpChanges($header); } - /** * Write user configuration file * @@ -635,22 +372,24 @@ class Config extends Singleton * * @throws \Exception if config file not writable */ - protected function writeConfig($configLocal, $configGlobal, $configCommon, $configCache, $pathLocal, $clear = true) + protected function writeConfig($clear = true) { - if ($this->isTest) { + if ($this->doNotWriteConfigInTests) { return; } - $output = $this->dumpConfig($configLocal, $configGlobal, $configCommon, $configCache); - if ($output !== false) { - $success = @file_put_contents($pathLocal, $output); - if (!$success) { + $output = $this->dumpConfig(); + if ($output !== null + && $output !== false + ) { + $success = @file_put_contents($this->getLocalPath(), $output); + if ($success === false) { throw $this->getConfigNotWritableException(); } } if ($clear) { - $this->clear(); + $this->reload(); } } @@ -662,7 +401,7 @@ class Config extends Singleton */ public function forceSave() { - $this->writeConfig($this->configLocal, $this->configGlobal, $this->configCommon, $this->configCache, $this->pathLocal); + $this->writeConfig(); } /** @@ -670,45 +409,7 @@ class Config extends Singleton */ public function getConfigNotWritableException() { - $path = "config/" . basename($this->pathLocal); + $path = "config/" . basename($this->getLocalPath()); return new Exception(Piwik::translate('General_ConfigFileIsNotWritable', array("(" . $path . ")", ""))); } - - /** - * array_merge_recursive does indeed merge arrays, but it converts values with duplicate - * keys to arrays rather than overwriting the value in the first array with the duplicate - * value in the second array, as array_merge does. I.e., with array_merge_recursive, - * this happens (documented behavior): - * - * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value')); - * => array('key' => array('org value', 'new value')); - * - * array_merge_recursive_distinct does not change the datatypes of the values in the arrays. - * Matching keys' values in the second array overwrite those in the first array, as is the - * case with array_merge, i.e.: - * - * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value')); - * => array('key' => array('new value')); - * - * Parameters are passed by reference, though only for performance reasons. They're not - * altered by this function. - * - * @param array $array1 - * @param array $array2 - * @return array - * @author Daniel - * @author Gabriel Sobrinho - */ - function array_merge_recursive_distinct ( array &$array1, array &$array2 ) - { - $merged = $array1; - foreach ( $array2 as $key => &$value ) { - if ( is_array ( $value ) && isset ( $merged [$key] ) && is_array ( $merged [$key] ) ) { - $merged [$key] = $this->array_merge_recursive_distinct ( $merged [$key], $value ); - } else { - $merged [$key] = $value; - } - } - return $merged; - } } diff --git a/www/analytics/core/Config/ConfigNotFoundException.php b/www/analytics/core/Config/ConfigNotFoundException.php new file mode 100644 index 00000000..5860367b --- /dev/null +++ b/www/analytics/core/Config/ConfigNotFoundException.php @@ -0,0 +1,16 @@ +reload($defaultSettingsFiles, $userSettingsFile); + } + + /** + * Return setting section by reference. + * + * @param string $name + * @return mixed + */ + public function &get($name) + { + if (!isset($this->mergedSettings[$name])) { + $this->mergedSettings[$name] = array(); + } + + $result =& $this->mergedSettings[$name]; + return $result; + } + + /** + * Return setting section from a specific file, rather than the current merged settings. + * + * @param string $file The path of the file. Should be the path used in construction or reload(). + * @param string $name The name of the section to access. + */ + public function getFrom($file, $name) + { + return @$this->settingsChain[$file][$name]; + } + + /** + * Sets a setting value. + * + * @param string $name + * @param mixed $value + */ + public function set($name, $value) + { + $this->mergedSettings[$name] = $value; + } + + /** + * Returns all settings. Changes made to the array result will be reflected in the + * IniFileChain instance. + * + * @return array + */ + public function &getAll() + { + return $this->mergedSettings; + } + + /** + * Dumps the current in-memory setting values to a string in INI format and returns it. + * + * @param string $header The header of the output INI file. + * @return string The dumped INI contents. + */ + public function dump($header = '') + { + return $this->dumpSettings($this->mergedSettings, $header); + } + + /** + * Writes the difference of the in-memory setting values and the on-disk user settings file setting + * values to a string in INI format, and returns it. + * + * If a config section is identical to the default settings section (as computed by merging + * all default setting files), it is not written to the user settings file. + * + * @param string $header The header of the INI output. + * @return string The dumped INI contents. + */ + public function dumpChanges($header = '') + { + $userSettingsFile = $this->getUserSettingsFile(); + + $defaultSettings = $this->getMergedDefaultSettings(); + $existingMutableSettings = $this->settingsChain[$userSettingsFile]; + + $dirty = false; + + $configToWrite = array(); + foreach ($this->mergedSettings as $sectionName => $changedSection) { + if(isset($existingMutableSettings[$sectionName])){ + $existingMutableSection = $existingMutableSettings[$sectionName]; + } else{ + $existingMutableSection = array(); + } + + // remove default values from both (they should not get written to local) + if (isset($defaultSettings[$sectionName])) { + $changedSection = $this->arrayUnmerge($defaultSettings[$sectionName], $changedSection); + $existingMutableSection = $this->arrayUnmerge($defaultSettings[$sectionName], $existingMutableSection); + } + + // if either local/config have non-default values and the other doesn't, + // OR both have values, but different values, we must write to config.ini.php + if (empty($changedSection) xor empty($existingMutableSection) + || (!empty($changedSection) + && !empty($existingMutableSection) + && self::compareElements($changedSection, $existingMutableSection)) + ) { + $dirty = true; + } + + $configToWrite[$sectionName] = $changedSection; + } + + if ($dirty) { + // sort config sections by how early they appear in the file chain + $self = $this; + uksort($configToWrite, function ($sectionNameLhs, $sectionNameRhs) use ($self) { + $lhsIndex = $self->findIndexOfFirstFileWithSection($sectionNameLhs); + $rhsIndex = $self->findIndexOfFirstFileWithSection($sectionNameRhs); + + if ($lhsIndex == $rhsIndex) { + $lhsIndexInFile = $self->getIndexOfSectionInFile($lhsIndex, $sectionNameLhs); + $rhsIndexInFile = $self->getIndexOfSectionInFile($rhsIndex, $sectionNameRhs); + + if ($lhsIndexInFile == $rhsIndexInFile) { + return 0; + } elseif ($lhsIndexInFile < $rhsIndexInFile) { + return -1; + } else { + return 1; + } + } elseif ($lhsIndex < $rhsIndex) { + return -1; + } else { + return 1; + } + }); + + return $this->dumpSettings($configToWrite, $header); + } else { + return null; + } + } + + /** + * Reloads settings from disk. + */ + public function reload($defaultSettingsFiles = array(), $userSettingsFile = null) + { + if (!empty($defaultSettingsFiles) + || !empty($userSettingsFile) + ) { + $this->resetSettingsChain($defaultSettingsFiles, $userSettingsFile); + } + + $reader = new IniReader(); + foreach ($this->settingsChain as $file => $ignore) { + if (is_readable($file)) { + try { + $contents = $reader->readFile($file); + $this->settingsChain[$file] = $this->decodeValues($contents); + } catch (IniReadingException $ex) { + throw new IniReadingException('Unable to read INI file {' . $file . '}: ' . $ex->getMessage() . "\n Your host may have disabled parse_ini_file()."); + } + + $this->decodeValues($this->settingsChain[$file]); + } + } + + $merged = $this->mergeFileSettings(); + // remove reference to $this->settingsChain... otherwise dump() or compareElements() will never notice a difference + // on PHP 7+ as they would be always equal + $this->mergedSettings = $this->copy($merged); + } + + private function copy($merged) + { + $copy = array(); + foreach ($merged as $index => $value) { + if (is_array($value)) { + $copy[$index] = $this->copy($value); + } else { + $copy[$index] = $value; + } + } + return $copy; + } + + private function resetSettingsChain($defaultSettingsFiles, $userSettingsFile) + { + $this->settingsChain = array(); + + if (!empty($defaultSettingsFiles)) { + foreach ($defaultSettingsFiles as $file) { + $this->settingsChain[$file] = null; + } + } + + if (!empty($userSettingsFile)) { + $this->settingsChain[$userSettingsFile] = null; + } + } + + protected function mergeFileSettings() + { + $mergedSettings = $this->getMergedDefaultSettings(); + + $userSettings = end($this->settingsChain) ?: array(); + foreach ($userSettings as $sectionName => $section) { + if (!isset($mergedSettings[$sectionName])) { + $mergedSettings[$sectionName] = $section; + } else { + // the last user settings file completely overwrites INI sections. the other files in the chain + // can add to array options + $mergedSettings[$sectionName] = array_merge($mergedSettings[$sectionName], $section); + } + } + + return $mergedSettings; + } + + protected function getMergedDefaultSettings() + { + $userSettingsFile = $this->getUserSettingsFile(); + + $mergedSettings = array(); + foreach ($this->settingsChain as $file => $settings) { + if ($file == $userSettingsFile + || empty($settings) + ) { + continue; + } + + foreach ($settings as $sectionName => $section) { + if (!isset($mergedSettings[$sectionName])) { + $mergedSettings[$sectionName] = $section; + } else { + $mergedSettings[$sectionName] = $this->array_merge_recursive_distinct($mergedSettings[$sectionName], $section); + } + } + } + return $mergedSettings; + } + + protected function getUserSettingsFile() + { + // the user settings file is the last key in $settingsChain + end($this->settingsChain); + return key($this->settingsChain); + } + + /** + * Comparison function + * + * @param mixed $elem1 + * @param mixed $elem2 + * @return int; + */ + public static function compareElements($elem1, $elem2) + { + if (is_array($elem1)) { + if (is_array($elem2)) { + return strcmp(serialize($elem1), serialize($elem2)); + } + + return 1; + } + + if (is_array($elem2)) { + return -1; + } + + if ((string)$elem1 === (string)$elem2) { + return 0; + } + + return ((string)$elem1 > (string)$elem2) ? 1 : -1; + } + + /** + * Compare arrays and return difference, such that: + * + * $modified = array_merge($original, $difference); + * + * @param array $original original array + * @param array $modified modified array + * @return array differences between original and modified + */ + public function arrayUnmerge($original, $modified) + { + // return key/value pairs for keys in $modified but not in $original + // return key/value pairs for keys in both $modified and $original, but values differ + // ignore keys that are in $original but not in $modified + + return array_udiff_assoc($modified, $original, array(__CLASS__, 'compareElements')); + } + + /** + * array_merge_recursive does indeed merge arrays, but it converts values with duplicate + * keys to arrays rather than overwriting the value in the first array with the duplicate + * value in the second array, as array_merge does. I.e., with array_merge_recursive, + * this happens (documented behavior): + * + * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value')); + * => array('key' => array('org value', 'new value')); + * + * array_merge_recursive_distinct does not change the datatypes of the values in the arrays. + * Matching keys' values in the second array overwrite those in the first array, as is the + * case with array_merge, i.e.: + * + * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value')); + * => array('key' => array('new value')); + * + * Parameters are passed by reference, though only for performance reasons. They're not + * altered by this function. + * + * @param array $array1 + * @param array $array2 + * @return array + * @author Daniel + * @author Gabriel Sobrinho + */ + private function array_merge_recursive_distinct(array &$array1, array &$array2) + { + $merged = $array1; + foreach ($array2 as $key => &$value) { + if (is_array($value) && isset($merged [$key]) && is_array($merged [$key])) { + $merged [$key] = $this->array_merge_recursive_distinct($merged [$key], $value); + } else { + $merged [$key] = $value; + } + } + return $merged; + } + + /** + * public for use in closure. + */ + public function findIndexOfFirstFileWithSection($sectionName) + { + $count = 0; + foreach ($this->settingsChain as $file => $settings) { + if (isset($settings[$sectionName])) { + break; + } + + ++$count; + } + return $count; + } + + /** + * public for use in closure. + */ + public function getIndexOfSectionInFile($fileIndex, $sectionName) + { + reset($this->settingsChain); + for ($i = 0; $i != $fileIndex; ++$i) { + next($this->settingsChain); + } + + $settingsData = current($this->settingsChain); + if (empty($settingsData)) { + return -1; + } + + $settingsDataSectionNames = array_keys($settingsData); + + return array_search($sectionName, $settingsDataSectionNames); + } + + /** + * Encode HTML entities + * + * @param mixed $values + * @return mixed + */ + protected function encodeValues(&$values) + { + if (is_array($values)) { + foreach ($values as &$value) { + $value = $this->encodeValues($value); + } + } elseif (is_float($values)) { + $values = Common::forceDotAsSeparatorForDecimalPoint($values); + } elseif (is_string($values)) { + $values = htmlentities($values, ENT_COMPAT, 'UTF-8'); + $values = str_replace('$', '$', $values); + } + return $values; + } + + /** + * Decode HTML entities + * + * @param mixed $values + * @return mixed + */ + protected function decodeValues(&$values) + { + if (is_array($values)) { + foreach ($values as &$value) { + $value = $this->decodeValues($value); + } + return $values; + } elseif (is_string($values)) { + return html_entity_decode($values, ENT_COMPAT, 'UTF-8'); + } + return $values; + } + + private function dumpSettings($values, $header) + { + $values = $this->encodeValues($values); + + $writer = new IniWriter(); + return $writer->writeToString($values, $header); + } +} diff --git a/www/analytics/core/Console.php b/www/analytics/core/Console.php index cdeb6a08..6b0c5929 100644 --- a/www/analytics/core/Console.php +++ b/www/analytics/core/Console.php @@ -1,6 +1,6 @@ setServerArgsIfPhpCgi(); + parent::__construct(); + $this->environment = $environment; + $option = new InputOption('piwik-domain', null, InputOption::VALUE_OPTIONAL, @@ -28,42 +41,60 @@ class Console extends Application ); $this->getDefinition()->addOption($option); - } - /** - * @deprecated - */ - public function init() - { - // TODO: remove + $option = new InputOption('xhprof', + null, + InputOption::VALUE_NONE, + 'Enable profiling with XHProf' + ); + + $this->getDefinition()->addOption($option); } public function doRun(InputInterface $input, OutputInterface $output) { - $this->initPiwikHost($input); - $this->initConfig($output); - try { - self::initPlugins(); - } catch(\Exception $e) { - // Piwik not installed yet, no config file? + if ($input->hasParameterOption('--xhprof')) { + Profiler::setupProfilerXHProf(true, true); } - Translate::reloadLanguage('en'); + $this->initPiwikHost($input); + $this->initEnvironment($output); + $this->initLoggerOutput($output); + + try { + self::initPlugins(); + } catch (ConfigNotFoundException $e) { + // Piwik not installed yet, no config file? + Log::warning($e->getMessage()); + } $commands = $this->getAvailableCommands(); foreach ($commands as $command) { - - if (!class_exists($command)) { - Log::warning(sprintf('Cannot add command %s, class does not exist', $command)); - } elseif (!is_subclass_of($command, 'Piwik\Plugin\ConsoleCommand')) { - Log::warning(sprintf('Cannot add command %s, class does not extend Piwik\Plugin\ConsoleCommand', $command)); - } else { - $this->add(new $command); - } + $this->addCommandIfExists($command); } - return parent::doRun($input, $output); + $self = $this; + return Access::doAsSuperUser(function () use ($input, $output, $self) { + return call_user_func(array($self, 'Symfony\Component\Console\Application::doRun'), $input, $output); + }); + } + + private function addCommandIfExists($command) + { + if (!class_exists($command)) { + Log::warning(sprintf('Cannot add command %s, class does not exist', $command)); + } elseif (!is_subclass_of($command, 'Piwik\Plugin\ConsoleCommand')) { + Log::warning(sprintf('Cannot add command %s, class does not extend Piwik\Plugin\ConsoleCommand', $command)); + } else { + /** @var Command $commandInstance */ + $commandInstance = new $command; + + // do not add the command if it already exists; this way we can add the command ourselves in tests + if (!$this->has($commandInstance->getName())) { + $this->add($commandInstance); + } + } } /** @@ -74,11 +105,9 @@ class Console extends Application private function getAvailableCommands() { $commands = $this->getDefaultPiwikCommands(); + $detected = PluginManager::getInstance()->findMultipleComponents('Commands', 'Piwik\\Plugin\\ConsoleCommand'); - $pluginNames = PluginManager::getInstance()->getLoadedPluginsName(); - foreach ($pluginNames as $pluginName) { - $commands = array_merge($commands, $this->findCommandsInPlugin($pluginName)); - } + $commands = array_merge($commands, $detected); /** * Triggered to filter / restrict console commands. Plugins that want to restrict commands @@ -103,52 +132,76 @@ class Console extends Application return $commands; } - private function findCommandsInPlugin($pluginName) + private function setServerArgsIfPhpCgi() { - $commands = array(); + if (Common::isPhpCgiType()) { + $_SERVER['argv'] = array(); + foreach ($_GET as $name => $value) { + $argument = $name; + if (!empty($value)) { + $argument .= '=' . $value; + } - $files = Filesystem::globr(PIWIK_INCLUDE_PATH . '/plugins/' . $pluginName .'/Commands', '*.php'); - - foreach ($files as $file) { - $klassName = sprintf('Piwik\\Plugins\\%s\\Commands\\%s', $pluginName, basename($file, '.php')); - - if (!class_exists($klassName) || !is_subclass_of($klassName, 'Piwik\\Plugin\\ConsoleCommand')) { - continue; + $_SERVER['argv'][] = $argument; } - $klass = new \ReflectionClass($klassName); - - if ($klass->isAbstract()) { - continue; + if (!defined('STDIN')) { + define('STDIN', fopen('php://stdin', 'r')); } - - $commands[] = $klassName; } + } - return $commands; + public static function isSupported() + { + return Common::isPhpCliMode() && !Common::isPhpCgiType(); } protected function initPiwikHost(InputInterface $input) { $piwikHostname = $input->getParameterOption('--piwik-domain'); + + if (empty($piwikHostname)) { + $piwikHostname = $input->getParameterOption('--url'); + } + $piwikHostname = UrlHelper::getHostFromUrl($piwikHostname); Url::setHost($piwikHostname); } - protected function initConfig(OutputInterface $output) + protected function initEnvironment(OutputInterface $output) { - $config = Config::getInstance(); try { - $config->checkLocalConfigFound(); + if ($this->environment === null) { + $this->environment = new Environment('cli'); + $this->environment->init(); + } + + $config = Config::getInstance(); return $config; } catch (\Exception $e) { $output->writeln($e->getMessage() . "\n"); } } + /** + * Register the console output into the logger. + * + * Ideally, this should be done automatically with events: + * @see http://symfony.com/fr/doc/current/components/console/events.html + * @see Symfony\Bridge\Monolog\Handler\ConsoleHandler::onCommand() + * But it would require to install Symfony's Event Dispatcher. + */ + private function initLoggerOutput(OutputInterface $output) + { + /** @var ConsoleHandler $consoleLogHandler */ + $consoleLogHandler = StaticContainer::get('Symfony\Bridge\Monolog\Handler\ConsoleHandler'); + $consoleLogHandler->setOutput($output); + } + public static function initPlugins() { Plugin\Manager::getInstance()->loadActivatedPlugins(); + Plugin\Manager::getInstance()->loadPluginTranslations(); } private function getDefaultPiwikCommands() @@ -156,11 +209,12 @@ class Console extends Application $commands = array( 'Piwik\CliMulti\RequestCommand' ); - + if (class_exists('Piwik\Plugins\EnterpriseAdmin\EnterpriseAdmin')) { $extra = new \Piwik\Plugins\EnterpriseAdmin\EnterpriseAdmin(); $extra->addConsoleCommands($commands); } + return $commands; } } diff --git a/www/analytics/core/Container/ContainerDoesNotExistException.php b/www/analytics/core/Container/ContainerDoesNotExistException.php new file mode 100644 index 00000000..4d1d33ed --- /dev/null +++ b/www/analytics/core/Container/ContainerDoesNotExistException.php @@ -0,0 +1,18 @@ +pluginList = $pluginList; + $this->settings = $settings; + $this->environments = $environments; + $this->definitions = $definitions; + } + + /** + * @link http://php-di.org/doc/container-configuration.html + * @throws \Exception + * @return Container + */ + public function create() + { + $builder = new ContainerBuilder(); + + $builder->useAnnotations(false); + $builder->setDefinitionCache(new ArrayCache()); + + // INI config + $builder->addDefinitions(new IniConfigDefinitionSource($this->settings)); + + // Global config + $builder->addDefinitions(PIWIK_USER_PATH . '/config/global.php'); + + // Plugin configs + $this->addPluginConfigs($builder); + + // Development config + if ($this->isDevelopmentModeEnabled()) { + $this->addEnvironmentConfig($builder, 'dev'); + } + + // Environment config + foreach ($this->environments as $environment) { + $this->addEnvironmentConfig($builder, $environment); + } + + // User config + if (file_exists(PIWIK_USER_PATH . '/config/config.php')) { + $builder->addDefinitions(PIWIK_USER_PATH . '/config/config.php'); + } + + if (!empty($this->definitions)) { + foreach ($this->definitions as $definitionArray) { + $builder->addDefinitions($definitionArray); + } + } + + $container = $builder->build(); + $container->set('Piwik\Application\Kernel\PluginList', $this->pluginList); + $container->set('Piwik\Application\Kernel\GlobalSettingsProvider', $this->settings); + + return $container; + } + + private function addEnvironmentConfig(ContainerBuilder $builder, $environment) + { + if (!$environment) { + return; + } + + $file = sprintf('%s/config/environment/%s.php', PIWIK_USER_PATH, $environment); + + if (file_exists($file)) { + $builder->addDefinitions($file); + } + + // add plugin environment configs + $plugins = $this->pluginList->getActivatedPlugins(); + foreach ($plugins as $plugin) { + $baseDir = Manager::getPluginsDirectory() . $plugin; + + $environmentFile = $baseDir . '/config/' . $environment . '.php'; + if (file_exists($environmentFile)) { + $builder->addDefinitions($environmentFile); + } + } + } + + private function addPluginConfigs(ContainerBuilder $builder) + { + $plugins = $this->pluginList->getActivatedPlugins(); + + foreach ($plugins as $plugin) { + $baseDir = Manager::getPluginsDirectory() . $plugin; + + $file = $baseDir . '/config/config.php'; + if (file_exists($file)) { + $builder->addDefinitions($file); + } + } + } + + private function isDevelopmentModeEnabled() + { + $section = $this->settings->getSection('Development'); + return (bool) @$section['enabled']; // TODO: code redundancy w/ Development. hopefully ok for now. + } +} diff --git a/www/analytics/core/Container/IniConfigDefinitionSource.php b/www/analytics/core/Container/IniConfigDefinitionSource.php new file mode 100644 index 00000000..4b56d1aa --- /dev/null +++ b/www/analytics/core/Container/IniConfigDefinitionSource.php @@ -0,0 +1,95 @@ +get('ini.General.maintenance_mode'); + */ +class IniConfigDefinitionSource implements DefinitionSource +{ + /** + * @var GlobalSettingsProvider + */ + private $config; + + /** + * @var string + */ + private $prefix; + + /** + * @param GlobalSettingsProvider $config + * @param string $prefix Prefix for the container entries. + */ + public function __construct(GlobalSettingsProvider $config, $prefix = 'ini.') + { + $this->config = $config; + $this->prefix = $prefix; + } + + /** + * {@inheritdoc} + */ + public function getDefinition($name) + { + if (strpos($name, $this->prefix) !== 0) { + return null; + } + + list($sectionName, $configKey) = $this->parseEntryName($name); + + $section = $this->getSection($sectionName); + + if ($configKey === null) { + return new ValueDefinition($name, $section); + } + + if (! array_key_exists($configKey, $section)) { + return null; + } + + return new ValueDefinition($name, $section[$configKey]); + } + + private function parseEntryName($name) + { + $parts = explode('.', $name, 3); + + array_shift($parts); + + if (! isset($parts[1])) { + $parts[1] = null; + } + + return $parts; + } + + private function getSection($sectionName) + { + $section = $this->config->getSection($sectionName); + + if (!is_array($section)) { + throw new DefinitionException(sprintf( + 'IniFileChain did not return an array for the config section %s', + $section + )); + } + + return $section; + } +} diff --git a/www/analytics/core/Container/StaticContainer.php b/www/analytics/core/Container/StaticContainer.php new file mode 100644 index 00000000..68246716 --- /dev/null +++ b/www/analytics/core/Container/StaticContainer.php @@ -0,0 +1,87 @@ +get($name); + } + + public static function getDefinitions() + { + return self::$definitions; + } +} diff --git a/www/analytics/core/Cookie.php b/www/analytics/core/Cookie.php index 08db3580..24f8a3f6 100644 --- a/www/analytics/core/Cookie.php +++ b/www/analytics/core/Cookie.php @@ -1,6 +1,6 @@ extractSignedContent($_COOKIE[$this->name]); + if ($cookieStr === false) { return; } @@ -255,6 +260,7 @@ class Cookie protected function generateContentString() { $cookieStr = ''; + foreach ($this->value as $name => $value) { if (!is_numeric($value)) { $value = base64_encode(safe_serialize($value)); @@ -335,6 +341,7 @@ class Cookie $this->value[$name] = $value; return; } + $this->value[$this->keyStore][$name] = $value; } @@ -347,14 +354,19 @@ class Cookie public function get($name) { $name = self::escapeValue($name); - if ($this->keyStore === false) { - return isset($this->value[$name]) - ? self::escapeValue($this->value[$name]) - : false; + if (false === $this->keyStore) { + if (isset($this->value[$name])) { + return self::escapeValue($this->value[$name]); + } + + return false; } - return isset($this->value[$this->keyStore][$name]) - ? self::escapeValue($this->value[$this->keyStore][$name]) - : false; + + if (isset($this->value[$this->keyStore][$name])) { + return self::escapeValue($this->value[$this->keyStore][$name]); + } + + return false; } /** @@ -364,8 +376,10 @@ class Cookie */ public function __toString() { - $str = 'COOKIE ' . $this->name . ', rows count: ' . count($this->value) . ', cookie size = ' . strlen($this->generateContentString()) . " bytes\n"; + $str = 'COOKIE ' . $this->name . ', rows count: ' . count($this->value) . ', cookie size = ' . strlen($this->generateContentString()) . " bytes, "; + $str .= 'path: ' . $this->path. ', expire: ' . $this->expire . "\n"; $str .= var_export($this->value, $return = true); + return $str; } diff --git a/www/analytics/core/CronArchive.php b/www/analytics/core/CronArchive.php index 8d5ff009..28ddf49c 100644 --- a/www/analytics/core/CronArchive.php +++ b/www/analytics/core/CronArchive.php @@ -1,6 +1,6 @@ logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface'); + $this->formatter = new Formatter(); + + $processNewSegmentsFrom = $processNewSegmentsFrom ?: StaticContainer::get('ini.General.process_new_segments_from'); + $this->segmentArchivingRequestUrlProvider = new SegmentArchivingRequestUrlProvider($processNewSegmentsFrom); + + $this->invalidator = StaticContainer::get('Piwik\Archive\ArchiveInvalidator'); } /** @@ -100,81 +282,67 @@ class CronArchive */ public function main() { - $this->init(); - $this->run(); - $this->runScheduledTasks(); - $this->end(); + $self = $this; + Access::doAsSuperUser(function () use ($self) { + $self->init(); + $self->run(); + $self->runScheduledTasks(); + $self->end(); + }); } public function init() { + /** + * This event is triggered during initializing archiving. + * + * @param CronArchive $this + */ + Piwik::postEvent('CronArchive.init.start', array($this)); + + SettingsServer::setMaxExecutionTime(0); + + $this->archivingStartingTime = time(); + // Note: the order of methods call matters here. - $this->initLog(); - $this->initPiwikHost(); - $this->initCore(); - $this->initTokenAuth(); - $this->initCheckCli(); $this->initStateFromParameters(); - Piwik::setUserHasSuperUserAccess(true); $this->logInitInfo(); - $this->checkPiwikUrlIsValid(); $this->logArchiveTimeoutInfo(); // record archiving start time Option::set(self::OPTION_ARCHIVING_STARTED_TS, time()); - $this->segments = $this->initSegmentsToArchive(); + $this->segments = $this->initSegmentsToArchive(); $this->allWebsites = APISitesManager::getInstance()->getAllSitesId(); + if (!empty($this->shouldArchiveOnlySpecificPeriods)) { + $this->logger->info("- Will only process the following periods: " . implode(", ", $this->shouldArchiveOnlySpecificPeriods) . " (--force-periods)"); + } + + $this->invalidateArchivedReportsForSitesThatNeedToBeArchivedAgain(); + $websitesIds = $this->initWebsiteIds(); $this->filterWebsiteIds($websitesIds); - if (!empty($this->shouldArchiveSpecifiedSites) - || !empty($this->shouldArchiveAllSites) - || !SharedSiteIds::isSupported()) { - $this->websites = new FixedSiteIds($websitesIds); - } else { - $this->websites = new SharedSiteIds($websitesIds); - if ($this->websites->getInitialSiteIds() != $websitesIds) { - $this->log('Will ignore websites and help finish a previous started queue instead. IDs: ' . implode(',', $this->websites->getInitialSiteIds())); - } + $this->websites = $this->createSitesToArchiveQueue($websitesIds); + + if ($this->websites->getInitialSiteIds() != $websitesIds) { + $this->logger->info('Will ignore websites and help finish a previous started queue instead. IDs: ' . implode(', ', $this->websites->getInitialSiteIds())); } - if ($this->shouldStartProfiler) { - \Piwik\Profiler::setupProfilerXHProf($mainRun = true); - $this->log("XHProf profiling is enabled."); - } + $this->logForcedSegmentInfo(); /** * This event is triggered after a CronArchive instance is initialized. * * @param array $websiteIds The list of website IDs this CronArchive instance is processing. - * This will be the enitre list of IDs regardless of whether some have + * This will be the entire list of IDs regardless of whether some have * already been processed. */ Piwik::postEvent('CronArchive.init.finish', array($this->websites->getInitialSiteIds())); } - public function runScheduledTasksInTrackerMode() - { - $this->initPiwikHost(); - $this->initLog(); - $this->initCore(); - $this->initTokenAuth(); - $this->logInitInfo(); - $this->checkPiwikUrlIsValid(); - $this->runScheduledTasks(); - } - - // TODO: replace w/ $this-> - private $websitesWithVisitsSinceLastRun = 0; - private $skippedPeriodsArchivesWebsite = 0; - private $skippedDayArchivesWebsites = 0; - private $skipped = 0; - private $processed = 0; - private $archivedPeriodsArchivesWebsite = 0; - /** * Main function, runs archiving on all websites with new activity */ @@ -183,37 +351,66 @@ class CronArchive $timer = new Timer; $this->logSection("START"); - $this->log("Starting Piwik reports archiving..."); + $this->logger->info("Starting Piwik reports archiving..."); do { - $idsite = $this->websites->getNextSiteId(); + $idSite = $this->websites->getNextSiteId(); - if (null === $idsite) { + if (null === $idSite) { break; } flush(); $requestsBefore = $this->requests; - if ($idsite <= 0) { + if ($idSite <= 0) { continue; } - $skipWebsiteForced = in_array($idsite, $this->shouldSkipSpecifiedSites); - if($skipWebsiteForced) { - $this->log("Skipped website id $idsite, found in --skip-idsites "); + $skipWebsiteForced = in_array($idSite, $this->shouldSkipSpecifiedSites); + if ($skipWebsiteForced) { + $this->logger->info("Skipped website id $idSite, found in --skip-idsites "); $this->skipped++; continue; } + $shouldCheckIfArchivingIsNeeded = !$this->shouldArchiveSpecifiedSites && !$this->shouldArchiveAllSites && !$this->dateLastForced; + $hasWebsiteDayFinishedSinceLastRun = in_array($idSite, $this->websiteDayHasFinishedSinceLastRun); + $isOldReportInvalidatedForWebsite = $this->isOldReportInvalidatedForWebsite($idSite); + + if ($shouldCheckIfArchivingIsNeeded) { + // if not specific sites and not all websites should be archived, we check whether we actually have + // to process the archives for this website (only if there were visits since midnight) + if (!$hasWebsiteDayFinishedSinceLastRun && !$isOldReportInvalidatedForWebsite) { + + if ($this->isWebsiteUsingTheTracker($idSite)) { + + if(!$this->hadWebsiteTrafficSinceMidnightInTimezone($idSite)) { + $this->logger->info("Skipped website id $idSite as archiving is not needed"); + + $this->skippedDayNoRecentData++; + $this->skipped++; + continue; + } + } else { + $this->logger->info("- website id $idSite is not using the tracker"); + } + + } elseif ($hasWebsiteDayFinishedSinceLastRun) { + $this->logger->info("Day has finished for website id $idSite since last run"); + } elseif ($isOldReportInvalidatedForWebsite) { + $this->logger->info("Old report was invalidated for website id $idSite"); + } + } + /** * This event is triggered before the cron archiving process starts archiving data for a single * site. * * @param int $idSite The ID of the site we're archiving data for. */ - Piwik::postEvent('CronArchive.archiveSingleSite.start', array($idsite)); + Piwik::postEvent('CronArchive.archiveSingleSite.start', array($idSite)); - $completed = $this->archiveSingleSite($idsite, $requestsBefore); + $completed = $this->archiveSingleSite($idSite, $requestsBefore); /** * This event is triggered immediately after the cron archiving process starts archiving data for a single @@ -221,36 +418,46 @@ class CronArchive * * @param int $idSite The ID of the site we're archiving data for. */ - Piwik::postEvent('CronArchive.archiveSingleSite.finish', array($idsite, $completed)); - } while (!empty($idsite)); + Piwik::postEvent('CronArchive.archiveSingleSite.finish', array($idSite, $completed)); + } while (!empty($idSite)); - $this->log("Done archiving!"); + $this->logger->info("Done archiving!"); $this->logSection("SUMMARY"); - $this->log("Total visits for today across archived websites: " . $this->visitsToday); + $this->logger->info("Total visits for today across archived websites: " . $this->visitsToday); $totalWebsites = count($this->allWebsites); $this->skipped = $totalWebsites - $this->websitesWithVisitsSinceLastRun; - $this->log("Archived today's reports for {$this->websitesWithVisitsSinceLastRun} websites"); - $this->log("Archived week/month/year for {$this->archivedPeriodsArchivesWebsite} websites"); - $this->log("Skipped {$this->skipped} websites: no new visit since the last script execution"); - $this->log("Skipped {$this->skippedDayArchivesWebsites} websites day archiving: existing daily reports are less than {$this->todayArchiveTimeToLive} seconds old"); - $this->log("Skipped {$this->skippedPeriodsArchivesWebsite} websites week/month/year archiving: existing periods reports are less than {$this->processPeriodsMaximumEverySeconds} seconds old"); - $this->log("Total API requests: {$this->requests}"); + $this->logger->info("Archived today's reports for {$this->websitesWithVisitsSinceLastRun} websites"); + $this->logger->info("Archived week/month/year for {$this->archivedPeriodsArchivesWebsite} websites"); + $this->logger->info("Skipped {$this->skipped} websites"); + $this->logger->info("- {$this->skippedDayNoRecentData} skipped because no new visit since the last script execution"); + $this->logger->info("- {$this->skippedDayArchivesWebsites} skipped because existing daily reports are less than {$this->todayArchiveTimeToLive} seconds old"); + $this->logger->info("- {$this->skippedPeriodsArchivesWebsite} skipped because existing week/month/year periods reports are less than {$this->processPeriodsMaximumEverySeconds} seconds old"); + + if($this->skippedPeriodsNoDataInPeriod) { + $this->logger->info("- {$this->skippedPeriodsNoDataInPeriod} skipped periods archiving because no visit in recent days"); + } + + if($this->skippedDayOnApiError) { + $this->logger->info("- {$this->skippedDayOnApiError} skipped because got an error while querying reporting API"); + } + $this->logger->info("Total API requests: {$this->requests}"); //DONE: done/total, visits, wtoday, wperiods, reqs, time, errors[count]: first eg. $percent = $this->websites->getNumSites() == 0 ? "" : " " . round($this->processed * 100 / $this->websites->getNumSites(), 0) . "%"; - $this->log("done: " . + $this->logger->info("done: " . $this->processed . "/" . $this->websites->getNumSites() . "" . $percent . ", " . $this->visitsToday . " vtoday, $this->websitesWithVisitsSinceLastRun wtoday, {$this->archivedPeriodsArchivesWebsite} wperiods, " . $this->requests . " req, " . round($timer->getTimeMs()) . " ms, " . (empty($this->errors) - ? "no error" - : (count($this->errors) . " errors. eg. '" . reset($this->errors) . "'")) + ? self::NO_ERROR + : (count($this->errors) . " errors.")) ); - $this->log($timer->__toString()); + + $this->logger->info($timer->__toString()); } /** @@ -258,67 +465,90 @@ class CronArchive */ public function end() { - // How to test the error handling code? - // - Generate some hits since last archive.php run - // - Start the script, in the middle, shutdown apache, then restore - // Some errors should be logged and script should successfully finish and then report the errors and trigger a PHP error - if (!empty($this->errors)) { - $this->logSection("SUMMARY OF ERRORS"); + /** + * This event is triggered after archiving. + * + * @param CronArchive $this + */ + Piwik::postEvent('CronArchive.end', array($this)); - foreach ($this->errors as $error) { - $this->log("Error: " . $error); - } - $summary = count($this->errors) . " total errors during this script execution, please investigate and try and fix these errors"; - $this->log($summary); - - $summary .= '. First error was: ' . reset($this->errors); - $this->logFatalError($summary); - } else { + if (empty($this->errors)) { // No error -> Logs the successful script execution until completion Option::set(self::OPTION_ARCHIVING_FINISHED_TS, time()); + return; } + + $this->logSection("SUMMARY OF ERRORS"); + foreach ($this->errors as $error) { + // do not logError since errors are already in stderr + $this->logger->info("Error: " . $error); + } + + $summary = count($this->errors) . " total errors during this script execution, please investigate and try and fix these errors."; + $this->logFatalError($summary); } - public function logFatalError($m, $backtrace = true) + public function logFatalError($m) { - throw new CronArchiveFatalException($m, $backtrace ? $this->output : false); + $this->logError($m); + + throw new Exception($m); } - public function logFatalExceptionAndExit($ex, $backtrace = true) + /** + * @param int[] $idSegments + */ + public function setSegmentsToForceFromSegmentIds($idSegments) { - $wrapped = new CronArchiveFatalException($ex->getMessage(), $backtrace ? $this->output : false); - $wrapped->logAndExit($this); + /** @var SegmentEditorModel $segmentEditorModel */ + $segmentEditorModel = StaticContainer::get('Piwik\Plugins\SegmentEditor\Model'); + $segments = $segmentEditorModel->getAllSegmentsAndIgnoreVisibility(); + + $segments = array_filter($segments, function ($segment) use ($idSegments) { + return in_array($segment['idsegment'], $idSegments); + }); + + $segments = array_map(function ($segment) { + return $segment['definition']; + }, $segments); + + $this->segmentsToForce = $segments; } public function runScheduledTasks() { $this->logSection("SCHEDULED TASKS"); - if($this->getParameterFromCli('--disable-scheduled-tasks')) { - $this->log("Scheduled tasks are disabled with --disable-scheduled-tasks"); + + if ($this->disableScheduledTasks) { + $this->logger->info("Scheduled tasks are disabled with --disable-scheduled-tasks"); return; } - $this->log("Starting Scheduled tasks... "); - $tasksOutput = $this->request("?module=API&method=CoreAdminHome.runScheduledTasks&format=csv&convertToUnicode=0&token_auth=" . $this->token_auth); - if ($tasksOutput == \Piwik\DataTable\Renderer\Csv::NO_DATA_AVAILABLE) { - $tasksOutput = " No task to run"; - } - $this->log($tasksOutput); - $this->log("done"); + // TODO: this is a HACK to get the purgeOutdatedArchives task to work when run below. without + // it, the task will not run because we no longer run the tasks through CliMulti. + // harder to implement alternatives include: + // - moving CronArchive logic to DI and setting a flag in the class when the whole process + // runs + // - setting a new DI environment for core:archive which CoreAdminHome can use to conditionally + // enable/disable the task + $_GET['trigger'] = 'archivephp'; + CoreAdminHomeAPI::getInstance()->runScheduledTasks(); + $this->logSection(""); } - private function archiveSingleSite($idsite, $requestsBefore) + private function archiveSingleSite($idSite, $requestsBefore) { $timerWebsite = new Timer; $lastTimestampWebsiteProcessedPeriods = $lastTimestampWebsiteProcessedDay = false; - if ($this->archiveAndRespectTTL) { - Option::clearCachedOption($this->lastRunKey($idsite, "periods")); - $lastTimestampWebsiteProcessedPeriods = Option::get($this->lastRunKey($idsite, "periods")); - Option::clearCachedOption($this->lastRunKey($idsite, "day")); - $lastTimestampWebsiteProcessedDay = Option::get($this->lastRunKey($idsite, "day")); + if ($this->archiveAndRespectTTL) { + Option::clearCachedOption($this->lastRunKey($idSite, "periods")); + $lastTimestampWebsiteProcessedPeriods = $this->getPeriodLastProcessedTimestamp($idSite); + + Option::clearCachedOption($this->lastRunKey($idSite, "day")); + $lastTimestampWebsiteProcessedDay = $this->getDayLastProcessedTimestamp($idSite); } $this->updateIdSitesInvalidatedOldReports(); @@ -331,6 +561,7 @@ class CronArchive if ($this->processPeriodsMaximumEverySeconds > 10 * 60) { $secondsSinceLastExecution += 5 * 60; } + $shouldArchivePeriods = $secondsSinceLastExecution > $this->processPeriodsMaximumEverySeconds; if (empty($lastTimestampWebsiteProcessedPeriods)) { // 2) OR always if script never executed for this website before @@ -339,21 +570,21 @@ class CronArchive // (*) If the website is archived because it is a new day in its timezone // We make sure all periods are archived, even if there is 0 visit today - $dayHasEndedMustReprocess = in_array($idsite, $this->websiteDayHasFinishedSinceLastRun); + $dayHasEndedMustReprocess = in_array($idSite, $this->websiteDayHasFinishedSinceLastRun); if ($dayHasEndedMustReprocess) { $shouldArchivePeriods = true; } // (*) If there was some old reports invalidated for this website // we make sure all these old reports are triggered at least once - $websiteIsOldDataInvalidate = in_array($idsite, $this->idSitesInvalidatedOldReports); + $websiteInvalidatedShouldReprocess = $this->isOldReportInvalidatedForWebsite($idSite); - if ($websiteIsOldDataInvalidate) { + if ($websiteInvalidatedShouldReprocess) { $shouldArchivePeriods = true; } - $websiteIdIsForced = in_array($idsite, $this->shouldArchiveSpecifiedSites); - if($websiteIdIsForced) { + $websiteIdIsForced = in_array($idSite, $this->shouldArchiveSpecifiedSites); + if ($websiteIdIsForced) { $shouldArchivePeriods = true; } @@ -366,12 +597,12 @@ class CronArchive $skipDayArchive = $existingArchiveIsValid; // Invalidate old website forces the archiving for this site - $skipDayArchive = $skipDayArchive && !$websiteIsOldDataInvalidate; + $skipDayArchive = $skipDayArchive && !$websiteInvalidatedShouldReprocess; // Also reprocess when day has ended since last run if ($dayHasEndedMustReprocess // it might have reprocessed for that day by another cron - && !$this->hasBeenProcessedSinceMidnight($idsite, $lastTimestampWebsiteProcessedDay) + && !$this->hasBeenProcessedSinceMidnight($idSite, $lastTimestampWebsiteProcessedDay) && !$existingArchiveIsValid) { $skipDayArchive = false; } @@ -380,106 +611,61 @@ class CronArchive $skipDayArchive = false; } - if ($skipDayArchive) { - $this->log("Skipped website id $idsite, already done " - . \Piwik\MetricsFormatter::getPrettyTimeFromSeconds($elapsedSinceLastArchiving, true, $isHtml = false) + if ($skipDayArchive) { + $this->logger->info("Skipped website id $idSite, already done " + . $this->formatter->getPrettyTimeFromSeconds($elapsedSinceLastArchiving, true) . " ago, " . $timerWebsite->__toString()); $this->skippedDayArchivesWebsites++; $this->skipped++; return false; } - // Fake that the request is already done, so that other archive.php - // running do not grab the same website from the queue - Option::set($this->lastRunKey($idsite, "day"), time()); - - // Remove this website from the list of websites to be invalidated - // since it's now just about to being re-processed, makes sure another running cron archiving process - // does not archive the same idsite - if ($websiteIsOldDataInvalidate) { - $this->setSiteIsArchived($idsite); + /** + * Trigger archiving for days + */ + try { + $shouldProceed = $this->processArchiveDays($idSite, $lastTimestampWebsiteProcessedDay, $shouldArchivePeriods, $timerWebsite); + } catch (UnexpectedWebsiteFoundException $e) { + // this website was deleted in the meantime + $shouldProceed = false; + $this->logger->info("Skipped website id $idSite, got: UnexpectedWebsiteFoundException, " . $timerWebsite->__toString()); } - // when some data was purged from this website - // we make sure we query all previous days/weeks/months - $processDaysSince = $lastTimestampWebsiteProcessedDay; - if($websiteIsOldDataInvalidate - // when --force-all-websites option, - // also forces to archive last52 days to be safe - || $this->shouldArchiveAllSites) { - $processDaysSince = false; - } - - - $timer = new Timer; - $dateLast = $this->getApiDateLastParameter($idsite, "day", $processDaysSince); - $url = $this->getVisitsRequestUrl($idsite, "day", $dateLast); - $content = $this->request($url); - $response = @unserialize($content); - $visitsToday = $this->getVisitsLastPeriodFromApiResponse($response); - $visitsLastDays = $this->getVisitsFromApiResponse($response); - - if (empty($content) - || !is_array($response) - || count($response) == 0 - ) { - // cancel the succesful run flag - Option::set($this->lastRunKey($idsite, "day"), 0); - - $this->log("WARNING: Empty or invalid response '$content' for website id $idsite, " . $timerWebsite->__toString() . ", skipping"); - $this->skipped++; + if (!$shouldProceed) { return false; } - $this->requests++; - $this->processed++; - - // If there is no visit today and we don't need to process this website, we can skip remaining archives - if ($visitsToday == 0 - && !$shouldArchivePeriods - ) { - $this->log("Skipped website id $idsite, no visit today, " . $timerWebsite->__toString()); - $this->skipped++; - return false; - } - - if ($visitsLastDays == 0 - && !$shouldArchivePeriods - && $this->shouldArchiveAllSites - ) { - $this->log("Skipped website id $idsite, no visits in the last " . $dateLast . " days, " . $timerWebsite->__toString()); - $this->skipped++; - return false; - } - - - $this->visitsToday += $visitsToday; - $this->websitesWithVisitsSinceLastRun++; - $this->archiveVisitsAndSegments($idsite, "day", $lastTimestampWebsiteProcessedDay); - $this->logArchivedWebsite($idsite, "day", $dateLast, $visitsLastDays, $visitsToday, $timer); - if (!$shouldArchivePeriods) { - $this->log("Skipped website id $idsite periods processing, already done " - . \Piwik\MetricsFormatter::getPrettyTimeFromSeconds($elapsedSinceLastArchiving, true, $isHtml = false) + $this->logger->info("Skipped website id $idSite periods processing, already done " + . $this->formatter->getPrettyTimeFromSeconds($elapsedSinceLastArchiving, true) . " ago, " . $timerWebsite->__toString()); - $this->skippedDayArchivesWebsites++; + $this->skippedPeriodsArchivesWebsite++; $this->skipped++; return false; } - $success = true; - foreach (array('week', 'month', 'year') as $period) { - $success = $this->archiveVisitsAndSegments($idsite, $period, $lastTimestampWebsiteProcessedPeriods) - && $success; - } + /** + * Trigger archiving for non-day periods + */ + $success = $this->processArchiveForPeriods($idSite, $lastTimestampWebsiteProcessedPeriods); + // Record succesful run of this website's periods archiving if ($success) { - Option::set($this->lastRunKey($idsite, "periods"), time()); + Option::set($this->lastRunKey($idSite, "periods"), time()); } + + if (!$success) { + // cancel marking the site as reprocessed + if ($websiteInvalidatedShouldReprocess) { + $store = new SitesToReprocessDistributedList(); + $store->add($idSite); + } + } + $this->archivedPeriodsArchivesWebsite++; $requestsWebsite = $this->requests - $requestsBefore; - Log::info("Archived website id = $idsite, " + $this->logger->info("Archived website id = $idSite, " . $requestsWebsite . " API requests, " . $timerWebsite->__toString() . " [" . $this->websites->getNumProcessedWebsites() . "/" @@ -490,78 +676,206 @@ class CronArchive } /** - * Checks the config file is found. - * - * @param $piwikUrl - * @throws Exception + * @param $idSite + * @param $lastTimestampWebsiteProcessedPeriods + * @return bool */ - protected function initConfigObject($piwikUrl) + private function processArchiveForPeriods($idSite, $lastTimestampWebsiteProcessedPeriods) { - // HOST is required for the Config object - $parsed = parse_url($piwikUrl); - Url::setHost($parsed['host']); + $success = true; - Config::getInstance()->clear(); + foreach (array('week', 'month', 'year') as $period) { + if (!$this->shouldProcessPeriod($period)) { + // if any period was skipped, we do not mark the Periods archiving as successful + $success = false; + continue; + } - try { - Config::getInstance()->checkLocalConfigFound(); - } catch (Exception $e) { - throw new Exception("The configuration file for Piwik could not be found. " . - "Please check that config/config.ini.php is readable by the user " . - get_current_user()); + $timer = new Timer(); + + $date = $this->getApiDateParameter($idSite, $period, $lastTimestampWebsiteProcessedPeriods); + $periodArchiveWasSuccessful = $this->archiveReportsFor($idSite, $period, $date, $archiveSegments = true, $timer); + $success = $periodArchiveWasSuccessful && $success; } + + if ($this->shouldProcessPeriod('range')) { + // period=range + $customDateRangesToPreProcessForSite = $this->getCustomDateRangeToPreProcess($idSite); + foreach ($customDateRangesToPreProcessForSite as $dateRange) { + $timer = new Timer(); + $archiveSegments = false; // do not pre-process segments for period=range #7611 + $periodArchiveWasSuccessful = $this->archiveReportsFor($idSite, 'range', $dateRange, $archiveSegments, $timer); + $success = $periodArchiveWasSuccessful && $success; + } + } + + return $success; } /** - * Returns base URL to process reports for the $idsite on a given $period + * Returns base URL to process reports for the $idSite on a given $period */ - private function getVisitsRequestUrl($idsite, $period, $dateLast) + private function getVisitsRequestUrl($idSite, $period, $date, $segment = false) { - return "?module=API&method=API.get&idSite=$idsite&period=$period&date=last" . $dateLast . "&format=php&token_auth=" . $this->token_auth; + $request = "?module=API&method=API.get&idSite=$idSite&period=$period&date=" . $date . "&format=php"; + if ($segment) { + $request .= '&segment=' . urlencode($segment); + ; + } + return $request; } private function initSegmentsToArchive() { $segments = \Piwik\SettingsPiwik::getKnownSegmentsToArchive(); + if (empty($segments)) { return array(); } - $this->log("- Will pre-process " . count($segments) . " Segments for each website and each period: " . implode(", ", $segments)); - return $segments; - } - private function getSegmentsForSite($idsite) - { - $segmentsAllSites = $this->segments; - $segmentsThisSite = \Piwik\SettingsPiwik::getKnownSegmentsToArchiveForSite($idsite); - if (!empty($segmentsThisSite)) { - $this->log("Will pre-process the following " . count($segmentsThisSite) . " Segments for this website (id = $idsite): " . implode(", ", $segmentsThisSite)); - } - $segments = array_unique(array_merge($segmentsAllSites, $segmentsThisSite)); + $this->logger->info("- Will pre-process " . count($segments) . " Segments for each website and each period: " . implode(", ", $segments)); return $segments; } /** - * Will trigger API requests for the specified Website $idsite, + * @param $idSite + * @param $lastTimestampWebsiteProcessedDay + * @param $shouldArchivePeriods + * @param $timerWebsite + * @return bool + */ + protected function processArchiveDays($idSite, $lastTimestampWebsiteProcessedDay, $shouldArchivePeriods, Timer $timerWebsite) + { + if (!$this->shouldProcessPeriod("day")) { + // skip day archiving and proceed to period processing + return true; + } + + $timer = new Timer(); + + // Fake that the request is already done, so that other core:archive commands + // running do not grab the same website from the queue + Option::set($this->lastRunKey($idSite, "day"), time()); + + // Remove this website from the list of websites to be invalidated + // since it's now just about to being re-processed, makes sure another running cron archiving process + // does not archive the same idSite + $websiteInvalidatedShouldReprocess = $this->isOldReportInvalidatedForWebsite($idSite); + if ($websiteInvalidatedShouldReprocess) { + $store = new SitesToReprocessDistributedList(); + $store->remove($idSite); + } + + // when some data was purged from this website + // we make sure we query all previous days/weeks/months + $processDaysSince = $lastTimestampWebsiteProcessedDay; + if ($websiteInvalidatedShouldReprocess + // when --force-all-websites option, + // also forces to archive last52 days to be safe + || $this->shouldArchiveAllSites) { + $processDaysSince = false; + } + + $date = $this->getApiDateParameter($idSite, "day", $processDaysSince); + $url = $this->getVisitsRequestUrl($idSite, "day", $date); + + $this->logArchiveWebsite($idSite, "day", $date); + + $content = $this->request($url); + $daysResponse = @unserialize($content); + + if (empty($content) + || !is_array($daysResponse) + || count($daysResponse) == 0 + ) { + // cancel the succesful run flag + Option::set($this->lastRunKey($idSite, "day"), 0); + + // cancel marking the site as reprocessed + if ($websiteInvalidatedShouldReprocess) { + $store = new SitesToReprocessDistributedList(); + $store->add($idSite); + } + + $this->logError("Empty or invalid response '$content' for website id $idSite, " . $timerWebsite->__toString() . ", skipping"); + $this->skippedDayOnApiError++; + $this->skipped++; + return false; + } + + $visitsToday = $this->getVisitsLastPeriodFromApiResponse($daysResponse); + $visitsLastDays = $this->getVisitsFromApiResponse($daysResponse); + + $this->requests++; + $this->processed++; + + // If there is no visit today and we don't need to process this website, we can skip remaining archives + if ( + 0 == $visitsToday + && !$shouldArchivePeriods + ) { + $this->logger->info("Skipped website id $idSite, no visit today, " . $timerWebsite->__toString()); + $this->skippedDayNoRecentData++; + $this->skipped++; + return false; + } + + if (0 == $visitsLastDays + && !$shouldArchivePeriods + && $this->shouldArchiveAllSites + ) { + $humanReadableDate = $this->formatReadableDateRange($date); + $this->logger->info("Skipped website id $idSite, no visits in the $humanReadableDate days, " . $timerWebsite->__toString()); + $this->skippedPeriodsNoDataInPeriod++; + $this->skipped++; + return false; + } + + $this->visitsToday += $visitsToday; + $this->websitesWithVisitsSinceLastRun++; + + $this->archiveReportsFor($idSite, "day", $this->getApiDateParameter($idSite, "day", $processDaysSince), $archiveSegments = true, $timer); + + return true; + } + + private function getSegmentsForSite($idSite, $period) + { + $segmentsAllSites = $this->segments; + $segmentsThisSite = SettingsPiwik::getKnownSegmentsToArchiveForSite($idSite); + $segments = array_unique(array_merge($segmentsAllSites, $segmentsThisSite)); + return $segments; + } + + private function formatReadableDateRange($date) + { + if (0 === strpos($date, 'last')) { + $readable = 'last ' . str_replace('last', '', $date); + } elseif (0 === strpos($date, 'previous')) { + $readable = 'previous ' . str_replace('previous', '', $date); + } else { + $readable = 'last ' . $date; + } + + return $readable; + } + + /** + * Will trigger API requests for the specified Website $idSite, * for the specified $period, for all segments that are pre-processed for this website. * Requests are triggered using cURL multi handle * - * @param $idsite int - * @param $period - * @param $lastTimestampWebsiteProcessed + * @param $idSite int + * @param $period string + * @param $date string + * @param $archiveSegments bool Whether to pre-process all custom segments + * @param Timer $periodTimer * @return bool True on success, false if some request failed */ - private function archiveVisitsAndSegments($idsite, $period, $lastTimestampWebsiteProcessed) + private function archiveReportsFor($idSite, $period, $date, $archiveSegments, Timer $periodTimer) { - $timer = new Timer(); - - $url = $this->piwikUrl; - - $dateLast = $this->getApiDateLastParameter($idsite, $period, $lastTimestampWebsiteProcessed); - $url .= $this->getVisitsRequestUrl($idsite, $period, $dateLast); - - - $url .= self::APPEND_TO_API_REQUEST; + $url = $this->getVisitsRequestUrl($idSite, $period, $date, $segment = false); + $url = $this->makeRequestUrl($url); $visitsInLastPeriods = $visitsLastPeriod = 0; $success = true; @@ -572,17 +886,23 @@ class CronArchive // already processed above for "day" if ($period != "day") { $urls[] = $url; - $this->requests++; + $this->logArchiveWebsite($idSite, $period, $date); } - foreach ($this->getSegmentsForSite($idsite) as $segment) { - $urlWithSegment = $url . '&segment=' . urlencode($segment); - $urls[] = $urlWithSegment; - $this->requests++; + $segmentRequestsCount = 0; + if ($archiveSegments) { + $urlsWithSegment = $this->getUrlsWithSegment($idSite, $period, $date); + $urls = array_merge($urls, $urlsWithSegment); + $segmentRequestsCount = count($urlsWithSegment); + + // in case several segment URLs for period=range had the date= rewritten to the same value, we only call API once + $urls = array_unique($urls); } - $cliMulti = new CliMulti(); - $cliMulti->setAcceptInvalidSSLCertificate($this->acceptInvalidSSLCertificate); + $this->requests += count($urls); + + $cliMulti = $this->makeCliMulti(); + $cliMulti->setConcurrentProcessesLimit($this->getConcurrentRequestsPerWebsite()); $response = $cliMulti->request($urls); foreach ($urls as $index => $url) { @@ -590,21 +910,23 @@ class CronArchive $success = $success && $this->checkResponse($content, $url); if ($noSegmentUrl === $url && $success) { - $stats = @unserialize($content); if (!is_array($stats)) { $this->logError("Error unserializing the following response from $url: " . $content); } + if ($period == 'range') { + // range returns one dataset (the sum of data between the two dates), + // whereas other periods return lastN which is N datasets in an array. Here we make our period=range dataset look like others: + $stats = array($stats); + } + $visitsInLastPeriods = $this->getVisitsFromApiResponse($stats); $visitsLastPeriod = $this->getVisitsLastPeriodFromApiResponse($stats); } } - // we have already logged the daily archive above - if($period != "day") { - $this->logArchivedWebsite($idsite, $period, $dateLast, $visitsInLastPeriods, $visitsLastPeriod, $timer); - } + $this->logArchivedWebsite($idSite, $period, $date, $segmentRequestsCount, $visitsInLastPeriods, $visitsLastPeriod, $periodTimer); return $success; } @@ -614,45 +936,48 @@ class CronArchive */ private function logSection($title = "") { - $this->log("---------------------------"); - if(!empty($title)) { - $this->log($title); + $this->logger->info("---------------------------"); + if (!empty($title)) { + $this->logger->info($title); } } - private function log($m) + public function logError($m) { - $this->output .= $m . "\n"; - try { - Log::info($m); - } catch(Exception $e) { - print($m . "\n"); + if (!defined('PIWIK_ARCHIVE_NO_TRUNCATE')) { + $m = substr($m, 0, self::TRUNCATE_ERROR_MESSAGE_SUMMARY); } + $m = str_replace(array("\n", "\t"), " ", $m); + $this->errors[] = $m; + $this->logger->error($m); + } + + private function logNetworkError($url, $response) + { + $message = "Got invalid response from API request: $url. "; + if (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 '$response'"; + } + + $this->logError($message); + return false; } /** - * Issues a request to $url + * Issues a request to $url eg. "?module=API&method=API.getDefaultMetricTranslations&format=original&serialize=1" + * */ private function request($url) { - $url = $this->piwikUrl . $url . self::APPEND_TO_API_REQUEST; - - if($this->shouldStartProfiler) { - $url .= "&xhprof=2"; - } - - if ($this->testmode) { - $url .= "&testmode=1"; - } + $url = $this->makeRequestUrl($url); try { - - $cliMulti = new CliMulti(); - $cliMulti->setAcceptInvalidSSLCertificate($this->acceptInvalidSSLCertificate); + $cliMulti = $this->makeCliMulti(); $responses = $cliMulti->request(array($url)); $response = !empty($responses) ? array_shift($responses) : null; - } catch (Exception $e) { return $this->logNetworkError($url, $e->getMessage()); } @@ -672,99 +997,6 @@ class CronArchive return true; } - public function logError($m) - { - if (!defined('PIWIK_ARCHIVE_NO_TRUNCATE')) { - $m = substr($m, 0, self::TRUNCATE_ERROR_MESSAGE_SUMMARY); - } - - $this->errors[] = $m; - $this->log("ERROR: $m"); - } - - private function logNetworkError($url, $response) - { - $message = "Got invalid response from API request: $url. "; - if (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 '$response'"; - } - $this->logError($message); - return false; - } - - /** - * Configures Piwik\Log so messages are written in output - */ - private function initLog() - { - $config = Config::getInstance(); - /** - * access a property that is not overriden by TestingEnvironment before accessing log as the - * log section is used in TestingEnvironment. Otherwise access to magic __get('log') fails in - * TestingEnvironment as it tries to acccess it already here with __get('log'). - * $config->log ==> __get('log') ==> Config.createConfigInstance ==> nested __get('log') ==> returns null - */ - $initConfigToPreventErrorWhenAccessingLog = $config->mail; - - $log = $config->log; - $log['log_only_when_debug_parameter'] = 0; - $log[\Piwik\Log::LOG_WRITERS_CONFIG_OPTION] = array("screen"); - - if (!empty($_GET['forcelogtoscreen'])) { - Log::getInstance()->addLogWriter('screen'); - } - - // Make sure we log at least INFO (if logger is set to DEBUG then keep it) - $logLevel = @$log[\Piwik\Log::LOG_LEVEL_CONFIG_OPTION]; - if ($logLevel != 'VERBOSE' - && $logLevel != 'DEBUG' - ) { - $log[\Piwik\Log::LOG_LEVEL_CONFIG_OPTION] = 'INFO'; - Log::getInstance()->setLogLevel(Log::INFO); - } - - $config->log = $log; - } - - /** - * Script does run on http:// ONLY if the SU token is specified - */ - private function initCheckCli() - { - if (Common::isPhpCliMode()) { - return; - } - $token_auth = Common::getRequestVar('token_auth', '', 'string'); - if ($token_auth != $this->token_auth - || strlen($token_auth) != 32 - ) { - die('You must specify the Super User token_auth as a parameter to this script, eg. ?token_auth=XYZ if you wish to run this script through the browser.
- However it is recommended to run it via cron in the command line, since it can take a long time to run.
- In a shell, execute for example the following to trigger archiving on the local Piwik server:
- $ /path/to/php /path/to/piwik/misc/cron/archive.php --url=http://your-website.org/path/to/piwik/'); - } - } - - /** - * Init Piwik, connect DB, create log & config objects, etc. - */ - private function initCore() - { - try { - FrontController::getInstance()->init(); - $this->isCoreInited = true; - } catch (Exception $e) { - throw new CronArchiveFatalException("ERROR: During Piwik init, Message: " . $e->getMessage()); - } - } - - public function isCoreInited() - { - return $this->isCoreInited; - } - /** * Initializes the various parameters to the script, based on input parameters. * @@ -772,179 +1004,112 @@ class CronArchive private function initStateFromParameters() { $this->todayArchiveTimeToLive = Rules::getTodayArchiveTimeToLive(); - $this->acceptInvalidSSLCertificate = $this->getParameterFromCli("accept-invalid-ssl-certificate"); $this->processPeriodsMaximumEverySeconds = $this->getDelayBetweenPeriodsArchives(); - $this->shouldArchiveAllSites = (bool) $this->getParameterFromCli("force-all-websites"); - $this->shouldStartProfiler = (bool) $this->getParameterFromCli("xhprof"); - $restrictToIdSites = $this->getParameterFromCli("force-idsites", true); - $skipIdSites = $this->getParameterFromCli("skip-idsites", true); - $this->shouldArchiveSpecifiedSites = \Piwik\Site::getIdSitesFromIdSitesString($restrictToIdSites); - $this->shouldSkipSpecifiedSites = \Piwik\Site::getIdSitesFromIdSitesString($skipIdSites); - $this->lastSuccessRunTimestamp = Option::get(self::OPTION_ARCHIVING_FINISHED_TS); + $this->lastSuccessRunTimestamp = $this->getLastSuccessRunTimestamp(); $this->shouldArchiveOnlySitesWithTrafficSince = $this->isShouldArchiveAllSitesWithTrafficSince(); + $this->shouldArchiveOnlySpecificPeriods = $this->getPeriodsToProcess(); - if($this->shouldArchiveOnlySitesWithTrafficSince === false) { - // force-all-periods is not set here - if (empty($this->lastSuccessRunTimestamp)) { - // First time we run the script - $this->shouldArchiveOnlySitesWithTrafficSince = self::ARCHIVE_SITES_WITH_TRAFFIC_SINCE; - } else { - // there was a previous successful run - $this->shouldArchiveOnlySitesWithTrafficSince = time() - $this->lastSuccessRunTimestamp; - } - } else { + if ($this->shouldArchiveOnlySitesWithTrafficSince !== false) { // force-all-periods is set here $this->archiveAndRespectTTL = false; - - if($this->shouldArchiveOnlySitesWithTrafficSince === true) { - // force-all-periods without value - $this->shouldArchiveOnlySitesWithTrafficSince = self::ARCHIVE_SITES_WITH_TRAFFIC_SINCE; - } } } + private function getSecondsSinceLastArchive() + { + $wasNotCustomTimeRequested = $this->shouldArchiveOnlySitesWithTrafficSince === false; + + if ($wasNotCustomTimeRequested && !empty($this->lastSuccessRunTimestamp)) { + // there was a previous successful run + + return time() - $this->lastSuccessRunTimestamp; + + } elseif (is_numeric($this->shouldArchiveOnlySitesWithTrafficSince)) { + // $shouldArchiveAllPeriodsSince was specified + $secondsSinceStart = time() - $this->archivingStartingTime; + return $this->shouldArchiveOnlySitesWithTrafficSince + $secondsSinceStart; + } + + // force-all-periods without value + return self::ARCHIVE_SITES_WITH_TRAFFIC_SINCE; + } + public function filterWebsiteIds(&$websiteIds) { // Keep only the websites that do exist $websiteIds = array_intersect($websiteIds, $this->allWebsites); /** - * Triggered by the **archive.php** cron script so plugins can modify the list of + * Triggered by the **core:archive** console command so plugins can modify the list of * websites that the archiving process will be launched for. - * + * * Plugins can use this hook to add websites to archive, remove websites to archive, or change * the order in which websites will be archived. - * + * * @param array $websiteIds The list of website IDs to launch the archiving process for. */ Piwik::postEvent('CronArchive.filterWebsiteIds', array(&$websiteIds)); } + /** + * @internal + */ + public function setApiToInvalidateArchivedReport($api) + { + $this->apiToInvalidateArchivedReport = $api; + } + + private function getApiToInvalidateArchivedReport() + { + if ($this->apiToInvalidateArchivedReport) { + return $this->apiToInvalidateArchivedReport; + } + + return CoreAdminHomeAPI::getInstance(); + } + + public function invalidateArchivedReportsForSitesThatNeedToBeArchivedAgain() + { + $sitesPerDays = $this->invalidator->getRememberedArchivedReportsThatShouldBeInvalidated(); + + foreach ($sitesPerDays as $date => $siteIds) { + $listSiteIds = implode(',', $siteIds); + + try { + $this->logger->info('Will invalidate archived reports for ' . $date . ' for following websites ids: ' . $listSiteIds); + $this->getApiToInvalidateArchivedReport()->invalidateArchivedReports($siteIds, $date); + } catch (Exception $e) { + $this->logger->info('Failed to invalidate archived reports: ' . $e->getMessage()); + } + } + } + /** * Returns the list of sites to loop over and archive. * @return array */ public function initWebsiteIds() { - if(count($this->shouldArchiveSpecifiedSites) > 0) { - $this->log("- Will process " . count($this->shouldArchiveSpecifiedSites) . " websites (--force-idsites)"); + if (count($this->shouldArchiveSpecifiedSites) > 0) { + $this->logger->info("- Will process " . count($this->shouldArchiveSpecifiedSites) . " websites (--force-idsites)"); return $this->shouldArchiveSpecifiedSites; } + + $this->findWebsiteIdsInTimezoneWithNewDay($this->allWebsites); + $this->findInvalidatedSitesToReprocess(); + if ($this->shouldArchiveAllSites) { - $this->log("- Will process all " . count($this->allWebsites) . " websites"); - return $this->allWebsites; + $this->logger->info("- Will process all " . count($this->allWebsites) . " websites"); } - $websiteIds = array_merge( - $this->addWebsiteIdsWithVisitsSinceLastRun(), - $this->getWebsiteIdsToInvalidate() - ); - $websiteIds = array_merge($websiteIds, $this->addWebsiteIdsInTimezoneWithNewDay($websiteIds)); - return array_unique($websiteIds); - } - - private function initTokenAuth() - { - $superUser = Db::get()->fetchRow("SELECT login, token_auth - FROM " . Common::prefixTable("user") . " - WHERE superuser_access = 1 - ORDER BY date_registered ASC"); - $this->token_auth = $superUser['token_auth']; - } - - private function initPiwikHost() - { - // If archive.php run as a web cron, we use the current hostname+path - if (!Common::isPhpCliMode()) { - if (!empty(self::$url)) { - $piwikUrl = self::$url; - } else { - // example.org/piwik/ - $piwikUrl = SettingsPiwik::getPiwikUrl(); - } - } else { - // If archive.php run as CLI/shell we require the piwik url to be set - $piwikUrl = $this->getParameterFromCli("url", true); - - if (!$piwikUrl) { - $this->logFatalErrorUrlExpected(); - } - - if(!\Piwik\UrlHelper::isLookLikeUrl($piwikUrl)) { - // try adding http:// in case it's missing - $piwikUrl = "http://" . $piwikUrl; - } - if(!\Piwik\UrlHelper::isLookLikeUrl($piwikUrl)) { - $this->logFatalErrorUrlExpected(); - } - - // ensure there is a trailing slash - if ($piwikUrl[strlen($piwikUrl) - 1] != '/' && !Common::stringEndsWith($piwikUrl, 'index.php')) { - $piwikUrl .= '/'; - } - } - - $this->initConfigObject($piwikUrl); - - if (Config::getInstance()->General['force_ssl'] == 1) { - $piwikUrl = str_replace('http://', 'https://', $piwikUrl); - } - - if (!Common::stringEndsWith($piwikUrl, 'index.php')) { - $piwikUrl .= 'index.php'; - } - - $this->piwikUrl = $piwikUrl; - } - - /** - * Returns if the requested parameter is defined in the command line arguments. - * If $valuePossible is true, then a value is possibly set for this parameter, - * ie. --force-timeout-for-periods=3600 would return 3600 - * - * @param $parameter - * @param bool $valuePossible - * @return true or the value (int,string) if set, false otherwise - */ - public static function getParameterFromCli($parameter, $valuePossible = false) - { - if (!Common::isPhpCliMode()) { - return false; - } - if($parameter == 'url' && self::$url) { - return self::$url; - } - $parameters = array( - "--$parameter", - "-$parameter", - $parameter - ); - if(empty($_SERVER['argv'])) { - return false; - } - foreach ($parameters as $parameter) { - foreach ($_SERVER['argv'] as $arg) { - if (strpos($arg, $parameter) === 0) { - if ($valuePossible) { - $parameterFound = $arg; - if (($posEqual = strpos($parameterFound, '=')) !== false) { - $return = substr($parameterFound, $posEqual + 1); - if ($return !== false) { - return $return; - } - } - } - return true; - } - } - } - return false; + return $this->allWebsites; } private function updateIdSitesInvalidatedOldReports() { - $this->idSitesInvalidatedOldReports = APICoreAdminHome::getWebsiteIdsToInvalidate(); + $store = new SitesToReprocessDistributedList(); + $this->idSitesInvalidatedOldReports = $store->getAll(); } /** @@ -954,13 +1119,13 @@ class CronArchive * * @return array */ - private function getWebsiteIdsToInvalidate() + private function findInvalidatedSitesToReprocess() { $this->updateIdSitesInvalidatedOldReports(); if (count($this->idSitesInvalidatedOldReports) > 0) { $ids = ", IDs: " . implode(", ", $this->idSitesInvalidatedOldReports); - $this->log("- Will process " . count($this->idSitesInvalidatedOldReports) + $this->logger->info("- Will process " . count($this->idSitesInvalidatedOldReports) . " other websites because some old data reports have been invalidated (eg. using the Log Import script) " . $ids); } @@ -969,20 +1134,37 @@ class CronArchive } /** - * Returns all sites that had visits since specified time + * Detects whether a site had visits since midnight in the websites timezone * - * @return string + * @return bool */ - private function addWebsiteIdsWithVisitsSinceLastRun() + private function hadWebsiteTrafficSinceMidnightInTimezone($idSite) { - $sitesIdWithVisits = APISitesManager::getInstance()->getSitesIdWithVisits(time() - $this->shouldArchiveOnlySitesWithTrafficSince); - $websiteIds = !empty($sitesIdWithVisits) ? ", IDs: " . implode(", ", $sitesIdWithVisits) : ""; - $prettySeconds = \Piwik\MetricsFormatter::getPrettyTimeFromSeconds( $this->shouldArchiveOnlySitesWithTrafficSince, true, false); - $this->log("- Will process " . count($sitesIdWithVisits) . " websites with new visits since " - . $prettySeconds - . " " - . $websiteIds); - return $sitesIdWithVisits; + $timezone = Site::getTimezoneFor($idSite); + + $nowInTimezone = Date::factory('now', $timezone); + $midnightInTimezone = $nowInTimezone->setTime('00:00:00'); + + $secondsSinceMidnight = $nowInTimezone->getTimestamp() - $midnightInTimezone->getTimestamp(); + + $secondsSinceLastArchive = $this->getSecondsSinceLastArchive(); + if ($secondsSinceLastArchive < $secondsSinceMidnight) { + $secondsSinceMidnight = $secondsSinceLastArchive; + } + + $from = Date::now()->subSeconds($secondsSinceMidnight)->getDatetime(); + $to = Date::now()->addHour(1)->getDatetime(); + + $dao = new RawLogDao(); + $hasVisits = $dao->hasSiteVisitsBetweenTimeframe($from, $to, $idSite); + + if ($hasVisits) { + $this->logger->info("- tracking data found for website id $idSite (between $from and $to)"); + } else { + $this->logger->info("- no new tracking data for website id $idSite (between $from and $to)"); + } + + return $hasVisits; } /** @@ -993,7 +1175,7 @@ class CronArchive */ private function getTimezonesHavingNewDay() { - $timestamp = time() - $this->shouldArchiveOnlySitesWithTrafficSince; + $timestamp = $this->lastSuccessRunTimestamp; $uniqueTimezones = APISitesManager::getInstance()->getUniqueSiteTimezones(); $timezoneToProcess = array(); foreach ($uniqueTimezones as &$timezone) { @@ -1007,13 +1189,13 @@ class CronArchive return $timezoneToProcess; } - private function hasBeenProcessedSinceMidnight($idsite, $lastTimestampWebsiteProcessedDay) + private function hasBeenProcessedSinceMidnight($idSite, $lastTimestampWebsiteProcessedDay) { if (false === $lastTimestampWebsiteProcessedDay) { return true; } - $timezone = Site::getTimezoneFor($idsite); + $timezone = Site::getTimezoneFor($idSite); $dateInTimezone = Date::factory('now', $timezone); $midnightInTimezone = $dateInTimezone->setTime('00:00:00'); @@ -1030,40 +1212,27 @@ class CronArchive * @param $websiteIds * @return array Website IDs */ - private function addWebsiteIdsInTimezoneWithNewDay($websiteIds) + private function findWebsiteIdsInTimezoneWithNewDay($websiteIds) { $timezones = $this->getTimezonesHavingNewDay(); $websiteDayHasFinishedSinceLastRun = APISitesManager::getInstance()->getSitesIdFromTimezones($timezones); $websiteDayHasFinishedSinceLastRun = array_diff($websiteDayHasFinishedSinceLastRun, $websiteIds); $this->websiteDayHasFinishedSinceLastRun = $websiteDayHasFinishedSinceLastRun; + if (count($websiteDayHasFinishedSinceLastRun) > 0) { $ids = !empty($websiteDayHasFinishedSinceLastRun) ? ", IDs: " . implode(", ", $websiteDayHasFinishedSinceLastRun) : ""; - $this->log("- Will process " . count($websiteDayHasFinishedSinceLastRun) + $this->logger->info("- Will process " . count($websiteDayHasFinishedSinceLastRun) . " other websites because the last time they were archived was on a different day (in the website's timezone) " . $ids); } - return $websiteDayHasFinishedSinceLastRun; - } - /** - * Test that the specified piwik URL is a valid Piwik endpoint. - */ - private function checkPiwikUrlIsValid() - { - $response = $this->request("?module=API&method=API.getDefaultMetricTranslations&format=original&serialize=1"); - $responseUnserialized = @unserialize($response); - if ($response === false - || !is_array($responseUnserialized) - ) { - $this->logFatalError("The Piwik URL {$this->piwikUrl} does not seem to be pointing to a Piwik server. Response was '$response'."); - } + return $websiteDayHasFinishedSinceLastRun; } private function logInitInfo() { $this->logSection("INIT"); - $this->log("Piwik is installed at: {$this->piwikUrl}"); - $this->log("Running Piwik " . Version::VERSION . " as Super User"); + $this->logger->info("Running Piwik " . Version::VERSION . " as Super User"); } private function logArchiveTimeoutInfo() @@ -1072,18 +1241,19 @@ class CronArchive // Recommend to disable browser archiving when using this script if (Rules::isBrowserTriggerEnabled()) { - $this->log("- If you execute this script at least once per hour (or more often) in a crontab, you may disable 'Browser trigger archiving' in Piwik UI > Settings > General Settings. "); - $this->log(" See the doc at: http://piwik.org/docs/setup-auto-archiving/"); + $this->logger->info("- If you execute this script at least once per hour (or more often) in a crontab, you may disable 'Browser trigger archiving' in Piwik UI > Settings > General Settings."); + $this->logger->info(" See the doc at: http://piwik.org/docs/setup-auto-archiving/"); } - $this->log("- Reports for today will be processed at most every " . $this->todayArchiveTimeToLive + $this->logger->info("- Reports for today will be processed at most every " . $this->todayArchiveTimeToLive . " seconds. You can change this value in Piwik UI > Settings > General Settings."); - $this->log("- Reports for the current week/month/year will be refreshed at most every " + $this->logger->info("- Reports for the current week/month/year will be refreshed at most every " . $this->processPeriodsMaximumEverySeconds . " seconds."); // Try and not request older data we know is already archived if ($this->lastSuccessRunTimestamp !== false) { $dateLast = time() - $this->lastSuccessRunTimestamp; - $this->log("- Archiving was last executed without error " . \Piwik\MetricsFormatter::getPrettyTimeFromSeconds($dateLast, true, $isHtml = false) . " ago"); + $this->logger->info("- Archiving was last executed without error " + . $this->formatter->getPrettyTimeFromSeconds($dateLast, true) . " ago"); } } @@ -1095,88 +1265,199 @@ class CronArchive */ private function getDelayBetweenPeriodsArchives() { - $forceTimeoutPeriod = $this->getParameterFromCli("force-timeout-for-periods", $valuePossible = true); - if (empty($forceTimeoutPeriod) || $forceTimeoutPeriod === true) { + if (empty($this->forceTimeoutPeriod)) { return self::SECONDS_DELAY_BETWEEN_PERIOD_ARCHIVES; } // Ensure the cache for periods is at least as high as cache for today - if ($forceTimeoutPeriod > $this->todayArchiveTimeToLive) { - return $forceTimeoutPeriod; + if ($this->forceTimeoutPeriod > $this->todayArchiveTimeToLive) { + return $this->forceTimeoutPeriod; } - $this->log("WARNING: Automatically increasing --force-timeout-for-periods from $forceTimeoutPeriod to " + $this->logger->info("WARNING: Automatically increasing --force-timeout-for-periods from {$this->forceTimeoutPeriod} to " . $this->todayArchiveTimeToLive . " to match the cache timeout for Today's report specified in Piwik UI > Settings > General Settings"); + return $this->todayArchiveTimeToLive; } private function isShouldArchiveAllSitesWithTrafficSince() { - $shouldArchiveAllPeriodsSince = $this->getParameterFromCli("force-all-periods", $valuePossible = true); - if(empty($shouldArchiveAllPeriodsSince)) { + if (empty($this->shouldArchiveAllPeriodsSince)) { return false; } - if ( is_numeric($shouldArchiveAllPeriodsSince) - && $shouldArchiveAllPeriodsSince > 1 + + if (is_numeric($this->shouldArchiveAllPeriodsSince) + && $this->shouldArchiveAllPeriodsSince > 1 ) { - return (int)$shouldArchiveAllPeriodsSince; + return (int)$this->shouldArchiveAllPeriodsSince; } + return true; } - /** - * @param $idsite - */ - protected function setSiteIsArchived($idsite) - { - $websiteIdsInvalidated = APICoreAdminHome::getWebsiteIdsToInvalidate(); - if (count($websiteIdsInvalidated)) { - $found = array_search($idsite, $websiteIdsInvalidated); - if ($found !== false) { - unset($websiteIdsInvalidated[$found]); - Option::set(APICoreAdminHome::OPTION_INVALIDATED_IDSITES, serialize($websiteIdsInvalidated)); - } - } - } - - private function logFatalErrorUrlExpected() - { - $this->logFatalError("archive.php expects the argument 'url' to be set to your Piwik URL, for example: --url=http://example.org/piwik/ " - . "\n--help for more information", $backtrace = false); - } - private function getVisitsLastPeriodFromApiResponse($stats) { - if(empty($stats)) { + if (empty($stats)) { return 0; } + $today = end($stats); + + if (empty($today['nb_visits'])) { + return 0; + } + return $today['nb_visits']; } private function getVisitsFromApiResponse($stats) { - if(empty($stats)) { + if (empty($stats)) { return 0; } + $visits = 0; - foreach($stats as $metrics) { - if(empty($metrics['nb_visits'])) { + foreach ($stats as $metrics) { + if (empty($metrics['nb_visits'])) { continue; } $visits += $metrics['nb_visits']; } + return $visits; } /** - * @param $idsite + * @param $idSite * @param $period * @param $lastTimestampWebsiteProcessed * @return float|int|true */ - private function getApiDateLastParameter($idsite, $period, $lastTimestampWebsiteProcessed = false) + private function getApiDateParameter($idSite, $period, $lastTimestampWebsiteProcessed = false) + { + $dateRangeForced = $this->getDateRangeToProcess(); + + if (!empty($dateRangeForced)) { + return $dateRangeForced; + } + + return $this->getDateLastN($idSite, $period, $lastTimestampWebsiteProcessed); + } + + /** + * @param $idSite + * @param $period + * @param $date + * @param $segmentsCount + * @param $visitsInLastPeriods + * @param $visitsToday + * @param $timer + */ + private function logArchivedWebsite($idSite, $period, $date, $segmentsCount, $visitsInLastPeriods, $visitsToday, Timer $timer) + { + if (strpos($date, 'last') === 0 || strpos($date, 'previous') === 0) { + $humanReadable = $this->formatReadableDateRange($date); + $visitsInLastPeriods = (int)$visitsInLastPeriods . " visits in $humanReadable " . $period . "s, "; + $thisPeriod = $period == "day" ? "today" : "this " . $period; + $visitsInLastPeriod = (int)$visitsToday . " visits " . $thisPeriod . ", "; + } else { + $visitsInLastPeriods = (int)$visitsInLastPeriods . " visits in " . $period . "s included in: $date, "; + $visitsInLastPeriod = ''; + } + + $this->logger->info("Archived website id = $idSite, period = $period, $segmentsCount segments, " + . $visitsInLastPeriods + . $visitsInLastPeriod + . $timer->__toString()); + } + + private function getDateRangeToProcess() + { + if (empty($this->restrictToDateRange)) { + return false; + } + + if (strpos($this->restrictToDateRange, ',') === false) { + throw new Exception("--force-date-range expects a date range ie. YYYY-MM-DD,YYYY-MM-DD"); + } + + return $this->restrictToDateRange; + } + + /** + * @return array + */ + private function getPeriodsToProcess() + { + $this->restrictToPeriods = array_intersect($this->restrictToPeriods, $this->getDefaultPeriodsToProcess()); + $this->restrictToPeriods = array_intersect($this->restrictToPeriods, PeriodFactory::getPeriodsEnabledForAPI()); + + return $this->restrictToPeriods; + } + + /** + * @return array + */ + private function getDefaultPeriodsToProcess() + { + return array('day', 'week', 'month', 'year', 'range'); + } + + /** + * @param $idSite + * @return bool + */ + private function isOldReportInvalidatedForWebsite($idSite) + { + return in_array($idSite, $this->idSitesInvalidatedOldReports); + } + + private function isWebsiteUsingTheTracker($idSite) + { + if (!isset($this->idSitesNotUsingTracker)) { + // we want to trigger event only once + $this->idSitesNotUsingTracker = array(); + + /** + * This event is triggered when detecting whether there are sites that do not use the tracker. + * + * By default we only archive a site when there was actually any visit since the last archiving. + * However, some plugins do import data from another source instead of using the tracker and therefore + * will never have any visits for this site. To make sure we still archive data for such a site when + * archiving for this site is requested, you can listen to this event and add the idSite to the list of + * sites that do not use the tracker. + * + * @param bool $idSitesNotUsingTracker The list of idSites that rather import data instead of using the tracker + */ + Piwik::postEvent('CronArchive.getIdSitesNotUsingTracker', array(&$this->idSitesNotUsingTracker)); + + if (!empty($this->idSitesNotUsingTracker)) { + $this->logger->info("- The following websites do not use the tracker: " . implode(',', $this->idSitesNotUsingTracker)); + } + } + + $isUsingTracker = !in_array($idSite, $this->idSitesNotUsingTracker); + + return $isUsingTracker; + } + + private function shouldProcessPeriod($period) + { + if (empty($this->shouldArchiveOnlySpecificPeriods)) { + return true; + } + + return in_array($period, $this->shouldArchiveOnlySpecificPeriods); + } + + /** + * @param $idSite + * @param $period + * @param $lastTimestampWebsiteProcessed + * @return string + */ + private function getDateLastN($idSite, $period, $lastTimestampWebsiteProcessed) { $dateLastMax = self::DEFAULT_DATE_LAST; if ($period == 'year') { @@ -1185,7 +1466,8 @@ class CronArchive $dateLastMax = self::DEFAULT_DATE_LAST_WEEKS; } if (empty($lastTimestampWebsiteProcessed)) { - $lastTimestampWebsiteProcessed = strtotime(\Piwik\Site::getCreationDateFor($idsite)); + $creationDateFor = \Piwik\Site::getCreationDateFor($idSite); + $lastTimestampWebsiteProcessed = strtotime($creationDateFor); } // Enforcing last2 at minimum to work around timing issues and ensure we make most archives available @@ -1194,54 +1476,254 @@ class CronArchive $dateLast = $dateLastMax; } - $dateLastForced = $this->getParameterFromCli('--force-date-last-n', true); - if (!empty($dateLastForced)) { - $dateLast = $dateLastForced; - return $dateLast; + if (!empty($this->dateLastForced)) { + $dateLast = $this->dateLastForced; } - return $dateLast; + + return "last" . $dateLast; } /** - * @param $idsite - * @param $period - * @param $dateLast - * @param $visitsInLastPeriods - * @param $visitsToday - * @param Timer $timerWebsite - * @param $timer + * @return int */ - private function logArchivedWebsite($idsite, $period, $dateLast, $visitsInLastPeriods, $visitsToday, Timer $timer) + private function getConcurrentRequestsPerWebsite() { - $thisPeriod = $period == "day" ? "today" : "this " . $period; - $this->log("Archived website id = $idsite, period = $period, " - . (int)$visitsInLastPeriods . " visits in last " . $dateLast . " " . $period . "s, " - . (int)$visitsToday . " visits " . $thisPeriod . ", " - . $timer->__toString()); - } -} - -class CronArchiveFatalException extends Exception -{ - private $fullOutput = null; - - public function __construct($message, $fullOutput = null) - { - parent::__construct($message); - - $this->fullOutput = $fullOutput; - } - - public function logAndExit(CronArchive$cronArchiver) - { - if ($cronArchiver->isCoreInited()) { - $cronArchiver->logError($this->getMessage()); + if (false !== $this->concurrentRequestsPerWebsite) { + return $this->concurrentRequestsPerWebsite; } - $fe = fopen('php://stderr', 'w'); - fwrite($fe, "Error in the last Piwik archive.php run: \n" . $this->getMessage() . "\n" - . (!empty($this->fullOutput) ? "\n\n Here is the full errors output:\n\n" . $this->fullOutput : '')); - - exit(1); + return self::MAX_CONCURRENT_API_REQUESTS; } -} \ No newline at end of file + + /** + * @param $idSite + * @return false|string + */ + private function getPeriodLastProcessedTimestamp($idSite) + { + $timestamp = Option::get($this->lastRunKey($idSite, "periods")); + return $this->sanitiseTimestamp($timestamp); + } + + /** + * @param $idSite + * @return false|string + */ + private function getDayLastProcessedTimestamp($idSite) + { + $timestamp = Option::get($this->lastRunKey($idSite, "day")); + return $this->sanitiseTimestamp($timestamp); + } + + /** + * @return false|string + */ + private function getLastSuccessRunTimestamp() + { + $timestamp = Option::get(self::OPTION_ARCHIVING_FINISHED_TS); + return $this->sanitiseTimestamp($timestamp); + } + + private function sanitiseTimestamp($timestamp) + { + $now = time(); + return ($timestamp < $now) ? $timestamp : $now; + } + + /** + * @param $idSite + * @return array of date strings + */ + private function getCustomDateRangeToPreProcess($idSite) + { + static $cache = null; + if (is_null($cache)) { + $cache = $this->loadCustomDateRangeToPreProcess(); + } + if (empty($cache[$idSite])) { + return array(); + } + $dates = array_unique($cache[$idSite]); + return $dates; + } + + /** + * @return array + */ + private function loadCustomDateRangeToPreProcess() + { + $customDateRangesToProcessForSites = array(); + + // For all users who have selected this website to load by default, + // we load the default period/date that will be loaded for this user + // and make sure it's pre-archived + $allUsersPreferences = APIUsersManager::getInstance()->getAllUsersPreferences(array( + APIUsersManager::PREFERENCE_DEFAULT_REPORT_DATE, + APIUsersManager::PREFERENCE_DEFAULT_REPORT + )); + + foreach ($allUsersPreferences as $userLogin => $userPreferences) { + if (!isset($userPreferences[APIUsersManager::PREFERENCE_DEFAULT_REPORT_DATE])) { + continue; + } + + $defaultDate = $userPreferences[APIUsersManager::PREFERENCE_DEFAULT_REPORT_DATE]; + $preference = new UserPreferences(); + $period = $preference->getDefaultPeriod($defaultDate); + if ($period != 'range') { + continue; + } + + if (isset($userPreferences[APIUsersManager::PREFERENCE_DEFAULT_REPORT]) + && is_numeric($userPreferences[APIUsersManager::PREFERENCE_DEFAULT_REPORT])) { + // If user selected one particular website ID + $idSites = array($userPreferences[APIUsersManager::PREFERENCE_DEFAULT_REPORT]); + } else { + // If user selected "All websites" or some other random value, we pre-process all websites that he has access to + $idSites = APISitesManager::getInstance()->getSitesIdWithAtLeastViewAccess($userLogin); + } + + foreach ($idSites as $idSite) { + $customDateRangesToProcessForSites[$idSite][] = $defaultDate; + } + } + + return $customDateRangesToProcessForSites; + } + + /** + * @param $url + * @return string + */ + private function makeRequestUrl($url) + { + $url = $url . self::APPEND_TO_API_REQUEST; + + if ($this->shouldStartProfiler) { + $url .= "&xhprof=2"; + } + + if ($this->testmode) { + $url .= "&testmode=1"; + } + + return $url; + } + + /** + * @param $idSite + * @param $period + * @param $date + * @return Request[] + */ + private function getUrlsWithSegment($idSite, $period, $date) + { + $urlsWithSegment = array(); + $segmentsForSite = $this->getSegmentsForSite($idSite, $period); + + $segments = array(); + foreach ($segmentsForSite as $segment) { + if ($this->shouldSkipSegmentArchiving($segment)) { + $this->logger->info("- skipping segment archiving for '{segment}'.", array('segment' => $segment)); + + continue; + } + + $segments[] = $segment; + } + + $segmentCount = count($segments); + $processedSegmentCount = 0; + foreach ($segments as $segment) { + $dateParamForSegment = $this->segmentArchivingRequestUrlProvider->getUrlParameterDateString($idSite, $period, $date, $segment); + + $urlWithSegment = $this->getVisitsRequestUrl($idSite, $period, $dateParamForSegment, $segment); + $urlWithSegment = $this->makeRequestUrl($urlWithSegment); + + $request = new Request($urlWithSegment); + $logger = $this->logger; + $request->before(function () use ($logger, $segment, $segmentCount, &$processedSegmentCount) { + $processedSegmentCount++; + $logger->info(sprintf( + '- pre-processing segment %d/%d %s', + $processedSegmentCount, + $segmentCount, + $segment + )); + }); + + $urlsWithSegment[] = $request; + } + + return $urlsWithSegment; + } + + private function createSitesToArchiveQueue($websitesIds) + { + // use synchronous, single process queue if --force-idsites is used or sharing site IDs isn't supported + if (!SharedSiteIds::isSupported() || !empty($this->shouldArchiveSpecifiedSites)) { + return new FixedSiteIds($websitesIds); + } + + // use separate shared queue if --force-all-websites is used + if (!empty($this->shouldArchiveAllSites)) { + return new SharedSiteIds($websitesIds, SharedSiteIds::OPTION_ALL_WEBSITES); + } + + return new SharedSiteIds($websitesIds); + } + + /** + * @param $idSite + * @param $period + */ + private function logArchiveWebsite($idSite, $period, $date) + { + $this->logger->info(sprintf( + "Will pre-process for website id = %s, period = %s, date = %s", + $idSite, + $period, + $date + )); + $this->logger->info('- pre-processing all visits'); + } + + private function shouldSkipSegmentArchiving($segment) + { + if ($this->disableSegmentsArchiving) { + return true; + } + + return !empty($this->segmentsToForce) && !in_array($segment, $this->segmentsToForce); + } + + private function logForcedSegmentInfo() + { + if (empty($this->segmentsToForce)) { + return; + } + + $this->logger->info("- Limiting segment archiving to following segments:"); + foreach ($this->segmentsToForce as $segmentDefinition) { + $this->logger->info(" * " . $segmentDefinition); + } + } + + /** + * @return CliMulti + */ + private function makeCliMulti() + { + $cliMulti = StaticContainer::get('Piwik\CliMulti'); + $cliMulti->setUrlToPiwik($this->urlToPiwik); + $cliMulti->setPhpCliConfigurationOptions($this->phpCliConfigurationOptions); + $cliMulti->setAcceptInvalidSSLCertificate($this->acceptInvalidSSLCertificate); + $cliMulti->runAsSuperUser(); + return $cliMulti; + } + + public function setUrlToPiwik($url) + { + $this->urlToPiwik = $url; + } +} diff --git a/www/analytics/core/CronArchive/FixedSiteIds.php b/www/analytics/core/CronArchive/FixedSiteIds.php index 73db0b9d..685d7ebe 100644 --- a/www/analytics/core/CronArchive/FixedSiteIds.php +++ b/www/analytics/core/CronArchive/FixedSiteIds.php @@ -1,6 +1,6 @@ processNewSegmentsFrom = $processNewSegmentsFrom; + $this->segmentEditorModel = $segmentEditorModel ?: new Model(); + $this->segmentListCache = $segmentListCache ?: new Transient(); + $this->now = $now ?: Date::factory('now'); + $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface'); + } + + public function getUrlParameterDateString($idSite, $period, $date, $segment) + { + $oldestDateToProcessForNewSegment = $this->getOldestDateToProcessForNewSegment($idSite, $segment); + if (empty($oldestDateToProcessForNewSegment)) { + return $date; + } + + // if the start date for the archiving request is before the minimum date allowed for processing this segment, + // use the minimum allowed date as the start date + $periodObj = PeriodFactory::build($period, $date); + if ($periodObj->getDateStart()->getTimestamp() < $oldestDateToProcessForNewSegment->getTimestamp()) { + $this->logger->debug("Start date of archiving request period ({start}) is older than configured oldest date to process for the segment.", array( + 'start' => $periodObj->getDateStart() + )); + + $endDate = $periodObj->getDateEnd(); + + // if the creation time of a segment is older than the end date of the archiving request range, we cannot + // blindly rewrite the date string, since the resulting range would be incorrect. instead we make the + // start date equal to the end date, so less archiving occurs, and no fatal error occurs. + if ($oldestDateToProcessForNewSegment->getTimestamp() > $endDate->getTimestamp()) { + $this->logger->debug("Oldest date to process is greater than end date of archiving request period ({end}), so setting oldest date to end date.", array( + 'end' => $endDate + )); + + $oldestDateToProcessForNewSegment = $endDate; + } + + $date = $oldestDateToProcessForNewSegment->toString().','.$endDate; + + $this->logger->debug("Archiving request date range changed to {date} w/ period {period}.", array('date' => $date, 'period' => $period)); + } + + return $date; + } + + private function getOldestDateToProcessForNewSegment($idSite, $segment) + { + /** + * @var Date $segmentCreatedTime + * @var Date $segmentLastEditedTime + */ + list($segmentCreatedTime, $segmentLastEditedTime) = $this->getCreatedTimeOfSegment($idSite, $segment); + + if ($this->processNewSegmentsFrom == self::CREATION_TIME) { + $this->logger->debug("process_new_segments_from set to segment_creation_time, oldest date to process is {time}", array('time' => $segmentCreatedTime)); + + return $segmentCreatedTime; + } elseif ($this->processNewSegmentsFrom == self::LAST_EDIT_TIME) { + $this->logger->debug("process_new_segments_from set to segment_last_edit_time, segment last edit time is {time}", + array('time' => $segmentLastEditedTime)); + + if ($segmentLastEditedTime === null + || $segmentLastEditedTime->getTimestamp() < $segmentCreatedTime->getTimestamp() + ) { + $this->logger->debug("segment last edit time is older than created time, using created time instead"); + + $segmentLastEditedTime = $segmentCreatedTime; + } + + return $segmentLastEditedTime; + } elseif (preg_match("/^last([0-9]+)$/", $this->processNewSegmentsFrom, $matches)) { + $lastN = $matches[1]; + + list($lastDate, $lastPeriod) = Range::getDateXPeriodsAgo($lastN, $segmentCreatedTime, 'day'); + $result = Date::factory($lastDate); + + $this->logger->debug("process_new_segments_from set to last{N}, oldest date to process is {time}", array('N' => $lastN, 'time' => $result)); + + return $result; + } else { + $this->logger->debug("process_new_segments_from set to beginning_of_time or cannot recognize value"); + + return null; + } + } + + private function getCreatedTimeOfSegment($idSite, $segmentDefinition) + { + $segments = $this->getAllSegments(); + + /** @var Date $latestEditTime */ + $latestEditTime = null; + $earliestCreatedTime = $this->now; + foreach ($segments as $segment) { + if (empty($segment['ts_created']) + || empty($segment['definition']) + || !isset($segment['enable_only_idsite']) + ) { + continue; + } + + if ($this->isSegmentForSite($segment, $idSite) + && $segment['definition'] == $segmentDefinition + ) { + // check for an earlier ts_created timestamp + $createdTime = Date::factory($segment['ts_created']); + if ($createdTime->getTimestamp() < $earliestCreatedTime->getTimestamp()) { + $earliestCreatedTime = $createdTime; + } + + // if there is no ts_last_edit timestamp, initialize it to ts_created + if (empty($segment['ts_last_edit'])) { + $segment['ts_last_edit'] = $segment['ts_created']; + } + + // check for a later ts_last_edit timestamp + $lastEditTime = Date::factory($segment['ts_last_edit']); + if ($latestEditTime === null + || $latestEditTime->getTimestamp() < $lastEditTime->getTimestamp() + ) { + $latestEditTime = $lastEditTime; + } + } + } + + $this->logger->debug( + "Earliest created time of segment '{segment}' w/ idSite = {idSite} is found to be {createdTime}. Latest " . + "edit time is found to be {latestEditTime}.", + array( + 'segment' => $segmentDefinition, + 'idSite' => $idSite, + 'createdTime' => $earliestCreatedTime, + 'latestEditTime' => $latestEditTime, + ) + ); + + return array($earliestCreatedTime, $latestEditTime); + } + + private function getAllSegments() + { + if (!$this->segmentListCache->contains('all')) { + $segments = $this->segmentEditorModel->getAllSegmentsAndIgnoreVisibility(); + + $this->segmentListCache->save('all', $segments); + } + + return $this->segmentListCache->fetch('all'); + } + + private function isSegmentForSite($segment, $idSite) + { + return $segment['enable_only_idsite'] == 0 + || $segment['enable_only_idsite'] == $idSite; + } +} diff --git a/www/analytics/core/CronArchive/SharedSiteIds.php b/www/analytics/core/CronArchive/SharedSiteIds.php index c36b449b..f77f3d3f 100644 --- a/www/analytics/core/CronArchive/SharedSiteIds.php +++ b/www/analytics/core/CronArchive/SharedSiteIds.php @@ -1,6 +1,6 @@ optionName = $optionName; + if (empty($websiteIds)) { $websiteIds = array(); } @@ -86,16 +96,16 @@ class SharedSiteIds public function setSiteIdsToArchive($siteIds) { if (!empty($siteIds)) { - Option::set('SharedSiteIdsToArchive', implode(',', $siteIds)); + Option::set($this->optionName, implode(',', $siteIds)); } else { - Option::delete('SharedSiteIdsToArchive'); + Option::delete($this->optionName); } } public function getAllSiteIdsToArchive() { - Option::clearCachedOption('SharedSiteIdsToArchive'); - $siteIdsToArchive = Option::get('SharedSiteIdsToArchive'); + Option::clearCachedOption($this->optionName); + $siteIdsToArchive = Option::get($this->optionName); if (empty($siteIdsToArchive)) { return array(); @@ -171,6 +181,4 @@ class SharedSiteIds { return Process::isSupported(); } - } - diff --git a/www/analytics/core/CronArchive/SitesToReprocessDistributedList.php b/www/analytics/core/CronArchive/SitesToReprocessDistributedList.php new file mode 100644 index 00000000..ad58cc3a --- /dev/null +++ b/www/analytics/core/CronArchive/SitesToReprocessDistributedList.php @@ -0,0 +1,40 @@ +getPeriod()->getDateStart(); - $bindSQL = array($params->getSite()->getId(), - $dateStart->toString('Y-m-d'), - $params->getPeriod()->getDateEnd()->toString('Y-m-d'), - $params->getPeriod()->getId(), - ); + return new Model(); + } - $timeStampWhere = ''; + public static function getArchiveIdAndVisits(ArchiveProcessor\Parameters $params, $minDatetimeArchiveProcessedUTC) + { + $idSite = $params->getSite()->getId(); + $period = $params->getPeriod()->getId(); + $dateStart = $params->getPeriod()->getDateStart(); + $dateStartIso = $dateStart->toString('Y-m-d'); + $dateEndIso = $params->getPeriod()->getDateEnd()->toString('Y-m-d'); + + $numericTable = ArchiveTableCreator::getNumericTable($dateStart); + + $minDatetimeIsoArchiveProcessedUTC = null; if ($minDatetimeArchiveProcessedUTC) { - $timeStampWhere = " AND ts_archived >= ? "; - $bindSQL[] = Date::factory($minDatetimeArchiveProcessedUTC)->getDatetime(); + $minDatetimeIsoArchiveProcessedUTC = Date::factory($minDatetimeArchiveProcessedUTC)->getDatetime(); } $requestedPlugin = $params->getRequestedPlugin(); - $segment = $params->getSegment(); - $isSkipAggregationOfSubTables = $params->isSkipAggregationOfSubTables(); - + $segment = $params->getSegment(); $plugins = array("VisitsSummary", $requestedPlugin); - $sqlWhereArchiveName = self::getNameCondition($plugins, $segment, $isSkipAggregationOfSubTables); - $sqlQuery = " SELECT idarchive, value, name, date1 as startDate - FROM " . ArchiveTableCreator::getNumericTable($dateStart) . "`` - WHERE idsite = ? - AND date1 = ? - AND date2 = ? - AND period = ? - AND ( ($sqlWhereArchiveName) - OR name = '" . self::NB_VISITS_RECORD_LOOKED_UP . "' - OR name = '" . self::NB_VISITS_CONVERTED_RECORD_LOOKED_UP . "') - $timeStampWhere - ORDER BY idarchive DESC"; - $results = Db::fetchAll($sqlQuery, $bindSQL); + $doneFlags = Rules::getDoneFlags($plugins, $segment); + $doneFlagValues = Rules::getSelectableDoneFlagValues(); + + $results = self::getModel()->getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStartIso, $dateEndIso, $minDatetimeIsoArchiveProcessedUTC, $doneFlags, $doneFlagValues); + if (empty($results)) { return false; } - $idArchive = self::getMostRecentIdArchiveFromResults($segment, $requestedPlugin, $isSkipAggregationOfSubTables, $results); - $idArchiveVisitsSummary = self::getMostRecentIdArchiveFromResults($segment, "VisitsSummary", $isSkipAggregationOfSubTables, $results); + $idArchive = self::getMostRecentIdArchiveFromResults($segment, $requestedPlugin, $results); + + $idArchiveVisitsSummary = self::getMostRecentIdArchiveFromResults($segment, "VisitsSummary", $results); list($visits, $visitsConverted) = self::getVisitsMetricsFromResults($idArchive, $idArchiveVisitsSummary, $results); - if ($visits === false - && $idArchive === false - ) { + if (false === $visits && false === $idArchive) { return false; } @@ -98,9 +90,11 @@ class ArchiveSelector { $visits = $visitsConverted = false; $archiveWithVisitsMetricsWasFound = ($idArchiveVisitsSummary !== false); + if ($archiveWithVisitsMetricsWasFound) { $visits = $visitsConverted = 0; } + foreach ($results as $result) { if (in_array($result['idarchive'], array($idArchive, $idArchiveVisitsSummary))) { $value = (int)$result['value']; @@ -116,13 +110,15 @@ class ArchiveSelector } } } + return array($visits, $visitsConverted); } - protected static function getMostRecentIdArchiveFromResults(Segment $segment, $requestedPlugin, $isSkipAggregationOfSubTables, $results) + protected static function getMostRecentIdArchiveFromResults(Segment $segment, $requestedPlugin, $results) { $idArchive = false; - $namesRequestedPlugin = Rules::getDoneFlags(array($requestedPlugin), $segment, $isSkipAggregationOfSubTables); + $namesRequestedPlugin = Rules::getDoneFlags(array($requestedPlugin), $segment); + foreach ($results as $result) { if ($idArchive === false && in_array($result['name'], $namesRequestedPlugin) @@ -131,6 +127,7 @@ class ArchiveSelector break; } } + return $idArchive; } @@ -141,21 +138,29 @@ class ArchiveSelector * @param array $periods * @param Segment $segment * @param array $plugins List of plugin names for which data is being requested. - * @param bool $isSkipAggregationOfSubTables Whether we are selecting an archive that may be partial (no sub-tables) * @return array Archive IDs are grouped by archive name and period range, ie, * array( * 'VisitsSummary.done' => array( * '2010-01-01' => array(1,2,3) * ) * ) + * @throws */ - static public function getArchiveIds($siteIds, $periods, $segment, $plugins, $isSkipAggregationOfSubTables = false) + public static function getArchiveIds($siteIds, $periods, $segment, $plugins) { + if (empty($siteIds)) { + throw new \Exception("Website IDs could not be read from the request, ie. idSite="); + } + + foreach ($siteIds as $index => $siteId) { + $siteIds[$index] = (int) $siteId; + } + $getArchiveIdsSql = "SELECT idsite, name, date1, date2, MAX(idarchive) as idarchive FROM %s - WHERE %s - AND " . self::getNameCondition($plugins, $segment, $isSkipAggregationOfSubTables) . " - AND idsite IN (" . implode(',', $siteIds) . ") + WHERE idsite IN (" . implode(',', $siteIds) . ") + AND " . self::getNameCondition($plugins, $segment) . " + AND %s GROUP BY idsite, date1, date2"; $monthToPeriods = array(); @@ -197,14 +202,14 @@ class ArchiveSelector $sql = sprintf($getArchiveIdsSql, $table, $dateCondition); + $archiveIds = Db::fetchAll($sql, $bind); + // get the archive IDs - foreach (Db::fetchAll($sql, $bind) as $row) { - $archiveName = $row['name']; - + foreach ($archiveIds as $row) { //FIXMEA duplicate with Archive.php - $dateStr = $row['date1'] . "," . $row['date2']; + $dateStr = $row['date1'] . ',' . $row['date2']; - $result[$archiveName][$dateStr][] = $row['idarchive']; + $result[$row['name']][$dateStr][] = $row['idarchive']; } } @@ -215,29 +220,52 @@ class ArchiveSelector * Queries and returns archive data using a set of archive IDs. * * @param array $archiveIds The IDs of the archives to get data from. - * @param array $recordNames The names of the data to retrieve (ie, nb_visits, nb_actions, etc.) + * @param array $recordNames The names of the data to retrieve (ie, nb_visits, nb_actions, etc.). + * Note: You CANNOT pass multiple recordnames if $loadAllSubtables=true. * @param string $archiveDataType The archive data type (either, 'blob' or 'numeric'). - * @param bool $loadAllSubtables Whether to pre-load all subtables + * @param int|null|string $idSubtable null if the root blob should be loaded, an integer if a subtable should be + * loaded and 'all' if all subtables should be loaded. * @throws Exception * @return array */ - static public function getArchiveData($archiveIds, $recordNames, $archiveDataType, $loadAllSubtables) + public static function getArchiveData($archiveIds, $recordNames, $archiveDataType, $idSubtable) { + $chunk = new Chunk(); + // create the SQL to select archive data - $inNames = Common::getSqlStringFieldsArray($recordNames); + $loadAllSubtables = $idSubtable == Archive::ID_SUBTABLE_LOAD_ALL_SUBTABLES; if ($loadAllSubtables) { $name = reset($recordNames); // select blobs w/ name like "$name_[0-9]+" w/o using RLIKE - $nameEnd = strlen($name) + 2; - $whereNameIs = "(name = ? - OR (name LIKE ? - AND SUBSTRING(name, $nameEnd, 1) >= '0' - AND SUBSTRING(name, $nameEnd, 1) <= '9') )"; + $nameEnd = strlen($name) + 1; + $nameEndAppendix = $nameEnd + 1; + $appendix = $chunk->getAppendix(); + $lenAppendix = strlen($appendix); + + $checkForChunkBlob = "SUBSTRING(name, $nameEnd, $lenAppendix) = '$appendix'"; + $checkForSubtableId = "(SUBSTRING(name, $nameEndAppendix, 1) >= '0' + AND SUBSTRING(name, $nameEndAppendix, 1) <= '9')"; + + $whereNameIs = "(name = ? OR (name LIKE ? AND ( $checkForChunkBlob OR $checkForSubtableId ) ))"; $bind = array($name, $name . '%'); } else { + if ($idSubtable === null) { + // select root table or specific record names + $bind = array_values($recordNames); + } else { + // select a subtable id + $bind = array(); + foreach ($recordNames as $recordName) { + // to be backwards compatibe we need to look for the exact idSubtable blob and for the chunk + // that stores the subtables (a chunk stores many blobs in one blob) + $bind[] = $chunk->getRecordNameForTableId($recordName, $idSubtable); + $bind[] = self::appendIdSubtable($recordName, $idSubtable); + } + } + + $inNames = Common::getSqlStringFieldsArray($bind); $whereNameIs = "name IN ($inNames)"; - $bind = array_values($recordNames); } $getValuesSql = "SELECT value, name, idsite, date1, date2, ts_archived @@ -251,110 +279,91 @@ class ArchiveSelector if (empty($ids)) { throw new Exception("Unexpected: id archive not found for period '$period' '"); } + // $period = "2009-01-04,2009-01-04", $date = Date::factory(substr($period, 0, 10)); - if ($archiveDataType == 'numeric') { + + $isNumeric = $archiveDataType == 'numeric'; + if ($isNumeric) { $table = ArchiveTableCreator::getNumericTable($date); } else { $table = ArchiveTableCreator::getBlobTable($date); } - $sql = sprintf($getValuesSql, $table, implode(',', $ids)); + + $sql = sprintf($getValuesSql, $table, implode(',', $ids)); $dataRows = Db::fetchAll($sql, $bind); + foreach ($dataRows as $row) { - $rows[] = $row; + if ($isNumeric) { + $rows[] = $row; + } else { + $row['value'] = self::uncompress($row['value']); + + if ($chunk->isRecordNameAChunk($row['name'])) { + self::moveChunkRowToRows($rows, $row, $chunk, $loadAllSubtables, $idSubtable); + } else { + $rows[] = $row; + } + } } } return $rows; } + private static function moveChunkRowToRows(&$rows, $row, Chunk $chunk, $loadAllSubtables, $idSubtable) + { + // $blobs = array([subtableID] = [blob of subtableId]) + $blobs = unserialize($row['value']); + + if (!is_array($blobs)) { + return; + } + + // $rawName = eg 'PluginName_ArchiveName' + $rawName = $chunk->getRecordNameWithoutChunkAppendix($row['name']); + + if ($loadAllSubtables) { + foreach ($blobs as $subtableId => $blob) { + $row['value'] = $blob; + $row['name'] = self::appendIdSubtable($rawName, $subtableId); + $rows[] = $row; + } + } elseif (array_key_exists($idSubtable, $blobs)) { + $row['value'] = $blobs[$idSubtable]; + $row['name'] = self::appendIdSubtable($rawName, $idSubtable); + $rows[] = $row; + } + } + + public static function appendIdSubtable($recordName, $id) + { + return $recordName . "_" . $id; + } + + private static function uncompress($data) + { + return @gzuncompress($data); + } + /** * Returns the SQL condition used to find successfully completed archives that * this instance is querying for. * * @param array $plugins * @param Segment $segment - * @param bool $isSkipAggregationOfSubTables * @return string */ - static private function getNameCondition(array $plugins, Segment $segment, $isSkipAggregationOfSubTables) + private static function getNameCondition(array $plugins, Segment $segment) { // the flags used to tell how the archiving process for a specific archive was completed, // if it was completed - $doneFlags = Rules::getDoneFlags($plugins, $segment, $isSkipAggregationOfSubTables); - + $doneFlags = Rules::getDoneFlags($plugins, $segment); $allDoneFlags = "'" . implode("','", $doneFlags) . "'"; + $possibleValues = Rules::getSelectableDoneFlagValues(); + // create the SQL to find archives that are DONE - return "((name IN ($allDoneFlags)) AND " . - " (value = '" . ArchiveWriter::DONE_OK . "' OR " . - " value = '" . ArchiveWriter::DONE_OK_TEMPORARY . "'))"; - } - - static public function purgeOutdatedArchives(Date $dateStart) - { - $purgeArchivesOlderThan = Rules::shouldPurgeOutdatedArchives($dateStart); - if (!$purgeArchivesOlderThan) { - return; - } - - $idArchivesToDelete = self::getTemporaryArchiveIdsOlderThan($dateStart, $purgeArchivesOlderThan); - if (!empty($idArchivesToDelete)) { - self::deleteArchiveIds($dateStart, $idArchivesToDelete); - } - self::deleteArchivesWithPeriodRange($dateStart); - - Log::debug("Purging temporary archives: done [ purged archives older than %s in %s ] [Deleted IDs: %s]", - $purgeArchivesOlderThan, $dateStart->toString("Y-m"), implode(',', $idArchivesToDelete)); - } - - /* - * Deleting "Custom Date Range" reports after 1 day, since they can be re-processed and would take up un-necessary space - */ - protected static function deleteArchivesWithPeriodRange(Date $date) - { - $query = "DELETE FROM %s WHERE period = ? AND ts_archived < ?"; - - $yesterday = Date::factory('yesterday')->getDateTime(); - $bind = array(Piwik::$idPeriods['range'], $yesterday); - $numericTable = ArchiveTableCreator::getNumericTable($date); - Db::query(sprintf($query, $numericTable), $bind); - Log::debug("Purging Custom Range archives: done [ purged archives older than %s from %s / blob ]", $yesterday, $numericTable); - try { - Db::query(sprintf($query, ArchiveTableCreator::getBlobTable($date)), $bind); - } catch (Exception $e) { - // Individual blob tables could be missing - } - } - - protected static function deleteArchiveIds(Date $date, $idArchivesToDelete) - { - $query = "DELETE FROM %s WHERE idarchive IN (" . implode(',', $idArchivesToDelete) . ")"; - - Db::query(sprintf($query, ArchiveTableCreator::getNumericTable($date))); - try { - Db::query(sprintf($query, ArchiveTableCreator::getBlobTable($date))); - } catch (Exception $e) { - // Individual blob tables could be missing - } - } - - protected static function getTemporaryArchiveIdsOlderThan(Date $date, $purgeArchivesOlderThan) - { - $query = "SELECT idarchive - FROM " . ArchiveTableCreator::getNumericTable($date) . " - WHERE name LIKE 'done%' - AND (( value = " . ArchiveWriter::DONE_OK_TEMPORARY . " - AND ts_archived < ?) - OR value = " . ArchiveWriter::DONE_ERROR . ")"; - - $result = Db::fetchAll($query, array($purgeArchivesOlderThan)); - $idArchivesToDelete = array(); - if (!empty($result)) { - foreach ($result as $row) { - $idArchivesToDelete[] = $row['idarchive']; - } - } - return $idArchivesToDelete; + return "((name IN ($allDoneFlags)) AND (value IN (" . implode(',', $possibleValues) . ")))"; } } diff --git a/www/analytics/core/DataAccess/ArchiveTableCreator.php b/www/analytics/core/DataAccess/ArchiveTableCreator.php index 6f8d0b90..ad95863d 100644 --- a/www/analytics/core/DataAccess/ArchiveTableCreator.php +++ b/www/analytics/core/DataAccess/ArchiveTableCreator.php @@ -1,6 +1,6 @@ toString('Y_m'); + $tableName = $tableNamePrefix . "_" . self::getTableMonthFromDate($date); $tableName = Common::prefixTable($tableName); + self::createArchiveTablesIfAbsent($tableName, $tableNamePrefix); + return $tableName; } - static protected function createArchiveTablesIfAbsent($tableName, $tableNamePrefix) + protected static function createArchiveTablesIfAbsent($tableName, $tableNamePrefix) { if (is_null(self::$tablesAlreadyInstalled)) { self::refreshTableList(); } if (!in_array($tableName, self::$tablesAlreadyInstalled)) { - $db = Db::get(); - $sql = DbHelper::getTableCreateSql($tableNamePrefix); - - // replace table name template by real name - $tableNamePrefix = Common::prefixTable($tableNamePrefix); - $sql = str_replace($tableNamePrefix, $tableName, $sql); - try { - $db->query($sql); - } catch (Exception $e) { - // accept mysql error 1050: table already exists, throw otherwise - if (!$db->isErrNo($e, '1050')) { - throw $e; - } - } + self::getModel()->createArchiveTable($tableName, $tableNamePrefix); self::$tablesAlreadyInstalled[] = $tableName; } } - static public function clear() + private static function getModel() + { + return new Model(); + } + + public static function clear() { self::$tablesAlreadyInstalled = null; } - static public function refreshTableList($forceReload = false) + public static function refreshTableList($forceReload = false) { self::$tablesAlreadyInstalled = DbHelper::getTablesInstalled($forceReload); } @@ -80,40 +71,53 @@ class ArchiveTableCreator /** * Returns all table names archive_* * + * @param string $type The type of table to return. Either `self::NUMERIC_TABLE` or `self::BLOB_TABLE`. * @return array */ - static public function getTablesArchivesInstalled() + public static function getTablesArchivesInstalled($type = null) { if (is_null(self::$tablesAlreadyInstalled)) { self::refreshTableList(); } + if (empty($type)) { + $tableMatchRegex = '/archive_(numeric|blob)_/'; + } else { + $tableMatchRegex = '/archive_' . preg_quote($type) . '_/'; + } + $archiveTables = array(); foreach (self::$tablesAlreadyInstalled as $table) { - if (strpos($table, 'archive_numeric_') !== false - || strpos($table, 'archive_blob_') !== false - ) { + if (preg_match($tableMatchRegex, $table)) { $archiveTables[] = $table; } } return $archiveTables; } - static public function getDateFromTableName($tableName) + public static function getDateFromTableName($tableName) { $tableName = Common::unprefixTable($tableName); - $date = str_replace(array('archive_numeric_', 'archive_blob_'), '', $tableName); + $date = str_replace(array('archive_numeric_', 'archive_blob_'), '', $tableName); + return $date; } - static public function getTypeFromTableName($tableName) + public static function getTableMonthFromDate(Date $date) + { + return $date->toString('Y_m'); + } + + public static function getTypeFromTableName($tableName) { if (strpos($tableName, 'archive_numeric_') !== false) { return self::NUMERIC_TABLE; } + if (strpos($tableName, 'archive_blob_') !== false) { return self::BLOB_TABLE; } + return false; } } diff --git a/www/analytics/core/DataAccess/ArchiveTableDao.php b/www/analytics/core/DataAccess/ArchiveTableDao.php new file mode 100644 index 00000000..889b170b --- /dev/null +++ b/www/analytics/core/DataAccess/ArchiveTableDao.php @@ -0,0 +1,89 @@ + '-', + 'count_invalidated_archives' => '-', + 'count_temporary_archives' => '-', + 'count_error_archives' => '-', + 'count_segment_archives' => '-', + 'count_numeric_rows' => '-', + ); + + $tableDate = str_replace("`", "", $tableDate); // for sanity + + $numericTable = Common::prefixTable("archive_numeric_$tableDate"); + $blobTable = Common::prefixTable("archive_blob_$tableDate"); + + // query numeric table + $sql = "SELECT CONCAT_WS('.', idsite, date1, date2, period) AS label, + SUM(CASE WHEN name LIKE 'done%' THEN 1 ELSE 0 END) AS count_archives, + SUM(CASE WHEN name LIKE 'done%' AND value = ? THEN 1 ELSE 0 END) AS count_invalidated_archives, + SUM(CASE WHEN name LIKE 'done%' AND value = ? THEN 1 ELSE 0 END) AS count_temporary_archives, + SUM(CASE WHEN name LIKE 'done%' AND value = ? THEN 1 ELSE 0 END) AS count_error_archives, + SUM(CASE WHEN name LIKE 'done%' AND CHAR_LENGTH(name) > 32 THEN 1 ELSE 0 END) AS count_segment_archives, + SUM(CASE WHEN name NOT LIKE 'done%' THEN 1 ELSE 0 END) AS count_numeric_rows, + 0 AS count_blob_rows + FROM `$numericTable` + GROUP BY idsite, date1, date2, period"; + + $rows = Db::fetchAll($sql, array(ArchiveWriter::DONE_INVALIDATED, ArchiveWriter::DONE_OK_TEMPORARY, + ArchiveWriter::DONE_ERROR)); + + // index result + $result = array(); + foreach ($rows as $row) { + $result[$row['label']] = $row; + } + + // query blob table & manually merge results (no FULL OUTER JOIN in mysql) + $sql = "SELECT CONCAT_WS('.', idsite, date1, date2, period) AS label, + COUNT(*) AS count_blob_rows + FROM `$blobTable` + GROUP BY idsite, date1, date1, period"; + + foreach (Db::fetchAll($sql) as $blobStatsRow) { + $label = $blobStatsRow['label']; + if (isset($result[$label])) { + $result[$label] = array_merge($result[$label], $blobStatsRow); + } else { + $result[$label] = $blobStatsRow + $numericQueryEmptyRow; + } + } + + return $result; + } +} \ No newline at end of file diff --git a/www/analytics/core/DataAccess/ArchiveWriter.php b/www/analytics/core/DataAccess/ArchiveWriter.php index 8c54dd52..3473a33c 100644 --- a/www/analytics/core/DataAccess/ArchiveWriter.php +++ b/www/analytics/core/DataAccess/ArchiveWriter.php @@ -1,6 +1,6 @@ idArchive = false; - $this->idSite = $params->getSite()->getId(); - $this->segment = $params->getSegment(); - $this->period = $params->getPeriod(); + $this->idSite = $params->getSite()->getId(); + $this->segment = $params->getSegment(); + $this->period = $params->getPeriod(); + $idSites = array($this->idSite); - $this->doneFlag = Rules::getDoneStringFlagFor($idSites, $this->segment, $this->period->getLabel(), $params->getRequestedPlugin(), $params->isSkipAggregationOfSubTables()); + $this->doneFlag = Rules::getDoneStringFlagFor($idSites, $this->segment, $this->period->getLabel(), $params->getRequestedPlugin()); $this->isArchiveTemporary = $isArchiveTemporary; $this->dateStart = $this->period->getDateStart(); @@ -74,25 +77,32 @@ class ArchiveWriter /** * @param string $name - * @param string[] $values + * @param string|string[] $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 splitted into chunks. All subtables + * within one chunk will be serialized as an array where the index is the + * subtableId. */ public function insertBlobRecord($name, $values) { if (is_array($values)) { $clean = array(); - foreach ($values as $id => $value) { - // for the parent Table we keep the name - // for example for the Table of searchEngines we keep the name 'referrer_search_engine' - // but for the child table of 'Google' which has the ID = 9 the name would be 'referrer_search_engine_9' - $newName = $name; - if ($id != 0) { - //FIXMEA: refactor - $newName = $name . '_' . $id; - } - $value = $this->compress($value); - $clean[] = array($newName, $value); + if (isset($values[0])) { + // we always store the root table in a single blob for fast access + $clean[] = array($name, $this->compress($values[0])); + unset($values[0]); } + + if (!empty($values)) { + // we move all subtables into chunks + $chunk = new Chunk(); + $chunks = $chunk->moveArchiveBlobsIntoChunks($name, $values); + foreach ($chunks as $index => $subtables) { + $clean[] = array($index, $this->compress(serialize($subtables))); + } + } + $this->insertBulkRecords($clean); return; } @@ -106,6 +116,7 @@ class ArchiveWriter if ($this->idArchive === false) { throw new Exception("Must call allocateNewArchiveId() first"); } + return $this->idArchive; } @@ -117,108 +128,49 @@ class ArchiveWriter public function finalizeArchive() { - $this->deletePreviousArchiveStatus(); + $numericTable = $this->getTableNumeric(); + $idArchive = $this->getIdArchive(); + + $this->getModel()->deletePreviousArchiveStatus($numericTable, $idArchive, $this->doneFlag); + $this->logArchiveStatusAsFinal(); } - static protected function compress($data) + protected function compress($data) { if (Db::get()->hasBlobDataType()) { return gzcompress($data); } + return $data; } - protected function getArchiveLockName() - { - $numericTable = $this->getTableNumeric(); - $dbLockName = "allocateNewArchiveId.$numericTable"; - return $dbLockName; - } - - protected function acquireArchiveTableLock() - { - $dbLockName = $this->getArchiveLockName(); - if (Db::getDbLock($dbLockName, $maxRetries = 30) === false) { - throw new Exception("allocateNewArchiveId: Cannot get named lock $dbLockName."); - } - } - - protected function releaseArchiveTableLock() - { - $dbLockName = $this->getArchiveLockName(); - Db::releaseDbLock($dbLockName); - } - protected function allocateNewArchiveId() { - $this->idArchive = $this->insertNewArchiveId(); + $numericTable = $this->getTableNumeric(); + + $this->idArchive = $this->getModel()->allocateNewArchiveId($numericTable); return $this->idArchive; } - /** - * Locks the archive table to generate a new archive ID. - * - * We lock to make sure that - * if several archiving processes are running at the same time (for different websites and/or periods) - * then they will each use a unique archive ID. - * - * @return int - */ - protected function insertNewArchiveId() + private function getModel() { - $numericTable = $this->getTableNumeric(); - $idSite = $this->idSite; - - $this->acquireArchiveTableLock(); - - $locked = self::PREFIX_SQL_LOCK . Common::generateUniqId(); - $date = date("Y-m-d H:i:s"); - $insertSql = "INSERT INTO $numericTable " - . " SELECT IFNULL( MAX(idarchive), 0 ) + 1, - '" . $locked . "', - " . (int)$idSite . ", - '" . $date . "', - '" . $date . "', - 0, - '" . $date . "', - 0 " - . " FROM $numericTable as tb1"; - Db::get()->exec($insertSql); - - $this->releaseArchiveTableLock(); - - $selectIdSql = "SELECT idarchive FROM $numericTable WHERE name = ? LIMIT 1"; - $id = Db::get()->fetchOne($selectIdSql, $locked); - return $id; + return new Model(); } protected function logArchiveStatusAsIncomplete() { - $statusWhileProcessing = self::DONE_ERROR; - $this->insertRecord($this->doneFlag, $statusWhileProcessing); - } - - protected function deletePreviousArchiveStatus() - { - // without advisory lock here, the DELETE would acquire Exclusive Lock - $this->acquireArchiveTableLock(); - - Db::query("DELETE FROM " . $this->getTableNumeric() . " - WHERE idarchive = ? AND (name = '" . $this->doneFlag - . "' OR name LIKE '" . self::PREFIX_SQL_LOCK . "%')", - array($this->getIdArchive()) - ); - - $this->releaseArchiveTableLock(); + $this->insertRecord($this->doneFlag, self::DONE_ERROR); } protected function logArchiveStatusAsFinal() { $status = self::DONE_OK; + if ($this->isArchiveTemporary) { $status = self::DONE_OK_TEMPORARY; } + $this->insertRecord($this->doneFlag, $status); } @@ -231,27 +183,37 @@ class ArchiveWriter foreach ($records as $record) { $this->insertRecord($record[0], $record[1]); } + return true; } + $bindSql = $this->getInsertRecordBind(); - $values = array(); + $values = array(); $valueSeen = false; foreach ($records as $record) { // don't record zero - if (empty($record[1])) continue; + if (empty($record[1])) { + continue; + } - $bind = $bindSql; - $bind[] = $record[0]; // name - $bind[] = $record[1]; // value + $bind = $bindSql; + $bind[] = $record[0]; // name + $bind[] = $record[1]; // value $values[] = $bind; $valueSeen = $record[1]; } - if (empty($values)) return true; + + if (empty($values)) { + return true; + } $tableName = $this->getTableNameToInsert($valueSeen); - BatchInsert::tableInsertBatch($tableName, $this->getInsertFields(), $values); + $fields = $this->getInsertFields(); + + BatchInsert::tableInsertBatch($tableName, $fields, $values, $throwException = false, $charset = 'latin1'); + return true; } @@ -270,26 +232,22 @@ class ArchiveWriter } $tableName = $this->getTableNameToInsert($value); + $fields = $this->getInsertFields(); + $record = $this->getInsertRecordBind(); + + $this->getModel()->insertRecord($tableName, $fields, $record, $name, $value); - // duplicate idarchives are Ignored, see http://dev.piwik.org/trac/ticket/987 - $query = "INSERT IGNORE INTO " . $tableName . " - (" . implode(", ", $this->getInsertFields()) . ") - VALUES (?,?,?,?,?,?,?,?)"; - $bindSql = $this->getInsertRecordBind(); - $bindSql[] = $name; - $bindSql[] = $value; - Db::query($query, $bindSql); return true; } protected function getInsertRecordBind() { return array($this->getIdArchive(), - $this->idSite, - $this->dateStart->toString('Y-m-d'), - $this->period->getDateEnd()->toString('Y-m-d'), - $this->period->getId(), - date("Y-m-d H:i:s")); + $this->idSite, + $this->dateStart->toString('Y-m-d'), + $this->period->getDateEnd()->toString('Y-m-d'), + $this->period->getId(), + date("Y-m-d H:i:s")); } protected function getTableNameToInsert($value) @@ -297,6 +255,7 @@ class ArchiveWriter if (is_numeric($value)) { return $this->getTableNumeric(); } + return ArchiveTableCreator::getBlobTable($this->dateStart); } diff --git a/www/analytics/core/DataAccess/LogAggregator.php b/www/analytics/core/DataAccess/LogAggregator.php index f3a3a740..ceeba0b1 100644 --- a/www/analytics/core/DataAccess/LogAggregator.php +++ b/www/analytics/core/DataAccess/LogAggregator.php @@ -1,6 +1,6 @@ getLogAggregator(); - * + * * // get metrics for every used browser language of all visits by returning visitors * $query = $logAggregator->queryVisitsByDimension( * $dimensions = array('log_visit.location_browser_lang'), * $where = 'log_visit.visitor_returning = 1', - * + * * // also count visits for each browser language that are not located in the US * $additionalSelects = array('sum(case when log_visit.location_country <> 'us' then 1 else 0 end) as nonus'), - * + * * // we're only interested in visits, unique visitors & actions, so don't waste time calculating anything else * $metrics = array(Metrics::INDEX_NB_UNIQ_VISITORS, Metrics::INDEX_NB_VISITS, Metrics::INDEX_NB_ACTIONS), * ); * if ($query === false) { * return; * } - * + * * while ($row = $query->fetch()) { * $uniqueVisitors = $row[Metrics::INDEX_NB_UNIQ_VISITORS]; * $visits = $row[Metrics::INDEX_NB_VISITS]; @@ -89,8 +87,8 @@ use Piwik\Tracker\GoalManager; * $country = $row['location_country']; * $numEcommerceSales = $row[Metrics::INDEX_GOAL_NB_CONVERSIONS]; * $numVisitsWithEcommerceSales = $row[Metrics::INDEX_GOAL_NB_VISITS_CONVERTED]; - * $avgTaxForCountry = $country['avg_tax']; - * $maxShippingForCountry = $country['max_shipping']; + * $avgTaxForCountry = $row['avg_tax']; + * $maxShippingForCountry = $row['max_shipping']; * * // ... do something with aggregated data ... * } @@ -131,15 +129,20 @@ class LogAggregator /** @var \Piwik\Date */ protected $dateEnd; - /** @var \Piwik\Site */ - protected $site; + /** @var int[] */ + protected $sites; /** @var \Piwik\Segment */ protected $segment; + /** + * @var string + */ + private $queryOriginHint = ''; + /** * Constructor. - * + * * @param \Piwik\ArchiveProcessor\Parameters $params */ public function __construct(Parameters $params) @@ -147,30 +150,44 @@ class LogAggregator $this->dateStart = $params->getDateStart(); $this->dateEnd = $params->getDateEnd(); $this->segment = $params->getSegment(); - $this->site = $params->getSite(); + $this->sites = $params->getIdSites(); + } + + public function setQueryOriginHint($nameOfOrigiin) + { + $this->queryOriginHint = $nameOfOrigiin; } public function generateQuery($select, $from, $where, $groupBy, $orderBy) { - $bind = $this->getBindDatetimeSite(); + $bind = $this->getGeneralQueryBindParams(); $query = $this->segment->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); + + $select = 'SELECT'; + if ($this->queryOriginHint && is_array($query) && 0 === strpos(trim($query['sql']), $select)) { + $query['sql'] = trim($query['sql']); + $query['sql'] = 'SELECT /* ' . $this->queryOriginHint . ' */' . substr($query['sql'], strlen($select)); + } + return $query; } protected function getVisitsMetricFields() { return array( - Metrics::INDEX_NB_UNIQ_VISITORS => "count(distinct " . self::LOG_VISIT_TABLE . ".idvisitor)", - Metrics::INDEX_NB_VISITS => "count(*)", - Metrics::INDEX_NB_ACTIONS => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_actions)", - Metrics::INDEX_MAX_ACTIONS => "max(" . self::LOG_VISIT_TABLE . ".visit_total_actions)", - Metrics::INDEX_SUM_VISIT_LENGTH => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_time)", - Metrics::INDEX_BOUNCE_COUNT => "sum(case " . self::LOG_VISIT_TABLE . ".visit_total_actions when 1 then 1 when 0 then 1 else 0 end)", - Metrics::INDEX_NB_VISITS_CONVERTED => "sum(case " . self::LOG_VISIT_TABLE . ".visit_goal_converted when 1 then 1 else 0 end)", + Metrics::INDEX_NB_UNIQ_VISITORS => "count(distinct " . self::LOG_VISIT_TABLE . ".idvisitor)", + Metrics::INDEX_NB_UNIQ_FINGERPRINTS => "count(distinct " . self::LOG_VISIT_TABLE . ".config_id)", + Metrics::INDEX_NB_VISITS => "count(*)", + Metrics::INDEX_NB_ACTIONS => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_actions)", + Metrics::INDEX_MAX_ACTIONS => "max(" . self::LOG_VISIT_TABLE . ".visit_total_actions)", + Metrics::INDEX_SUM_VISIT_LENGTH => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_time)", + Metrics::INDEX_BOUNCE_COUNT => "sum(case " . self::LOG_VISIT_TABLE . ".visit_total_actions when 1 then 1 when 0 then 1 else 0 end)", + Metrics::INDEX_NB_VISITS_CONVERTED => "sum(case " . self::LOG_VISIT_TABLE . ".visit_goal_converted when 1 then 1 else 0 end)", + Metrics::INDEX_NB_USERS => "count(distinct " . self::LOG_VISIT_TABLE . ".user_id)", ); } - static public function getConversionsMetricFields() + public static function getConversionsMetricFields() { return array( Metrics::INDEX_GOAL_NB_CONVERSIONS => "count(*)", @@ -184,12 +201,12 @@ class LogAggregator ); } - static private function getSqlConversionRevenueSum($field) + private static function getSqlConversionRevenueSum($field) { return self::getSqlRevenue('SUM(' . self::LOG_CONVERSION_TABLE . '.' . $field . ')'); } - static public function getSqlRevenue($field) + public static function getSqlRevenue($field) { return "ROUND(" . $field . "," . GoalManager::REVENUE_PRECISION . ")"; } @@ -271,7 +288,7 @@ class LogAggregator * clause. These can be aggregate expressions, eg, `SUM(somecol)`. * @param bool|array $metrics The set of metrics to calculate and return. If false, the query will select * all of them. The following values can be used: - * + * * - {@link Piwik\Metrics::INDEX_NB_UNIQ_VISITORS} * - {@link Piwik\Metrics::INDEX_NB_VISITS} * - {@link Piwik\Metrics::INDEX_NB_ACTIONS} @@ -293,52 +310,61 @@ class LogAggregator $tableName = self::LOG_VISIT_TABLE; $availableMetrics = $this->getVisitsMetricFields(); - $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics); - $from = array($tableName); - $where = $this->getWhereStatement($tableName, self::VISIT_DATETIME_FIELD, $where); + $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics); + $from = array($tableName); + $where = $this->getWhereStatement($tableName, self::VISIT_DATETIME_FIELD, $where); $groupBy = $this->getGroupByStatement($dimensions, $tableName); $orderBy = false; if ($rankingQuery) { $orderBy = '`' . Metrics::INDEX_NB_VISITS . '` DESC'; } + $query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy); if ($rankingQuery) { unset($availableMetrics[Metrics::INDEX_MAX_ACTIONS]); $sumColumns = array_keys($availableMetrics); + if ($metrics) { $sumColumns = array_intersect($sumColumns, $metrics); } + $rankingQuery->addColumn($sumColumns, 'sum'); if ($this->isMetricRequested(Metrics::INDEX_MAX_ACTIONS, $metrics)) { $rankingQuery->addColumn(Metrics::INDEX_MAX_ACTIONS, 'max'); } + return $rankingQuery->execute($query['sql'], $query['bind']); } + return $this->getDb()->query($query['sql'], $query['bind']); } protected function getSelectsMetrics($metricsAvailable, $metricsRequested = false) { $selects = array(); + foreach ($metricsAvailable as $metricId => $statement) { if ($this->isMetricRequested($metricId, $metricsRequested)) { - $aliasAs = $this->getSelectAliasAs($metricId); + $aliasAs = $this->getSelectAliasAs($metricId); $selects[] = $statement . $aliasAs; } } + return $selects; } protected function getSelectStatement($dimensions, $tableName, $additionalSelects, array $availableMetrics, $requestedMetrics = false) { $dimensionsToSelect = $this->getDimensionsToSelect($dimensions, $additionalSelects); + $selects = array_merge( $this->getSelectDimensions($dimensionsToSelect, $tableName), $this->getSelectsMetrics($availableMetrics, $requestedMetrics), !empty($additionalSelects) ? $additionalSelects : array() ); + $select = implode(self::FIELDS_SEPARATOR, $selects); return $select; } @@ -355,6 +381,7 @@ class LogAggregator if (empty($additionalSelects)) { return $dimensions; } + $dimensionsToSelect = array(); foreach ($dimensions as $selectAs => $dimension) { $asAlias = $this->getSelectAliasAs($dimension); @@ -364,6 +391,7 @@ class LogAggregator } } } + $dimensionsToSelect = array_unique($dimensionsToSelect); return $dimensionsToSelect; } @@ -382,6 +410,7 @@ class LogAggregator { foreach ($dimensions as $selectAs => &$field) { $selectAsString = $field; + if (!is_numeric($selectAs)) { $selectAsString = $selectAs; } else { @@ -390,16 +419,18 @@ class LogAggregator $selectAsString = $appendSelectAs = false; } } + $isKnownField = !in_array($field, array('referrer_data')); - if ($selectAsString == $field - && $isKnownField - ) { + + if ($selectAsString == $field && $isKnownField) { $field = $this->prefixColumn($field, $tableName); } + if ($appendSelectAs && $selectAsString) { $field = $this->prefixColumn($field, $tableName) . $this->getSelectAliasAs($selectAsString); } } + return $dimensions; } @@ -422,7 +453,7 @@ class LogAggregator protected function isFieldFunctionOrComplexExpression($field) { return strpos($field, "(") !== false - || strpos($field, "CASE") !== false; + || strpos($field, "CASE") !== false; } protected function getSelectAliasAs($metricId) @@ -432,32 +463,50 @@ class LogAggregator protected function isMetricRequested($metricId, $metricsRequested) { - return $metricsRequested === false - || in_array($metricId, $metricsRequested); + // do not process INDEX_NB_UNIQ_FINGERPRINTS unless specifically asked for + if ($metricsRequested === false) { + if ($metricId == Metrics::INDEX_NB_UNIQ_FINGERPRINTS) { + return false; + } + return true; + } + return in_array($metricId, $metricsRequested); } protected function getWhereStatement($tableName, $datetimeField, $extraWhere = false) { $where = "$tableName.$datetimeField >= ? AND $tableName.$datetimeField <= ? - AND $tableName.idsite = ?"; + AND $tableName.idsite IN (". Common::getSqlStringFieldsArray($this->sites) . ")"; + if (!empty($extraWhere)) { $extraWhere = sprintf($extraWhere, $tableName, $tableName); - $where .= ' AND ' . $extraWhere; + $where .= ' AND ' . $extraWhere; } + return $where; } protected function getGroupByStatement($dimensions, $tableName) { $dimensions = $this->getSelectDimensions($dimensions, $tableName, $appendSelectAs = false); - $groupBy = implode(", ", $dimensions); + $groupBy = implode(", ", $dimensions); + return $groupBy; } - protected function getBindDatetimeSite() + /** + * Returns general bind parameters for all log aggregation queries. This includes the datetime + * start of entities, datetime end of entities and IDs of all sites. + * + * @return array + */ + protected function getGeneralQueryBindParams() { - return array($this->dateStart->getDateStartUTC(), $this->dateEnd->getDateEndUTC(), $this->site->getId()); + $bind = array($this->dateStart->getDateStartUTC(), $this->dateEnd->getDateEndUTC()); + $bind = array_merge($bind, $this->sites); + + return $bind; } /** @@ -487,7 +536,7 @@ class LogAggregator * * @param string $dimension One or more **log\_conversion\_item** columns to group aggregated data by. * Eg, `'idaction_sku'` or `'idaction_sku, idaction_category'`. - * @return Zend_Db_Statement A statement object that can be used to iterate through the query's + * @return \Zend_Db_Statement A statement object that can be used to iterate through the query's * result set. See [above](#queryEcommerceItems-result-set) to learn more * about what this query selects. * @api @@ -547,7 +596,7 @@ class LogAggregator array( 'log_conversion_item.server_time >= ?', 'log_conversion_item.server_time <= ?', - 'log_conversion_item.idsite = ?', + 'log_conversion_item.idsite IN (' . Common::getSqlStringFieldsArray($this->sites) . ')', 'log_conversion_item.deleted = 0' ) ), @@ -593,7 +642,7 @@ class LogAggregator * clause. These can be aggregate expressions, eg, `SUM(somecol)`. * @param bool|array $metrics The set of metrics to calculate and return. If `false`, the query will select * all of them. The following values can be used: - * + * * - {@link Piwik\Metrics::INDEX_NB_UNIQ_VISITORS} * - {@link Piwik\Metrics::INDEX_NB_VISITS} * - {@link Piwik\Metrics::INDEX_NB_ACTIONS} @@ -604,7 +653,7 @@ class LogAggregator * log_action should be joined on. The table alias used for each join * is `"log_action$i"` where `$i` is the index of the column in this * array. - * + * * If a string is used for this parameter, the table alias is not * suffixed (since there is only one column). * @return mixed A Zend_Db_Statement if `$rankingQuery` isn't supplied, otherwise the result of @@ -617,9 +666,9 @@ class LogAggregator $tableName = self::LOG_ACTIONS_TABLE; $availableMetrics = $this->getActionsMetricFields(); - $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics); - $from = array($tableName); - $where = $this->getWhereStatement($tableName, self::ACTION_DATETIME_FIELD, $where); + $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics); + $from = array($tableName); + $where = $this->getWhereStatement($tableName, self::ACTION_DATETIME_FIELD, $where); $groupBy = $this->getGroupByStatement($dimensions, $tableName); $orderBy = false; @@ -631,12 +680,14 @@ class LogAggregator foreach ($joinLogActionOnColumn as $i => $joinColumn) { $tableAlias = 'log_action' . ($multiJoin ? $i + 1 : ''); + if (strpos($joinColumn, ' ') === false) { $joinOn = $tableAlias . '.idaction = ' . $tableName . '.' . $joinColumn; } else { - // more complex join column like IF(...) + // more complex join column like if (...) $joinOn = $tableAlias . '.idaction = ' . $joinColumn; } + $from[] = array( 'table' => 'log_action', 'tableAlias' => $tableAlias, @@ -656,7 +707,9 @@ class LogAggregator if ($metrics) { $sumColumns = array_intersect($sumColumns, $metrics); } + $rankingQuery->addColumn($sumColumns, 'sum'); + return $rankingQuery->execute($query['sql'], $query['bind']); } @@ -665,7 +718,7 @@ class LogAggregator protected function getActionsMetricFields() { - return $availableMetrics = array( + return array( Metrics::INDEX_NB_VISITS => "count(distinct " . self::LOG_ACTIONS_TABLE . ".idvisit)", Metrics::INDEX_NB_UNIQ_VISITORS => "count(distinct " . self::LOG_ACTIONS_TABLE . ".idvisitor)", Metrics::INDEX_NB_ACTIONS => "count(*)", @@ -691,32 +744,32 @@ class LogAggregator * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL}**: The total cost of all ecommerce items sold * within these conversions. This value does not * include tax, shipping or any applied discount. - * + * * _This metric is only applicable to the special * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._ * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_TAX}**: The total tax applied to every transaction in these * conversions. - * + * * _This metric is only applicable to the special * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._ * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING}**: The total shipping cost for every transaction * in these conversions. - * + * * _This metric is only applicable to the special * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._ * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT}**: The total discount applied to every transaction * in these conversions. - * + * * _This metric is only applicable to the special * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._ * - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_ITEMS}**: The total number of ecommerce items sold in each transaction * in these conversions. - * + * * _This metric is only applicable to the special * **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._ - * + * * Additional data can be selected through the `$additionalSelects` parameter. - * + * * _Note: This method will only query the **log_conversion** table. Other tables cannot be joined * using this method._ * @@ -726,21 +779,22 @@ class LogAggregator * @param bool|string $where An optional SQL expression used in the SQL's **WHERE** clause. * @param array $additionalSelects Additional SELECT fields that are not included in the group by * clause. These can be aggregate expressions, eg, `SUM(somecol)`. - * @return Zend_Db_Statement + * @return \Zend_Db_Statement */ public function queryConversionsByDimension($dimensions = array(), $where = false, $additionalSelects = array()) { $dimensions = array_merge(array(self::IDGOAL_FIELD), $dimensions); + $tableName = self::LOG_CONVERSION_TABLE; $availableMetrics = $this->getConversionsMetricFields(); - $tableName = self::LOG_CONVERSION_TABLE; $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics); - $from = array($tableName); - $where = $this->getWhereStatement($tableName, self::CONVERSION_DATETIME_FIELD, $where); + $from = array($tableName); + $where = $this->getWhereStatement($tableName, self::CONVERSION_DATETIME_FIELD, $where); $groupBy = $this->getGroupByStatement($dimensions, $tableName); $orderBy = false; - $query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy); + $query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy); + return $this->getDb()->query($query['sql'], $query['bind']); } @@ -750,9 +804,9 @@ class LogAggregator * * **Note:** The result of this function is meant for use in the `$additionalSelects` parameter * in one of the query... methods (for example {@link queryVisitsByDimension()}). - * + * * **Example** - * + * * // summarize one column * $visitTotalActionsRanges = array( * array(1, 1), @@ -760,7 +814,7 @@ class LogAggregator * array(10) * ); * $selects = LogAggregator::getSelectsFromRangedColumn('visit_total_actions', $visitTotalActionsRanges, 'log_visit', 'vta'); - * + * * // summarize another column in the same request * $visitCountVisitsRanges = array( * array(1, 1), @@ -771,17 +825,17 @@ class LogAggregator * $selects, * LogAggregator::getSelectsFromRangedColumn('visitor_count_visits', $visitCountVisitsRanges, 'log_visit', 'vcv') * ); - * + * * // perform the query * $logAggregator = // get the LogAggregator somehow * $query = $logAggregator->queryVisitsByDimension($dimensions = array(), $where = false, $selects); * $tableSummary = $query->fetch(); - * + * * $numberOfVisitsWithOneAction = $tableSummary['vta0']; * $numberOfVisitsBetweenTwoAnd10 = $tableSummary['vta1']; - * + * * $numberOfVisitsWithVisitCountOfOne = $tableSummary['vcv0']; - * + * * @param string $column The name of a column in `$table` that will be summarized. * @param array $ranges The array of ranges over which the data in the table * will be summarized. For example, @@ -817,14 +871,16 @@ class LogAggregator { $selects = array(); $extraCondition = ''; + if ($restrictToReturningVisitors) { // extra condition for the SQL SELECT that makes sure only returning visits are counted // when creating the 'days since last visit' report $extraCondition = 'and log_visit.visitor_returning = 1'; - $extraSelect = "sum(case when log_visit.visitor_returning = 0 then 1 else 0 end) " - . " as `" . $selectColumnPrefix . 'General_NewVisits' . "`"; + $extraSelect = "sum(case when log_visit.visitor_returning = 0 then 1 else 0 end) " + . " as `" . $selectColumnPrefix . 'General_NewVisits' . "`"; $selects[] = $extraSelect; } + foreach ($ranges as $gap) { if (count($gap) == 2) { $lowerBound = $gap[0]; @@ -833,12 +889,11 @@ class LogAggregator $selectAs = "$selectColumnPrefix$lowerBound-$upperBound"; $selects[] = "sum(case when $table.$column between $lowerBound and $upperBound $extraCondition" . - " then 1 else 0 end) as `$selectAs`"; + " then 1 else 0 end) as `$selectAs`"; } else { $lowerBound = $gap[0]; - $selectAs = $selectColumnPrefix . ($lowerBound + 1) . urlencode('+'); - + $selectAs = $selectColumnPrefix . ($lowerBound + 1) . urlencode('+'); $selects[] = "sum(case when $table.$column > $lowerBound $extraCondition then 1 else 0 end) as `$selectAs`"; } } @@ -859,9 +914,10 @@ class LogAggregator * value is used. * @return array */ - static public function makeArrayOneColumn($row, $columnName, $lookForThisPrefix = false) + public static function makeArrayOneColumn($row, $columnName, $lookForThisPrefix = false) { $cleanRow = array(); + foreach ($row as $label => $count) { if (empty($lookForThisPrefix) || strpos($label, $lookForThisPrefix) === 0 @@ -870,6 +926,7 @@ class LogAggregator $cleanRow[$cleanLabel] = array($columnName => $count); } } + return $cleanRow; } diff --git a/www/analytics/core/DataAccess/LogQueryBuilder.php b/www/analytics/core/DataAccess/LogQueryBuilder.php new file mode 100644 index 00000000..b0f2b73a --- /dev/null +++ b/www/analytics/core/DataAccess/LogQueryBuilder.php @@ -0,0 +1,391 @@ +isEmpty()) { + $segmentExpression->parseSubExpressionsIntoSqlExpressions($from); + $segmentSql = $segmentExpression->getSql(); + $where = $this->getWhereMatchBoth($where, $segmentSql['where']); + $bind = array_merge($bind, $segmentSql['bind']); + } + + $joins = $this->generateJoinsString($from); + $joinWithSubSelect = $joins['joinWithSubSelect']; + $from = $joins['sql']; + + // hack for https://github.com/piwik/piwik/issues/9194#issuecomment-164321612 + $useSpecialConversionGroupBy = (!empty($segmentSql) + && strpos($groupBy, 'log_conversion.idgoal') !== false + && $fromInitially == array('log_conversion') + && strpos($from, 'log_link_visit_action') !== false); + + if ($useSpecialConversionGroupBy) { + $innerGroupBy = "CONCAT(log_conversion.idvisit, '_' , log_conversion.idgoal, '_', log_conversion.buster)"; + $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit, $innerGroupBy); + } elseif ($joinWithSubSelect) { + $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit); + } else { + $sql = $this->buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit); + } + return array( + 'sql' => $sql, + 'bind' => $bind + ); + } + + private function hasJoinedTableAlreadyManually($tableToFind, $joinToFind, $tables) + { + foreach ($tables as $index => $table) { + if (is_array($table) + && !empty($table['table']) + && $table['table'] === $tableToFind + && (!isset($table['tableAlias']) || $table['tableAlias'] === $tableToFind) + && isset($table['joinOn']) && $table['joinOn'] === $joinToFind) { + return true; + } + } + + return false; + } + + private function findIndexOfManuallyAddedTable($tableToFind, $tables) + { + foreach ($tables as $index => $table) { + if (is_array($table) + && !empty($table['table']) + && $table['table'] === $tableToFind + && (!isset($table['tableAlias']) || $table['tableAlias'] === $tableToFind)) { + return $index; + } + } + } + + private function hasTableAddedManually($tableToFind, $tables) + { + $table = $this->findIndexOfManuallyAddedTable($tableToFind, $tables); + + return isset($table); + } + + /** + * Generate the join sql based on the needed tables + * @param array $tables tables to join + * @throws Exception if tables can't be joined + * @return array + */ + private function generateJoinsString(&$tables) + { + $knownTables = array("log_action", "log_visit", "log_link_visit_action", "log_conversion", "log_conversion_item"); + $visitsAvailable = $linkVisitActionsTableAvailable = $conversionsAvailable = $conversionItemAvailable = $actionsTableAvailable = false; + $defaultLogActionJoin = "log_link_visit_action.idaction_url = log_action.idaction"; + + $joinWithSubSelect = false; + $sql = ''; + + // make sure the tables are joined in the right order + // base table first, then action before conversion + // this way, conversions can be left joined on idvisit + $actionIndex = array_search("log_link_visit_action", $tables); + $conversionIndex = array_search("log_conversion", $tables); + if ($actionIndex > 0 && $conversionIndex > 0 && $actionIndex > $conversionIndex) { + $tables[$actionIndex] = "log_conversion"; + $tables[$conversionIndex] = "log_link_visit_action"; + } + // same as above: action before visit + $actionIndex = array_search("log_link_visit_action", $tables); + $visitIndex = array_search("log_visit", $tables); + if ($actionIndex > 0 && $visitIndex > 0 && $actionIndex > $visitIndex) { + $tables[$actionIndex] = "log_visit"; + $tables[$visitIndex] = "log_link_visit_action"; + } + + // we need to add log_link_visit_action dynamically to join eg visit with action + $linkVisitAction = array_search("log_link_visit_action", $tables); + $actionIndex = array_search("log_action", $tables); + if ($linkVisitAction === false && $actionIndex > 0) { + $tables[] = "log_link_visit_action"; + } + + if ($actionIndex > 0 + && $this->hasTableAddedManually('log_action', $tables) + && !$this->hasJoinedTableAlreadyManually('log_action', $defaultLogActionJoin, $tables)) { + // we cannot join the same table with same alias twice, therefore we need to combine the join via AND + $tableIndex = $this->findIndexOfManuallyAddedTable('log_action', $tables); + $defaultLogActionJoin = '(' . $tables[$tableIndex]['joinOn'] . ' AND ' . $defaultLogActionJoin . ')'; + unset($tables[$tableIndex]); + } + + $linkVisitAction = array_search("log_link_visit_action", $tables); + $actionIndex = array_search("log_action", $tables); + if ($linkVisitAction > 0 && $actionIndex > 0 && $linkVisitAction > $actionIndex) { + $tables[$actionIndex] = "log_link_visit_action"; + $tables[$linkVisitAction] = "log_action"; + } + + foreach ($tables as $i => $table) { + if (is_array($table)) { + // join condition provided + $alias = isset($table['tableAlias']) ? $table['tableAlias'] : $table['table']; + $sql .= " + LEFT JOIN " . Common::prefixTable($table['table']) . " AS " . $alias + . " ON " . $table['joinOn']; + continue; + } + + if (!in_array($table, $knownTables)) { + throw new Exception("Table '$table' can't be used for segmentation"); + } + + $tableSql = Common::prefixTable($table) . " AS $table"; + + if ($i == 0) { + // first table + $sql .= $tableSql; + } else { + + if ($linkVisitActionsTableAvailable && $table === 'log_action') { + $join = $defaultLogActionJoin; + + if ($this->hasJoinedTableAlreadyManually($table, $join, $tables)) { + $actionsTableAvailable = true; + continue; + } + + } elseif ($linkVisitActionsTableAvailable && $table == "log_conversion") { + // have actions, need conversions => join on idvisit + $join = "log_conversion.idvisit = log_link_visit_action.idvisit"; + } elseif ($linkVisitActionsTableAvailable && $table == "log_visit") { + // have actions, need visits => join on idvisit + $join = "log_visit.idvisit = log_link_visit_action.idvisit"; + + if ($this->hasJoinedTableAlreadyManually($table, $join, $tables)) { + $visitsAvailable = true; + continue; + } + + } elseif ($visitsAvailable && $table == "log_link_visit_action") { + // have visits, need actions => we have to use a more complex join + // we don't hande this here, we just return joinWithSubSelect=true in this case + $joinWithSubSelect = true; + $join = "log_link_visit_action.idvisit = log_visit.idvisit"; + + if ($this->hasJoinedTableAlreadyManually($table, $join, $tables)) { + $linkVisitActionsTableAvailable = true; + continue; + } + + } elseif ($conversionsAvailable && $table == "log_link_visit_action") { + // have conversions, need actions => join on idvisit + $join = "log_conversion.idvisit = log_link_visit_action.idvisit"; + } elseif (($visitsAvailable && $table == "log_conversion") + || ($conversionsAvailable && $table == "log_visit") + ) { + // have visits, need conversion (or vice versa) => join on idvisit + // notice that joining conversions on visits has lower priority than joining it on actions + $join = "log_conversion.idvisit = log_visit.idvisit"; + + // if conversions are joined on visits, we need a complex join + if ($table == "log_conversion") { + $joinWithSubSelect = true; + } + } elseif ($conversionItemAvailable && $table === 'log_visit') { + $join = "log_conversion_item.idvisit = log_visit.idvisit"; + } elseif ($conversionItemAvailable && $table === 'log_link_visit_action') { + $join = "log_conversion_item.idvisit = log_link_visit_action.idvisit"; + } elseif ($conversionItemAvailable && $table === 'log_conversion') { + $join = "log_conversion_item.idvisit = log_conversion.idvisit"; + } else { + throw new Exception("Table '$table' can't be joined for segmentation"); + } + + // the join sql the default way + $sql .= " + LEFT JOIN $tableSql ON $join"; + } + + // remember which tables are available + $visitsAvailable = ($visitsAvailable || $table == "log_visit"); + $linkVisitActionsTableAvailable = ($linkVisitActionsTableAvailable || $table == "log_link_visit_action"); + $actionsTableAvailable = ($actionsTableAvailable || $table == "log_action"); + $conversionsAvailable = ($conversionsAvailable || $table == "log_conversion"); + $conversionItemAvailable = ($conversionItemAvailable || $table == "log_conversion_item"); + } + + $return = array( + 'sql' => $sql, + 'joinWithSubSelect' => $joinWithSubSelect + ); + return $return; + } + + + /** + * Build a select query where actions have to be joined on visits (or conversions) + * In this case, the query gets wrapped in another query so that grouping by visit is possible + * @param string $select + * @param string $from + * @param string $where + * @param string $groupBy + * @param string $orderBy + * @param string $limit + * @param null|string $innerGroupBy If given, this inner group by will be used. If not, we try to detect one + * @throws Exception + * @return string + */ + private function buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit, $innerGroupBy = null) + { + $matchTables = "(log_visit|log_conversion_item|log_conversion|log_action)"; + preg_match_all("/". $matchTables ."\.[a-z0-9_\*]+/", $select, $matches); + $neededFields = array_unique($matches[0]); + + if (count($neededFields) == 0) { + throw new Exception("No needed fields found in select expression. " + . "Please use a table prefix."); + } + + preg_match_all("/". $matchTables . "/", $from, $matchesFrom); + + $innerSelect = implode(", \n", $neededFields); + $innerFrom = $from; + $innerWhere = $where; + + $innerLimit = $limit; + + if (!isset($innerGroupBy) && in_array('log_visit', $matchesFrom[1])) { + $innerGroupBy = "log_visit.idvisit"; + } elseif (!isset($innerGroupBy)) { + throw new Exception('Cannot use subselect for join as no group by rule is specified'); + } + + $innerOrderBy = "NULL"; + if ($innerLimit && $orderBy) { + // only When LIMITing we can apply to the inner query the same ORDER BY as the parent query + $innerOrderBy = $orderBy; + } + if ($innerLimit) { + // When LIMITing, no need to GROUP BY (GROUPing by is done before the LIMIT which is super slow when large amount of rows is matched) + $innerGroupBy = false; + } + + $innerQuery = $this->buildSelectQuery($innerSelect, $innerFrom, $innerWhere, $innerGroupBy, $innerOrderBy, $innerLimit); + + $select = preg_replace('/'.$matchTables.'\./', 'log_inner.', $select); + $from = " + ( + $innerQuery + ) AS log_inner"; + $where = false; + $orderBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $orderBy); + $groupBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $groupBy); + $query = $this->buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit); + return $query; + } + + + /** + * Build select query the normal way + * + * @param string $select fieldlist to be selected + * @param string $from tablelist to select from + * @param string $where where clause + * @param string $groupBy group by clause + * @param string $orderBy order by clause + * @param string|int $limit limit by clause eg '5' for Limit 5 Offset 0 or '10, 5' for Limit 5 Offset 10 + * @return string + */ + private function buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit) + { + $sql = " + SELECT + $select + FROM + $from"; + + if ($where) { + $sql .= " + WHERE + $where"; + } + + if ($groupBy) { + $sql .= " + GROUP BY + $groupBy"; + } + + if ($orderBy) { + $sql .= " + ORDER BY + $orderBy"; + } + + $sql = $this->appendLimitClauseToQuery($sql, $limit); + + return $sql; + } + + private function appendLimitClauseToQuery($sql, $limit) + { + $limitParts = explode(',', (string) $limit); + $isLimitWithOffset = 2 === count($limitParts); + + if ($isLimitWithOffset) { + // $limit = "10, 5". We would not have to do this but we do to prevent possible injections. + $offset = trim($limitParts[0]); + $limit = trim($limitParts[1]); + $sql .= sprintf(' LIMIT %d, %d', $offset, $limit); + } else { + // $limit = "5" + $limit = (int)$limit; + if ($limit >= 1) { + $sql .= " LIMIT $limit"; + } + } + + return $sql; + } + + /** + * @param $where + * @param $segmentWhere + * @return string + * @throws + */ + protected function getWhereMatchBoth($where, $segmentWhere) + { + if (empty($segmentWhere) && empty($where)) { + throw new \Exception("Segment where clause should be non empty."); + } + if (empty($segmentWhere)) { + return $where; + } + if (empty($where)) { + return $segmentWhere; + } + return "( $where ) + AND + ($segmentWhere)"; + } +} diff --git a/www/analytics/core/DataAccess/Model.php b/www/analytics/core/DataAccess/Model.php new file mode 100644 index 00000000..ba04962f --- /dev/null +++ b/www/analytics/core/DataAccess/Model.php @@ -0,0 +1,358 @@ +logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface'); + } + + /** + * Returns the archives IDs that have already been invalidated and have been since re-processed. + * + * These archives { archive name (includes segment hash) , idsite, date, period } will be deleted. + * + * @param string $archiveTable + * @param array $idSites + * @return array + * @throws Exception + */ + public function getInvalidatedArchiveIdsSafeToDelete($archiveTable, array $idSites) + { + try { + Db::get()->query('SET SESSION group_concat_max_len=' . (128 * 1024)); + } catch (\Exception $ex) { + $this->logger->info("Could not set group_concat_max_len MySQL session variable."); + } + + $idSites = array_map(function ($v) { return (int)$v; }, $idSites); + + $sql = "SELECT idsite, date1, date2, period, name, + GROUP_CONCAT(idarchive, '.', value ORDER BY ts_archived DESC) as archives + FROM `$archiveTable` + WHERE name LIKE 'done%' + AND value IN (" . ArchiveWriter::DONE_INVALIDATED . ',' + . ArchiveWriter::DONE_OK . ',' + . ArchiveWriter::DONE_OK_TEMPORARY . ") + AND idsite IN (" . implode(',', $idSites) . ") + GROUP BY idsite, date1, date2, period, name"; + + $archiveIds = array(); + + $rows = Db::fetchAll($sql); + foreach ($rows as $row) { + $duplicateArchives = explode(',', $row['archives']); + + $firstArchive = array_shift($duplicateArchives); + list($firstArchiveId, $firstArchiveValue) = explode('.', $firstArchive); + + // if the first archive (ie, the newest) is an 'ok' or 'ok temporary' archive, then + // all invalidated archives after it can be deleted + if ($firstArchiveValue == ArchiveWriter::DONE_OK + || $firstArchiveValue == ArchiveWriter::DONE_OK_TEMPORARY + ) { + foreach ($duplicateArchives as $pair) { + if (strpos($pair, '.') === false) { + $this->logger->info("GROUP_CONCAT cut off the query result, you may have to purge archives again."); + break; + } + + list($idarchive, $value) = explode('.', $pair); + if ($value == ArchiveWriter::DONE_INVALIDATED) { + $archiveIds[] = $idarchive; + } + } + } + } + + return $archiveIds; + } + + /** + * @param string $archiveTable Prefixed table name + * @param int[] $idSites + * @param string[][] $datesByPeriodType + * @param Segment $segment + * @return \Zend_Db_Statement + * @throws Exception + */ + public function updateArchiveAsInvalidated($archiveTable, $idSites, $datesByPeriodType, Segment $segment = null) + { + $idSites = array_map('intval', $idSites); + + $bind = array(); + + $periodConditions = array(); + foreach ($datesByPeriodType as $periodType => $dates) { + $dateConditions = array(); + + foreach ($dates as $date) { + $dateConditions[] = "(date1 <= ? AND ? <= date2)"; + $bind[] = $date; + $bind[] = $date; + } + + $dateConditionsSql = implode(" OR ", $dateConditions); + if (empty($periodType) + || $periodType == Period\Day::PERIOD_ID + ) { + // invalidate all periods if no period supplied or period is day + $periodConditions[] = "($dateConditionsSql)"; + } else if ($periodType == Period\Range::PERIOD_ID) { + $periodConditions[] = "(period = " . Period\Range::PERIOD_ID . " AND ($dateConditionsSql))"; + } else { + // for non-day periods, invalidate greater periods, but not range periods + $periodConditions[] = "(period >= " . (int)$periodType . " AND period < " . Period\Range::PERIOD_ID . " AND ($dateConditionsSql))"; + } + } + + if ($segment) { + $nameCondition = "name LIKE '" . Rules::getDoneFlagArchiveContainsAllPlugins($segment) . "%'"; + } else { + $nameCondition = "name LIKE 'done%'"; + } + + $sql = "UPDATE $archiveTable SET value = " . ArchiveWriter::DONE_INVALIDATED + . " WHERE $nameCondition + AND idsite IN (" . implode(", ", $idSites) . ") + AND (" . implode(" OR ", $periodConditions) . ")"; + + return Db::query($sql, $bind); + } + + + public function getTemporaryArchivesOlderThan($archiveTable, $purgeArchivesOlderThan) + { + $query = "SELECT idarchive FROM " . $archiveTable . " + WHERE name LIKE 'done%' + AND (( value = " . ArchiveWriter::DONE_OK_TEMPORARY . " + AND ts_archived < ?) + OR value = " . ArchiveWriter::DONE_ERROR . ")"; + + return Db::fetchAll($query, array($purgeArchivesOlderThan)); + } + + public function deleteArchivesWithPeriod($numericTable, $blobTable, $period, $date) + { + $query = "DELETE FROM %s WHERE period = ? AND ts_archived < ?"; + $bind = array($period, $date); + + $queryObj = Db::query(sprintf($query, $numericTable), $bind); + $deletedRows = $queryObj->rowCount(); + + try { + $queryObj = Db::query(sprintf($query, $blobTable), $bind); + $deletedRows += $queryObj->rowCount(); + } catch (Exception $e) { + // Individual blob tables could be missing + $this->logger->debug("Unable to delete archives by period from {blobTable}.", array( + 'blobTable' => $blobTable, + 'exception' => $e, + )); + } + + return $deletedRows; + } + + public function deleteArchiveIds($numericTable, $blobTable, $idsToDelete) + { + $idsToDelete = array_values($idsToDelete); + $query = "DELETE FROM %s WHERE idarchive IN (" . Common::getSqlStringFieldsArray($idsToDelete) . ")"; + + $queryObj = Db::query(sprintf($query, $numericTable), $idsToDelete); + $deletedRows = $queryObj->rowCount(); + + try { + $queryObj = Db::query(sprintf($query, $blobTable), $idsToDelete); + $deletedRows += $queryObj->rowCount(); + } catch (Exception $e) { + // Individual blob tables could be missing + $this->logger->debug("Unable to delete archive IDs from {blobTable}.", array( + 'blobTable' => $blobTable, + 'exception' => $e, + )); + } + + return $deletedRows; + } + + public function getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStartIso, $dateEndIso, $minDatetimeIsoArchiveProcessedUTC, $doneFlags, $doneFlagValues) + { + $bindSQL = array($idSite, + $dateStartIso, + $dateEndIso, + $period, + ); + + $timeStampWhere = ''; + if ($minDatetimeIsoArchiveProcessedUTC) { + $timeStampWhere = " AND ts_archived >= ? "; + $bindSQL[] = $minDatetimeIsoArchiveProcessedUTC; + } + + $sqlWhereArchiveName = self::getNameCondition($doneFlags, $doneFlagValues); + + $sqlQuery = "SELECT idarchive, value, name, date1 as startDate FROM $numericTable + WHERE idsite = ? + AND date1 = ? + AND date2 = ? + AND period = ? + AND ( ($sqlWhereArchiveName) + OR name = '" . ArchiveSelector::NB_VISITS_RECORD_LOOKED_UP . "' + OR name = '" . ArchiveSelector::NB_VISITS_CONVERTED_RECORD_LOOKED_UP . "') + $timeStampWhere + ORDER BY idarchive DESC"; + $results = Db::fetchAll($sqlQuery, $bindSQL); + + return $results; + } + + public function createArchiveTable($tableName, $tableNamePrefix) + { + $db = Db::get(); + $sql = DbHelper::getTableCreateSql($tableNamePrefix); + + // replace table name template by real name + $tableNamePrefix = Common::prefixTable($tableNamePrefix); + $sql = str_replace($tableNamePrefix, $tableName, $sql); + + try { + $db->query($sql); + } catch (Exception $e) { + // accept mysql error 1050: table already exists, throw otherwise + if (!$db->isErrNo($e, '1050')) { + throw $e; + } + } + + try { + if (ArchiveTableCreator::NUMERIC_TABLE === ArchiveTableCreator::getTypeFromTableName($tableName)) { + $sequence = new Sequence($tableName); + $sequence->create(); + } + } catch (Exception $e) { + } + } + + public function allocateNewArchiveId($numericTable) + { + $sequence = new Sequence($numericTable); + + try { + $idarchive = $sequence->getNextId(); + } catch (Exception $e) { + // edge case: sequence was not found, create it now + $sequence->create(); + + $idarchive = $sequence->getNextId(); + } + + return $idarchive; + } + + public function deletePreviousArchiveStatus($numericTable, $archiveId, $doneFlag) + { + $tableWithoutLeadingPrefix = $numericTable; + $lenNumericTableWithoutPrefix = strlen('archive_numeric_MM_YYYY'); + + if (strlen($numericTable) >= $lenNumericTableWithoutPrefix) { + $tableWithoutLeadingPrefix = substr($numericTable, strlen($numericTable) - $lenNumericTableWithoutPrefix); + // we need to make sure lock name is less than 64 characters see https://github.com/piwik/piwik/issues/9131 + } + $dbLockName = "rmPrevArchiveStatus.$tableWithoutLeadingPrefix.$archiveId"; + + // without advisory lock here, the DELETE would acquire Exclusive Lock + $this->acquireArchiveTableLock($dbLockName); + + Db::query("DELETE FROM $numericTable WHERE idarchive = ? AND (name = '" . $doneFlag . "')", + array($archiveId) + ); + + $this->releaseArchiveTableLock($dbLockName); + } + + public function insertRecord($tableName, $fields, $record, $name, $value) + { + // duplicate idarchives are Ignored, see https://github.com/piwik/piwik/issues/987 + $query = "INSERT IGNORE INTO " . $tableName . " (" . implode(", ", $fields) . ") + VALUES (?,?,?,?,?,?,?,?)"; + + $bindSql = $record; + $bindSql[] = $name; + $bindSql[] = $value; + + Db::query($query, $bindSql); + + return true; + } + + /** + * Returns the site IDs for invalidated archives in an archive table. + * + * @param string $numericTable The numeric table to search through. + * @return int[] + */ + public function getSitesWithInvalidatedArchive($numericTable) + { + $rows = Db::fetchAll("SELECT DISTINCT idsite FROM `$numericTable` WHERE name LIKE 'done%' AND value = " . ArchiveWriter::DONE_INVALIDATED); + + $result = array(); + foreach ($rows as $row) { + $result[] = $row['idsite']; + } + return $result; + } + + /** + * Returns the SQL condition used to find successfully completed archives that + * this instance is querying for. + */ + private static function getNameCondition($doneFlags, $possibleValues) + { + $allDoneFlags = "'" . implode("','", $doneFlags) . "'"; + + // create the SQL to find archives that are DONE + return "((name IN ($allDoneFlags)) AND (value IN (" . implode(',', $possibleValues) . ")))"; + } + + protected function acquireArchiveTableLock($dbLockName) + { + if (Db::getDbLock($dbLockName, $maxRetries = 30) === false) { + throw new Exception("Cannot get named lock $dbLockName."); + } + } + + protected function releaseArchiveTableLock($dbLockName) + { + Db::releaseDbLock($dbLockName); + } +} diff --git a/www/analytics/core/DataAccess/RawLogDao.php b/www/analytics/core/DataAccess/RawLogDao.php new file mode 100644 index 00000000..7310267b --- /dev/null +++ b/www/analytics/core/DataAccess/RawLogDao.php @@ -0,0 +1,402 @@ +dimensionMetadataProvider = $provider ?: StaticContainer::get('Piwik\Plugin\Dimension\DimensionMetadataProvider'); + } + + /** + * @param array $values + * @param string $idVisit + */ + public function updateVisits(array $values, $idVisit) + { + $sql = "UPDATE " . Common::prefixTable('log_visit') + . " SET " . $this->getColumnSetExpressions(array_keys($values)) + . " WHERE idvisit = ?"; + + $this->update($sql, $values, $idVisit); + } + + /** + * @param array $values + * @param string $idVisit + */ + public function updateConversions(array $values, $idVisit) + { + $sql = "UPDATE " . Common::prefixTable('log_conversion') + . " SET " . $this->getColumnSetExpressions(array_keys($values)) + . " WHERE idvisit = ?"; + + $this->update($sql, $values, $idVisit); + } + + /** + * @param string $from + * @param string $to + * @return int + */ + public function countVisitsWithDatesLimit($from, $to) + { + $sql = "SELECT COUNT(*) AS num_rows" + . " FROM " . Common::prefixTable('log_visit') + . " WHERE visit_last_action_time >= ? AND visit_last_action_time < ?"; + + $bind = array($from, $to); + + return (int) Db::fetchOne($sql, $bind); + } + + /** + * Iterates over logs in a log table in chunks. Parameters to this function are as backend agnostic + * as possible w/o dramatically increasing code complexity. + * + * @param string $logTable The log table name. Unprefixed, eg, `log_visit`. + * @param array[] $conditions An array describing the conditions logs must match in the query. Translates to + * the WHERE part of a SELECT statement. Each element must contain three elements: + * + * * the column name + * * the operator (ie, '=', '<>', '<', etc.) + * * the operand (ie, a value) + * + * The elements are AND-ed together. + * + * Example: + * + * ``` + * array( + * array('visit_first_action_time', '>=', ...), + * array('visit_first_action_time', '<', ...) + * ) + * ``` + * @param int $iterationStep The number of rows to query at a time. + * @param callable $callback The callback that processes each chunk of rows. + */ + public function forAllLogs($logTable, $fields, $conditions, $iterationStep, $callback) + { + $idField = $this->getIdFieldForLogTable($logTable); + list($query, $bind) = $this->createLogIterationQuery($logTable, $idField, $fields, $conditions, $iterationStep); + + $lastId = 0; + do { + $rows = Db::fetchAll($query, array_merge(array($lastId), $bind)); + if (!empty($rows)) { + $lastId = $rows[count($rows) - 1][$idField]; + + $callback($rows); + } + } while (count($rows) == $iterationStep); + } + + /** + * Deletes visits with the supplied IDs from log_visit. This method does not cascade, so rows in other tables w/ + * the same visit ID will still exist. + * + * @param int[] $idVisits + * @return int The number of deleted rows. + */ + public function deleteVisits($idVisits) + { + $sql = "DELETE FROM `" . Common::prefixTable('log_visit') . "` WHERE idvisit IN " + . $this->getInFieldExpressionWithInts($idVisits); + + $statement = Db::query($sql); + return $statement->rowCount(); + } + + /** + * Deletes visit actions for the supplied visit IDs from log_link_visit_action. + * + * @param int[] $visitIds + * @return int The number of deleted rows. + */ + public function deleteVisitActionsForVisits($visitIds) + { + $sql = "DELETE FROM `" . Common::prefixTable('log_link_visit_action') . "` WHERE idvisit IN " + . $this->getInFieldExpressionWithInts($visitIds); + + $statement = Db::query($sql); + return $statement->rowCount(); + } + + /** + * Deletes conversions for the supplied visit IDs from log_conversion. This method does not cascade, so + * conversion items will not be deleted. + * + * @param int[] $visitIds + * @return int The number of deleted rows. + */ + public function deleteConversions($visitIds) + { + $sql = "DELETE FROM `" . Common::prefixTable('log_conversion') . "` WHERE idvisit IN " + . $this->getInFieldExpressionWithInts($visitIds); + + $statement = Db::query($sql); + return $statement->rowCount(); + } + + /** + * Deletes conversion items for the supplied visit IDs from log_conversion_item. + * + * @param int[] $visitIds + * @return int The number of deleted rows. + */ + public function deleteConversionItems($visitIds) + { + $sql = "DELETE FROM `" . Common::prefixTable('log_conversion_item') . "` WHERE idvisit IN " + . $this->getInFieldExpressionWithInts($visitIds); + + $statement = Db::query($sql); + return $statement->rowCount(); + } + + /** + * Deletes all unused entries from the log_action table. This method uses a temporary table to store used + * actions, and then deletes rows from log_action that are not in this temporary table. + * + * Table locking is required to avoid concurrency issues. + * + * @throws \Exception If table locking permission is not granted to the current MySQL user. + */ + public function deleteUnusedLogActions() + { + if (!Db::isLockPrivilegeGranted()) { + throw new \Exception("RawLogDao.deleteUnusedLogActions() requires table locking permission in order to complete without error."); + } + + // get current max ID in log tables w/ idaction references. + $maxIds = $this->getMaxIdsInLogTables(); + + $this->createTempTableForStoringUsedActions(); + + // do large insert (inserting everything before maxIds) w/o locking tables... + $this->insertActionsToKeep($maxIds, $deleteOlderThanMax = true); + + // ... then do small insert w/ locked tables to minimize the amount of time tables are locked. + $this->lockLogTables(); + $this->insertActionsToKeep($maxIds, $deleteOlderThanMax = false); + + // delete before unlocking tables so there's no chance a new log row that references an + // unused action will be inserted. + $this->deleteUnusedActions(); + Db::unlockAllTables(); + } + + + /** + * Returns the list of the website IDs that received some visits between the specified timestamp. + * + * @param string $fromDateTime + * @param string $toDateTime + * @return bool true if there are visits for this site between the given timeframe, false if not + */ + public function hasSiteVisitsBetweenTimeframe($fromDateTime, $toDateTime, $idSite) + { + $sites = Db::fetchOne("SELECT 1 + FROM " . Common::prefixTable('log_visit') . " + WHERE idsite = ? + AND visit_last_action_time > ? + AND visit_last_action_time < ? + LIMIT 1", array($idSite, $fromDateTime, $toDateTime)); + + return (bool) $sites; + } + + /** + * @param array $columnsToSet + * @return string + */ + protected function getColumnSetExpressions(array $columnsToSet) + { + $columnsToSet = array_map( + function ($column) { + return $column . ' = ?'; + }, + $columnsToSet + ); + + return implode(', ', $columnsToSet); + } + + /** + * @param array $values + * @param $idVisit + * @param $sql + * @return \Zend_Db_Statement + * @throws \Exception + */ + protected function update($sql, array $values, $idVisit) + { + return Db::query($sql, array_merge(array_values($values), array($idVisit))); + } + + private function getIdFieldForLogTable($logTable) + { + switch ($logTable) { + case 'log_visit': + return 'idvisit'; + case 'log_link_visit_action': + return 'idlink_va'; + case 'log_conversion': + return 'idvisit'; + case 'log_conversion_item': + return 'idvisit'; + case 'log_action': + return 'idaction'; + default: + throw new \InvalidArgumentException("Unknown log table '$logTable'."); + } + } + + // TODO: instead of creating a log query like this, we should re-use segments. to do this, however, there must be a 1-1 + // mapping for dimensions => segments, and each dimension should automatically have a segment. + private function createLogIterationQuery($logTable, $idField, $fields, $conditions, $iterationStep) + { + $bind = array(); + + $sql = "SELECT " . implode(', ', $fields) . " FROM `" . Common::prefixTable($logTable) . "` WHERE $idField > ?"; + + foreach ($conditions as $condition) { + list($column, $operator, $value) = $condition; + + if (is_array($value)) { + $sql .= " AND $column IN (" . Common::getSqlStringFieldsArray($value) . ")"; + + $bind = array_merge($bind, $value); + } else { + $sql .= " AND $column $operator ?"; + + $bind[] = $value; + } + } + + $sql .= " ORDER BY $idField ASC LIMIT " . (int)$iterationStep; + + return array($sql, $bind); + } + + private function getInFieldExpressionWithInts($idVisits) + { + $sql = "("; + + $isFirst = true; + foreach ($idVisits as $idVisit) { + if ($isFirst) { + $isFirst = false; + } else { + $sql .= ', '; + } + + $sql .= (int)$idVisit; + } + + $sql .= ")"; + + return $sql; + } + + + private function getMaxIdsInLogTables() + { + $tables = array('log_conversion', 'log_link_visit_action', 'log_visit', 'log_conversion_item'); + $idColumns = $this->getTableIdColumns(); + + $result = array(); + foreach ($tables as $table) { + $idCol = $idColumns[$table]; + $result[$table] = Db::fetchOne("SELECT MAX($idCol) FROM " . Common::prefixTable($table)); + } + + return $result; + } + + private function createTempTableForStoringUsedActions() + { + $sql = "CREATE TEMPORARY TABLE " . Common::prefixTable(self::DELETE_UNUSED_ACTIONS_TEMP_TABLE_NAME) . " ( + idaction INT(11), + PRIMARY KEY (idaction) + )"; + Db::query($sql); + } + + // protected for testing purposes + protected function insertActionsToKeep($maxIds, $olderThan = true, $insertIntoTempIterationStep = 100000) + { + $tempTableName = Common::prefixTable(self::DELETE_UNUSED_ACTIONS_TEMP_TABLE_NAME); + + $idColumns = $this->getTableIdColumns(); + foreach ($this->dimensionMetadataProvider->getActionReferenceColumnsByTable() as $table => $columns) { + $idCol = $idColumns[$table]; + + foreach ($columns as $col) { + $select = "SELECT $col FROM " . Common::prefixTable($table) . " WHERE $idCol >= ? AND $idCol < ?"; + $sql = "INSERT IGNORE INTO $tempTableName $select"; + + if ($olderThan) { + $start = 0; + $finish = $maxIds[$table]; + } else { + $start = $maxIds[$table]; + $finish = Db::fetchOne("SELECT MAX($idCol) FROM " . Common::prefixTable($table)); + } + + Db::segmentedQuery($sql, $start, $finish, $insertIntoTempIterationStep); + } + } + } + + private function lockLogTables() + { + Db::lockTables( + $readLocks = Common::prefixTables('log_conversion', 'log_link_visit_action', 'log_visit', 'log_conversion_item'), + $writeLocks = Common::prefixTables('log_action') + ); + } + + private function deleteUnusedActions() + { + list($logActionTable, $tempTableName) = Common::prefixTables("log_action", self::DELETE_UNUSED_ACTIONS_TEMP_TABLE_NAME); + + $deleteSql = "DELETE LOW_PRIORITY QUICK IGNORE $logActionTable + FROM $logActionTable + LEFT JOIN $tempTableName tmp ON tmp.idaction = $logActionTable.idaction + WHERE tmp.idaction IS NULL"; + + Db::query($deleteSql); + } + + private function getTableIdColumns() + { + return array( + 'log_link_visit_action' => 'idlink_va', + 'log_conversion' => 'idvisit', + 'log_visit' => 'idvisit', + 'log_conversion_item' => 'idvisit' + ); + } +} \ No newline at end of file diff --git a/www/analytics/core/DataAccess/TableMetadata.php b/www/analytics/core/DataAccess/TableMetadata.php new file mode 100644 index 00000000..a07d3780 --- /dev/null +++ b/www/analytics/core/DataAccess/TableMetadata.php @@ -0,0 +1,56 @@ +getColumns($table); + + $columns = array_filter($columns, function ($columnName) { + return strpos($columnName, 'idaction') !== false; + }); + + return array_values($columns); + } +} diff --git a/www/analytics/core/DataArray.php b/www/analytics/core/DataArray.php index 72b31fe3..4f375730 100644 --- a/www/analytics/core/DataArray.php +++ b/www/analytics/core/DataArray.php @@ -1,6 +1,6 @@ data[$label])) { - $this->data[$label] = self::makeEmptyRow(); + $this->data[$label] = static::makeEmptyRow(); } $this->doSumVisitsMetrics($row, $this->data[$label]); } @@ -57,11 +57,12 @@ class DataArray * * @return array */ - static public function makeEmptyRow() + public static function makeEmptyRow() { return array(Metrics::INDEX_NB_UNIQ_VISITORS => 0, Metrics::INDEX_NB_VISITS => 0, Metrics::INDEX_NB_ACTIONS => 0, + Metrics::INDEX_NB_USERS => 0, Metrics::INDEX_MAX_ACTIONS => 0, Metrics::INDEX_SUM_VISIT_LENGTH => 0, Metrics::INDEX_BOUNCE_COUNT => 0, @@ -79,7 +80,7 @@ class DataArray * * @return void */ - protected function doSumVisitsMetrics($newRowToAdd, &$oldRowToUpdate, $onlyMetricsAvailableInActionsTable = false) + protected function doSumVisitsMetrics($newRowToAdd, &$oldRowToUpdate) { // Pre 1.2 format: string indexed rows are returned from the DB // Left here for Backward compatibility with plugins doing custom SQL queries using these metrics as string @@ -87,9 +88,7 @@ class DataArray $oldRowToUpdate[Metrics::INDEX_NB_VISITS] += $newRowToAdd['nb_visits']; $oldRowToUpdate[Metrics::INDEX_NB_ACTIONS] += $newRowToAdd['nb_actions']; $oldRowToUpdate[Metrics::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd['nb_uniq_visitors']; - if ($onlyMetricsAvailableInActionsTable) { - return; - } + $oldRowToUpdate[Metrics::INDEX_NB_USERS] += $newRowToAdd['nb_users']; $oldRowToUpdate[Metrics::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd['max_actions'], $oldRowToUpdate[Metrics::INDEX_MAX_ACTIONS]); $oldRowToUpdate[Metrics::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd['sum_visit_length']; $oldRowToUpdate[Metrics::INDEX_BOUNCE_COUNT] += $newRowToAdd['bounce_count']; @@ -97,36 +96,81 @@ class DataArray return; } - $oldRowToUpdate[Metrics::INDEX_NB_VISITS] += $newRowToAdd[Metrics::INDEX_NB_VISITS]; - $oldRowToUpdate[Metrics::INDEX_NB_ACTIONS] += $newRowToAdd[Metrics::INDEX_NB_ACTIONS]; - $oldRowToUpdate[Metrics::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd[Metrics::INDEX_NB_UNIQ_VISITORS]; - if ($onlyMetricsAvailableInActionsTable) { + // Edge case fail safe + if (!isset($oldRowToUpdate[Metrics::INDEX_NB_VISITS])) { return; } + $oldRowToUpdate[Metrics::INDEX_NB_VISITS] += $newRowToAdd[Metrics::INDEX_NB_VISITS]; + $oldRowToUpdate[Metrics::INDEX_NB_ACTIONS] += $newRowToAdd[Metrics::INDEX_NB_ACTIONS]; + $oldRowToUpdate[Metrics::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd[Metrics::INDEX_NB_UNIQ_VISITORS]; + // In case the existing Row had no action metrics (eg. Custom Variable XYZ with "visit" scope) // but the new Row has action metrics (eg. same Custom Variable XYZ this time with a "page" scope) - if(!isset($oldRowToUpdate[Metrics::INDEX_MAX_ACTIONS])) { - $toZero = array(Metrics::INDEX_MAX_ACTIONS, + if (!isset($oldRowToUpdate[Metrics::INDEX_MAX_ACTIONS])) { + $toZero = array( + Metrics::INDEX_NB_USERS, + Metrics::INDEX_MAX_ACTIONS, Metrics::INDEX_SUM_VISIT_LENGTH, Metrics::INDEX_BOUNCE_COUNT, - Metrics::INDEX_NB_VISITS_CONVERTED); - foreach($toZero as $metric) { + Metrics::INDEX_NB_VISITS_CONVERTED + ); + foreach ($toZero as $metric) { $oldRowToUpdate[$metric] = 0; } } + $oldRowToUpdate[Metrics::INDEX_NB_USERS] += $newRowToAdd[Metrics::INDEX_NB_USERS]; $oldRowToUpdate[Metrics::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd[Metrics::INDEX_MAX_ACTIONS], $oldRowToUpdate[Metrics::INDEX_MAX_ACTIONS]); $oldRowToUpdate[Metrics::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd[Metrics::INDEX_SUM_VISIT_LENGTH]; $oldRowToUpdate[Metrics::INDEX_BOUNCE_COUNT] += $newRowToAdd[Metrics::INDEX_BOUNCE_COUNT]; $oldRowToUpdate[Metrics::INDEX_NB_VISITS_CONVERTED] += $newRowToAdd[Metrics::INDEX_NB_VISITS_CONVERTED]; } + /** + * Adds the given row $newRowToAdd to the existing $oldRowToUpdate passed by reference + * The rows are php arrays Name => value + * + * @param array $newRowToAdd + * @param array $oldRowToUpdate + * @param bool $onlyMetricsAvailableInActionsTable + * + * @return void + */ + protected function doSumActionsMetrics($newRowToAdd, &$oldRowToUpdate) + { + // Pre 1.2 format: string indexed rows are returned from the DB + // Left here for Backward compatibility with plugins doing custom SQL queries using these metrics as string + if (!isset($newRowToAdd[Metrics::INDEX_NB_VISITS])) { + $oldRowToUpdate[Metrics::INDEX_NB_VISITS] += $newRowToAdd['nb_visits']; + $oldRowToUpdate[Metrics::INDEX_NB_ACTIONS] += $newRowToAdd['nb_actions']; + $oldRowToUpdate[Metrics::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd['nb_uniq_visitors']; + return; + } + + // Edge case fail safe + if (!isset($oldRowToUpdate[Metrics::INDEX_NB_VISITS])) { + return; + } + + $oldRowToUpdate[Metrics::INDEX_NB_VISITS] += $newRowToAdd[Metrics::INDEX_NB_VISITS]; + if (array_key_exists(Metrics::INDEX_NB_ACTIONS, $newRowToAdd)) { + $oldRowToUpdate[Metrics::INDEX_NB_ACTIONS] += $newRowToAdd[Metrics::INDEX_NB_ACTIONS]; + } + if (array_key_exists(Metrics::INDEX_PAGE_NB_HITS, $newRowToAdd)) { + if (!array_key_exists(Metrics::INDEX_PAGE_NB_HITS, $oldRowToUpdate)) { + $oldRowToUpdate[Metrics::INDEX_PAGE_NB_HITS] = 0; + } + $oldRowToUpdate[Metrics::INDEX_PAGE_NB_HITS] += $newRowToAdd[Metrics::INDEX_PAGE_NB_HITS]; + } + $oldRowToUpdate[Metrics::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd[Metrics::INDEX_NB_UNIQ_VISITORS]; + } + public function sumMetricsGoals($label, $row) { $idGoal = $row['idgoal']; if (!isset($this->data[$label][Metrics::INDEX_GOALS][$idGoal])) { - $this->data[$label][Metrics::INDEX_GOALS][$idGoal] = self::makeEmptyGoalRow($idGoal); + $this->data[$label][Metrics::INDEX_GOALS][$idGoal] = static::makeEmptyGoalRow($idGoal); } $this->doSumGoalsMetrics($row, $this->data[$label][Metrics::INDEX_GOALS][$idGoal]); } @@ -190,12 +234,13 @@ class DataArray public function sumMetricsActions($label, $row) { if (!isset($this->data[$label])) { - $this->data[$label] = self::makeEmptyActionRow(); + $this->data[$label] = static::makeEmptyActionRow(); } - $this->doSumVisitsMetrics($row, $this->data[$label], $onlyMetricsAvailableInActionsTable = true); + + $this->doSumActionsMetrics($row, $this->data[$label]); } - static protected function makeEmptyActionRow() + protected static function makeEmptyActionRow() { return array( Metrics::INDEX_NB_UNIQ_VISITORS => 0, @@ -207,12 +252,12 @@ class DataArray public function sumMetricsEvents($label, $row) { if (!isset($this->data[$label])) { - $this->data[$label] = self::makeEmptyEventRow(); + $this->data[$label] = static::makeEmptyEventRow(); } $this->doSumEventsMetrics($row, $this->data[$label], $onlyMetricsAvailableInActionsTable = true); } - static protected function makeEmptyEventRow() + protected static function makeEmptyEventRow() { return array( Metrics::INDEX_NB_UNIQ_VISITORS => 0, @@ -220,7 +265,7 @@ class DataArray Metrics::INDEX_EVENT_NB_HITS => 0, Metrics::INDEX_EVENT_NB_HITS_WITH_VALUE => 0, Metrics::INDEX_EVENT_SUM_EVENT_VALUE => 0, - Metrics::INDEX_EVENT_MIN_EVENT_VALUE => 0, + Metrics::INDEX_EVENT_MIN_EVENT_VALUE => false, Metrics::INDEX_EVENT_MAX_EVENT_VALUE => 0, ); } @@ -239,16 +284,16 @@ class DataArray $oldRowToUpdate[Metrics::INDEX_EVENT_NB_HITS] += $newRowToAdd[Metrics::INDEX_EVENT_NB_HITS]; $oldRowToUpdate[Metrics::INDEX_EVENT_NB_HITS_WITH_VALUE] += $newRowToAdd[Metrics::INDEX_EVENT_NB_HITS_WITH_VALUE]; - $newRowToAdd[Metrics::INDEX_EVENT_SUM_EVENT_VALUE] = round($newRowToAdd[Metrics::INDEX_EVENT_SUM_EVENT_VALUE], self::EVENT_VALUE_PRECISION); + $newRowToAdd[Metrics::INDEX_EVENT_SUM_EVENT_VALUE] = round($newRowToAdd[Metrics::INDEX_EVENT_SUM_EVENT_VALUE], static::EVENT_VALUE_PRECISION); $oldRowToUpdate[Metrics::INDEX_EVENT_SUM_EVENT_VALUE] += $newRowToAdd[Metrics::INDEX_EVENT_SUM_EVENT_VALUE]; - $oldRowToUpdate[Metrics::INDEX_EVENT_MAX_EVENT_VALUE] = round(max($newRowToAdd[Metrics::INDEX_EVENT_MAX_EVENT_VALUE], $oldRowToUpdate[Metrics::INDEX_EVENT_MAX_EVENT_VALUE]), self::EVENT_VALUE_PRECISION); + $oldRowToUpdate[Metrics::INDEX_EVENT_MAX_EVENT_VALUE] = round(max($newRowToAdd[Metrics::INDEX_EVENT_MAX_EVENT_VALUE], $oldRowToUpdate[Metrics::INDEX_EVENT_MAX_EVENT_VALUE]), static::EVENT_VALUE_PRECISION); // Update minimum only if it is set - if($newRowToAdd[Metrics::INDEX_EVENT_MIN_EVENT_VALUE] !== false) { - if($oldRowToUpdate[Metrics::INDEX_EVENT_MIN_EVENT_VALUE] === false) { - $oldRowToUpdate[Metrics::INDEX_EVENT_MIN_EVENT_VALUE] = round($newRowToAdd[Metrics::INDEX_EVENT_MIN_EVENT_VALUE], self::EVENT_VALUE_PRECISION); + if ($newRowToAdd[Metrics::INDEX_EVENT_MIN_EVENT_VALUE] !== false) { + if ($oldRowToUpdate[Metrics::INDEX_EVENT_MIN_EVENT_VALUE] === false) { + $oldRowToUpdate[Metrics::INDEX_EVENT_MIN_EVENT_VALUE] = round($newRowToAdd[Metrics::INDEX_EVENT_MIN_EVENT_VALUE], static::EVENT_VALUE_PRECISION); } else { - $oldRowToUpdate[Metrics::INDEX_EVENT_MIN_EVENT_VALUE] = round(min($newRowToAdd[Metrics::INDEX_EVENT_MIN_EVENT_VALUE], $oldRowToUpdate[Metrics::INDEX_EVENT_MIN_EVENT_VALUE]), self::EVENT_VALUE_PRECISION); + $oldRowToUpdate[Metrics::INDEX_EVENT_MIN_EVENT_VALUE] = round(min($newRowToAdd[Metrics::INDEX_EVENT_MIN_EVENT_VALUE], $oldRowToUpdate[Metrics::INDEX_EVENT_MIN_EVENT_VALUE]), static::EVENT_VALUE_PRECISION); } } } @@ -279,7 +324,7 @@ class DataArray public function sumMetricsVisitsPivot($parentLabel, $label, $row) { if (!isset($this->dataTwoLevels[$parentLabel][$label])) { - $this->dataTwoLevels[$parentLabel][$label] = self::makeEmptyRow(); + $this->dataTwoLevels[$parentLabel][$label] = static::makeEmptyRow(); } $this->doSumVisitsMetrics($row, $this->dataTwoLevels[$parentLabel][$label]); } @@ -288,7 +333,7 @@ class DataArray { $idGoal = $row['idgoal']; if (!isset($this->dataTwoLevels[$parentLabel][$label][Metrics::INDEX_GOALS][$idGoal])) { - $this->dataTwoLevels[$parentLabel][$label][Metrics::INDEX_GOALS][$idGoal] = self::makeEmptyGoalRow($idGoal); + $this->dataTwoLevels[$parentLabel][$label][Metrics::INDEX_GOALS][$idGoal] = static::makeEmptyGoalRow($idGoal); } $this->doSumGoalsMetrics($row, $this->dataTwoLevels[$parentLabel][$label][Metrics::INDEX_GOALS][$idGoal]); } @@ -298,7 +343,7 @@ class DataArray if (!isset($this->dataTwoLevels[$parentLabel][$label])) { $this->dataTwoLevels[$parentLabel][$label] = $this->makeEmptyActionRow(); } - $this->doSumVisitsMetrics($row, $this->dataTwoLevels[$parentLabel][$label], $onlyMetricsAvailableInActionsTable = true); + $this->doSumActionsMetrics($row, $this->dataTwoLevels[$parentLabel][$label]); } public function sumMetricsEventsPivot($parentLabel, $label, $row) @@ -332,7 +377,7 @@ class DataArray */ protected function enrichWithConversions(&$data) { - foreach ($data as $label => &$values) { + foreach ($data as &$values) { if (!isset($values[Metrics::INDEX_GOALS])) { continue; } @@ -357,7 +402,7 @@ class DataArray // if there are no "visit" column, we force one to prevent future complications // eg. This helps the setDefaultColumnsToDisplay() call - if(!isset($values[Metrics::INDEX_NB_VISITS])) { + if (!isset($values[Metrics::INDEX_NB_VISITS])) { $values[Metrics::INDEX_NB_VISITS] = 0; } } @@ -369,9 +414,9 @@ class DataArray * @param $row * @return bool */ - static public function isRowActions($row) + public static function isRowActions($row) { - return (count($row) == count(self::makeEmptyActionRow())) && isset($row[Metrics::INDEX_NB_ACTIONS]); + return (count($row) == count(static::makeEmptyActionRow())) && isset($row[Metrics::INDEX_NB_ACTIONS]); } /** diff --git a/www/analytics/core/DataFiles/Countries.php b/www/analytics/core/DataFiles/Countries.php deleted file mode 100644 index 38a60827..00000000 --- a/www/analytics/core/DataFiles/Countries.php +++ /dev/null @@ -1,326 +0,0 @@ - 'eur', - 'ae' => 'asi', - 'af' => 'asi', - 'ag' => 'amc', - 'ai' => 'amc', - 'al' => 'eur', - 'am' => 'asi', - 'ao' => 'afr', - 'aq' => 'ant', - 'ar' => 'ams', - 'as' => 'oce', - 'at' => 'eur', - 'au' => 'oce', - 'aw' => 'amc', - 'ax' => 'eur', - 'az' => 'asi', - 'ba' => 'eur', - 'bb' => 'amc', - 'bd' => 'asi', - 'be' => 'eur', - 'bf' => 'afr', - 'bg' => 'eur', - 'bh' => 'asi', - 'bi' => 'afr', - 'bj' => 'afr', - 'bl' => 'amc', - 'bm' => 'amc', - 'bn' => 'asi', - 'bo' => 'ams', - 'bq' => 'amc', - 'br' => 'ams', - 'bs' => 'amc', - 'bt' => 'asi', - 'bv' => 'ant', - 'bw' => 'afr', - 'by' => 'eur', - 'bz' => 'amc', - 'ca' => 'amn', - 'cc' => 'asi', - 'cd' => 'afr', - 'cf' => 'afr', - 'cg' => 'afr', - 'ch' => 'eur', - 'ci' => 'afr', - 'ck' => 'oce', - 'cl' => 'ams', - 'cm' => 'afr', - 'cn' => 'asi', - 'co' => 'ams', - 'cr' => 'amc', - 'cu' => 'amc', - 'cv' => 'afr', - 'cw' => 'amc', - 'cx' => 'asi', - 'cy' => 'eur', - 'cz' => 'eur', - 'de' => 'eur', - 'dj' => 'afr', - 'dk' => 'eur', - 'dm' => 'amc', - 'do' => 'amc', - 'dz' => 'afr', - 'ec' => 'ams', - 'ee' => 'eur', - 'eg' => 'afr', - 'eh' => 'afr', - 'er' => 'afr', - 'es' => 'eur', - 'et' => 'afr', - 'fi' => 'eur', - 'fj' => 'oce', - 'fk' => 'ams', - 'fm' => 'oce', - 'fo' => 'eur', - 'fr' => 'eur', - 'ga' => 'afr', - 'gb' => 'eur', - 'gd' => 'amc', - 'ge' => 'asi', - 'gf' => 'ams', - 'gg' => 'eur', - 'gh' => 'afr', - 'gi' => 'eur', - 'gl' => 'amn', - 'gm' => 'afr', - 'gn' => 'afr', - 'gp' => 'amc', - 'gq' => 'afr', - 'gr' => 'eur', - 'gs' => 'ant', - 'gt' => 'amc', - 'gu' => 'oce', - 'gw' => 'afr', - 'gy' => 'ams', - 'hk' => 'asi', - 'hm' => 'ant', - 'hn' => 'amc', - 'hr' => 'eur', - 'ht' => 'amc', - 'hu' => 'eur', - 'id' => 'asi', - 'ie' => 'eur', - 'il' => 'asi', - 'im' => 'eur', - 'in' => 'asi', - 'io' => 'asi', - 'iq' => 'asi', - 'ir' => 'asi', - 'is' => 'eur', - 'it' => 'eur', - 'je' => 'eur', - 'jm' => 'amc', - 'jo' => 'asi', - 'jp' => 'asi', - 'ke' => 'afr', - 'kg' => 'asi', - 'kh' => 'asi', - 'ki' => 'oce', - 'km' => 'afr', - 'kn' => 'amc', - 'kp' => 'asi', - 'kr' => 'asi', - 'kw' => 'asi', - 'ky' => 'amc', - 'kz' => 'asi', - 'la' => 'asi', - 'lb' => 'asi', - 'lc' => 'amc', - 'li' => 'eur', - 'lk' => 'asi', - 'lr' => 'afr', - 'ls' => 'afr', - 'lt' => 'eur', - 'lu' => 'eur', - 'lv' => 'eur', - 'ly' => 'afr', - 'ma' => 'afr', - 'mc' => 'eur', - 'md' => 'eur', - 'me' => 'eur', - 'mf' => 'amc', - 'mg' => 'afr', - 'mh' => 'oce', - 'mk' => 'eur', - 'ml' => 'afr', - 'mm' => 'asi', - 'mn' => 'asi', - 'mo' => 'asi', - 'mp' => 'oce', - 'mq' => 'amc', - 'mr' => 'afr', - 'ms' => 'amc', - 'mt' => 'eur', - 'mu' => 'afr', - 'mv' => 'asi', - 'mw' => 'afr', - 'mx' => 'amn', - 'my' => 'asi', - 'mz' => 'afr', - 'na' => 'afr', - 'nc' => 'oce', - 'ne' => 'afr', - 'nf' => 'oce', - 'ng' => 'afr', - 'ni' => 'amc', - 'nl' => 'eur', - 'no' => 'eur', - 'np' => 'asi', - 'nr' => 'oce', - 'nu' => 'oce', - 'nz' => 'oce', - 'om' => 'asi', - 'pa' => 'amc', - 'pe' => 'ams', - 'pf' => 'oce', - 'pg' => 'oce', - 'ph' => 'asi', - 'pk' => 'asi', - 'pl' => 'eur', - 'pm' => 'amn', - 'pn' => 'oce', - 'pr' => 'amc', - 'ps' => 'asi', - 'pt' => 'eur', - 'pw' => 'oce', - 'py' => 'ams', - 'qa' => 'asi', - 're' => 'afr', - 'ro' => 'eur', - 'rs' => 'eur', - 'ru' => 'eur', - 'rw' => 'afr', - 'sa' => 'asi', - 'sb' => 'oce', - 'sc' => 'afr', - 'sd' => 'afr', - 'se' => 'eur', - 'sg' => 'asi', - 'sh' => 'afr', - 'si' => 'eur', - 'sj' => 'eur', - 'sk' => 'eur', - 'sl' => 'afr', - 'sm' => 'eur', - 'sn' => 'afr', - 'so' => 'afr', - 'sr' => 'ams', - 'ss' => 'afr', - 'st' => 'afr', - 'sv' => 'amc', - 'sx' => 'amc', - 'sy' => 'asi', - 'sz' => 'afr', - 'tc' => 'amc', - 'td' => 'afr', - 'tf' => 'ant', - 'tg' => 'afr', - 'th' => 'asi', - 'ti' => 'asi', - 'tj' => 'asi', - 'tk' => 'oce', - 'tl' => 'asi', - 'tm' => 'asi', - 'tn' => 'afr', - 'to' => 'oce', - 'tr' => 'eur', - 'tt' => 'amc', - 'tv' => 'oce', - 'tw' => 'asi', - 'tz' => 'afr', - 'ua' => 'eur', - 'ug' => 'afr', - 'um' => 'oce', - 'us' => 'amn', - 'uy' => 'ams', - 'uz' => 'asi', - 'va' => 'eur', - 'vc' => 'amc', - 've' => 'ams', - 'vg' => 'amc', - 'vi' => 'amc', - 'vn' => 'asi', - 'vu' => 'oce', - 'wf' => 'oce', - 'ws' => 'oce', - 'ye' => 'asi', - 'yt' => 'afr', - 'za' => 'afr', - 'zm' => 'afr', - 'zw' => 'afr', - ); - - // codes for internal use - $GLOBALS['Piwik_CountryList_Extras'] = array( - // unknown - 'xx' => 'unk', - - // exceptionally reserved - 'ac' => 'afr', // .ac TLD - 'cp' => 'amc', - 'dg' => 'asi', - 'ea' => 'afr', - 'eu' => 'eur', // .eu TLD - 'fx' => 'eur', - 'ic' => 'afr', - 'su' => 'eur', // .su TLD - 'ta' => 'afr', - 'uk' => 'eur', // .uk TLD - - // transitionally reserved - 'an' => 'amc', // former Netherlands Antilles - 'bu' => 'asi', - 'cs' => 'eur', // former Serbia and Montenegro - 'nt' => 'asi', - 'sf' => 'eur', - 'tp' => 'oce', // .tp TLD - 'yu' => 'eur', // .yu TLD - 'zr' => 'afr', - - // MaxMind GeoIP specific - 'a1' => 'unk', - 'a2' => 'unk', - 'ap' => 'asi', - 'o1' => 'unk', - - // Catalonia (Spain) - 'cat' => 'eur', - ); -} - -if (!isset($GLOBALS['Piwik_ContinentList'])) { - // Primary reference: ISO 3166-1 alpha-2 - $GLOBALS['Piwik_ContinentList'] = array( - 'unk', // unknown - 'amn', // North America - 'amc', // Central America - 'ams', // South America - 'eur', // Europe - 'afr', // Africa - 'asi', // Asia - 'oce', // Oceania - 'ant', // Antarctica - ); -} diff --git a/www/analytics/core/DataFiles/Currencies.php b/www/analytics/core/DataFiles/Currencies.php deleted file mode 100644 index ccf10f61..00000000 --- a/www/analytics/core/DataFiles/Currencies.php +++ /dev/null @@ -1,186 +0,0 @@ - array('currency symbol', 'description'), - - // Top 5 by global trading volume - 'USD' => array('$', 'US dollar'), - 'EUR' => array('€', 'Euro'), - 'JPY' => array('¥', 'Japanese yen'), - 'GBP' => array('£', 'British pound'), - 'CHF' => array('Fr', 'Swiss franc'), - - 'AFN' => array('؋', 'Afghan afghani'), - 'ALL' => array('L', 'Albanian lek'), - 'DZD' => array('د.ج', 'Algerian dinar'), - 'AOA' => array('Kz', 'Angolan kwanza'), - 'ARS' => array('$', 'Argentine peso'), - 'AMD' => array('դր.', 'Armenian dram'), - 'AWG' => array('ƒ', 'Aruban florin'), - 'AUD' => array('$', 'Australian dollar'), - 'AZN' => array('m', 'Azerbaijani manat'), - 'BSD' => array('$', 'Bahamian dollar'), - 'BHD' => array('.د.ب', 'Bahraini dinar'), - 'BDT' => array('৳', 'Bangladeshi taka'), - 'BBD' => array('$', 'Barbadian dollar'), - 'BYR' => array('Br', 'Belarusian ruble'), - 'BZD' => array('$', 'Belize dollar'), - 'BMD' => array('$', 'Bermudian dollar'), - 'BTC' => array('BTC', 'Bitcoin'), - 'BTN' => array('Nu.', 'Bhutanese ngultrum'), - 'BOB' => array('Bs.', 'Bolivian boliviano'), - 'BAM' => array('KM', 'Bosnia Herzegovina mark'), - 'BWP' => array('P', 'Botswana pula'), - 'BRL' => array('R$', 'Brazilian real'), -// 'GBP' => array('£', 'British pound'), - 'BND' => array('$', 'Brunei dollar'), - 'BGN' => array('лв', 'Bulgarian lev'), - 'BIF' => array('Fr', 'Burundian franc'), - 'KHR' => array('៛', 'Cambodian riel'), - 'CAD' => array('$', 'Canadian dollar'), - 'CVE' => array('$', 'Cape Verdean escudo'), - 'KYD' => array('$', 'Cayman Islands dollar'), - 'XAF' => array('Fr', 'Central African CFA franc'), - 'CLP' => array('$', 'Chilean peso'), - 'CNY' => array('元', 'Chinese yuan'), - 'COP' => array('$', 'Colombian peso'), - 'KMF' => array('Fr', 'Comorian franc'), - 'CDF' => array('Fr', 'Congolese franc'), - 'CRC' => array('₡', 'Costa Rican colón'), - 'HRK' => array('kn', 'Croatian kuna'), - 'XPF' => array('F', 'CFP franc'), - 'CUC' => array('$', 'Cuban convertible peso'), - 'CUP' => array('$', 'Cuban peso'), - 'CMG' => array('ƒ', 'Curaçao and Sint Maarten guilder'), - 'CZK' => array('Kč', 'Czech koruna'), - 'DKK' => array('kr', 'Danish krone'), - 'DJF' => array('Fr', 'Djiboutian franc'), - 'DOP' => array('$', 'Dominican peso'), - 'XCD' => array('$', 'East Caribbean dollar'), - 'EGP' => array('ج.م', 'Egyptian pound'), - 'ERN' => array('Nfk', 'Eritrean nakfa'), - 'ETB' => array('Br', 'Ethiopian birr'), -// 'EUR' => array('€', 'Euro'), - 'FKP' => array('£', 'Falkland Islands pound'), - 'FJD' => array('$', 'Fijian dollar'), - 'GMD' => array('D', 'Gambian dalasi'), - 'GEL' => array('ლ', 'Georgian lari'), - 'GHS' => array('₵', 'Ghanaian cedi'), - 'GIP' => array('£', 'Gibraltar pound'), - 'GTQ' => array('Q', 'Guatemalan quetzal'), - 'GNF' => array('Fr', 'Guinean franc'), - 'GYD' => array('$', 'Guyanese dollar'), - 'HTG' => array('G', 'Haitian gourde'), - 'HNL' => array('L', 'Honduran lempira'), - 'HKD' => array('$', 'Hong Kong dollar'), - 'HUF' => array('Ft', 'Hungarian forint'), - 'ISK' => array('kr', 'Icelandic króna'), - 'INR' => array('‎₹', 'Indian rupee'), - 'IDR' => array('Rp', 'Indonesian rupiah'), - 'IRR' => array('﷼', 'Iranian rial'), - 'IQD' => array('ع.د', 'Iraqi dinar'), - 'ILS' => array('₪', 'Israeli new shekel'), - 'JMD' => array('$', 'Jamaican dollar'), -// 'JPY' => array('¥', 'Japanese yen'), - 'JOD' => array('د.ا', 'Jordanian dinar'), - 'KZT' => array('₸', 'Kazakhstani tenge'), - 'KES' => array('Sh', 'Kenyan shilling'), - 'KWD' => array('د.ك', 'Kuwaiti dinar'), - 'KGS' => array('лв', 'Kyrgyzstani som'), - 'LAK' => array('₭', 'Lao kip'), - 'LBP' => array('ل.ل', 'Lebanese pound'), - 'LSL' => array('L', 'Lesotho loti'), - 'LRD' => array('$', 'Liberian dollar'), - 'LYD' => array('ل.د', 'Libyan dinar'), - 'LTL' => array('Lt', 'Lithuanian litas'), - 'MOP' => array('P', 'Macanese pataca'), - 'MKD' => array('ден', 'Macedonian denar'), - 'MGA' => array('Ar', 'Malagasy ariary'), - 'MWK' => array('MK', 'Malawian kwacha'), - 'MYR' => array('RM', 'Malaysian ringgit'), - 'MVR' => array('ރ.', 'Maldivian rufiyaa'), - 'MRO' => array('UM', 'Mauritanian ouguiya'), - 'MUR' => array('₨', 'Mauritian rupee'), - 'MXN' => array('$', 'Mexican peso'), - 'MDL' => array('L', 'Moldovan leu'), - 'MNT' => array('₮', 'Mongolian tögrög'), - 'MAD' => array('د.م.', 'Moroccan dirham'), - 'MZN' => array('MTn', 'Mozambican metical'), - 'MMK' => array('K', 'Myanma kyat'), - 'NAD' => array('$', 'Namibian dollar'), - 'NPR' => array('₨', 'Nepalese rupee'), - 'ANG' => array('ƒ', 'Netherlands Antillean guilder'), - 'TWD' => array('$', 'New Taiwan dollar'), - 'NZD' => array('$', 'New Zealand dollar'), - 'NIO' => array('C$', 'Nicaraguan córdoba'), - 'NGN' => array('₦', 'Nigerian naira'), - 'KPW' => array('₩', 'North Korean won'), - 'NOK' => array('kr', 'Norwegian krone'), - 'OMR' => array('ر.ع.', 'Omani rial'), - 'PKR' => array('₨', 'Pakistani rupee'), - 'PAB' => array('B/.', 'Panamanian balboa'), - 'PGK' => array('K', 'Papua New Guinean kina'), - 'PYG' => array('₲', 'Paraguayan guaraní'), - 'PEN' => array('S/.', 'Peruvian nuevo sol'), - 'PHP' => array('₱', 'Philippine peso'), - 'PLN' => array('zł', 'Polish złoty'), - 'QAR' => array('ر.ق', 'Qatari riyal'), - 'RON' => array('L', 'Romanian leu'), - 'RUB' => array('руб.', 'Russian ruble'), - 'RWF' => array('Fr', 'Rwandan franc'), - 'SHP' => array('£', 'Saint Helena pound'), - 'SVC' => array('₡', 'Salvadoran colón'), - 'WST' => array('T', 'Samoan tala'), - 'STD' => array('Db', 'São Tomé and Príncipe dobra'), - 'SAR' => array('ر.س', 'Saudi riyal'), - 'RSD' => array('дин. or din.', 'Serbian dinar'), - 'SCR' => array('₨', 'Seychellois rupee'), - 'SLL' => array('Le', 'Sierra Leonean leone'), - 'SGD' => array('$', 'Singapore dollar'), - 'SBD' => array('$', 'Solomon Islands dollar'), - 'SOS' => array('Sh', 'Somali shilling'), - 'ZAR' => array('R', 'South African rand'), - 'KRW' => array('₩', 'South Korean won'), - 'LKR' => array('Rs', 'Sri Lankan rupee'), - 'SDG' => array('جنيه سوداني', 'Sudanese pound'), - 'SRD' => array('$', 'Surinamese dollar'), - 'SZL' => array('L', 'Swazi lilangeni'), - 'SEK' => array('kr', 'Swedish krona'), -// 'CHF' => array('Fr', 'Swiss franc'), - 'SYP' => array('ل.س', 'Syrian pound'), - 'TJS' => array('ЅМ', 'Tajikistani somoni'), - 'TZS' => array('Sh', 'Tanzanian shilling'), - 'THB' => array('฿', 'Thai baht'), - 'TOP' => array('T$', 'Tongan paʻanga'), - 'TTD' => array('$', 'Trinidad and Tobago dollar'), - 'TND' => array('د.ت', 'Tunisian dinar'), - 'TRY' => array('TL', 'Turkish lira'), - 'TMM' => array('m', 'Turkmenistani manat'), - 'UGX' => array('Sh', 'Ugandan shilling'), - 'UAH' => array('₴', 'Ukrainian hryvnia'), - 'AED' => array('د.إ', 'United Arab Emirates dirham'), -// 'USD' => array('$', 'United States dollar'), - 'UYU' => array('$', 'Uruguayan peso'), - 'UZS' => array('лв', 'Uzbekistani som'), - 'VUV' => array('Vt', 'Vanuatu vatu'), - 'VEF' => array('Bs F', 'Venezuelan bolívar'), - 'VND' => array('₫', 'Vietnamese đồng'), - 'XOF' => array('Fr', 'West African CFA franc'), - 'YER' => array('﷼', 'Yemeni rial'), - 'ZMW' => array('ZK', 'Zambian kwacha'), - 'ZWL' => array('$', 'Zimbabwean dollar'), - ); -} diff --git a/www/analytics/core/DataFiles/LanguageToCountry.php b/www/analytics/core/DataFiles/LanguageToCountry.php deleted file mode 100644 index 7e3ffafd..00000000 --- a/www/analytics/core/DataFiles/LanguageToCountry.php +++ /dev/null @@ -1,63 +0,0 @@ - 'bg', // Bulgarian => Bulgaria - 'ca' => 'es', // Catalan => Spain - 'cs' => 'cz', // Czech => Czech Republic - 'da' => 'dk', // Danish => Denmark - 'de' => 'de', // German => Germany - 'el' => 'gr', // Greek => Greece - 'es' => 'es', // Spanish => Spain - 'et' => 'ee', // Estonian => Estonia - 'fa' => 'ir', // Farsi => Iran - 'fi' => 'fi', // Finnish => Finland - 'fr' => 'fr', // French => France - 'he' => 'il', // Hebrew => Israel - 'hr' => 'hr', // Croatian => Croatia - 'hu' => 'hu', // Hungarian => Hungary - 'id' => 'id', // Indonesian => Indonesia - 'is' => 'is', // Icelandic => Iceland - 'it' => 'it', // Italian => Italy - 'ja' => 'jp', // Japanese => Japan - 'ko' => 'kr', // Korean => South Korea - 'lt' => 'lt', // Lithuanian => Lithuania - 'lv' => 'lv', // Latvian => Latvia - 'mk' => 'mk', // Macedonian => Macedonia - 'ms' => 'my', // Malay => Malaysia - 'nb' => 'no', // Bokmål => Norway - 'nl' => 'nl', // Dutch => Netherlands - 'nn' => 'no', // Nynorsk => Norway - 'no' => 'no', // Norwegian => Norway - 'pl' => 'pl', // Polish => Poland - 'pt' => 'pt', // Portugese => Portugal - 'ro' => 'ro', // Romanian => Romania - 'ru' => 'ru', // Russian => Russia - 'sk' => 'sk', // Slovak => Slovakia - 'sl' => 'si', // Slovene => Slovenia - 'sq' => 'al', // Albanian => Albania - 'sr' => 'rs', // Serbian => Serbia - 'sv' => 'se', // Swedish => Sweden - 'th' => 'th', // Thai => Thailand - 'bo' => 'ti', // Tibetan => Tibet - 'tr' => 'tr', // Turkish => Turkey - 'uk' => 'ua', // Ukrainian => Ukraine - ); -} diff --git a/www/analytics/core/DataFiles/Languages.php b/www/analytics/core/DataFiles/Languages.php deleted file mode 100644 index 47720149..00000000 --- a/www/analytics/core/DataFiles/Languages.php +++ /dev/null @@ -1,203 +0,0 @@ - array('Afar'), - 'ab' => array('Abkhazian'), - 'ae' => array('Avestan'), - 'af' => array('Afrikaans'), - 'ak' => array('Akan'), - 'am' => array('Amharic'), - 'an' => array('Aragonese'), - 'ar' => array('Arabic'), - 'as' => array('Assamese'), - 'av' => array('Avaric'), - 'ay' => array('Aymara'), - 'az' => array('Azerbaijani'), - 'ba' => array('Bashkir'), - 'be' => array('Belarusian'), - 'bg' => array('Bulgarian'), - 'bh' => array('Bihari'), // 'Bihari languages' - 'bi' => array('Bislama'), - 'bm' => array('Bambara'), - 'bn' => array('Bengali'), - 'bo' => array('Tibetan'), - 'br' => array('Breton'), - 'bs' => array('Bosnian'), - 'ca' => array('Catalan', 'Valencian'), - 'ce' => array('Chechen'), - 'ch' => array('Chamorro'), - 'co' => array('Corsican'), - 'cr' => array('Cree'), - 'cs' => array('Czech'), - 'cu' => array('Church Slavic', 'Old Slavonic', 'Church Slavonic', 'Old Bulgarian', 'Old Church Slavonic'), - 'cv' => array('Chuvash'), - 'cy' => array('Welsh'), - 'da' => array('Danish'), - 'de' => array('German'), - 'dv' => array('Divehi', 'Dhivehi', 'Maldivian'), - 'dz' => array('Dzongkha'), - 'ee' => array('Ewe'), - 'el' => array('Greek', 'Modern Greek', 'Hellenic'), // Greek, Modern (1453-) - 'en' => array('English'), - 'eo' => array('Esperanto'), - 'es' => array('Spanish', 'Castilian'), - 'et' => array('Estonian'), - 'eu' => array('Basque'), - 'fa' => array('Persian'), - 'ff' => array('Fulah'), - 'fi' => array('Finnish'), - 'fj' => array('Fijian'), - 'fo' => array('Faroese'), - 'fr' => array('French'), - 'fy' => array('Western Frisian'), - 'ga' => array('Irish'), - 'gd' => array('Gaelic', 'Scottish Gaelic'), - 'gl' => array('Galician'), - 'gn' => array('Guarani'), - 'gu' => array('Gujarati'), - 'gv' => array('Manx'), - 'ha' => array('Hausa'), - 'he' => array('Hebrew'), - 'hi' => array('Hindi'), - 'ho' => array('Hiri Motu'), - 'hr' => array('Croatian'), - 'ht' => array('Haitian', 'Haitian Creole'), - 'hu' => array('Hungarian'), - 'hy' => array('Armenian'), - 'hz' => array('Herero'), - 'ia' => array('Interlingua'), // 'Interlingua (International Auxiliary Language Association)' - 'id' => array('Indonesian'), - 'ie' => array('Interlingue', 'Occidental'), - 'ig' => array('Igbo'), - 'ii' => array('Sichuan Yi', 'Nuosu'), - 'ik' => array('Inupiaq'), - 'io' => array('Ido'), - 'is' => array('Icelandic'), - 'it' => array('Italian'), - 'iu' => array('Inuktitut'), - 'ja' => array('Japanese'), - 'jv' => array('Javanese'), - 'ka' => array('Georgian'), - 'kg' => array('Kongo'), - 'ki' => array('Kikuyu', 'Gikuyu'), - 'kj' => array('Kuanyama', 'Kwanyama'), - 'kk' => array('Kazakh'), - 'kl' => array('Kalaallisut', 'Greenlandic'), - 'km' => array('Central Khmer'), - 'kn' => array('Kannada'), - 'ko' => array('Korean'), - 'kr' => array('Kanuri'), - 'ks' => array('Kashmiri'), - 'ku' => array('Kurdish'), - 'kv' => array('Komi'), - 'kw' => array('Cornish'), - 'ky' => array('Kirghiz', 'Kyrgyz'), - 'la' => array('Latin'), - 'lb' => array('Luxembourgish', 'Letzeburgesch'), - 'lg' => array('Ganda'), - 'li' => array('Limburgan', 'Limburger', 'Limburgish'), - 'ln' => array('Lingala'), - 'lo' => array('Lao'), - 'lt' => array('Lithuanian'), - 'lu' => array('Luba-Katanga'), - 'lv' => array('Latvian'), - 'mg' => array('Malagasy'), - 'mh' => array('Marshallese'), - 'mi' => array('Maori'), - 'mk' => array('Macedonian'), - 'ml' => array('Malayalam'), - 'mn' => array('Mongolian'), -// 'mo' => array('Moldavian'), // deprecated - 'mr' => array('Marathi'), - 'ms' => array('Malay'), - 'mt' => array('Maltese'), - 'my' => array('Burmese'), - 'na' => array('Nauru'), - 'nb' => array('Norwegian Bokmål'), - 'nd' => array('North Ndebele'), - 'ne' => array('Nepali'), - 'ng' => array('Ndonga'), - 'nl' => array('Dutch', 'Flemish'), - 'nn' => array('Norwegian Nynorsk'), - 'no' => array('Norwegian'), - 'nr' => array('South Ndebele'), - 'nv' => array('Navajo', 'Navaho'), - 'ny' => array('Chichewa', 'Chewa', 'Nyanja'), - 'oc' => array('Occitan', 'Provençal'), // Occitan (post 1500) - 'oj' => array('Ojibwa'), - 'om' => array('Oromo'), - 'or' => array('Oriya'), - 'os' => array('Ossetian', 'Ossetic'), - 'pa' => array('Panjabi', 'Punjabi'), - 'pi' => array('Pali'), - 'pl' => array('Polish'), - 'ps' => array('Pushto', 'Pashto'), - 'pt' => array('Portuguese'), - 'qu' => array('Quechua'), - 'rm' => array('Romansh'), - 'rn' => array('Rundi'), - 'ro' => array('Romanian', 'Moldavian', 'Moldovan'), - 'ru' => array('Russian'), - 'rw' => array('Kinyarwanda'), - 'sa' => array('Sanskrit'), - 'sc' => array('Sardinian'), - 'sd' => array('Sindhi'), - 'se' => array('Northern Sami'), - 'sg' => array('Sango'), -// 'sh' => array('Serbo-Croatian'), // deprecated - 'si' => array('Sinhala', 'Sinhalese'), - 'sk' => array('Slovak'), - 'sl' => array('Slovenian'), - 'sm' => array('Samoan'), - 'sn' => array('Shona'), - 'so' => array('Somali'), - 'sq' => array('Albanian'), - 'sr' => array('Serbian'), - 'ss' => array('Swati'), - 'st' => array('Southern Soth'), - 'su' => array('Sundanese'), - 'sv' => array('Swedish'), - 'sw' => array('Swahili'), - 'ta' => array('Tamil'), - 'te' => array('Telugu'), - 'tg' => array('Tajik'), - 'th' => array('Thai'), - 'ti' => array('Tigrinya'), - 'tk' => array('Turkmen'), - 'tl' => array('Tagalog'), - 'tn' => array('Tswana'), - 'to' => array('Tonga'), // Tonga (Tonga Islands) - 'tr' => array('Turkish'), - 'ts' => array('Tsonga'), - 'tt' => array('Tatar'), - 'tw' => array('Twi'), - 'ty' => array('Tahitian'), - 'ug' => array('Uighur', 'Uyghur'), - 'uk' => array('Ukrainian'), - 'ur' => array('Urdu'), - 'uz' => array('Uzbek'), - 've' => array('Venda'), - 'vi' => array('Vietnamese'), - 'vo' => array('Volapük'), - 'wa' => array('Walloon'), - 'wo' => array('Wolof'), - 'xh' => array('Xhosa'), - 'yi' => array('Yiddish'), - 'yo' => array('Yoruba'), - 'za' => array('Zhuang', 'Chuang'), - 'zh' => array('Chinese'), - 'zu' => array('Zulu'), - ); -} diff --git a/www/analytics/core/DataFiles/Providers.php b/www/analytics/core/DataFiles/Providers.php index ff26661c..d26d85b6 100644 --- a/www/analytics/core/DataFiles/Providers.php +++ b/www/analytics/core/DataFiles/Providers.php @@ -1,6 +1,6 @@ array( SearchEngineName, KeywordParameter, [path containing the keyword], [charset used by the search engine]) - * - * The main search engine URL has to be at the top of the list for the given - * search Engine. This serves as the master record so additional URLs - * don't have to duplicate all the information, but can override when needed. - * - * The URL, "example.com", will match "example.com", "m.example.com", - * "www.example.com", and "search.example.com". - * - * For region-specific search engines, the URL, "{}.example.com" will match - * any ISO3166-1 alpha2 country code against "{}". Similarly, "example.{}" - * will match against valid country TLDs, but should be used sparingly to - * avoid false positives. - * - * The charset should be an encoding supported by mbstring. If unspecified, - * we'll assume it's UTF-8. - * Reference: http://www.php.net/manual/en/mbstring.encodings.php - * - * You can add new search engines icons by adding the icon in the - * plugins/Referrers/images/searchEngines directory using the format - * 'mainSearchEngineUrl.png'. Example: www.google.com.png - * - * To help Piwik link directly the search engine result page for the keyword, - * specify the third entry in the array using the macro {k} that will - * automatically be replaced by the keyword. - * - * A simple example is: - * 'www.google.com' => array('Google', 'q', 'search?q={k}'), - * - * A more complicated example, with an array of possible variable names, and a custom charset: - * 'www.baidu.com' => array('Baidu', array('wd', 'word', 'kw'), 's?wd={k}', 'gb2312'), - * - * Another example using a regular expression to parse the path for keywords: - * 'infospace.com' => array('InfoSpace', array('/dir1\/(pattern)\/dir2/'), '/dir1/{k}/dir2/stuff/'), - */ -if (!isset($GLOBALS['Piwik_SearchEngines'])) { - $GLOBALS['Piwik_SearchEngines'] = array( - // 1 - '1.cz' => array('1.cz', array('/s\/([^\/]+)/', 'q'), 's/{k}', 'iso-8859-2'), - - // 123people - 'www.123people.com' => array('123people', array('/s\/([^\/]+)/', 'search_term'), 's/{k}'), - '123people.{}' => array('123people'), - - // 360search - 'so.360.cn' => array('360search', 'q', 's?q={k}', array('UTF-8', 'gb2312')), - 'www.so.com' => array('360search', 'q', 's?q={k}', array('UTF-8', 'gb2312')), - - // Abacho - 'www.abacho.de' => array('Abacho', 'q', 'suche?q={k}'), - 'www.abacho.com' => array('Abacho'), - 'www.abacho.co.uk' => array('Abacho'), - 'www.se.abacho.com' => array('Abacho'), - 'www.tr.abacho.com' => array('Abacho'), - 'www.abacho.at' => array('Abacho'), - 'www.abacho.fr' => array('Abacho'), - 'www.abacho.es' => array('Abacho'), - 'www.abacho.ch' => array('Abacho'), - 'www.abacho.it' => array('Abacho'), - - // ABCsøk - 'abcsok.no' => array('ABCsøk', 'q', '?q={k}'), - 'verden.abcsok.no' => array('ABCsøk'), - - // Acoon - 'www.acoon.de' => array('Acoon', 'begriff', 'cgi-bin/search.exe?begriff={k}'), - - // Alexa - 'alexa.com' => array('Alexa', 'q', 'search?q={k}'), - 'search.toolbars.alexa.com' => array('Alexa'), - - // Alice Adsl - 'rechercher.aliceadsl.fr' => array('Alice Adsl', 'qs', 'google.pl?qs={k}'), - - // Allesklar - 'www.allesklar.de' => array('Allesklar', 'words', '?words={k}'), - 'www.allesklar.at' => array('Allesklar'), - 'www.allesklar.ch' => array('Allesklar'), - - // AllTheWeb - 'www.alltheweb.com' => array('AllTheWeb', 'q', 'search?q={k}'), - - // all.by - 'all.by' => array('All.by', 'query', 'cgi-bin/search.cgi?mode=by&query={k}'), - - // Altavista - 'www.altavista.com' => array('AltaVista', 'q', 'web/results?q={k}'), - 'search.altavista.com' => array('AltaVista'), - 'listings.altavista.com' => array('AltaVista'), - 'altavista.de' => array('AltaVista'), - 'altavista.fr' => array('AltaVista'), - '{}.altavista.com' => array('AltaVista'), - 'be-nl.altavista.com' => array('AltaVista'), - 'be-fr.altavista.com' => array('AltaVista'), - - // Apollo Latvia - 'apollo.lv/portal/search/' => array('Apollo lv', 'q', '?cof=FORID%3A11&q={k}&search_where=www'), - - // APOLLO7 - 'apollo7.de' => array('Apollo7', 'query', 'a7db/index.php?query={k}&de_sharelook=true&de_bing=true&de_witch=true&de_google=true&de_yahoo=true&de_lycos=true'), - - // AOL - 'search.aol.com' => array('AOL', array('query', 'q'), 'aol/search?q={k}'), - 'search.aol.it' => array('AOL'), - 'aolsearch.aol.com' => array('AOL'), - 'www.aolrecherche.aol.fr' => array('AOL'), - 'www.aolrecherches.aol.fr' => array('AOL'), - 'www.aolimages.aol.fr' => array('AOL'), - 'aim.search.aol.com' => array('AOL'), - 'www.recherche.aol.fr' => array('AOL'), - 'recherche.aol.fr' => array('AOL'), - 'find.web.aol.com' => array('AOL'), - 'recherche.aol.ca' => array('AOL'), - 'aolsearch.aol.co.uk' => array('AOL'), - 'search.aol.co.uk' => array('AOL'), - 'aolrecherche.aol.fr' => array('AOL'), - 'sucheaol.aol.de' => array('AOL'), - 'suche.aol.de' => array('AOL'), - 'o2suche.aol.de' => array('AOL'), - 'suche.aolsvc.de' => array('AOL'), - 'aolbusqueda.aol.com.mx' => array('AOL'), - 'alicesuche.aol.de' => array('AOL'), - 'alicesuchet.aol.de' => array('AOL'), - 'suchet2.aol.de' => array('AOL'), - 'search.hp.my.aol.com.au' => array('AOL'), - 'search.hp.my.aol.de' => array('AOL'), - 'search.hp.my.aol.it' => array('AOL'), - 'search-intl.netscape.com' => array('AOL'), - 'de.aolsearch.com' => array('AOL', 'q', 'search?q={k}'), - - // Aport - 'sm.aport.ru' => array('Aport', 'r', 'search?r={k}'), - - // arama - 'arama.com' => array('Arama', 'q', 'search.php3?q={k}'), - - // Arcor - 'www.arcor.de' => array('Arcor', 'Keywords', 'content/searchresult.jsp?Keywords={k}'), - - // Arianna (Libero.it) - 'arianna.libero.it' => array('Arianna', 'query', 'search/abin/integrata.cgi?query={k}'), - 'www.arianna.com' => array('Arianna'), - - // Ask (IAC Search & Media) - 'ask.com' => array('Ask', array('ask', 'q', 'searchfor'), 'web?q={k}'), - 'web.ask.com' => array('Ask'), - 'int.ask.com' => array('Ask'), - 'mws.ask.com' => array('Ask'), - 'images.ask.com' => array('Ask'), - 'images.{}.ask.com' => array('Ask'), - 'ask.reference.com' => array('Ask'), - 'www.askkids.com' => array('Ask'), - 'iwon.ask.com' => array('Ask'), - 'www.ask.co.uk' => array('Ask'), - '{}.ask.com' => array('Ask'), - 'www.qbyrd.com' => array('Ask'), - '{}.qbyrd.com' => array('Ask'), - 'www.search-results.com' => array('Ask'), - 'int.search-results.com' => array('Ask'), - '{}.search-results.com' => array('Ask'), - 'search.ask.com' => array('Ask'), - '{}.search.ask.com' => array('Ask'), - 'avira-int.ask.com' => array('Ask'), - 'searchqu.com' => array('Ask'), - - // Atlas - 'searchatlas.centrum.cz' => array('Atlas', 'q', '?q={k}'), - - // Austronaut - 'www2.austronaut.at' => array('Austronaut', 'q'), - 'www1.austronaut.at' => array('Austronaut'), - - // Babylon (Enhanced by Google) - 'search.babylon.com' => array('Babylon', array('q', '/\/web\/(.*)/'), '?q={k}'), - 'searchassist.babylon.com' => array('Babylon'), - - // Baidu - 'www.baidu.com' => array('Baidu', array('wd', 'word', 'kw'), 's?wd={k}', array('UTF-8', 'gb2312')), - 'www1.baidu.com' => array('Baidu'), - 'zhidao.baidu.com' => array('Baidu'), - 'tieba.baidu.com' => array('Baidu'), - 'news.baidu.com' => array('Baidu'), - 'web.gougou.com' => array('Baidu', 'search', 'search?search={k}'), // uses baidu search - - // Biglobe - 'cgi.search.biglobe.ne.jp' => array('Biglobe', 'q', 'cgi-bin/search-st?q={k}'), - - // Bing - 'bing.com' => array('Bing', array('q', 'Q'), 'search?q={k}'), - '{}.bing.com' => array('Bing'), - 'msnbc.msn.com' => array('Bing'), - 'dizionario.it.msn.com' => array('Bing'), - 'enciclopedia.it.msn.com' => array('Bing'), - - // Bing Cache - 'cc.bingj.com' => array('Bing'), - - // Bing Images - 'bing.com/images/search' => array('Bing Images', array('q', 'Q'), '?q={k}'), - '{}.bing.com/images/search' => array('Bing Images'), - - // blekko - 'blekko.com' => array('blekko', array('q', '/\/ws\/(.*)/'), 'ws/{k}'), - - // Blogdigger - 'www.blogdigger.com' => array('Blogdigger', 'q'), - - // Blogpulse - 'www.blogpulse.com' => array('Blogpulse', 'query', 'search?query={k}'), - - // Bluewin - 'search.bluewin.ch' => array('Bluewin', array('searchTerm', 'q'), 'v2/index.php?q={k}'), - - // canoe.ca - 'web.canoe.ca' => array('Canoe.ca', 'q', 'search?q={k}'), - - // Centrum - 'search.centrum.cz' => array('Centrum', 'q', '?q={k}'), - 'morfeo.centrum.cz' => array('Centrum'), - - // Charter - 'www.charter.net' => array('Charter', 'q', 'search/index.php?q={k}'), - - // Claro Search - 'claro-search.com' => array('Claro Search', 'q', '?q={k}'), - - // Clix (Enhanced by Google) - 'pesquisa.clix.pt' => array('Clix', 'question', 'resultado.html?in=Mundial&question={k}'), - - // Conduit - 'search.conduit.com' => array('Conduit.com', 'q', 'Results.aspx?q={k}'), - 'images.search.conduit.com' => array('Conduit.com'), - - // Comcast - 'search.comcast.net' => array('Comcast', 'q', '?q={k}'), - - // Crawler - 'www.crawler.com' => array('Crawler', 'q', 'search/results1.aspx?q={k}'), - - // Compuserve - 'websearch.cs.com' => array('Compuserve.com (Enhanced by Google)', 'query', 'cs/search?query={k}'), - - // Cuil - 'www.cuil.com' => array('Cuil', 'q', 'search?q={k}'), - - // Daemon search - 'daemon-search.com' => array('Daemon search', 'q', 'explore/web?q={k}'), - 'my.daemon-search.com' => array('Daemon search'), - - // DasOertliche - 'www.dasoertliche.de' => array('DasOertliche', 'kw'), - - // DasTelefonbuch - 'www1.dastelefonbuch.de' => array('DasTelefonbuch', 'kw'), - - // Daum - 'search.daum.net' => array('Daum', 'q', 'search?q={k}'), - - // Delfi Latvia - 'smart.delfi.lv' => array('Delfi lv', 'q', 'find?q={k}'), - - // Delfi - 'otsing.delfi.ee' => array('Delfi EE', 'q', 'find?q={k}'), - - // Digg - 'digg.com' => array('Digg', 's', 'search?s={k}'), - - // dir.com - 'fr.dir.com' => array('dir.com', 'req'), - - // dmoz - 'dmoz.org' => array('dmoz', 'search'), - 'editors.dmoz.org' => array('dmoz'), - - // DuckDuckGo - 'duckduckgo.com' => array('DuckDuckGo', 'q', '?q={k}'), - - // earthlink - 'search.earthlink.net' => array('Earthlink', 'q', 'search?q={k}'), - - // Ecosia (powered by Bing) - 'ecosia.org' => array('Ecosia', 'q', 'search.php?q={k}'), - - // Eniro - 'www.eniro.se' => array('Eniro', array('q', 'search_word'), 'query?q={k}'), - - // Eurip - 'www.eurip.com' => array('Eurip', 'q', 'search/?q={k}'), - - // Euroseek - 'www.euroseek.com' => array('Euroseek', 'string', 'system/search.cgi?string={k}'), - - // Everyclick - 'www.everyclick.com' => array('Everyclick', 'keyword'), - - // Excite - 'search.excite.it' => array('Excite', 'q', 'web/?q={k}'), - 'search.excite.fr' => array('Excite'), - 'search.excite.de' => array('Excite'), - 'search.excite.co.uk' => array('Excite'), - 'search.excite.es' => array('Excite'), - 'search.excite.nl' => array('Excite'), - 'msxml.excite.com' => array('Excite', '/\/[^\/]+\/ws\/results\/[^\/]+\/([^\/]+)/'), - 'www.excite.co.jp' => array('Excite', 'search', 'search.gw?search={k}', 'SHIFT_JIS'), - - // Exalead - 'www.exalead.fr' => array('Exalead', 'q', 'search/results?q={k}'), - 'www.exalead.com' => array('Exalead'), - - // eo - 'eo.st' => array('eo', 'x_query', 'cgi-bin/eolost.cgi?x_query={k}'), - - // Facebook - 'www.facebook.com' => array('Facebook', 'q', 'search/?q={k}'), - - // Fast Browser Search - 'www.fastbrowsersearch.com' => array('Fast Browser Search', 'q', 'results/results.aspx?q={k}'), - - // Francite - 'recherche.francite.com' => array('Francite', 'name'), - - // Fireball - 'www.fireball.de' => array('Fireball', 'q', 'ajax.asp?q={k}'), - - // Firstfind - 'www.firstsfind.com' => array('Firstsfind', 'qry'), - - // Fixsuche - 'www.fixsuche.de' => array('Fixsuche', 'q'), - - // Flix - 'www.flix.de' => array('Flix.de', 'keyword'), - - // Forestle - 'forestle.org' => array('Forestle', 'q', 'search.php?q={k}'), - '{}.forestle.org' => array('Forestle'), - 'forestle.mobi' => array('Forestle'), - - // Free - 'search.free.fr' => array('Free', 'q'), - 'search1-2.free.fr' => array('Free'), - 'search1-1.free.fr' => array('Free'), - - // Freecause - 'search.freecause.com' => array('FreeCause', 'p', '?p={k}'), - - // Freenet - 'suche.freenet.de' => array('Freenet', array('query', 'Keywords'), 'suche/?query={k}'), - - // FriendFeed - 'friendfeed.com' => array('FriendFeed', 'q', 'search?q={k}'), - - // GAIS - 'gais.cs.ccu.edu.tw' => array('GAIS', 'q', 'search.php?q={k}'), - - // Geona - 'geona.net' => array('Geona', 'q', 'search?q={k}'), - - // Gigablast - 'www.gigablast.com' => array('Gigablast', 'q', 'search?q={k}'), - 'dir.gigablast.com' => array('Gigablast (Directory)', 'q'), - - // Gnadenmeer - 'www.gnadenmeer.de' => array('Gnadenmeer', 'keyword'), - - // Gomeo - 'www.gomeo.com' => array('Gomeo', array('Keywords', '/\/search\/([^\/]+)/'), '/search/{k}'), - - // goo - 'search.goo.ne.jp' => array('goo', 'MT', 'web.jsp?MT={k}'), - 'ocnsearch.goo.ne.jp' => array('goo'), - - // Google - 'google.com' => array('Google', 'q', 'search?q={k}'), - 'google.{}' => array('Google'), - 'www2.google.com' => array('Google'), - 'ipv6.google.com' => array('Google'), - 'go.google.com' => array('Google'), - - // Google vs typo squatters - 'wwwgoogle.com' => array('Google'), - 'wwwgoogle.{}' => array('Google'), - 'gogole.com' => array('Google'), - 'gogole.{}' => array('Google'), - 'gppgle.com' => array('Google'), - 'gppgle.{}' => array('Google'), - 'googel.com' => array('Google'), - 'googel.{}' => array('Google'), - - // Powered by Google - 'search.avg.com' => array('Google'), - 'isearch.avg.com' => array('Google'), - 'www.cnn.com' => array('Google', 'query'), - 'darkoogle.com' => array('Google'), - 'search.darkoogle.com' => array('Google'), - 'search.foxtab.com' => array('Google'), - 'www.gooofullsearch.com' => array('Google', 'Keywords'), - 'search.hiyo.com' => array('Google'), - 'search.incredimail.com' => array('Google'), - 'search1.incredimail.com' => array('Google'), - 'search2.incredimail.com' => array('Google'), - 'search3.incredimail.com' => array('Google'), - 'search4.incredimail.com' => array('Google'), - 'search.sweetim.com' => array('Google'), - 'www.fastweb.it' => array('Google'), - 'search.juno.com' => array('Google', 'query'), - 'find.tdc.dk' => array('Google'), - 'it.luna.tv' => array('Google'), - 'searchresults.verizon.com' => array('Google'), - 'search.walla.co.il' => array('Google'), - 'search.alot.com' => array('Google'), - 'suche.gmx.net' => array('Google', 'q', 'web?q={k}'), - 'search.incredibar.com' => array('Google', 'q', 'search.php?q={k}'), - 'www.delta-search.com' => array('Google', 'q'), - 'search.1und1.de' => array('Google', 'q', 'web?q={k}'), - 'search.zonealarm.com' => array('Google'), - 'start.lenovo.com' => array('Google', 'q', 'search/index.php?q={k}'), - 'wow.com' => array('Google'), - '{}.wow.com' => array('Google'), - 'search.leonardo.it' => array('Google'), - 'www.optuszoo.com.au' => array('Google'), - 'search.smt.docomo.ne.jp' => array('Google', 'MT'), - 'image.search.smt.docomo.ne.jp' => array('Google', 'MT'), - - - // Google Earth - // - 2010-09-13: are these redirects now? - 'www.googleearth.de' => array('Google'), - 'www.googleearth.fr' => array('Google'), - - // Google Cache - 'webcache.googleusercontent.com' => array('Google', '/\/search\?q=cache:[A-Za-z0-9]+:[^+]+([^&]+)/', 'search?q={k}'), - - // Google SSL - 'encrypted.google.com' => array('Google SSL', 'q', 'search?q={k}'), - - // Google Blogsearch - 'blogsearch.google.com' => array('Google Blogsearch', 'q', 'blogsearch?q={k}'), - 'blogsearch.google.{}' => array('Google Blogsearch'), - - // Google Custom Search - 'google.com/cse' => array('Google Custom Search', array('q', 'query')), - 'google.{}/cse' => array('Google Custom Search'), - 'google.com/custom' => array('Google Custom Search'), - 'google.{}/custom' => array('Google Custom Search'), - - // Google Translation - 'translate.google.com' => array('Google Translations', 'q'), - - // Google Images - 'images.google.com' => array('Google Images', 'q', 'images?q={k}'), - 'images.google.{}' => array('Google Images'), - - // Google Maps - 'maps.google.com' => array('Google Maps', 'q', 'maps?q={k}'), - 'maps.google.{}' => array('Google Maps'), - - // Google News - 'news.google.com' => array('Google News', 'q'), - 'news.google.{}' => array('Google News'), - - // Google Shopping - 'google.com/products' => array('Google Shopping', 'q', '?q={k}&tbm=shop'), - 'google.{}/products' => array('Google Shopping'), - - // Google syndicated search - 'googlesyndicatedsearch.com' => array('Google syndicated search', 'q'), - - // Google Video - 'video.google.com' => array('Google Video', 'q', 'search?q={k}&tbm=vid'), - - // Google Scholar - 'scholar.google.com' => array('Google Scholar', 'q', 'scholar?q={k}'), - 'scholar.google.{}' => array('Google Scholar'), - - // Google Wireless Transcoder - // - does not appear to execute JavaScript -// 'google.com/gwt/n' => array('Google Wireless Transcoder'), - - // Goyellow.de - 'www.goyellow.de' => array('GoYellow.de', 'MDN'), - - // Gule Sider - 'www.gulesider.no' => array('Gule Sider', 'q'), - - // HighBeam - 'www.highbeam.com' => array('HighBeam', 'q', 'Search.aspx?q={k}'), - - // Hit-Parade - 'req.hit-parade.com' => array('Hit-Parade', 'p7', 'general/recherche.asp?p7={k}'), - 'class.hit-parade.com' => array('Hit-Parade'), - 'www.hit-parade.com' => array('Hit-Parade'), - - // Holmes.ge - 'holmes.ge' => array('Holmes', 'q', 'search.htm?q={k}'), - - // Hooseek.com - 'www.hooseek.com' => array('Hooseek', 'recherche', 'web?recherche={k}'), - - // Hotbot - 'www.hotbot.com' => array('Hotbot', 'query'), - - // Icerocket - 'blogs.icerocket.com' => array('Icerocket', 'q', 'search?q={k}'), - - // ICQ - 'www.icq.com' => array('ICQ', 'q', 'search/results.php?q={k}'), - 'search.icq.com' => array('ICQ'), - - // Ilse - 'www.ilse.nl' => array('Ilse NL', 'search_for', '?search_for={k}'), - - // iMesh - 'search.imesh.com' => array('iMesh', array('q', 'si'), 'web?q={k}'), - - // Inbox.com - 'www2.inbox.com' => array('Inbox', 'q', 'search/results1.aspx?q={k}'), - - // InfoSpace (and related web properties) - 'infospace.com' => array('InfoSpace', 'q', '/search/web?q={k}'), - 'dogpile.com' => array('InfoSpace'), - 'tattoodle.com' => array('InfoSpace'), - 'metacrawler.com' => array('InfoSpace'), - 'webfetch.com' => array('InfoSpace'), - 'webcrawler.com' => array('InfoSpace'), - 'search.kiwee.com' => array('InfoSpace'), - - // old infospace system - 'wsdsold.infospace.com' => array('InfoSpace', '/\/[^\/]+\/ws\/results\/[^\/]+\/([^\/]+)/', 'pemonitorhosted/ws/results/Web/{k}/1/417/TopNavigation/Source/'), - - // Powered by InfoSpace - 'isearch.babylon.com' => array('InfoSpace', 'q'), - 'start.facemoods.com' => array('InfoSpace', 's'), - 'start.funmoods.com' => array('InfoSpace', 'q'), - 'search.magentic.com' => array('InfoSpace', 'q'), - 'search.searchcompletion.com' => array('InfoSpace', 'q'), - 'www.searchmobileonline.com' => array('InfoSpace', 'q'), - 'isearch.glarysoft.com' => array('InfoSpace', 'q'), - 'search.chatzum.com' => array('InfoSpace', 'q'), - 'home.speedbit.com' => array('InfoSpace', 'q'), - 'search.b1.org' => array('InfoSpace', 'q'), - 'searchya.com' => array('InfoSpace', 'q'), - 'search.handycafe.com' => array('InfoSpace', 'q'), - 'search.v9.com' => array('InfoSpace', 'q'), - - /* - * Other InfoSpace powered metasearches are handled in Common::extractSearchEngineInformationFromUrl() - * - * This includes sites such as: - * - search.nation.com - * - ws.copernic.com - * - result.iminent.com - */ - - // Interia - 'www.google.interia.pl' => array('Interia', 'q', 'szukaj?q={k}'), - - // I-play - 'start.iplay.com' => array('I-play', 'q', 'searchresults.aspx?q={k}'), - - // Ixquick - 'ixquick.com' => array('Ixquick', 'query'), - 'www.eu.ixquick.com' => array('Ixquick'), - 'ixquick.de' => array('Ixquick'), - 'www.ixquick.de' => array('Ixquick'), - 'us.ixquick.com' => array('Ixquick'), - 's1.us.ixquick.com' => array('Ixquick'), - 's2.us.ixquick.com' => array('Ixquick'), - 's3.us.ixquick.com' => array('Ixquick'), - 's4.us.ixquick.com' => array('Ixquick'), - 's5.us.ixquick.com' => array('Ixquick'), - 'eu.ixquick.com' => array('Ixquick'), - 's8-eu.ixquick.com' => array('Ixquick'), - 's1-eu.ixquick.de' => array('Ixquick'), - - // Jyxo - 'jyxo.1188.cz' => array('Jyxo', 'q', 's?q={k}'), - - // Jungle Spider - 'www.jungle-spider.de' => array('Jungle Spider', 'q'), - - // Jungle key - 'junglekey.com' => array('Jungle Key', 'query', 'search.php?query={k}&type=web&lang=en'), - 'junglekey.fr' => array('Jungle Key'), - - // Kataweb - 'www.kataweb.it' => array('Kataweb', 'q'), - - // Kvasir - 'www.kvasir.no' => array('Kvasir', 'q', 'alle?q={k}'), - - // Latne - 'www.latne.lv' => array('Latne', 'q', 'siets.php?q={k}'), - - // La Toile Du Québec via Google - 'www.toile.com' => array('La Toile Du Québec (Google)', 'q', 'search?q={k}'), - 'web.toile.com' => array('La Toile Du Québec (Google)'), - - // Looksmart - 'www.looksmart.com' => array('Looksmart', 'key'), - - // Lo.st (Enhanced by Google) - 'lo.st' => array('Lo.st', 'x_query', 'cgi-bin/eolost.cgi?x_query={k}'), - - // Lycos - 'search.lycos.com' => array('Lycos', 'query', '?query={k}'), - 'lycos.{}' => array('Lycos'), - - // maailm.com - 'www.maailm.com' => array('maailm.com', 'tekst'), - - // Mail.ru - 'go.mail.ru' => array('Mailru', 'q', 'search?rch=e&q={k}', array('UTF-8', 'windows-1251')), - - // Mamma - 'www.mamma.com' => array('Mamma', 'query', 'result.php?q={k}'), - 'mamma75.mamma.com' => array('Mamma'), - - // Meta - 'meta.ua' => array('Meta.ua', 'q', 'search.asp?q={k}'), - - // MetaCrawler.de - 's1.metacrawler.de' => array('MetaCrawler DE', 'qry', '?qry={k}'), - 's2.metacrawler.de' => array('MetaCrawler DE'), - 's3.metacrawler.de' => array('MetaCrawler DE'), - - // Metager - 'meta.rrzn.uni-hannover.de' => array('Metager', 'eingabe', 'meta/cgi-bin/meta.ger1?eingabe={k}'), - 'www.metager.de' => array('Metager'), - - // Metager2 - 'metager2.de' => array('Metager2', 'q', 'search/index.php?q={k}'), - - // Meinestadt - 'www.meinestadt.de' => array('Meinestadt.de', 'words'), - - // Mister Wong - 'www.mister-wong.com' => array('Mister Wong', 'keywords', 'search/?keywords={k}'), - 'www.mister-wong.de' => array('Mister Wong'), - - // Monstercrawler - 'www.monstercrawler.com' => array('Monstercrawler', 'qry'), - - // Mozbot - 'www.mozbot.fr' => array('mozbot', 'q', 'results.php?q={k}'), - 'www.mozbot.co.uk' => array('mozbot'), - 'www.mozbot.com' => array('mozbot'), - - // El Mundo - 'ariadna.elmundo.es' => array('El Mundo', 'q'), - - // MySpace - 'searchservice.myspace.com' => array('MySpace', 'qry', 'index.cfm?fuseaction=sitesearch.results&type=Web&qry={k}'), - - // MySearch / MyWay / MyWebSearch (default: powered by Ask.com) - 'www.mysearch.com' => array('MyWebSearch', array('searchfor', 'searchFor'), 'search/Ajmain.jhtml?searchfor={k}'), - 'ms114.mysearch.com' => array('MyWebSearch'), - 'ms146.mysearch.com' => array('MyWebSearch'), - 'kf.mysearch.myway.com' => array('MyWebSearch'), - 'ki.mysearch.myway.com' => array('MyWebSearch'), - 'search.myway.com' => array('MyWebSearch'), - 'search.mywebsearch.com' => array('MyWebSearch'), - - - // Najdi - 'www.najdi.si' => array('Najdi.si', 'q', 'search.jsp?q={k}'), - - // Nate - 'search.nate.com' => array('Nate', 'q', 'search/all.html?q={k}', 'EUC-KR'), - - // Naver - 'search.naver.com' => array('Naver', 'query', 'search.naver?query={k}'), - - // Needtofind - 'ko.search.need2find.com' => array('Needtofind', 'searchfor', 'search/AJmain.jhtml?searchfor={k}'), - - // Neti - 'www.neti.ee' => array('Neti', 'query', 'cgi-bin/otsing?query={k}', 'iso-8859-1'), - - // Nifty - 'search.nifty.com' => array('Nifty', 'q', 'websearch/search?q={k}'), - - // Nigma - 'nigma.ru' => array('Nigma', 's', 'index.php?s={k}'), - - // Onet - 'szukaj.onet.pl' => array('Onet.pl', 'qt', 'query.html?qt={k}'), - - // Online.no - 'online.no' => array('Online.no', 'q', 'google/index.jsp?q={k}'), - - // Opplysningen 1881 - 'www.1881.no' => array('Opplysningen 1881', 'Query', 'Multi/?Query={k}'), - - // Orange - 'busca.orange.es' => array('Orange', 'q', 'search?q={k}'), - 'lemoteur.ke.voila.fr' => array('Orange', 'kw', '?kw={k}'), - - // Paperball - 'www.paperball.de' => array('Paperball', 'q', 'suche/s/?q={k}'), - - // PeoplePC - 'search.peoplepc.com' => array('PeoplePC', 'q', 'search?q={k}'), - - // Picsearch - 'www.picsearch.com' => array('Picsearch', 'q', 'index.cgi?q={k}'), - - // Plazoo - 'www.plazoo.com' => array('Plazoo', 'q'), - - // PlusNetwork - 'plusnetwork.com' => array('PlusNetwork', 'q', '?q={k}'), - - // Poisk.Ru - 'poisk.ru' => array('Poisk.Ru', 'text', 'cgi-bin/poisk?text={k}', 'windows-1251'), - - // qip - 'search.qip.ru' => array('qip.ru', 'query', 'search?query={k}'), - - // Qualigo - 'www.qualigo.at' => array('Qualigo', 'q'), - 'www.qualigo.ch' => array('Qualigo'), - 'www.qualigo.de' => array('Qualigo'), - 'www.qualigo.nl' => array('Qualigo'), - - // Rakuten - 'websearch.rakuten.co.jp' => array('Rakuten', 'qt', 'WebIS?qt={k}'), - - // Rambler - 'nova.rambler.ru' => array('Rambler', array('query', 'words'), 'search?query={k}'), - - // RPMFind - 'rpmfind.net' => array('rpmfind', 'query', 'linux/rpm2html/search.php?query={k}'), - 'fr2.rpmfind.net' => array('rpmfind'), - - // Road Runner Search - 'search.rr.com' => array('Road Runner', 'q', '?q={k}'), - - // Sapo - 'pesquisa.sapo.pt' => array('Sapo', 'q', '?q={k}'), - - // scour.com - 'scour.com' => array('Scour.com', '/search\/[^\/]+\/(.*)/', 'search/web/{k}'), - - // Search.com - 'www.search.com' => array('Search.com', 'q', 'search?q={k}'), - - // Search.ch - 'www.search.ch' => array('Search.ch', 'q', '?q={k}'), - - // Searchalot - 'searchalot.com' => array('Searchalot', 'q', '?q={k}'), - - // SearchCanvas - 'www.searchcanvas.com' => array('SearchCanvas', 'q', 'web?q={k}'), - - // Searchy - 'www.searchy.co.uk' => array('Searchy', 'q', 'index.html?q={k}'), - - // Setooz - // 2010-09-13: the mismatches are because subdomains are language codes - // (not country codes) - 'bg.setooz.com' => array('Setooz', 'query', 'search?query={k}'), - 'da.setooz.com' => array('Setooz'), - 'el.setooz.com' => array('Setooz'), - 'fa.setooz.com' => array('Setooz'), - 'ur.setooz.com' => array('Setooz'), - '{}.setooz.com' => array('Setooz'), - - // Seznam - 'search.seznam.cz' => array('Seznam', 'q', '?q={k}'), - - // Sharelook - 'www.sharelook.fr' => array('Sharelook', 'keyword'), - - // Skynet - 'www.skynet.be' => array('Skynet', 'q', 'services/recherche/google?q={k}'), - - // SmartAdressbar - 'search.smartaddressbar.com' => array('SmartAddressbar', 's', '?s={k}'), - - // Snap.do - 'search.snap.do' => array('Snap.do', 'q', '?q={k}'), - - // Sogou - 'www.sogou.com' => array('Sogou', 'query', 'web?query={k}', 'gb2312'), - - // Softonic - 'search.softonic.com' => array('Softonic', 'q', 'default/default?q={k}'), - - // soso.com - 'www.soso.com' => array('Soso', 'w', 'q?w={k}', 'gb2312'), - - // Startpagina - 'startgoogle.startpagina.nl' => array('Startpagina (Google)', 'q', '?q={k}'), - - // Startsiden - 'www.startsiden.no' => array('Startsiden', 'q', 'sok/index.html?q={k}'), - - // suche.info - 'suche.info' => array('Suche.info', 'Keywords', 'suche.php?Keywords={k}'), - - // Suchmaschine.com - 'www.suchmaschine.com' => array('Suchmaschine.com', 'suchstr', 'cgi-bin/wo.cgi?suchstr={k}'), - - // Suchnase - 'www.suchnase.de' => array('Suchnase', 'q'), - - // Surf Canyon - 'surfcanyon.com' => array('Surf Canyon', 'q'), - - // talimba - 'www.talimba.com' => array('talimba', 'search', 'index.php?page=search/web&search={k}'), - - // TalkTalk - 'www.talktalk.co.uk' => array('TalkTalk', 'query', 'search/results.html?query={k}'), - - // Technorati - 'technorati.com' => array('Technorati', 'q', 'search?return=sites&authority=all&q={k}'), - - // Teoma - 'www.teoma.com' => array('Teoma', 'q', 'web?q={k}'), - - // Terra -- referrer does not contain search phrase (keywords) - 'buscador.terra.es' => array('Terra', 'query', 'Default.aspx?source=Search&query={k}'), - 'buscador.terra.cl' => array('Terra'), - 'buscador.terra.com.br' => array('Terra'), - - // Tiscali - 'search.tiscali.it' => array('Tiscali', array('q', 'key'), '?q={k}'), - 'search-dyn.tiscali.it' => array('Tiscali'), - 'hledani.tiscali.cz' => array('Tiscali', 'query'), - - // Tixuma - 'www.tixuma.de' => array('Tixuma', 'sc', 'index.php?mp=search&stp=&sc={k}&tg=0'), - - // T-Online - 'suche.t-online.de' => array('T-Online', 'q', 'fast-cgi/tsc?mandant=toi&context=internet-tab&q={k}'), - 'brisbane.t-online.de' => array('T-Online'), - 'navigationshilfe.t-online.de' => array('T-Online', 'q', 'dtag/dns/results?mode=search_top&q={k}'), - - // Toolbarhome - 'www.toolbarhome.com' => array('Toolbarhome', 'q', 'search.aspx?q={k}'), - - 'vshare.toolbarhome.com' => array('Toolbarhome'), - - // Trouvez.com - 'www.trouvez.com' => array('Trouvez.com', 'query'), - - // TrovaRapido - 'www.trovarapido.com' => array('TrovaRapido', 'q', 'result.php?q={k}'), - - // Trusted-Search - 'www.trusted-search.com' => array('Trusted Search', 'w', 'search?w={k}'), - - // Twingly - 'www.twingly.com' => array('Twingly', 'q', 'search?q={k}'), - - // uol.com.br - 'busca.uol.com.br' => array('uol.com.br', 'q', '/web/?q={k}'), - - // URL.ORGanzier - 'www.url.org' => array('URL.ORGanzier', 'q', '?l=de&q={k}'), - - // Vinden - 'www.vinden.nl' => array('Vinden', 'q', '?q={k}'), - - // Vindex - 'www.vindex.nl' => array('Vindex', 'search_for', '/web?search_for={k}'), - 'search.vindex.nl' => array('Vindex'), - - // Virgilio - 'ricerca.virgilio.it' => array('Virgilio', 'qs', 'ricerca?qs={k}'), - 'ricercaimmagini.virgilio.it' => array('Virgilio'), - 'ricercavideo.virgilio.it' => array('Virgilio'), - 'ricercanews.virgilio.it' => array('Virgilio'), - 'mobile.virgilio.it' => array('Virgilio', 'qrs'), - - // Voila - 'search.ke.voila.fr' => array('Voila', 'rdata', 'S/voila?rdata={k}'), - 'www.lemoteur.fr' => array('Voila'), // uses voila search - - // Volny - 'web.volny.cz' => array('Volny', 'search', 'fulltext/?search={k}', 'windows-1250'), - - // Walhello - 'www.walhello.info' => array('Walhello', 'key', 'search?key={k}'), - 'www.walhello.com' => array('Walhello'), - 'www.walhello.de' => array('Walhello'), - 'www.walhello.nl' => array('Walhello'), - - // Web.de - 'suche.web.de' => array('Web.de', array('su', 'q'), 'search/web/?su={k}'), - - // Web.nl - 'www.web.nl' => array('Web.nl', 'zoekwoord'), - - // Weborama - 'www.weborama.fr' => array('weborama', 'QUERY'), - - // WebSearch - 'www.websearch.com' => array('WebSearch', array('qkw', 'q'), 'search/results2.aspx?q={k}'), - - // Wedoo - // 2011-02-15 - keyword no longer appears to be in Referrer URL; candidate for removal? - 'fr.wedoo.com' => array('Wedoo', 'keyword'), - 'en.wedoo.com' => array('Wedoo'), - 'es.wedoo.com' => array('Wedoo'), - - // Winamp (Enhanced by Google) - 'search.winamp.com' => array('Winamp', 'q', 'search/search?q={k}'), - - // Witch - 'www.witch.de' => array('Witch', 'search', 'search-result.php?cn=0&search={k}'), - - // Wirtualna Polska - 'szukaj.wp.pl' => array('Wirtualna Polska', 'szukaj', 'http://szukaj.wp.pl/szukaj.html?szukaj={k}'), - - // WWW - 'search.www.ee' => array('www värav', 'query'), - - // X-recherche - 'www.x-recherche.com' => array('X-Recherche', 'MOTS', 'cgi-bin/websearch?MOTS={k}'), - - // Yahoo - 'search.yahoo.com' => array('Yahoo!', array('p', 'q'), 'search?p={k}'), -// '*.search.yahoo.com' => array('Yahoo!'), // see built-in helper in Common.php - 'yahoo.com' => array('Yahoo!'), - 'yahoo.{}' => array('Yahoo!'), - '{}.yahoo.com' => array('Yahoo!'), - 'cade.yahoo.com' => array('Yahoo!'), - 'espanol.yahoo.com' => array('Yahoo!'), - 'qc.yahoo.com' => array('Yahoo!'), - 'one.cn.yahoo.com' => array('Yahoo!'), - 'video.search.yahoo.co.jp' => array('Yahoo!'), - 'image.search.yahoo.co.jp' => array('Yahoo!'), - - // Powered by Yahoo APIs - 'www.cercato.it' => array('Yahoo!', 'q'), - 'search.offerbox.com' => array('Yahoo!', 'q'), - 'www.benefind.de' => array('Yahoo!', 'q'), - - // Powered by Yahoo! Search Marketing (Overture) - 'ys.mirostart.com' => array('Yahoo!', 'q'), - - // Yahoo! Directory - 'search.yahoo.com/search/dir' => array('Yahoo! Directory', 'p', '?p={k}'), -// '{}.dir.yahoo.com' => array('Yahoo! Directory'), - - // Yahoo! Images - 'images.search.yahoo.com' => array('Yahoo! Images', 'p', 'search/images?p={k}'), -// '*.images.search.yahoo.com'=> array('Yahoo! Images'), // see built-in helper in Common.php - '{}.images.yahoo.com' => array('Yahoo! Images'), - 'cade.images.yahoo.com' => array('Yahoo! Images'), - 'espanol.images.yahoo.com' => array('Yahoo! Images'), - 'qc.images.yahoo.com' => array('Yahoo! Images'), - - // Yam - 'search.yam.com' => array('Yam', 'k', 'Search/Web/?SearchType=web&k={k}'), - - // Yandex - 'yandex.ru' => array('Yandex', 'text', 'yandsearch?text={k}'), - 'yandex.com' => array('Yandex'), - 'yandex.{}' => array('Yandex'), - - // Yandex Images - 'images.yandex.ru' => array('Yandex Images', 'text', 'yandsearch?text={k}'), - 'images.yandex.com' => array('Yandex Images'), - 'images.yandex.{}' => array('Yandex Images'), - - // Yasni - 'www.yasni.de' => array('Yasni', 'query'), - 'www.yasni.com' => array('Yasni'), - 'www.yasni.co.uk' => array('Yasni'), - 'www.yasni.ch' => array('Yasni'), - 'www.yasni.at' => array('Yasni'), - - // Yatedo - 'www.yatedo.com' => array('Yatedo', 'q', 'search/profil?q={k}'), - 'www.yatedo.fr' => array('Yatedo'), - - // Yellowmap - 'yellowmap.de' => array('Yellowmap', ' '), - - // Yippy - 'search.yippy.com' => array('Yippy', 'query', 'search?query={k}'), - - // YouGoo - 'www.yougoo.fr' => array('YouGoo', 'q', '?cx=search&q={k}'), - - // Zapmeta - 'www.zapmeta.com' => array('Zapmeta', array('q', 'query'), '?q={k}'), - 'www.zapmeta.nl' => array('Zapmeta'), - 'www.zapmeta.de' => array('Zapmeta'), - 'uk.zapmeta.com' => array('Zapmeta'), - - // Zoek - 'www3.zoek.nl' => array('Zoek', 'q'), - - // Zhongsou - 'p.zhongsou.com' => array('Zhongsou', 'w', 'p?w={k}'), - - // Zoeken - 'www.zoeken.nl' => array('Zoeken', 'q', '?q={k}'), - - // Zoohoo - 'zoohoo.cz' => array('Zoohoo', 'q', '?q={k}', 'windows-1250'), - - // Zoznam - 'www.zoznam.sk' => array('Zoznam', 's', 'hladaj.fcgi?s={k}&co=svet'), - ); -} diff --git a/www/analytics/core/DataFiles/Socials.php b/www/analytics/core/DataFiles/Socials.php deleted file mode 100755 index ebe28da7..00000000 --- a/www/analytics/core/DataFiles/Socials.php +++ /dev/null @@ -1,226 +0,0 @@ - 'Facebook', - 'fb.me' => 'Facebook', - - // Ozone - 'qzone.qq.com' => 'Qzone', - - // Haboo - 'habbo.com' => 'Haboo', - - // Twitter - 'twitter.com' => 'Twitter', - 't.co' => 'Twitter', - - // Renren - 'renren.com' => 'Renren', - - // Windows Live Spaces - 'login.live.com' => 'Windows Live Spaces', - - // LinkedIn - 'linkedin.com' => 'LinkedIn', - - // Bebo - 'bebo.com' => 'Bebo', - - // Vkontakte - 'vk.com' => 'Vkontakte', - 'vkontakte.ru' => 'Vkontakte', - - // Tagged - 'login.tagged.com' => 'Tagged', - - // Orkut - 'orkut.com' => 'Orkut', - - // Myspace - 'myspace.com' => 'Myspace', - - // Frinedster - 'friendster.com' => 'Friendster', - - // Badoo - 'badoo.com' => 'Badoo', - - // hi5 - 'hi5.com' => 'hi5', - - // Netlog - 'netlog.com' => 'Netlog', - - // Flixster - 'flixster.com' => 'Flixster', - - // MyLife - 'mylife.ru' => 'MyLife', - - // Classmates.com - 'classmates.com' => 'Classmates.com', - - // Github - 'github.com' => 'Github', - - // Google+ - 'url.google.com' => 'Google%2B', - - // douban - 'douban.com' => 'douban', - - // Odnoklassniki - 'odnoklassniki.ru' => 'Odnoklassniki', - - // Viadeo - 'viadeo.com' => 'Viadeo', - - // Flickr - 'flickr.com' => 'Flickr', - - // WeeWorld - 'weeworld.com' => 'WeeWorld', - - // Last.fm - 'last.fm' => 'Last.fm', - 'lastfm.ru' => 'Last.fm', - 'lastfm.de' => 'Last.fm', - 'lastfm.es' => 'Last.fm', - 'lastfm.fr' => 'Last.fm', - 'lastfm.it' => 'Last.fm', - 'lastfm.jp' => 'Last.fm', - 'lastfm.pl' => 'Last.fm', - 'lastfm.com.br' => 'Last.fm', - 'lastfm.se' => 'Last.fm', - 'lastfm.com.tr' => 'Last.fm', - - // MyHeritage - 'myheritage.com' => 'MyHeritage', - - // Xanga - 'xanga.com' => 'Xanga', - - // Mixi - 'mixi.jp' => 'Mixi', - - // Cyworld - 'global.cyworld.com' => 'Cyworld', - - // Gaia Online - 'gaiaonline.com' => 'Gaia Online', - - // Skyrock - 'skyrock.com' => 'Skyrock', - - // BlackPlanet - 'blackplanet.com' => 'BlackPlanet', - - // myYearbook - 'myyearbook.com' => 'myYearbook', - - // Fotolog - 'fotolog.com' => 'Fotolog', - - // Friends Reunited - 'friendsreunited.com' => 'Friends Reunited', - - // LiveJournal - 'livejournal.ru' => 'LiveJournal', - 'livejournal.com' => 'LiveJournal', - - // StudiVZ/MeinVZ - 'studivz.net' => 'StudiVZ', - 'meinvz.net' => 'MeinVZ', - - // StackOverflow - 'stackoverflow.com' => 'StackOverflow', - - // Sonico.com - 'sonico.com' => 'Sonico.com', - - // Pinterest - 'pinterest.com' => 'Pinterest', - - // Plaxo - 'plaxo.com' => 'Plaxo', - - // Geni.com - 'geni.com' => 'Geni.com', - - // Tuenti - 'tuenti.com' => 'Tuenti', - - // XING - 'xing.com' => 'XING', - - // Taringa! - 'taringa.net' => 'Taringa!', - - // Nasza-klasa.pl - 'nk.pl' => 'Nasza-klasa.pl', - - // StumbleUpon - 'stumbleupon.com' => 'StumbleUpon', - - // Sourceforge - 'sourceforge.net' => 'SourceForge', - - // Hyves - 'hyves.nl' => 'Hyves', - - // WAYN - 'wayn.com' => 'WAYN', - - // Buzznet - 'buzznet.com' => 'Buzznet', - - // Multiply - 'multiply.com' => 'Multiply', - - // Foursquare - 'foursquare.com' => 'Foursquare', - - // vkrugudruzei.ru - 'vkrugudruzei.ru' => 'vkrugudruzei.ru', - - // my.mail.ru - 'my.mail.ru' => 'my.mail.ru', - - //MoiKrug.ru - 'moikrug.ru' => 'moikrug.ru', - - // Reddit - 'reddit.com' => 'reddit', - - // HackerNews - 'news.ycombinator.com' => 'Hacker News', - - // Identi.ca - 'identi.ca' => 'identi.ca', - - // Weibo - 'weibo.com' => 'Weibo', - 't.cn' => 'Weibo', - - // YouTube - 'youtube.com' => 'YouTube', - 'youtu.be' => 'YouTube', - - // Vimeo - 'vimeo.com' => 'Vimeo', - - //tumblr - 'tumblr.com' => 'tumblr', - ); -} diff --git a/www/analytics/core/DataFiles/cacert.pem b/www/analytics/core/DataFiles/cacert.pem new file mode 100644 index 00000000..27078e45 --- /dev/null +++ b/www/analytics/core/DataFiles/cacert.pem @@ -0,0 +1,3981 @@ +## +## Bundle of CA Root Certificates +## +## Certificate data from Mozilla as of: Wed Oct 28 04:12:04 2015 +## +## This is a bundle of X.509 certificates of public Certificate Authorities +## (CA). These were automatically extracted from Mozilla's root certificates +## file (certdata.txt). This file can be found in the mozilla source tree: +## http://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt +## +## It contains the certificates in PEM format and therefore +## can be directly used with curl / libcurl / php_curl, or with +## an Apache+mod_ssl webserver for SSL client authentication. +## Just configure this file as the SSLCACertificateFile. +## +## Conversion done with mk-ca-bundle.pl version 1.25. +## SHA1: 6d7d2f0a4fae587e7431be191a081ac1257d300a +## + +Let’s Encrypt Authority X1 +========================== +-----BEGIN CERTIFICATE----- +MIIEqDCCA5CgAwIBAgIRAJgT9HUT5XULQ+dDHpceRL0wDQYJKoZIhvcNAQELBQAw +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzAeFw0xNTEwMTkyMjMzMzZaFw0yMDEwMTkyMjMzMzZa +MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD +ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAJzTDPBa5S5Ht3JdN4OzaGMw6tc1Jhkl4b2+NfFwki+3uEtB +BaupnjUIWOyxKsRohwuj43Xk5vOnYnG6eYFgH9eRmp/z0HhncchpDpWRz/7mmelg +PEjMfspNdxIknUcbWuu57B43ABycrHunBerOSuu9QeU2mLnL/W08lmjfIypCkAyG +dGfIf6WauFJhFBM/ZemCh8vb+g5W9oaJ84U/l4avsNwa72sNlRZ9xCugZbKZBDZ1 +gGusSvMbkEl4L6KWTyogJSkExnTA0DHNjzE4lRa6qDO4Q/GxH8Mwf6J5MRM9LTb4 +4/zyM2q5OTHFr8SNDR1kFjOq+oQpttQLwNh9w5MCAwEAAaOCAZIwggGOMBIGA1Ud +EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMH8GCCsGAQUFBwEBBHMwcTAy +BggrBgEFBQcwAYYmaHR0cDovL2lzcmcudHJ1c3RpZC5vY3NwLmlkZW50cnVzdC5j +b20wOwYIKwYBBQUHMAKGL2h0dHA6Ly9hcHBzLmlkZW50cnVzdC5jb20vcm9vdHMv +ZHN0cm9vdGNheDMucDdjMB8GA1UdIwQYMBaAFMSnsaR7LHH62+FLkHX/xBVghYkQ +MFQGA1UdIARNMEswCAYGZ4EMAQIBMD8GCysGAQQBgt8TAQEBMDAwLgYIKwYBBQUH +AgEWImh0dHA6Ly9jcHMucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcwPAYDVR0fBDUw +MzAxoC+gLYYraHR0cDovL2NybC5pZGVudHJ1c3QuY29tL0RTVFJPT1RDQVgzQ1JM +LmNybDATBgNVHR4EDDAKoQgwBoIELm1pbDAdBgNVHQ4EFgQUqEpqYwR93brm0Tm3 +pkVl7/Oo7KEwDQYJKoZIhvcNAQELBQADggEBANHIIkus7+MJiZZQsY14cCoBG1hd +v0J20/FyWo5ppnfjL78S2k4s2GLRJ7iD9ZDKErndvbNFGcsW+9kKK/TnY21hp4Dd +ITv8S9ZYQ7oaoqs7HwhEMY9sibED4aXw09xrJZTC9zK1uIfW6t5dHQjuOWv+HHoW +ZnupyxpsEUlEaFb+/SCI4KCSBdAsYxAcsHYI5xxEI4LutHp6s3OT2FuO90WfdsIk +6q78OMSdn875bNjdBYAqxUp2/LEIHfDBkLoQz0hFJmwAbYahqKaLn73PAAm1X2kj +f1w8DdnkabOLGeOVcj9LQ+s67vBykx4anTjURkbqZslUEUsn2k5xeua2zUk= +-----END CERTIFICATE----- + +Equifax Secure CA +================= +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJVUzEQMA4GA1UE +ChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoT +B0VxdWlmYXgxLTArBgNVBAsTJEVxdWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCB +nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPR +fM6fBeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+AcJkVV5MW +8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kCAwEAAaOCAQkwggEFMHAG +A1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UE +CxMkRXF1aWZheCBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoG +A1UdEAQTMBGBDzIwMTgwODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvS +spXXR9gjIBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQFMAMB +Af8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUAA4GBAFjOKer89961 +zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y7qj/WsjTVbJmcVfewCHrPSqnI0kB +BIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee95 +70+sB3c4 +-----END CERTIFICATE----- + +GlobalSign Root CA +================== +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx +GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds +b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV +BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD +VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa +DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc +THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb +Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP +c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX +gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF +AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj +Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG +j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH +hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC +X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +GlobalSign Root CA - R2 +======================= +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6 +ErPLv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8eoLrvozp +s6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjN +S7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CL +TfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pazq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6C +ygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUm+IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9i +YWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjAN +BgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp +9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu +01yiPqFbQfXf5WRDLenVOavSot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG7 +9G+dwfCMNYxdAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +Verisign Class 3 Public Primary Certification Authority - G3 +============================================================ +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv +cmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRy +dXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDMgUHVibGljIFByaW1hcnkg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAMu6nFL8eB8aHm8bN3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1 +EUGO+i2tKmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGukxUc +cLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBmCC+Vk7+qRy+oRpfw +EuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJXwzw3sJ2zq/3avL6QaaiMxTJ5Xpj +055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWuimi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA +ERSWwauSCPc/L8my/uRan2Te2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5f +j267Cz3qWhMeDGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565pF4ErWjfJXir0 +xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGtTxzhT5yvDwyd93gN2PQ1VoDa +t20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- + +Verisign Class 4 Public Primary Certification Authority - G3 +============================================================ +-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv +cmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRy +dXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDQgUHVibGljIFByaW1hcnkg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAK3LpRFpxlmr8Y+1GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaS +tBO3IFsJ+mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0GbdU6LM +8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLmNxdLMEYH5IBtptiW +Lugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XYufTsgsbSPZUd5cBPhMnZo0QoBmrX +Razwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA +j/ola09b5KROJ1WrIhVZPMq1CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXtt +mhwwjIDLk5Mqg6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm +fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c2NU8Qh0XwRJd +RTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/bLvSHgCwIe34QWKCudiyxLtG +UPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== +-----END CERTIFICATE----- + +Entrust.net Premium 2048 Secure Server CA +========================================= +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u +ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp +bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV +BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx +NzUwNTFaFw0yOTA3MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3 +d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl +MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u +ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL +Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr +hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW +nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi +VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJ +KoZIhvcNAQEFBQADggEBADubj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPy +T/4xmf3IDExoU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5bu/8j72gZyxKT +J1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+bYQLCIt+jerXmCHG8+c8eS9e +nNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/ErfF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- + +Baltimore CyberTrust Root +========================= +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJRTESMBAGA1UE +ChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3li +ZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoXDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMC +SUUxEjAQBgNVBAoTCUJhbHRpbW9yZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFs +dGltb3JlIEN5YmVyVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKME +uyKrmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjrIZ3AQSsB +UnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeKmpYcqWe4PwzV9/lSEy/C +G9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSuXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9 +XbIGevOF6uvUA65ehD5f/xXtabz5OTZydc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjpr +l3RjM71oGDHweI12v/yejl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoI +VDaGezq1BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB +BQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT929hkTI7gQCvlYpNRh +cL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3WgxjkzSswF07r51XgdIGn9w/xZchMB5 +hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsa +Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H +RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +AddTrust Low-Value Services Root +================================ +-----BEGIN CERTIFICATE----- +MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRU +cnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMwMTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQsw +CQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBO +ZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ulCDtbKRY6 +54eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6ntGO0/7Gcrjyvd7ZWxbWr +oulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyldI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1 +Zmne3yzxbrww2ywkEtvrNTVokMsAsJchPXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJui +GMx1I4S+6+JNM3GOGvDC+Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8w +HQYDVR0OBBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBlMQswCQYDVQQGEwJT +RTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEw +HwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxt +ZBsfzQ3duQH6lmM0MkhHma6X7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0Ph +iVYrqW9yTkkz43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY +eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJlpz/+0WatC7xr +mYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOAWiFeIc9TVPC6b4nbqKqVz4vj +ccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk= +-----END CERTIFICATE----- + +AddTrust External Root +====================== +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYD +VQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEw +NDgzOFowbzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRU +cnVzdCBFeHRlcm5hbCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0Eg +Um9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvtH7xsD821 ++iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9uMq/NzgtHj6RQa1wVsfw +Tz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzXmk6vBbOmcZSccbNQYArHE504B4YCqOmo +aSYYkKtMsE8jqzpPhNjfzp/haW+710LXa0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy +2xSoRcRdKn23tNbE7qzNE0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv7 +7+ldU9U0WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYDVR0P +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0Jvf6xCZU7wO94CTL +VBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEmMCQGA1UECxMdQWRk +VHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsxIjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENB +IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZl +j7DYd7usQWxHYINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvCNr4TDea9Y355 +e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEXc4g/VhsxOBi0cQ+azcgOno4u +G+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5amnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- + +AddTrust Public Services Root +============================= +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSAwHgYDVQQDExdBZGRU +cnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAxMDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJ +BgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5l +dHdvcmsxIDAeBgNVBAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV6tsfSlbu +nyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nXGCwwfQ56HmIexkvA/X1i +d9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnPdzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSG +Aa2Il+tmzV7R/9x98oTaunet3IAIx6eH1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAw +HM+A+WD+eeSI8t0A65RF62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0G +A1UdDgQWBBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRUcnVzdCBUVFAgTmV0d29yazEgMB4G +A1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4 +JNojVhaTdt02KLmuG7jD8WS6IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL ++YPoRNWyQSW/iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao +GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh4SINhwBk/ox9 +Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQmXiLsks3/QppEIW1cxeMiHV9H +EufOX1362KqxMy3ZdvJOOjMMK7MtkAY= +-----END CERTIFICATE----- + +AddTrust Qualified Certificates Root +==================================== +-----BEGIN CERTIFICATE----- +MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEUMBIGA1UEChML +QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSMwIQYDVQQDExpBZGRU +cnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcx +CzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQ +IE5ldHdvcmsxIzAhBgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwqxBb/4Oxx +64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G87B4pfYOQnrjfxvM0PC3 +KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i2O+tCBGaKZnhqkRFmhJePp1tUvznoD1o +L/BLcHwTOK28FSXx1s6rosAx1i+f4P8UWfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GR +wVY18BTcZTYJbqukB8c10cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HU +MIHRMB0GA1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6FrpGkwZzELMAkGA1UE +BhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRUcnVzdCBUVFAgTmV0d29y +azEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlmaWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBABmrder4i2VhlRO6aQTvhsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxG +GuoYQ992zPlmhpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X +dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3P6CxB9bpT9ze +RXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9YiQBCYz95OdBEsIJuQRno3eDB +iFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5noxqE= +-----END CERTIFICATE----- + +Entrust Root Certification Authority +==================================== +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw +b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG +A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0 +MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu +MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu +Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v +dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz +A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww +Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68 +j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN +rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1 +MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH +hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM +Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa +v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS +W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0 +tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +RSA Security 2048 v3 +==================== +-----BEGIN CERTIFICATE----- +MIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6MRkwFwYDVQQK +ExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJpdHkgMjA0OCBWMzAeFw0wMTAy +MjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAXBgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAb +BgNVBAsTFFJTQSBTZWN1cml0eSAyMDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAt49VcdKA3XtpeafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7 +Jylg/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGlwSMiuLgb +WhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnhAMFRD0xS+ARaqn1y07iH +KrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP ++Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpuAWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/ +MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4E +FgQUB8NRMKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYcHnmY +v/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/Zb5gEydxiKRz44Rj +0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+f00/FGj1EVDVwfSQpQgdMWD/YIwj +VAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVOrSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395 +nzIlQnQFgCi/vcEkllgVsRch6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kA +pKnXwiJPZ9d37CAFYd4= +-----END CERTIFICATE----- + +GeoTrust Global CA +================== +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9iYWwgQ0EwHhcNMDIwNTIxMDQw +MDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5j +LjEbMBkGA1UEAxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjo +BbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDviS2Aelet +8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU1XupGc1V3sjs0l44U+Vc +T4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagU +vTLrGAMoUgRx5aszPeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVk +DBF9qn1luMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKInZ57Q +zxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfStQWVYrmm3ok9Nns4 +d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcFPseKUgzbFbS9bZvlxrFUaKnjaZC2 +mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Unhw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6p +XE0zX5IJL4hmXXeXxx12E6nV5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvm +Mw== +-----END CERTIFICATE----- + +GeoTrust Global CA 2 +==================== +-----BEGIN CERTIFICATE----- +MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN +R2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFsIENBIDIwHhcNMDQwMzA0MDUw +MDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5j +LjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDvPE1APRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/ +NTL8Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hLTytCOb1k +LUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL5mkWRxHCJ1kDs6ZgwiFA +Vvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7S4wMcoKK+xfNAGw6EzywhIdLFnopsk/b +HdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNH +K266ZUapEBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6tdEPx7 +srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv/NgdRN3ggX+d6Yvh +ZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywNA0ZF66D0f0hExghAzN4bcLUprbqL +OzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkC +x1YAzUm5s2x7UwQa4qjJqhIFI8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqF +H4z1Ir+rzoPz4iIprn2DQKi6bA== +-----END CERTIFICATE----- + +GeoTrust Universal CA +===================== +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN +R2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVyc2FsIENBMB4XDTA0MDMwNDA1 +MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IElu +Yy4xHjAcBgNVBAMTFUdlb1RydXN0IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAKYVVaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9t +JPi8cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTTQjOgNB0e +RXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFhF7em6fgemdtzbvQKoiFs +7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2vc7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d +8Lsrlh/eezJS/R27tQahsiFepdaVaH/wmZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7V +qnJNk22CDtucvc+081xdVHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3Cga +Rr0BHdCXteGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZf9hB +Z3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfReBi9Fi1jUIxaS5BZu +KGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+nhutxx9z3SxPGWX9f5NAEC7S8O08 +ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0 +XG0D08DYj3rWMB8GA1UdIwQYMBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fXIwjhmF7DWgh2 +qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzynANXH/KttgCJwpQzgXQQpAvvL +oJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0zuzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsK +xr2EoyNB3tZ3b4XUhRxQ4K5RirqNPnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxF +KyDuSN/n3QmOGKjaQI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2 +DFKWkoRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9ER/frslK +xfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQtDF4JbAiXfKM9fJP/P6EU +p8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/SfuvmbJxPgWp6ZKy7PtXny3YuxadIwVyQD8vI +P/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- + +GeoTrust Universal CA 2 +======================= +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN +R2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwHhcNMDQwMzA0 +MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3Qg +SW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0 +DE81WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUGFF+3Qs17 +j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdqXbboW0W63MOhBW9Wjo8Q +JqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxLse4YuU6W3Nx2/zu+z18DwPw76L5GG//a +QMJS9/7jOvdqdzXQ2o3rXhhqMcceujwbKNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2 +WP0+GfPtDCapkzj4T8FdIgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP +20gaXT73y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRthAAn +ZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgocQIgfksILAAX/8sgC +SqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4Lt1ZrtmhN79UNdxzMk+MBB4zsslG +8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2 ++/CfXGJx7Tz0RzgQKzAfBgNVHSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8E +BAMCAYYwDQYJKoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQL1EuxBRa3ugZ +4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgrFg5fNuH8KrUwJM/gYwx7WBr+ +mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSoag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpq +A1Ihn0CoZ1Dy81of398j9tx4TuaYT1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpg +Y+RdM4kX2TGq2tbzGDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiP +pm8m1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJVOCiNUW7d +FGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH6aLcr34YEoP9VhdBLtUp +gn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwXQMAJKOSLakhT2+zNVVXxxvjpoixMptEm +X36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- + +Visa eCommerce Root +=================== +-----BEGIN CERTIFICATE----- +MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBrMQswCQYDVQQG +EwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5hdGlvbmFsIFNlcnZpY2Ug +QXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2 +WhcNMjIwNjI0MDAxNjEyWjBrMQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMm +VmlzYSBJbnRlcm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv +bW1lcmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h2mCxlCfL +F9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4ElpF7sDPwsRROEW+1QK8b +RaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdVZqW1LS7YgFmypw23RuwhY/81q6UCzyr0 +TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI +/k4+oKsGGelT84ATB+0tvz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzs +GHxBvfaLdXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG +MB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUFAAOCAQEAX/FBfXxc +CLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcRzCSs00Rsca4BIGsDoo8Ytyk6feUW +YFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pz +zkWKsKZJ/0x9nXGIxHYdkFsd7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBu +YQa7FkKMcPcw++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt +398znM/jra6O1I7mT1GvFpLgXPYHDw== +-----END CERTIFICATE----- + +Certum Root CA +============== +-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBMMRswGQYDVQQK +ExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBDQTAeFw0wMjA2MTExMDQ2Mzla +Fw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBMMRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8u +by4xEjAQBgNVBAMTCUNlcnR1bSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6x +wS7TT3zNJc4YPk/EjG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdL +kKWoePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GIULdtlkIJ +89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapuOb7kky/ZR6By6/qmW6/K +Uz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUgAKpoC6EahQGcxEZjgoi2IrHu/qpGWX7P +NSzVttpd90gzFFS269lvzs2I1qsb2pY7HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQUFAAOCAQEAuI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+ +GXYkHAQaTOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTgxSvg +GrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1qCjqTE5s7FCMTY5w/ +0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5xO/fIR/RpbxXyEV6DHpx8Uq79AtoS +qFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs6GAqm4VKQPNriiTsBhYscw== +-----END CERTIFICATE----- + +Comodo AAA Services root +======================== +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS +R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg +TGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAw +MFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hl +c3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV +BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQuaBtDFcCLNSS1UY8y2bmhG +C1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe3M/vg4aijJRPn2jymJBGhCfHdr/jzDUs +i14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszW +Y19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjH +Ypy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEK +Iz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0f +BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNl +cy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2Vz +LmNybDANBgkqhkiG9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm +7l3sAg9g1o1QGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z +8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C +12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +Comodo Secure Services root +=========================== +-----BEGIN CERTIFICATE----- +MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS +R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg +TGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAw +MDAwMFoXDTI4MTIzMTIzNTk1OVowfjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFu +Y2hlc3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAi +BgNVBAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPMcm3ye5drswfxdySRXyWP +9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3SHpR7LZQdqnXXs5jLrLxkU0C8j6ysNstc +rbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rC +oznl2yY4rYsK7hljxxwk3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3V +p6ea5EQz6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNVHQ4E +FgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w +gYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL1NlY3VyZUNlcnRpZmlj +YXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRwOi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlm +aWNhdGVTZXJ2aWNlcy5jcmwwDQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm +4J4oqF7Tt/Q05qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj +Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtIgKvcnDe4IRRL +DXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJaD61JlfutuC23bkpgHl9j6Pw +pCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDlizeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1H +RR3B7Hzs/Sk= +-----END CERTIFICATE----- + +Comodo Trusted Services root +============================ +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS +R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg +TGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEw +MDAwMDBaFw0yODEyMzEyMzU5NTlaMH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1h +bmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUw +IwYDVQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWWfnJSoBVC21ndZHoa0Lh7 +3TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMtTGo87IvDktJTdyR0nAducPy9C1t2ul/y +/9c3S0pgePfw+spwtOpZqqPOSC+pw7ILfhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6 +juljatEPmsbS9Is6FARW1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsS +ivnkBbA7kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0GA1Ud +DgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21vZG9jYS5jb20vVHJ1c3RlZENlcnRp +ZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRodHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENl +cnRpZmljYXRlU2VydmljZXMuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8Ntw +uleGFTQQuS9/HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32 +pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxISjBc/lDb+XbDA +BHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+xqFx7D+gIIxmOom0jtTYsU0l +R+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/AtyjcndBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O +9y5Xt5hwXsjEeLBi +-----END CERTIFICATE----- + +QuoVadis Root CA +================ +-----BEGIN CERTIFICATE----- +MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJCTTEZMBcGA1UE +ChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAz +MTkxODMzMzNaFw0yMTAzMTcxODMzMzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRp +cyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQD +EyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Ypli4kVEAkOPcahdxYTMuk +J0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2DrOpm2RgbaIr1VxqYuvXtdj182d6UajtL +F8HVj71lODqV0D1VNk7feVcxKh7YWWVJWCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeL +YzcS19Dsw3sgQUSj7cugF+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWen +AScOospUxbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCCAk4w +PQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVvdmFkaXNvZmZzaG9y +ZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREwggENMIIBCQYJKwYBBAG+WAABMIH7 +MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNlIG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmlj +YXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJs +ZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh +Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYIKwYBBQUHAgEW +Fmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3TKbkGGew5Oanwl4Rqy+/fMIGu +BgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rqy+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkw +FwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MS4wLAYDVQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6 +tlCLMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSkfnIYj9lo +fFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf87C9TqnN7Az10buYWnuul +LsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1RcHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2x +gI4JVrmcGmD+XcHXetwReNDWXcG31a0ymQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi +5upZIof4l/UO/erMkqQWxFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi +5nrQNiOKSnQ2+Q== +-----END CERTIFICATE----- + +QuoVadis Root CA 2 +================== +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0wNjExMjQx +ODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6 +XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55JWpzmM+Yk +lvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bBrrcCaoF6qUWD4gXmuVbB +lDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp+ARz8un+XJiM9XOva7R+zdRcAitMOeGy +lZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt +66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1Jdxn +wQ5hYIizPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOh +D7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyy +BNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENie +J0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1Ud +DgQWBBQahGK8SEwzJQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGU +a6FJpEcwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUv +Z+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3 +UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodm +VjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK ++JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrW +IozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPRTUIZ3Ph1 +WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWDmbA4CD/pXvk1B+TJYm5X +f6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II +4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8 +VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +QuoVadis Root CA 3 +================== +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0wNjExMjQx +OTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNgg +DhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUrH556VOij +KTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd8lyyBTNvijbO0BNO/79K +DDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9CabwvvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbv +BNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwp +p5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8 +nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEX +MJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyM +Gf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclz +uD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHT +BgkrBgEEAb5YAAMwgcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmlj +YXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYB +BQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD +VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4 +ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UE +AxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZV +qyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSemd1o417+s +hvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd+LJ2w/w4E6oM3kJpK27z +POuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2 +Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp +8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBC +bjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXu +g/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91p +vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr +qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +Security Communication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP +U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw +HhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP +U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw +8yl89f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJDKaVv0uM +DPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9Ms+k2Y7CI9eNqPPYJayX +5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/NQV3Is00qVUarH9oe4kA92819uZKAnDfd +DJZkndwi92SL32HeFZRSFaB9UslLqCHJxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2 +JChzAgMBAAGjPzA9MB0GA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vGkl3g +0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfrUj94nK9NrvjVT8+a +mCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5Bw+SUEmK3TGXX8npN6o7WWWXlDLJ +s58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJUJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ +6rBK+1YWc26sTfcioU+tHXotRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAi +FL39vmwLAw== +-----END CERTIFICATE----- + +Sonera Class 2 Root CA +====================== +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEPMA0GA1UEChMG +U29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAxMDQwNjA3Mjk0MFoXDTIxMDQw +NjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNVBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJh +IENsYXNzMiBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3 +/Ei9vX+ALTU74W+oZ6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybT +dXnt5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s3TmVToMG +f+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2EjvOr7nQKV0ba5cTppCD8P +tOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu8nYybieDwnPz3BjotJPqdURrBGAgcVeH +nfO+oJAjPYok4doh28MCAwEAAaMzMDEwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITT +XjwwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt +0jSv9zilzqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/3DEI +cbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvDFNr450kkkdAdavph +Oe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6Tk6ezAyNlNzZRZxe7EJQY670XcSx +EtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLH +llpwrN9M +-----END CERTIFICATE----- + +Staat der Nederlanden Root CA +============================= +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgIEAJiWijANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJOTDEeMBwGA1UE +ChMVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSYwJAYDVQQDEx1TdGFhdCBkZXIgTmVkZXJsYW5kZW4g +Um9vdCBDQTAeFw0wMjEyMTcwOTIzNDlaFw0xNTEyMTYwOTE1MzhaMFUxCzAJBgNVBAYTAk5MMR4w +HAYDVQQKExVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xJjAkBgNVBAMTHVN0YWF0IGRlciBOZWRlcmxh +bmRlbiBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNK1URF6gaYUmHFt +vsznExvWJw56s2oYHLZhWtVhCb/ekBPHZ+7d89rFDBKeNVU+LCeIQGv33N0iYfXCxw719tV2U02P +jLwYdjeFnejKScfST5gTCaI+Ioicf9byEGW07l8Y1Rfj+MX94p2i71MOhXeiD+EwR+4A5zN9RGca +C1Hoi6CeUJhoNFIfLm0B8mBF8jHrqTFoKbt6QZ7GGX+UtFE5A3+y3qcym7RHjm+0Sq7lr7HcsBth +vJly3uSJt3omXdozSVtSnA71iq3DuD3oBmrC1SoLbHuEvVYFy4ZlkuxEK7COudxwC0barbxjiDn6 +22r+I/q85Ej0ZytqERAhSQIDAQABo4GRMIGOMAwGA1UdEwQFMAMBAf8wTwYDVR0gBEgwRjBEBgRV +HSAAMDwwOgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cucGtpb3ZlcmhlaWQubmwvcG9saWNpZXMvcm9v +dC1wb2xpY3kwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSofeu8Y6R0E3QA7Jbg0zTBLL9s+DAN +BgkqhkiG9w0BAQUFAAOCAQEABYSHVXQ2YcG70dTGFagTtJ+k/rvuFbQvBgwp8qiSpGEN/KtcCFtR +EytNwiphyPgJWPwtArI5fZlmgb9uXJVFIGzmeafR2Bwp/MIgJ1HI8XxdNGdphREwxgDS1/PTfLbw +MVcoEoJz6TMvplW0C5GUR5z6u3pCMuiufi3IvKwUv9kP2Vv8wfl6leF9fpb8cbDCTMjfRTTJzg3y +nGQI0DvDKcWy7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR +iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw== +-----END CERTIFICATE----- + +UTN DATACorp SGC Root CA +======================== +-----BEGIN CERTIFICATE----- +MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCBkzELMAkGA1UE +BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl +IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZ +BgNVBAMTElVUTiAtIERBVEFDb3JwIFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBa +MIGTMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4w +HAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRy +dXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ys +raP6LnD43m77VkIVni5c7yPeIbkFdicZD0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlo +wHDyUwDAXlCCpVZvNvlK4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA +9P4yPykqlXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulWbfXv +33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQABo4GrMIGoMAsGA1Ud +DwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRTMtGzz3/64PGgXYVOktKeRR20TzA9 +BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dD +LmNybDAqBgNVHSUEIzAhBggrBgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3 +DQEBBQUAA4IBAQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft +Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyjj98C5OBxOvG0 +I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVHKWss5nbZqSl9Mt3JNjy9rjXx +EZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwP +DPafepE39peC4N1xaf92P2BNPM/3mfnGV/TJVTl4uix5yaaIK/QI +-----END CERTIFICATE----- + +UTN USERFirst Hardware Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCBlzELMAkGA1UE +BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl +IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAd +BgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgx +OTIyWjCBlzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0 +eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVz +ZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdhcmUwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlI +wrthdBKWHTxqctU8EGc6Oe0rE81m65UJM6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFd +tqdt++BxF2uiiPsA3/4aMXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8 +i4fDidNdoI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqIDsjf +Pe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9KsyoUhbAgMBAAGjgbkw +gbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFKFyXyYbKJhDlV0HN9WF +lp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNF +UkZpcnN0LUhhcmR3YXJlLmNybDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUF +BwMGBggrBgEFBQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM +//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28GpgoiskliCE7/yMgUsogW +XecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gECJChicsZUN/KHAG8HQQZexB2 +lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kn +iCrVWFCVH/A7HFe7fRQ5YiuayZSSKqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67 +nfhmqA== +-----END CERTIFICATE----- + +Camerfirma Chambers of Commerce Root +==================================== +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEnMCUGA1UEChMe +QUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1i +ZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAx +NjEzNDNaFw0zNzA5MzAxNjEzNDRaMH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZp +cm1hIFNBIENJRiBBODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3Jn +MSIwIAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0BAQEFAAOC +AQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtbunXF/KGIJPov7coISjlU +xFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0dBmpAPrMMhe5cG3nCYsS4No41XQEMIwRH +NaqbYE6gZj3LJgqcQKH0XZi/caulAGgq7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jW +DA+wWFjbw2Y3npuRVDM30pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFV +d9oKDMyXroDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIGA1Ud +EwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5jaGFtYmVyc2lnbi5v +cmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p26EpW1eLTXYGduHRooowDgYDVR0P +AQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hh +bWJlcnNpZ24ub3JnMCcGA1UdEgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYD +VR0gBFEwTzBNBgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz +aWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEBAAxBl8IahsAi +fJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZdp0AJPaxJRUXcLo0waLIJuvvD +L8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wN +UPf6s+xCX6ndbcj0dc97wXImsQEcXCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/n +ADydb47kMgkdTXg0eDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1 +erfutGWaIZDgqtCYvDi1czyL+Nw= +-----END CERTIFICATE----- + +Camerfirma Global Chambersign Root +================================== +-----BEGIN CERTIFICATE----- +MIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEnMCUGA1UEChMe +QUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1i +ZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYx +NDE4WhcNMzcwOTMwMTYxNDE4WjB9MQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJt +YSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEg +MB4GA1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUAA4IBDQAw +ggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0Mi+ITaFgCPS3CU6gSS9J +1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/sQJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8O +by4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpVeAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl +6DJWk0aJqCWKZQbua795B9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c +8lCrEqWhz0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0TAQH/ +BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1iZXJzaWduLm9yZy9j +aGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4wTcbOX60Qq+UDpfqpFDAOBgNVHQ8B +Af8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAHMCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBj +aGFtYmVyc2lnbi5vcmcwKgYDVR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9y +ZzBbBgNVHSAEVDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh +bWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0BAQUFAAOCAQEA +PDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUMbKGKfKX0j//U2K0X1S0E0T9Y +gOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXiryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJ +PJ7oKXqJ1/6v/2j1pReQvayZzKWGVwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4 +IBHNfTIzSJRUTN3cecQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREes +t2d/AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A== +-----END CERTIFICATE----- + +NetLock Notary (Class A) Root +============================= +-----BEGIN CERTIFICATE----- +MIIGfTCCBWWgAwIBAgICAQMwDQYJKoZIhvcNAQEEBQAwga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQI +EwdIdW5nYXJ5MREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6 +dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9j +ayBLb3pqZWd5em9pIChDbGFzcyBBKSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNDIzMTQ0N1oX +DTE5MDIxOTIzMTQ0N1owga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQH +EwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQuMRowGAYD +VQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBLb3pqZWd5em9pIChDbGFz +cyBBKSBUYW51c2l0dmFueWtpYWRvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvHSM +D7tM9DceqQWC2ObhbHDqeLVu0ThEDaiDzl3S1tWBxdRL51uUcCbbO51qTGL3cfNk1mE7PetzozfZ +z+qMkjvN9wfcZnSX9EUi3fRc4L9t875lM+QVOr/bmJBVOMTtplVjC7B4BPTjbsE/jvxReB+SnoPC +/tmwqcm8WgD/qaiYdPv2LD4VOQ22BFWoDpggQrOxJa1+mm9dU7GrDPzr4PN6s6iz/0b2Y6LYOph7 +tqyF/7AlT3Rj5xMHpQqPBffAZG9+pyeAlt7ULoZgx2srXnN7F+eRP2QM2EsiNCubMvJIH5+hCoR6 +4sKtlz2O1cH5VqNQ6ca0+pii7pXmKgOM3wIDAQABo4ICnzCCApswDgYDVR0PAQH/BAQDAgAGMBIG +A1UdEwEB/wQIMAYBAf8CAQQwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaC +Ak1GSUdZRUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pv +bGdhbHRhdGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQu +IEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2Vn +LWJpenRvc2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0 +ZXRlbGUgYXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFz +IGxlaXJhc2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBh +IGh0dHBzOi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVu +b3J6ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1YW5jZSBh +bmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sg +Q1BTIGF2YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFp +bCBhdCBjcHNAbmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4IBAQBIJEb3ulZv+sgoA0BO5TE5 +ayZrU3/b39/zcT0mwBQOxmd7I6gMc90Bu8bKbjc5VdXHjFYgDigKDtIqpLBJUsY4B/6+CgmM0ZjP +ytoUMaFP0jn8DxEsQ8Pdq5PHVT5HfBgaANzze9jyf1JsIPQLX2lS9O74silg6+NJMSEN1rUQQeJB +CWziGppWS3cC9qCbmieH6FUpccKQn0V4GuEVZD3QDtigdp+uxdAu6tYPVuxkf1qbFFgBJ34TUMdr +KuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK8CtmdWOMovsEPoMOmzbwGOQmIMOM +8CgHrTwXZoi1/baI +-----END CERTIFICATE----- + +XRamp Global CA Root +==================== +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UE +BhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2Vj +dXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDQxMTAxMTcxNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMx +HjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkg +U2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS638eMpSe2OAtp87ZOqCwu +IR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCPKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMx +foArtYzAQDsRhtDLooY2YKTVMIJt2W7QDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FE +zG+gSqmUsE3a56k0enI4qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqs +AxcZZPRaJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNViPvry +xS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASsjVy16bYbMDYGA1UdHwQvMC0wK6Ap +oCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMC +AQEwDQYJKoZIhvcNAQEFBQADggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc +/Kh4ZzXxHfARvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLaIR9NmXmd4c8n +nxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSyi6mx5O+aGtA9aZnuqCij4Tyz +8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQO+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +Go Daddy Class 2 CA +=================== +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMY +VGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkG +A1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g +RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQAD +ggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv +2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32 +qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6j +YGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmY +vLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0O +BBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2o +atTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMu +MTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wim +PQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKt +I3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VI +Ls9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/b +vZ8= +-----END CERTIFICATE----- + +Starfield Class 2 CA +==================== +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzElMCMGA1UEChMc +U3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZpZWxkIENsYXNzIDIg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQwNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBo +MQswCQYDVQQGEwJVUzElMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAG +A1UECxMpU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqG +SIb3DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf8MOh2tTY +bitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN+lq2cwQlZut3f+dZxkqZ +JRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVm +epsZGD3/cVE8MC5fvj13c7JdBmzDI1aaK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSN +F4Azbl5KXZnJHoe0nRrA1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HF +MIHCMB0GA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fRzt0f +hvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNo +bm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24g +QXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGs +afPzWdqbAYcaT1epoXkJKtv3L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLM +PUxA2IGvd56Deruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynpVSJYACPq4xJD +KVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEYWQPJIrSPnNVeKtelttQKbfi3 +QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +StartCom Certification Authority +================================ +-----BEGIN CERTIFICATE----- +MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN +U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu +ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0 +NjM2WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk +LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg +U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y +o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/ +Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d +eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt +2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z +6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ +osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/ +untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc +UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT +37uMdBNSSwIDAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE +FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9jZXJ0LnN0YXJ0 +Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3JsLnN0YXJ0Y29tLm9yZy9zZnNj +YS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFMBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUH +AgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRw +Oi8vY2VydC5zdGFydGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYg +U3RhcnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlhYmlsaXR5 +LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2YgdGhlIFN0YXJ0Q29tIENl +cnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFpbGFibGUgYXQgaHR0cDovL2NlcnQuc3Rh +cnRjb20ub3JnL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilT +dGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOC +AgEAFmyZ9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8jhvh +3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUWFjgKXlf2Ysd6AgXm +vB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJzewT4F+irsfMuXGRuczE6Eri8sxHk +fY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3 +fsNrarnDy0RLrHiQi+fHLB5LEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZ +EoalHmdkrQYuL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq +yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuCO3NJo2pXh5Tl +1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6Vum0ABj6y6koQOdjQK/W/7HW/ +lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkyShNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38coro +g14= +-----END CERTIFICATE----- + +Taiwan GRCA +=========== +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/MQswCQYDVQQG +EwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4X +DTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1owPzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dv +dmVybm1lbnQgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qN +w8XRIePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1qgQdW8or5 +BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKyyhwOeYHWtXBiCAEuTk8O +1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAtsF/tnyMKtsc2AtJfcdgEWFelq16TheEfO +htX7MfP6Mb40qij7cEwdScevLJ1tZqa2jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wov +J5pGfaENda1UhhXcSTvxls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7 +Q3hub/FCVGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHKYS1t +B6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoHEgKXTiCQ8P8NHuJB +O9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThNXo+EHWbNxWCWtFJaBYmOlXqYwZE8 +lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1UdDgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNV +HRMEBTADAQH/MDkGBGcqBwAEMTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg2 +09yewDL7MTqKUWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ +TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyfqzvS/3WXy6Tj +Zwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaKZEk9GhiHkASfQlK3T8v+R0F2 +Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFEJPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlU +D7gsL0u8qV1bYH+Mh6XgUmMqvtg7hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6Qz +DxARvBMB1uUO07+1EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+Hbk +Z6MmnD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WXudpVBrkk +7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44VbnzssQwmSNOXfJIoRIM3BKQ +CZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDeLMDDav7v3Aun+kbfYNucpllQdSNpc5Oy ++fwC00fmcc4QAu4njIT/rEUNE1yDMuAlpYYsfPQS +-----END CERTIFICATE----- + +Swisscom Root CA 1 +================== +-----BEGIN CERTIFICATE----- +MIIF2TCCA8GgAwIBAgIQXAuFXAvnWUHfV8w/f52oNjANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQG +EwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2VydGlmaWNhdGUgU2Vy +dmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3QgQ0EgMTAeFw0wNTA4MTgxMjA2MjBaFw0yNTA4 +MTgyMjA2MjBaMGQxCzAJBgNVBAYTAmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGln +aXRhbCBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAxMIIC +IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9m2BtRsiM +MW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdihFvkcxC7mlSpnzNApbjyF +NDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/TilftKaNXXsLmREDA/7n29uj/x2lzZAe +AR81sH8A25Bvxn570e56eqeqDFdvpG3FEzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkC +b6dJtDZd0KTeByy2dbcokdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn +7uHbHaBuHYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNFvJbN +cA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo19AOeCMgkckkKmUp +WyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjCL3UcPX7ape8eYIVpQtPM+GP+HkM5 +haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJWbjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNY +MUJDLXT5xp6mig/p/r+D5kNXJLrvRjSq1xIBOO0CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYw +HQYDVR0hBBYwFDASBgdghXQBUwABBgdghXQBUwABMBIGA1UdEwEB/wQIMAYBAf8CAQcwHwYDVR0j +BBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/0wHQYDVR0OBBYEFAMlL95vggE6XCzcK6FptWfUjNP9 +MA0GCSqGSIb3DQEBBQUAA4ICAQA1EMvspgQNDQ/NwNurqPKIlwzfky9NfEBWMXrrpA9gzXrzvsMn +jgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7IkVh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQ +MbFamIp1TpBcahQq4FJHgmDmHtqBsfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4H +VtA4oJVwIHaM190e3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtl +vrsRls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ipmXeascCl +OS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HHb6D0jqTsNFFbjCYDcKF3 +1QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksfrK/7DZBaZmBwXarNeNQk7shBoJMBkpxq +nvy5JMWzFYJ+vq6VK+uxwNrjAWALXmmshFZhvnEX/h0TD/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCy +x/yP2FS1k2Kdzs9Z+z0YzirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMW +NY6E0F/6MBr1mmz0DlP5OlvRHA== +-----END CERTIFICATE----- + +DigiCert Assured ID Root CA +=========================== +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx +MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO +9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy +UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW +/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy +oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf +GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF +66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq +hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc +EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn +SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i +8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +DigiCert Global Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw +MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn +TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5 +BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H +4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y +7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB +o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm +8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF +BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr +EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt +tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886 +UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +DigiCert High Assurance EV Root CA +================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw +KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw +MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ +MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu +Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t +Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS +OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3 +MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ +NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe +h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY +JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ +V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp +myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK +mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K +-----END CERTIFICATE----- + +Certplus Class 2 Primary CA +=========================== +-----BEGIN CERTIFICATE----- +MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAwPTELMAkGA1UE +BhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFzcyAyIFByaW1hcnkgQ0EwHhcN +OTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2Vy +dHBsdXMxGzAZBgNVBAMTEkNsYXNzIDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBANxQltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR +5aiRVhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyLkcAbmXuZ +Vg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCdEgETjdyAYveVqUSISnFO +YFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yasH7WLO7dDWWuwJKZtkIvEcupdM5i3y95e +e++U8Rs+yskhwcWYAqqi9lt3m/V+llU0HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRME +CDAGAQH/AgEKMAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJ +YIZIAYb4QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMuY29t +L0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/AN9WM2K191EBkOvD +P9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8yfFC82x/xXp8HVGIutIKPidd3i1R +TtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMRFcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+ +7UCmnYR0ObncHoUW2ikbhiMAybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW +//1IMwrh3KWBkJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7 +l7+ijrRU +-----END CERTIFICATE----- + +DST Root CA X3 +============== +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQK +ExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4X +DTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1 +cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmT +rE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9 +UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRy +xXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40d +utolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQ +MA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikug +dB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjE +GB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bw +RLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubS +fZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- + +DST ACES CA X6 +============== +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBbMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QxETAPBgNVBAsTCERTVCBBQ0VT +MRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0wMzExMjAyMTE5NThaFw0xNzExMjAyMTE5NTha +MFsxCzAJBgNVBAYTAlVTMSAwHgYDVQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UE +CxMIRFNUIEFDRVMxFzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPuktKe1jzI +DZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7gLFViYsx+tC3dr5BPTCa +pCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZHfAjIgrrep4c9oW24MFbCswKBXy314pow +GCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4aahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPy +MjwmR/onJALJfh1biEITajV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rkc3Qu +Y29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjtodHRwOi8vd3d3LnRy +dXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMtaW5kZXguaHRtbDAdBgNVHQ4EFgQU +CXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZIhvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V2 +5FYrnJmQ6AgwbN99Pe7lv7UkQIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6t +Fr8hlxCBPeP/h40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq +nExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpRrscL9yuwNwXs +vFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf29w4LTJxoeHtxMcfrHuBnQfO3 +oKfN5XozNmr6mis= +-----END CERTIFICATE----- + +TURKTRUST Certificate Services Provider Root 2 +============================================== +-----BEGIN CERTIFICATE----- +MIIEPDCCAySgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBF +bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJUUjEP +MA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUg +QmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwHhcN +MDUxMTA3MTAwNzU3WhcNMTUwOTE2MTAwNzU3WjCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBFbGVr +dHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJUUjEPMA0G +A1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmls +acWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpNn7DkUNMwxmYCMjHWHtPFoylzkkBH3MOrHUTpvqe +LCDe2JAOCtFp0if7qnefJ1Il4std2NiDUBd9irWCPwSOtNXwSadktx4uXyCcUHVPr+G1QRT0mJKI +x+XlZEdhR3n9wFHxwZnn3M5q+6+1ATDcRhzviuyV79z/rxAc653YsKpqhRgNF8k+v/Gb0AmJQv2g +QrSdiVFVKc8bcLyEVK3BEx+Y9C52YItdP5qtygy/p1Zbj3e41Z55SZI/4PGXJHpsmxcPbe9TmJEr +5A++WXkHeLuXlfSfadRYhwqp48y2WBmfJiGxxFmNskF1wK1pzpwACPI2/z7woQ8arBT9pmAPAgMB +AAGjQzBBMB0GA1UdDgQWBBTZN7NOBf3Zz58SFq62iS/rJTqIHDAPBgNVHQ8BAf8EBQMDBwYAMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHJglrfJ3NgpXiOFX7KzLXb7iNcX/ntt +Rbj2hWyfIvwqECLsqrkw9qtY1jkQMZkpAL2JZkH7dN6RwRgLn7Vhy506vvWolKMiVW4XSf/SKfE4 +Jl3vpao6+XF75tpYHdN0wgH6PmlYX63LaL4ULptswLbcoCb6dxriJNoaN+BnrdFzgw2lGh1uEpJ+ +hGIAF728JRhX8tepb1mIvDS3LoV4nZbcFMMsilKbloxSZj2GFotHuFEJjOp9zYhys2AzsfAKRO8P +9Qk3iCQOLGsgOqL6EfJANZxEaGM7rDNvY7wsu/LSy3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5 +UrbnBEI= +-----END CERTIFICATE----- + +SwissSign Gold CA - G2 +====================== +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNIMRUw +EwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0gRzIwHhcN +MDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dp +c3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUq +t2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+bbqBHH5C +jCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c6bM8K8vzARO/Ws/BtQpg +vd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqEemA8atufK+ze3gE/bk3lUIbLtK/tREDF +ylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvR +AiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuend +jIj3o02yMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkO +peUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR +7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGi +GqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64 +OfPAeGZe6Drn8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm +5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr +44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOf +Mke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6m +Gu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxp +mo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCChdiDyyJk +vC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid392qgQmwLOM7XdVAyksLf +KzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG2mqeSz53OiATIgHQv2ieY2Br +NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj +viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +SwissSign Silver CA - G2 +======================== +-----BEGIN CERTIFICATE----- +MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ0gxFTAT +BgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMB4X +DTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0NlowRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3 +aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644 +N0MvFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7brYT7QbNHm ++/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieFnbAVlDLaYQ1HTWBCrpJH +6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH6ATK72oxh9TAtvmUcXtnZLi2kUpCe2Uu +MGoM9ZDulebyzYLs2aFK7PayS+VFheZteJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5h +qAaEuSh6XzjZG6k4sIN/c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5 +FZGkECwJMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRHHTBs +ROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTfjNFusB3hB48IHpmc +celM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb65i/4z3GcRm25xBWNOHkDRUjvxF3X +CO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRB +tjpbO8tFnb0cwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 +cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBAHPGgeAn0i0P +4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShpWJHckRE1qTodvBqlYJ7YH39F +kWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L +3XWgwF15kIwb4FDm3jH+mHtwX6WQ2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx +/uNncqCxv1yL5PqZIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFa +DGi8aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2Xem1ZqSqP +e97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQRdAtq/gsD/KNVV4n+Ssuu +WxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJ +DIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ub +DgEj8Z+7fNzcbBGXJbLytGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority +======================================== +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMoR2VvVHJ1c3QgUHJpbWFyeSBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgx +CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQ +cmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9AWbK7hWN +b6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjAZIVcFU2Ix7e64HXprQU9 +nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE07e9GceBrAqg1cmuXm2bgyxx5X9gaBGge +RwLmnWDiNpcB3841kt++Z8dtd1k7j53WkBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGt +tm/81w7a4DSwDRp35+MImO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJKoZI +hvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ16CePbJC/kRYkRj5K +Ts4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl4b7UVXGYNTq+k+qurUKykG/g/CFN +NWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6KoKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHa +Floxt/m0cYASSJlyc1pZU8FjUjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG +1riR/aYNKxoUAT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- + +thawte Primary Root CA +====================== +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCBqTELMAkGA1UE +BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2 +aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3 +MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwg +SW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMv +KGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMT +FnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs +oPD7gFnUnMekz52hWXMJEEUMDSxuaPFsW0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ +1CRfBsDMRJSUjQJib+ta3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGc +q/gcfomk6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6Sk/K +aAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94JNqR32HuHUETVPm4p +afs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XPr87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUF +AAOCAQEAeRHAS7ORtvzw6WfUDW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeE +uzLlQRHAd9mzYJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2/qxAeeWsEG89 +jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/LHbTY5xZ3Y+m4Q6gLkH3LpVH +z7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7jVaMaA== +-----END CERTIFICATE----- + +VeriSign Class 3 Public Primary Certification Authority - G5 +============================================================ +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCByjELMAkGA1UE +BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO +ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk +IHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2ln +biBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBh +dXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCvJAgIKXo1nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKz +j/i5Vbext0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIzSdhD +Y2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQGBO+QueQA5N06tRn/ +Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+rCpSx4/VBEnkjWNHiDxpg8v+R70r +fk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/ +BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2Uv +Z2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKvMzEzMA0GCSqG +SIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzEp6B4Eq1iDkVwZMXnl2YtmAl+ +X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKE +KQsTb47bDN0lAtukixlE0kF6BWlKWE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiC +Km0oHw0LxOXnGiYZ4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vE +ZV8NhnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +SecureTrust CA +============== +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy +dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe +BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX +OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t +DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH +GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b +01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH +ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj +aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ +KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu +SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf +mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ +nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +Secure Global CA +================ +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH +bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg +MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg +Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx +YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ +bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g +8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV +HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi +0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn +oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA +MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+ +OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn +CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5 +3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +COMODO Certification Authority +============================== +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb +MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD +T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH ++7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww +xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV +4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA +1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI +rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k +b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC +AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP +OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc +IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN ++8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ== +-----END CERTIFICATE----- + +Network Solutions Certificate Authority +======================================= +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQG +EwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydOZXR3b3Jr +IFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMx +MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwzc7MEL7xx +jOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPPOCwGJgl6cvf6UDL4wpPT +aaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rlmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXT +crA/vGp97Eh/jcOrqnErU2lBUzS1sLnFBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc +/Qzpf14Dl847ABSHJ3A4qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMB +AAGjgZcwgZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwubmV0c29sc3NsLmNv +bS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3JpdHkuY3JsMA0GCSqGSIb3DQEBBQUA +A4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc86fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q +4LqILPxFzBiwmZVRDuwduIj/h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/ +GGUsyfJj4akH/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHNpGxlaKFJdlxD +ydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +WellsSecure Public Root Certificate Authority +============================================= +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoM +F1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYw +NAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcN +MDcxMjEzMTcwNzU0WhcNMjIxMjE0MDAwNzU0WjCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dl +bGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYD +VQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDub7S9eeKPCCGeOARBJe+rWxxTkqxtnt3CxC5FlAM1 +iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjUDk/41itMpBb570OYj7OeUt9tkTmPOL13 +i0Nj67eT/DBMHAGTthP796EfvyXhdDcsHqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8 +bJVhHlfXBIEyg1J55oNjz7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiB +K0HmOFafSZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/SlwxlAgMB +AAGjggE0MIIBMDAPBgNVHRMBAf8EBTADAQH/MDkGA1UdHwQyMDAwLqAsoCqGKGh0dHA6Ly9jcmwu +cGtpLndlbGxzZmFyZ28uY29tL3dzcHJjYS5jcmwwDgYDVR0PAQH/BAQDAgHGMB0GA1UdDgQWBBQm +lRkQ2eihl5H/3BnZtQQ+0nMKajCBsgYDVR0jBIGqMIGngBQmlRkQ2eihl5H/3BnZtQQ+0nMKaqGB +i6SBiDCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRww +GgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCAQEwDQYJKoZIhvcNAQEFBQADggEBALkVsUSRzCPI +K0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd/ZDJPHV3V3p9+N701NX3leZ0 +bh08rnyd2wIDBSxxSyU+B+NemvVmFymIGjifz6pBA4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSlj +qHyita04pO2t/caaH/+Xc/77szWnk4bGdpEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+es +E2fDbbFwRnzVlhE9iW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJ +tylv2G0xffX8oRAHh84vWdw+WNs= +-----END CERTIFICATE----- + +COMODO ECC Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix +GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X +4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni +wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG +FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA +U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +IGC/A +===== +-----BEGIN CERTIFICATE----- +MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYTAkZSMQ8wDQYD +VQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVE +Q1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZy +MB4XDTAyMTIxMzE0MjkyM1oXDTIwMTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQI +EwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NT +STEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaIs9z4iPf930Pfeo2aSVz2 +TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCW +So7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYy +HF2fYPepraX/z9E0+X1bF8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNd +frGoRpAxVs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGdPDPQ +tQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNVHSAEDjAMMAoGCCqB +egF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAxNjAfBgNVHSMEGDAWgBSjBS8YYFDC +iQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUFAAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RK +q89toB9RlPhJy3Q2FLwV3duJL92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3Q +MZsyK10XZZOYYLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg +Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2aNjSaTFR+FwNI +lQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R0982gaEbeC9xs/FZTEYYKKuF +0mBWWg== +-----END CERTIFICATE----- + +Security Communication EV RootCA1 +================================= +-----BEGIN CERTIFICATE----- +MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJKUDElMCMGA1UEChMc +U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEqMCgGA1UECxMhU2VjdXJpdHkgQ29tbXVuaWNh +dGlvbiBFViBSb290Q0ExMB4XDTA3MDYwNjAyMTIzMloXDTM3MDYwNjAyMTIzMlowYDELMAkGA1UE +BhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKjAoBgNVBAsTIVNl +Y3VyaXR5IENvbW11bmljYXRpb24gRVYgUm9vdENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALx/7FebJOD+nLpCeamIivqA4PUHKUPqjgo0No0c+qe1OXj/l3X3L+SqawSERMqm4miO +/VVQYg+kcQ7OBzgtQoVQrTyWb4vVog7P3kmJPdZkLjjlHmy1V4qe70gOzXppFodEtZDkBp2uoQSX +WHnvIEqCa4wiv+wfD+mEce3xDuS4GBPMVjZd0ZoeUWs5bmB2iDQL87PRsJ3KYeJkHcFGB7hj3R4z +ZbOOCVVSPbW9/wfrrWFVGCypaZhKqkDFMxRldAD5kd6vA0jFQFTcD4SQaCDFkpbcLuUCRarAX1T4 +bepJz11sS6/vmsJWXMY1VkJqMF/Cq/biPT+zyRGPMUzXn0kCAwEAAaNCMEAwHQYDVR0OBBYEFDVK +9U2vP9eCOKyrcWUXdYydVZPmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBBQUAA4IBAQCoh+ns+EBnXcPBZsdAS5f8hxOQWsTvoMpfi7ent/HWtWS3irO4G8za+6xm +iEHO6Pzk2x6Ipu0nUBsCMCRGef4Eh3CXQHPRwMFXGZpppSeZq51ihPZRwSzJIxXYKLerJRO1RuGG +Av8mjMSIkh1W/hln8lXkgKNrnKt34VFxDSDbEJrbvXZ5B3eZKK2aXtqxT0QsNY6llsf9g/BYxnnW +mHyojf6GPgcWkuF75x3sM3Z+Qi5KhfmRiWiEA4Glm5q+4zfFVKtWOxgtQaQM+ELbmaDgcm+7XeEW +T1MKZPlO9L9OVL14bIjqv5wTJMJwaaJ/D8g8rQjJsJhAoyrniIPtd490 +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GA CA +=============================== +-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCBijELMAkGA1UE +BhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHlyaWdodCAoYykgMjAwNTEiMCAG +A1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBH +bG9iYWwgUm9vdCBHQSBDQTAeFw0wNTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYD +VQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIw +IAYDVQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5 +IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy0+zAJs9 +Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxRVVuuk+g3/ytr6dTqvirdqFEr12bDYVxg +Asj1znJ7O7jyTmUIms2kahnBAbtzptf2w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbD +d50kc3vkDIzh2TbhmYsFmQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ +/yxViJGg4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t94B3R +LoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ +KoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOxSPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vIm +MMkQyh2I+3QZH4VFvbBsUfk2ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4 ++vg1YFkCExh8vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa +hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZiFj4A4xylNoEY +okxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ/L7fCg0= +-----END CERTIFICATE----- + +Microsec e-Szigno Root CA +========================= +-----BEGIN CERTIFICATE----- +MIIHqDCCBpCgAwIBAgIRAMy4579OKRr9otxmpRwsDxEwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UE +BhMCSFUxETAPBgNVBAcTCEJ1ZGFwZXN0MRYwFAYDVQQKEw1NaWNyb3NlYyBMdGQuMRQwEgYDVQQL +EwtlLVN6aWdubyBDQTEiMCAGA1UEAxMZTWljcm9zZWMgZS1Temlnbm8gUm9vdCBDQTAeFw0wNTA0 +MDYxMjI4NDRaFw0xNzA0MDYxMjI4NDRaMHIxCzAJBgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVz +dDEWMBQGA1UEChMNTWljcm9zZWMgTHRkLjEUMBIGA1UECxMLZS1Temlnbm8gQ0ExIjAgBgNVBAMT +GU1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQDtyADVgXvNOABHzNuEwSFpLHSQDCHZU4ftPkNEU6+r+ICbPHiN1I2uuO/TEdyB5s87lozWbxXG +d36hL+BfkrYn13aaHUM86tnsL+4582pnS4uCzyL4ZVX+LMsvfUh6PXX5qqAnu3jCBspRwn5mS6/N +oqdNAoI/gqyFxuEPkEeZlApxcpMqyabAvjxWTHOSJ/FrtfX9/DAFYJLG65Z+AZHCabEeHXtTRbjc +QR/Ji3HWVBTji1R4P770Yjtb9aPs1ZJ04nQw7wHb4dSrmZsqa/i9phyGI0Jf7Enemotb9HI6QMVJ +PqW+jqpx62z69Rrkav17fVVA71hu5tnVvCSrwe+3AgMBAAGjggQ3MIIEMzBnBggrBgEFBQcBAQRb +MFkwKAYIKwYBBQUHMAGGHGh0dHBzOi8vcmNhLmUtc3ppZ25vLmh1L29jc3AwLQYIKwYBBQUHMAKG +IWh0dHA6Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNydDAPBgNVHRMBAf8EBTADAQH/MIIBcwYD +VR0gBIIBajCCAWYwggFiBgwrBgEEAYGoGAIBAQEwggFQMCgGCCsGAQUFBwIBFhxodHRwOi8vd3d3 +LmUtc3ppZ25vLmh1L1NaU1ovMIIBIgYIKwYBBQUHAgIwggEUHoIBEABBACAAdABhAG4A+gBzAO0A +dAB2AOEAbgB5ACAA6QByAHQAZQBsAG0AZQB6AOkAcwDpAGgAZQB6ACAA6QBzACAAZQBsAGYAbwBn +AGEAZADhAHMA4QBoAG8AegAgAGEAIABTAHoAbwBsAGcA4QBsAHQAYQB0APMAIABTAHoAbwBsAGcA +4QBsAHQAYQB0AOEAcwBpACAAUwB6AGEAYgDhAGwAeQB6AGEAdABhACAAcwB6AGUAcgBpAG4AdAAg +AGsAZQBsAGwAIABlAGwAagDhAHIAbgBpADoAIABoAHQAdABwADoALwAvAHcAdwB3AC4AZQAtAHMA +egBpAGcAbgBvAC4AaAB1AC8AUwBaAFMAWgAvMIHIBgNVHR8EgcAwgb0wgbqggbeggbSGIWh0dHA6 +Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNybIaBjmxkYXA6Ly9sZGFwLmUtc3ppZ25vLmh1L0NO +PU1pY3Jvc2VjJTIwZS1Temlnbm8lMjBSb290JTIwQ0EsT1U9ZS1Temlnbm8lMjBDQSxPPU1pY3Jv +c2VjJTIwTHRkLixMPUJ1ZGFwZXN0LEM9SFU/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDtiaW5h +cnkwDgYDVR0PAQH/BAQDAgEGMIGWBgNVHREEgY4wgYuBEGluZm9AZS1zemlnbm8uaHWkdzB1MSMw +IQYDVQQDDBpNaWNyb3NlYyBlLVN6aWduw7MgUm9vdCBDQTEWMBQGA1UECwwNZS1TemlnbsOzIEhT +WjEWMBQGA1UEChMNTWljcm9zZWMgS2Z0LjERMA8GA1UEBxMIQnVkYXBlc3QxCzAJBgNVBAYTAkhV +MIGsBgNVHSMEgaQwgaGAFMegSXUWYYTbMUuE0vE3QJDvTtz3oXakdDByMQswCQYDVQQGEwJIVTER +MA8GA1UEBxMIQnVkYXBlc3QxFjAUBgNVBAoTDU1pY3Jvc2VjIEx0ZC4xFDASBgNVBAsTC2UtU3pp +Z25vIENBMSIwIAYDVQQDExlNaWNyb3NlYyBlLVN6aWdubyBSb290IENBghEAzLjnv04pGv2i3Gal +HCwPETAdBgNVHQ4EFgQUx6BJdRZhhNsxS4TS8TdAkO9O3PcwDQYJKoZIhvcNAQEFBQADggEBANMT +nGZjWS7KXHAM/IO8VbH0jgdsZifOwTsgqRy7RlRw7lrMoHfqaEQn6/Ip3Xep1fvj1KcExJW4C+FE +aGAHQzAxQmHl7tnlJNUb3+FKG6qfx1/4ehHqE5MAyopYse7tDk2016g2JnzgOsHVV4Lxdbb9iV/a +86g4nzUGCM4ilb7N1fy+W955a9x6qWVmvrElWl/tftOsRm1M9DKHtCAE4Gx4sHfRhUZLphK3dehK +yVZs15KrnfVJONJPU+NVkBHbmJbGSfI+9J8b4PeI3CVimUTYc78/MPMMNz7UwiiAc7EBt51alhQB +S6kRnSlqLtBdgcDPsiBDxwPgN05dCtxZICU= +-----END CERTIFICATE----- + +Certigna +======== +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw +EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3 +MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI +Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q +XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH +GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p +ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg +DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf +Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ +tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ +BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J +SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA +hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+ +ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu +PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY +1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +Deutsche Telekom Root CA 2 +========================== +-----BEGIN CERTIFICATE----- +MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEcMBoGA1UEChMT +RGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2VjIFRydXN0IENlbnRlcjEjMCEG +A1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENBIDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5 +MjM1OTAwWjBxMQswCQYDVQQGEwJERTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0G +A1UECxMWVC1UZWxlU2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBS +b290IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEUha88EOQ5 +bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhCQN/Po7qCWWqSG6wcmtoI +KyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1MjwrrFDa1sPeg5TKqAyZMg4ISFZbavva4VhY +AUlfckE8FQYBjl2tqriTtM2e66foai1SNNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aK +Se5TBY8ZTNXeWHmb0mocQqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTV +jlsB9WoHtxa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAPBgNV +HRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAlGRZrTlk5ynr +E/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756AbrsptJh6sTtU6zkXR34ajgv8HzFZMQSy +zhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpaIzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8 +rZ7/gFnkm0W09juwzTkZmDLl6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4G +dyd1Lx+4ivn+xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU +Cm26OWMohpLzGITY+9HPBVZkVw== +-----END CERTIFICATE----- + +Cybertrust Global Root +====================== +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYGA1UEChMPQ3li +ZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBSb290MB4XDTA2MTIxNTA4 +MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQD +ExZDeWJlcnRydXN0IEdsb2JhbCBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA ++Mi8vRRQZhP/8NN57CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW +0ozSJ8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2yHLtgwEZL +AfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iPt3sMpTjr3kfb1V05/Iin +89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNzFtApD0mpSPCzqrdsxacwOUBdrsTiXSZT +8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAYXSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2 +MDSgMqAwhi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3JsMB8G +A1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUAA4IBAQBW7wojoFRO +lZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMjWqd8BfP9IjsO0QbE2zZMcwSO5bAi +5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUxXOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2 +hO0j9n0Hq0V+09+zv+mKts2oomcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+T +X3EJIrduPuocA06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +ePKI Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG +EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx +MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq +MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs +IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi +lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv +qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX +12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O +WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+ +ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao +lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/ +vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi +Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi +MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0 +1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq +KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV +xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP +NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r +GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE +xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx +gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy +sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD +BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +T\xc3\x9c\x42\xC4\xB0TAK UEKAE K\xC3\xB6k Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1 - S\xC3\xBCr\xC3\xBCm 3 +============================================================================================================================= +-----BEGIN CERTIFICATE----- +MIIFFzCCA/+gAwIBAgIBETANBgkqhkiG9w0BAQUFADCCASsxCzAJBgNVBAYTAlRSMRgwFgYDVQQH +DA9HZWJ6ZSAtIEtvY2FlbGkxRzBFBgNVBAoMPlTDvHJraXllIEJpbGltc2VsIHZlIFRla25vbG9q +aWsgQXJhxZ90xLFybWEgS3VydW11IC0gVMOcQsSwVEFLMUgwRgYDVQQLDD9VbHVzYWwgRWxla3Ry +b25payB2ZSBLcmlwdG9sb2ppIEFyYcWfdMSxcm1hIEVuc3RpdMO8c8O8IC0gVUVLQUUxIzAhBgNV +BAsMGkthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppMUowSAYDVQQDDEFUw5xCxLBUQUsgVUVLQUUg +S8O2ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSAtIFPDvHLDvG0gMzAeFw0wNzA4 +MjQxMTM3MDdaFw0xNzA4MjExMTM3MDdaMIIBKzELMAkGA1UEBhMCVFIxGDAWBgNVBAcMD0dlYnpl +IC0gS29jYWVsaTFHMEUGA1UECgw+VMO8cmtpeWUgQmlsaW1zZWwgdmUgVGVrbm9sb2ppayBBcmHF +n3TEsXJtYSBLdXJ1bXUgLSBUw5xCxLBUQUsxSDBGBgNVBAsMP1VsdXNhbCBFbGVrdHJvbmlrIHZl +IEtyaXB0b2xvamkgQXJhxZ90xLFybWEgRW5zdGl0w7xzw7wgLSBVRUtBRTEjMCEGA1UECwwaS2Ft +dSBTZXJ0aWZpa2FzeW9uIE1lcmtlemkxSjBIBgNVBAMMQVTDnELEsFRBSyBVRUtBRSBLw7ZrIFNl +cnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIC0gU8O8csO8bSAzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAim1L/xCIOsP2fpTo6iBkcK4hgb46ezzb8R1Sf1n68yJMlaCQvEhO +Eav7t7WNeoMojCZG2E6VQIdhn8WebYGHV2yKO7Rm6sxA/OOqbLLLAdsyv9Lrhc+hDVXDWzhXcLh1 +xnnRFDDtG1hba+818qEhTsXOfJlfbLm4IpNQp81McGq+agV/E5wrHur+R84EpW+sky58K5+eeROR +6Oqeyjh1jmKwlZMq5d/pXpduIF9fhHpEORlAHLpVK/swsoHvhOPc7Jg4OQOFCKlUAwUp8MmPi+oL +hmUZEdPpCSPeaJMDyTYcIW7OjGbxmTDY17PDHfiBLqi9ggtm/oLL4eAagsNAgQIDAQABo0IwQDAd +BgNVHQ4EFgQUvYiHyY/2pAoLquvF/pEjnatKijIwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAB18+kmPNOm3JpIWmgV050vQbTlswyb2zrgxvMTfvCr4 +N5EY3ATIZJkrGG2AA1nJrvhY0D7twyOfaTyGOBye79oneNGEN3GKPEs5z35FBtYt2IpNeBLWrcLT +y9LQQfMmNkqblWwM7uXRQydmwYj3erMgbOqwaSvHIOgMA8RBBZniP+Rr+KCGgceExh/VS4ESshYh +LBOhgLJeDEoTniDYYkCrkOpkSi+sDQESeUWoL4cZaMjihccwsnX5OD+ywJO0a+IDRM5noN+J1q2M +dqMTw5RhK2vZbMEHCiIHhWyFJEapvj+LeISCfiQMnf2BN+MlqO02TpUsyZyQ2uypQjyttgI= +-----END CERTIFICATE----- + +Buypass Class 2 CA 1 +==================== +-----BEGIN CERTIFICATE----- +MIIDUzCCAjugAwIBAgIBATANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3MgQ2xhc3MgMiBDQSAxMB4XDTA2 +MTAxMzEwMjUwOVoXDTE2MTAxMzEwMjUwOVowSzELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBh +c3MgQVMtOTgzMTYzMzI3MR0wGwYDVQQDDBRCdXlwYXNzIENsYXNzIDIgQ0EgMTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAIs8B0XY9t/mx8q6jUPFR42wWsE425KEHK8T1A9vNkYgxC7M +cXA0ojTTNy7Y3Tp3L8DrKehc0rWpkTSHIln+zNvnma+WwajHQN2lFYxuyHyXA8vmIPLXl18xoS83 +0r7uvqmtqEyeIWZDO6i88wmjONVZJMHCR3axiFyCO7srpgTXjAePzdVBHfCuuCkslFJgNJQ72uA4 +0Z0zPhX0kzLFANq1KWYOOngPIVJfAuWSeyXTkh4vFZ2B5J2O6O+JzhRMVB0cgRJNcKi+EAUXfh/R +uFdV7c27UsKwHnjCTTZoy1YmwVLBvXb3WNVyfh9EdrsAiR0WnVE1703CVu9r4Iw7DekCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUP42aWYv8e3uco684sDntkHGA1sgwDgYDVR0P +AQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAVGn4TirnoB6NLJzKyQJHyIdFkhb5jatLPgcIV +1Xp+DCmsNx4cfHZSldq1fyOhKXdlyTKdqC5Wq2B2zha0jX94wNWZUYN/Xtm+DKhQ7SLHrQVMdvvt +7h5HZPb3J31cKA9FxVxiXqaakZG3Uxcu3K1gnZZkOb1naLKuBctN518fV4bVIJwo+28TOPX2EZL2 +fZleHwzoq0QkKXJAPTZSr4xYkHPB7GEseaHsh7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5w +wDX3OaJdZtB7WZ+oRxKaJyOkLY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho +-----END CERTIFICATE----- + +EBG Elektronik Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1 +========================================================================== +-----BEGIN CERTIFICATE----- +MIIF5zCCA8+gAwIBAgIITK9zQhyOdAIwDQYJKoZIhvcNAQEFBQAwgYAxODA2BgNVBAMML0VCRyBF +bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMTcwNQYDVQQKDC5FQkcg +QmlsacWfaW0gVGVrbm9sb2ppbGVyaSB2ZSBIaXptZXRsZXJpIEEuxZ4uMQswCQYDVQQGEwJUUjAe +Fw0wNjA4MTcwMDIxMDlaFw0xNjA4MTQwMDMxMDlaMIGAMTgwNgYDVQQDDC9FQkcgRWxla3Ryb25p +ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTE3MDUGA1UECgwuRUJHIEJpbGnFn2lt +IFRla25vbG9qaWxlcmkgdmUgSGl6bWV0bGVyaSBBLsWeLjELMAkGA1UEBhMCVFIwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDuoIRh0DpqZhAy2DE4f6en5f2h4fuXd7hxlugTlkaDT7by +X3JWbhNgpQGR4lvFzVcfd2NR/y8927k/qqk153nQ9dAktiHq6yOU/im/+4mRDGSaBUorzAzu8T2b +gmmkTPiab+ci2hC6X5L8GCcKqKpE+i4stPtGmggDg3KriORqcsnlZR9uKg+ds+g75AxuetpX/dfr +eYteIAbTdgtsApWjluTLdlHRKJ2hGvxEok3MenaoDT2/F08iiFD9rrbskFBKW5+VQarKD7JK/oCZ +TqNGFav4c0JqwmZ2sQomFd2TkuzbqV9UIlKRcF0T6kjsbgNs2d1s/OsNA/+mgxKb8amTD8UmTDGy +Y5lhcucqZJnSuOl14nypqZoaqsNW2xCaPINStnuWt6yHd6i58mcLlEOzrz5z+kI2sSXFCjEmN1Zn +uqMLfdb3ic1nobc6HmZP9qBVFCVMLDMNpkGMvQQxahByCp0OLna9XvNRiYuoP1Vzv9s6xiQFlpJI +qkuNKgPlV5EQ9GooFW5Hd4RcUXSfGenmHmMWOeMRFeNYGkS9y8RsZteEBt8w9DeiQyJ50hBs37vm +ExH8nYQKE3vwO9D8owrXieqWfo1IhR5kX9tUoqzVegJ5a9KK8GfaZXINFHDk6Y54jzJ0fFfy1tb0 +Nokb+Clsi7n2l9GkLqq+CxnCRelwXQIDAJ3Zo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQU587GT/wWZ5b6SqMHwQSny2re2kcwHwYDVR0jBBgwFoAU587GT/wW +Z5b6SqMHwQSny2re2kcwDQYJKoZIhvcNAQEFBQADggIBAJuYml2+8ygjdsZs93/mQJ7ANtyVDR2t +FcU22NU57/IeIl6zgrRdu0waypIN30ckHrMk2pGI6YNw3ZPX6bqz3xZaPt7gyPvT/Wwp+BVGoGgm +zJNSroIBk5DKd8pNSe/iWtkqvTDOTLKBtjDOWU/aWR1qeqRFsIImgYZ29fUQALjuswnoT4cCB64k +XPBfrAowzIpAoHMEwfuJJPaaHFy3PApnNgUIMbOv2AFoKuB4j3TeuFGkjGwgPaL7s9QJ/XvCgKqT +bCmYIai7FvOpEl90tYeY8pUm3zTvilORiF0alKM/fCL414i6poyWqD1SNGKfAB5UVUJnxk1Gj7sU +RT0KlhaOEKGXmdXTMIXM3rRyt7yKPBgpaP3ccQfuJDlq+u2lrDgv+R4QDgZxGhBM/nV+/x5XOULK +1+EVoVZVWRvRo68R2E7DpSvvkL/A7IITW43WciyTTo9qKd+FPNMN4KIYEsxVL0e3p5sC/kH2iExt +2qkBR4NkJ2IQgtYSe14DHzSpyZH+r11thie3I6p1GMog57AP14kOpmciY/SDQSsGS7tY1dHXt7kQ +Y9iJSrSq3RZj9W6+YKH47ejWkE8axsWgKdOnIaj1Wjz3x0miIZpKlVIglnKaZsv30oZDfCK+lvm9 +AahH3eU7QPl1K5srRmSGjR70j/sHd9DqSaIcjVIUpgqT +-----END CERTIFICATE----- + +certSIGN ROOT CA +================ +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD +VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa +Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE +CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I +JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH +rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2 +ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD +0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943 +AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B +Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB +AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8 +SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0 +x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt +vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz +TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +CNNIC ROOT +========== +-----BEGIN CERTIFICATE----- +MIIDVTCCAj2gAwIBAgIESTMAATANBgkqhkiG9w0BAQUFADAyMQswCQYDVQQGEwJDTjEOMAwGA1UE +ChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwHhcNMDcwNDE2MDcwOTE0WhcNMjcwNDE2MDcw +OTE0WjAyMQswCQYDVQQGEwJDTjEOMAwGA1UEChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1Qw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTNfc/c3et6FtzF8LRb+1VvG7q6KR5smzD +o+/hn7E7SIX1mlwhIhAsxYLO2uOabjfhhyzcuQxauohV3/2q2x8x6gHx3zkBwRP9SFIhxFXf2tiz +VHa6dLG3fdfA6PZZxU3Iva0fFNrfWEQlMhkqx35+jq44sDB7R3IJMfAw28Mbdim7aXZOV/kbZKKT +VrdvmW7bCgScEeOAH8tjlBAKqeFkgjH5jCftppkA9nCTGPihNIaj3XrCGHn2emU1z5DrvTOTn1Or +czvmmzQgLx3vqR1jGqCA2wMv+SYahtKNu6m+UjqHZ0gNv7Sg2Ca+I19zN38m5pIEo3/PIKe38zrK +y5nLAgMBAAGjczBxMBEGCWCGSAGG+EIBAQQEAwIABzAfBgNVHSMEGDAWgBRl8jGtKvf33VKWCscC +wQ7vptU7ETAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIB/jAdBgNVHQ4EFgQUZfIxrSr3991S +lgrHAsEO76bVOxEwDQYJKoZIhvcNAQEFBQADggEBAEs17szkrr/Dbq2flTtLP1se31cpolnKOOK5 +Gv+e5m4y3R6u6jW39ZORTtpC4cMXYFDy0VwmuYK36m3knITnA3kXr5g9lNvHugDnuL8BV8F3RTIM +O/G0HAiw/VGgod2aHRM2mm23xzy54cXZF/qD1T0VoDy7HgviyJA/qIYM/PmLXoXLT1tLYhFHxUV8 +BS9BsZ4QaRuZluBVeftOhpm4lNqGOGqTo+fLbuXf6iFViZx9fX+Y9QCJ7uOEwFyWtcVG6kbghVW2 +G8kS1sHNzYDzAgE8yGnLRUhj2JTQ7IUOO04RZfSCjKY9ri4ilAnIXOo8gV0WKgOXFlUJ24pBgp5m +mxE= +-----END CERTIFICATE----- + +ApplicationCA - Japanese Government +=================================== +-----BEGIN CERTIFICATE----- +MIIDoDCCAoigAwIBAgIBMTANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJKUDEcMBoGA1UEChMT +SmFwYW5lc2UgR292ZXJubWVudDEWMBQGA1UECxMNQXBwbGljYXRpb25DQTAeFw0wNzEyMTIxNTAw +MDBaFw0xNzEyMTIxNTAwMDBaMEMxCzAJBgNVBAYTAkpQMRwwGgYDVQQKExNKYXBhbmVzZSBHb3Zl +cm5tZW50MRYwFAYDVQQLEw1BcHBsaWNhdGlvbkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAp23gdE6Hj6UG3mii24aZS2QNcfAKBZuOquHMLtJqO8F6tJdhjYq+xpqcBrSGUeQ3DnR4 +fl+Kf5Sk10cI/VBaVuRorChzoHvpfxiSQE8tnfWuREhzNgaeZCw7NCPbXCbkcXmP1G55IrmTwcrN +wVbtiGrXoDkhBFcsovW8R0FPXjQilbUfKW1eSvNNcr5BViCH/OlQR9cwFO5cjFW6WY2H/CPek9AE +jP3vbb3QesmlOmpyM8ZKDQUXKi17safY1vC+9D/qDihtQWEjdnjDuGWk81quzMKq2edY3rZ+nYVu +nyoKb58DKTCXKB28t89UKU5RMfkntigm/qJj5kEW8DOYRwIDAQABo4GeMIGbMB0GA1UdDgQWBBRU +WssmP3HMlEYNllPqa0jQk/5CdTAOBgNVHQ8BAf8EBAMCAQYwWQYDVR0RBFIwUKROMEwxCzAJBgNV +BAYTAkpQMRgwFgYDVQQKDA/ml6XmnKzlm73mlL/lupwxIzAhBgNVBAsMGuOCouODl+ODquOCseOD +vOOCt+ODp+ODs0NBMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADlqRHZ3ODrs +o2dGD/mLBqj7apAxzn7s2tGJfHrrLgy9mTLnsCTWw//1sogJhyzjVOGjprIIC8CFqMjSnHH2HZ9g +/DgzE+Ge3Atf2hZQKXsvcJEPmbo0NI2VdMV+eKlmXb3KIXdCEKxmJj3ekav9FfBv7WxfEPjzFvYD +io+nEhEMy/0/ecGc/WLuo89UDNErXxc+4z6/wCs+CZv+iKZ+tJIX/COUgb1up8WMwusRRdv4QcmW +dupwX3kSa+SjB1oF7ydJzyGfikwJcGapJsErEU4z0g781mzSDjJkaP+tBXhfAx2o45CsJOAPQKdL +rosot4LKGAfmt1t06SAZf7IbiVQ= +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority - G3 +============================================= +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA4IEdlb1RydXN0 +IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFy +eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIz +NTk1OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAo +YykgMjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMT +LUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz+uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5j +K/BGvESyiaHAKAxJcCGVn2TAppMSAmUmhsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdE +c5IiaacDiGydY8hS2pgn5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3C +IShwiP/WJmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exALDmKu +dlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZChuOl1UcCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMR5yo6hTgMdHNxr +2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IBAQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9 +cr5HqQ6XErhK8WTTOd8lNNTBzU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbE +Ap7aDHdlDkQNkv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD +AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUHSJsMC8tJP33s +t/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2Gspki4cErx5z481+oghLrGREt +-----END CERTIFICATE----- + +thawte Primary Root CA - G2 +=========================== +-----BEGIN CERTIFICATE----- +MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDELMAkGA1UEBhMC +VVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMpIDIwMDcgdGhhd3RlLCBJbmMu +IC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3Qg +Q0EgLSBHMjAeFw0wNzExMDUwMDAwMDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEV +MBMGA1UEChMMdGhhd3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBG +b3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAt +IEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/BebfowJPDQfGAFG6DAJS +LSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6papu+7qzcMBniKI11KOasf2twu8x+qi5 +8/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU +mtgAMADna3+FGO6Lts6KDPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUN +G4k8VIZ3KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41oxXZ3K +rr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== +-----END CERTIFICATE----- + +thawte Primary Root CA - G3 +=========================== +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCBrjELMAkGA1UE +BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2 +aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0w +ODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh +d3RlLCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9uMTgwNgYD +VQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIG +A1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAsr8nLPvb2FvdeHsbnndmgcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2At +P0LMqmsywCPLLEHd5N/8YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC ++BsUa0Lfb1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS99irY +7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2SzhkGcuYMXDhpxwTW +vGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUkOQIDAQABo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJ +KoZIhvcNAQELBQADggEBABpA2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweK +A3rD6z8KLFIWoCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu +t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7cKUGRIjxpp7sC +8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fMm7v/OeZWYdMKp8RcTGB7BXcm +er/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZuMdRAGmI0Nj81Aa6sY6A= +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority - G2 +============================================= +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA3IEdlb1RydXN0IElu +Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1 +OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg +MjAwNyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMTLUdl +b1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjB2MBAGByqGSM49AgEG +BSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcLSo17VDs6bl8VAsBQps8lL33KSLjHUGMc +KiEIfJo22Av+0SbFWDEwKCXzXV2juLaltJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+ +EVXVMAoGCCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGTqQ7m +ndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBuczrD6ogRLQy7rQkgu2 +npaqBA+K +-----END CERTIFICATE----- + +VeriSign Universal Root Certification Authority +=============================================== +-----BEGIN CERTIFICATE----- +MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCBvTELMAkGA1UE +BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO +ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk +IHVzZSBvbmx5MTgwNgYDVQQDEy9WZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTAeFw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJV +UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv +cmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNhbCBSb290IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj +1mCOkdeQmIN65lgZOIzF9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGP +MiJhgsWHH26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+HLL72 +9fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN/BMReYTtXlT2NJ8I +AfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPTrJ9VAMf2CGqUuV/c4DPxhGD5WycR +tPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0G +CCsGAQUFBwEMBGEwX6FdoFswWTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2O +a8PPgGrUSBgsexkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud +DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4sAPmLGd75JR3 +Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+seQxIcaBlVZaDrHC1LGmWazx +Y8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTx +P/jgdFcrGJ2BtMQo2pSXpXDrrB2+BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+P +wGZsY6rp2aQW9IHRlRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4 +mJO37M2CYfE45k+XmCpajQ== +-----END CERTIFICATE----- + +VeriSign Class 3 Public Primary Certification Authority - G4 +============================================================ +-----BEGIN CERTIFICATE----- +MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjELMAkGA1UEBhMC +VVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3 +b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVz +ZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBU +cnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRo +b3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5 +IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8 +Utpkmw4tXNherJI9/gHmGUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGz +rl0Bp3vefLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEw +HzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVyaXNpZ24u +Y29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMWkf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMD +A2gAMGUCMGYhDBgmYFo4e1ZC4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIx +AJw9SDkjOVgaFRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== +-----END CERTIFICATE----- + +NetLock Arany (Class Gold) Főtanúsítvány +============================================ +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G +A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610 +dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB +cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx +MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO +ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6 +c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu +0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw +/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk +H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw +fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1 +neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW +qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta +YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna +NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu +dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +Staat der Nederlanden Root CA - G2 +================================== +-----BEGIN CERTIFICATE----- +MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJOTDEeMBwGA1UE +CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFhdCBkZXIgTmVkZXJsYW5kZW4g +Um9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oXDTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMC +TkwxHjAcBgNVBAoMFVN0YWF0IGRlciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5l +ZGVybGFuZGVuIFJvb3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ +5291qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8SpuOUfiUtn +vWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPUZ5uW6M7XxgpT0GtJlvOj +CwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvEpMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiil +e7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCR +OME4HYYEhLoaJXhena/MUGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpI +CT0ugpTNGmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy5V65 +48r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv6q012iDTiIJh8BIi +trzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEKeN5KzlW/HdXZt1bv8Hb/C3m1r737 +qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMB +AAGjgZcwgZQwDwYDVR0TAQH/BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcC +ARYxaHR0cDovL3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqGSIb3DQEBCwUA +A4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLySCZa59sCrI2AGeYwRTlHSeYAz ++51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwj +f/ST7ZwaUb7dRUG/kSS0H4zpX897IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaN +kqbG9AclVMwWVxJKgnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfk +CpYL+63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxLvJxxcypF +URmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkmbEgeqmiSBeGCc1qb3Adb +CG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvkN1trSt8sV4pAWja63XVECDdCcAz+3F4h +oKOKwJCcaNpQ5kUQR3i2TtJlycM33+FCY7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoV +IPVVYpbtbZNQvOSqeK3Zywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm +66+KAQ== +-----END CERTIFICATE----- + +CA Disig +======== +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBATANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQGEwJTSzETMBEGA1UEBxMK +QnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwHhcNMDYw +MzIyMDEzOTM0WhcNMTYwMzIyMDEzOTM0WjBKMQswCQYDVQQGEwJTSzETMBEGA1UEBxMKQnJhdGlz +bGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCS9jHBfYj9mQGp2HvycXXxMcbzdWb6UShGhJd4NLxs/LxFWYgm +GErENx+hSkS943EE9UQX4j/8SFhvXJ56CbpRNyIjZkMhsDxkovhqFQ4/61HhVKndBpnXmjxUizkD +Pw/Fzsbrg3ICqB9x8y34dQjbYkzo+s7552oftms1grrijxaSfQUMbEYDXcDtab86wYqg6I7ZuUUo +hwjstMoVvoLdtUSLLa2GDGhibYVW8qwUYzrG0ZmsNHhWS8+2rT+MitcE5eN4TPWGqvWP+j1scaMt +ymfraHtuM6kMgiioTGohQBUgDCZbg8KpFhXAJIJdKxatymP2dACw30PEEGBWZ2NFAgMBAAGjgf8w +gfwwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUjbJJaJ1yCCW5wCf1UJNWSEZx+Y8wDgYDVR0P +AQH/BAQDAgEGMDYGA1UdEQQvMC2BE2Nhb3BlcmF0b3JAZGlzaWcuc2uGFmh0dHA6Ly93d3cuZGlz +aWcuc2svY2EwZgYDVR0fBF8wXTAtoCugKYYnaHR0cDovL3d3dy5kaXNpZy5zay9jYS9jcmwvY2Ff +ZGlzaWcuY3JsMCygKqAohiZodHRwOi8vY2EuZGlzaWcuc2svY2EvY3JsL2NhX2Rpc2lnLmNybDAa +BgNVHSAEEzARMA8GDSuBHpGT5goAAAABAQEwDQYJKoZIhvcNAQEFBQADggEBAF00dGFMrzvY/59t +WDYcPQuBDRIrRhCA/ec8J9B6yKm2fnQwM6M6int0wHl5QpNt/7EpFIKrIYwvF/k/Ji/1WcbvgAa3 +mkkp7M5+cTxqEEHA9tOasnxakZzArFvITV734VP/Q3f8nktnbNfzg9Gg4H8l37iYC5oyOGwwoPP/ +CBUz91BKez6jPiCp3C9WgArtQVCwyfTssuMmRAAOb54GvCKWU3BlxFAKRmukLyeBEicTXxChds6K +ezfqwzlhA5WYOudsiCUI/HloDYd9Yvi0X/vF2Ey9WLw/Q1vUHgFNPGO+I++MzVpQuGhU+QqZMxEA +4Z7CRneC9VkGjCFMhwnN5ag= +-----END CERTIFICATE----- + +Juur-SK +======= +-----BEGIN CERTIFICATE----- +MIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcNAQkBFglwa2lA +c2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZpdHNlZXJpbWlza2Vza3VzMRAw +DgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMwMVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqG +SIb3DQEJARYJcGtpQHNrLmVlMQswCQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVy +aW1pc2tlc2t1czEQMA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOBSvZiF3tf +TQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkzABpTpyHhOEvWgxutr2TC ++Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvHLCu3GFH+4Hv2qEivbDtPL+/40UceJlfw +UR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMPPbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDa +Tpxt4brNj3pssAki14sL2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQF +MAMBAf8wggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwICMIHD +HoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDkAGwAagBhAHMAdABh +AHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0AHMAZQBlAHIAaQBtAGkAcwBrAGUA +cwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABzAGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABr +AGkAbgBuAGkAdABhAG0AaQBzAGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nw +cy8wKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE +FASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcYP2/v6X2+MA4G +A1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOiCfP+JmeaUOTDBS8rNXiRTHyo +ERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+gkcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyL +abVAyJRld/JXIWY7zoVAtjNjGr95HvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678 +IIbsSt4beDI3poHSna9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkh +Mp6qqIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0ZTbvGRNs2 +yyqcjg== +-----END CERTIFICATE----- + +Hongkong Post Root CA 1 +======================= +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoT +DUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMB4XDTAzMDUx +NTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25n +IFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1 +ApzQjVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEnPzlTCeqr +auh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjhZY4bXSNmO7ilMlHIhqqh +qZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9nnV0ttgCXjqQesBCNnLsak3c78QA3xMY +V18meMjWCnl3v/evt3a5pQuEF10Q6m/hq5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNV +HRMBAf8ECDAGAQH/AgEDMA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7i +h9legYsCmEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI37pio +l7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clBoiMBdDhViw+5Lmei +IAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJsEhTkYY2sEJCehFC78JZvRZ+K88ps +T/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpOfMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilT +c4afU9hDDl3WY4JxHYB0yvbiAmvZWg== +-----END CERTIFICATE----- + +SecureSign RootCA11 +=================== +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDErMCkGA1UEChMi +SmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoGA1UEAxMTU2VjdXJlU2lnbiBS +b290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSsw +KQYDVQQKEyJKYXBhbiBDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1 +cmVTaWduIFJvb3RDQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvL +TJszi1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8h9uuywGO +wvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOVMdrAG/LuYpmGYz+/3ZMq +g6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rP +O7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitA +bpSACW22s293bzUIUPsCh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZX +t94wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKCh +OBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xmKbabfSVSSUOrTC4r +bnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQX5Ucv+2rIrVls4W6ng+4reV6G4pQ +Oh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWrQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01 +y8hSyn+B/tlr0/cR7SXf+Of5pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061 +lgeLKBObjBmNQSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- + +ACEDICOM Root +============= +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIIYY3HhjsBggUwDQYJKoZIhvcNAQEFBQAwRDEWMBQGA1UEAwwNQUNFRElD +T00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00xCzAJBgNVBAYTAkVTMB4XDTA4 +MDQxODE2MjQyMloXDTI4MDQxMzE2MjQyMlowRDEWMBQGA1UEAwwNQUNFRElDT00gUm9vdDEMMAoG +A1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00xCzAJBgNVBAYTAkVTMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA/5KV4WgGdrQsyFhIyv2AVClVYyT/kGWbEHV7w2rbYgIB8hiGtXxaOLHk +WLn709gtn70yN78sFW2+tfQh0hOR2QetAQXW8713zl9CgQr5auODAKgrLlUTY4HKRxx7XBZXehuD +YAQ6PmXDzQHe3qTWDLqO3tkE7hdWIpuPY/1NFgu3e3eM+SW10W2ZEi5PGrjm6gSSrj0RuVFCPYew +MYWveVqc/udOXpJPQ/yrOq2lEiZmueIM15jO1FillUAKt0SdE3QrwqXrIhWYENiLxQSfHY9g5QYb +m8+5eaA9oiM/Qj9r+hwDezCNzmzAv+YbX79nuIQZ1RXve8uQNjFiybwCq0Zfm/4aaJQ0PZCOrfbk +HQl/Sog4P75n/TSW9R28MHTLOO7VbKvU/PQAtwBbhTIWdjPp2KOZnQUAqhbm84F9b32qhm2tFXTT +xKJxqvQUfecyuB+81fFOvW8XAjnXDpVCOscAPukmYxHqC9FK/xidstd7LzrZlvvoHpKuE1XI2Sf2 +3EgbsCTBheN3nZqk8wwRHQ3ItBTutYJXCb8gWH8vIiPYcMt5bMlL8qkqyPyHK9caUPgn6C9D4zq9 +2Fdx/c6mUlv53U3t5fZvie27k5x2IXXwkkwp9y+cAS7+UEaeZAwUswdbxcJzbPEHXEUkFDWug/Fq +TYl6+rPYLWbwNof1K1MCAwEAAaOBqjCBpzAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKaz +4SsrSbbXc6GqlPUB53NlTKxQMA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUprPhKytJttdzoaqU +9QHnc2VMrFAwRAYDVR0gBD0wOzA5BgRVHSAAMDEwLwYIKwYBBQUHAgEWI2h0dHA6Ly9hY2VkaWNv +bS5lZGljb21ncm91cC5jb20vZG9jMA0GCSqGSIb3DQEBBQUAA4ICAQDOLAtSUWImfQwng4/F9tqg +aHtPkl7qpHMyEVNEskTLnewPeUKzEKbHDZ3Ltvo/Onzqv4hTGzz3gvoFNTPhNahXwOf9jU8/kzJP +eGYDdwdY6ZXIfj7QeQCM8htRM5u8lOk6e25SLTKeI6RF+7YuE7CLGLHdztUdp0J/Vb77W7tH1Pwk +zQSulgUV1qzOMPPKC8W64iLgpq0i5ALudBF/TP94HTXa5gI06xgSYXcGCRZj6hitoocf8seACQl1 +ThCojz2GuHURwCRiipZ7SkXp7FnFvmuD5uHorLUwHv4FB4D54SMNUI8FmP8sX+g7tq3PgbUhh8oI +KiMnMCArz+2UW6yyetLHKKGKC5tNSixthT8Jcjxn4tncB7rrZXtaAWPWkFtPF2Y9fwsZo5NjEFIq +nxQWWOLcpfShFosOkYuByptZ+thrkQdlVV9SH686+5DdaaVbnG0OLLb6zqylfDJKZ0DcMDQj3dcE +I2bw/FWAp/tmGYI1Z2JwOV5vx+qQQEQIHriy1tvuWacNGHk0vFQYXlPKNFHtRQrmjseCNj6nOGOp +MCwXEGCSn1WHElkQwg9naRHMTh5+Spqtr0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3o +tkYNbn5XOmeUwssfnHdKZ05phkOTOPu220+DkdRgfks+KzgHVZhepA== +-----END CERTIFICATE----- + +Microsec e-Szigno Root CA 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER +MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv +c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE +BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt +U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA +fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG +0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA +pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm +1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC +AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf +QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE +FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o +lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX +I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02 +yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi +LXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +GlobalSign Root CA - R3 +======================= +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt +iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ +0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3 +rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl +OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2 +xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7 +lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8 +EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E +bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18 +YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r +kpeDMdmztcpHWD9f +-----END CERTIFICATE----- + +Autoridad de Certificacion Firmaprofesional CIF A62634068 +========================================================= +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UEBhMCRVMxQjBA +BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 +MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEyMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIw +QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB +NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD +Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P +B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY +7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH +ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI +plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX +MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX +LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK +bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU +vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1Ud +EwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNH +DhpkLzCBpgYDVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBvACAAZABlACAA +bABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBlAGwAbwBuAGEAIAAwADgAMAAx +ADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx +51tkljYyGOylMnfX40S2wBEqgLk9am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qk +R71kMrv2JYSiJ0L1ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaP +T481PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS3a/DTg4f +Jl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5kSeTy36LssUzAKh3ntLFl +osS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF3dvd6qJ2gHN99ZwExEWN57kci57q13XR +crHedUTnQn3iV2t93Jm8PYMo6oCTjcVMZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoR +saS8I8nkvof/uZS2+F0gStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTD +KCOM/iczQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQBjLMi +6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- + +Izenpe.com +========== +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG +EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz +MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu +QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ +03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK +ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU ++zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC +PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT +OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK +F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK +0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+ +0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB +leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID +AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+ +SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG +NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l +Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga +kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q +hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs +g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5 +aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5 +nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC +ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo +Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z +WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +Chambers of Commerce Root - 2008 +================================ +-----BEGIN CERTIFICATE----- +MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYDVQQGEwJFVTFD +MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv +bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu +QS4xKTAnBgNVBAMTIENoYW1iZXJzIG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEy +Mjk1MFoXDTM4MDczMTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNl +ZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQF +EwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJl +cnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW928sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKA +XuFixrYp4YFs8r/lfTJqVKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorj +h40G072QDuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR5gN/ +ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfLZEFHcpOrUMPrCXZk +NNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05aSd+pZgvMPMZ4fKecHePOjlO+Bd5g +D2vlGts/4+EhySnB8esHnFIbAURRPHsl18TlUlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331 +lubKgdaX8ZSD6e2wsWsSaR6s+12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ +0wlf2eOKNcx5Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj +ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAxhduub+84Mxh2 +EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNVHQ4EFgQU+SSsD7K1+HnA+mCI +G8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1+HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJ +BgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNh +bWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENh +bWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDiC +CQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUH +AgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAJASryI1 +wqM58C7e6bXpeHxIvj99RZJe6dqxGfwWPJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH +3qLPaYRgM+gQDROpI9CF5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbU +RWpGqOt1glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaHFoI6 +M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2pSB7+R5KBWIBpih1 +YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MDxvbxrN8y8NmBGuScvfaAFPDRLLmF +9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QGtjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcK +zBIKinmwPQN/aUv0NCB9szTqjktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvG +nrDQWzilm1DefhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg +OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZd0jQ +-----END CERTIFICATE----- + +Global Chambersign Root - 2008 +============================== +-----BEGIN CERTIFICATE----- +MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYDVQQGEwJFVTFD +MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv +bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu +QS4xJzAlBgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMx +NDBaFw0zODA3MzExMjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUg +Y3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJ +QTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD +aGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDf +VtPkOpt2RbQT2//BthmLN0EYlVJH6xedKYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXf +XjaOcNFccUMd2drvXNL7G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0 +ZJJ0YPP2zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4ddPB +/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyGHoiMvvKRhI9lNNgA +TH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2Id3UwD2ln58fQ1DJu7xsepeY7s2M +H/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3VyJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfe +Ox2YItaswTXbo6Al/3K1dh3ebeksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSF +HTynyQbehP9r6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh +wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsogzCtLkykPAgMB +AAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQWBBS5CcqcHtvTbDprru1U8VuT +BjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDprru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UE +BhMCRVUxQzBBBgNVBAcTOk1hZHJpZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJm +aXJtYS5jb20vYWRkcmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJm +aXJtYSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiCCQDJzdPp +1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0 +dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAICIf3DekijZBZRG +/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZUohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6 +ReAJ3spED8IXDneRRXozX1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/s +dZ7LoR/xfxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVza2Mg +9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yydYhz2rXzdpjEetrHH +foUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMdSqlapskD7+3056huirRXhOukP9Du +qqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9OAP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETr +P3iZ8ntxPjzxmKfFGBI/5rsoM0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVq +c5iJWzouE4gev8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z +09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B +-----END CERTIFICATE----- + +Go Daddy Root Certificate Authority - G2 +======================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu +MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G +A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq +9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD ++qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd +fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl +NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9 +BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac +vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r +5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV +N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1 +-----END CERTIFICATE----- + +Starfield Root Certificate Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0 +eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw +DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg +VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB +dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv +W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs +bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk +N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf +ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU +JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol +TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx +4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw +F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ +c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +Starfield Services Root Certificate Authority - G2 +================================================== +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl +IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT +dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2 +h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa +hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP +LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB +rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG +SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP +E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy +xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza +YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6 +-----END CERTIFICATE----- + +AffirmTrust Commercial +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw +MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb +DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV +C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6 +BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww +MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV +HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG +hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi +qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv +0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh +sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +AffirmTrust Networking +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw +MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE +Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI +dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24 +/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb +h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV +HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu +UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6 +12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23 +WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9 +/ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +AffirmTrust Premium +=================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy +OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy +dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn +BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV +5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs ++7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd +GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R +p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI +S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04 +6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5 +/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo ++Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv +MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC +6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S +L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK ++4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV +BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg +IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60 +g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb +zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw== +-----END CERTIFICATE----- + +AffirmTrust Premium ECC +======================= +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV +BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx +MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U +cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ +N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW +BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK +BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X +57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM +eQ== +-----END CERTIFICATE----- + +Certum Trusted Network CA +========================= +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK +ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy +MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU +ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC +l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J +J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4 +fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0 +cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw +DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj +jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1 +mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj +Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +Certinomis - Autorité Racine +============================= +-----BEGIN CERTIFICATE----- +MIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjETMBEGA1UEChMK +Q2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxJjAkBgNVBAMMHUNlcnRpbm9taXMg +LSBBdXRvcml0w6kgUmFjaW5lMB4XDTA4MDkxNzA4Mjg1OVoXDTI4MDkxNzA4Mjg1OVowYzELMAkG +A1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMSYw +JAYDVQQDDB1DZXJ0aW5vbWlzIC0gQXV0b3JpdMOpIFJhY2luZTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAJ2Fn4bT46/HsmtuM+Cet0I0VZ35gb5j2CN2DpdUzZlMGvE5x4jYF1AMnmHa +wE5V3udauHpOd4cN5bjr+p5eex7Ezyh0x5P1FMYiKAT5kcOrJ3NqDi5N8y4oH3DfVS9O7cdxbwly +Lu3VMpfQ8Vh30WC8Tl7bmoT2R2FFK/ZQpn9qcSdIhDWerP5pqZ56XjUl+rSnSTV3lqc2W+HN3yNw +2F1MpQiD8aYkOBOo7C+ooWfHpi2GR+6K/OybDnT0K0kCe5B1jPyZOQE51kqJ5Z52qz6WKDgmi92N +jMD2AR5vpTESOH2VwnHu7XSu5DaiQ3XV8QCb4uTXzEIDS3h65X27uK4uIJPT5GHfceF2Z5c/tt9q +c1pkIuVC28+BA5PY9OMQ4HL2AHCs8MF6DwV/zzRpRbWT5BnbUhYjBYkOjUjkJW+zeL9i9Qf6lSTC +lrLooyPCXQP8w9PlfMl1I9f09bze5N/NgL+RiH2nE7Q5uiy6vdFrzPOlKO1Enn1So2+WLhl+HPNb +xxaOu2B9d2ZHVIIAEWBsMsGoOBvrbpgT1u449fCfDu/+MYHB0iSVL1N6aaLwD4ZFjliCK0wi1F6g +530mJ0jfJUaNSih8hp75mxpZuWW/Bd22Ql095gBIgl4g9xGC3srYn+Y3RyYe63j3YcNBZFgCQfna +4NH4+ej9Uji29YnfAgMBAAGjWzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBQNjLZh2kS40RR9w759XkjwzspqsDAXBgNVHSAEEDAOMAwGCiqBegFWAgIAAQEwDQYJ +KoZIhvcNAQEFBQADggIBACQ+YAZ+He86PtvqrxyaLAEL9MW12Ukx9F1BjYkMTv9sov3/4gbIOZ/x +WqndIlgVqIrTseYyCYIDbNc/CMf4uboAbbnW/FIyXaR/pDGUu7ZMOH8oMDX/nyNTt7buFHAAQCva +R6s0fl6nVjBhK4tDrP22iCj1a7Y+YEq6QpA0Z43q619FVDsXrIvkxmUP7tCMXWY5zjKn2BCXwH40 +nJ+U8/aGH88bc62UeYdocMMzpXDn2NU4lG9jeeu/Cg4I58UvD0KgKxRA/yHgBcUn4YQRE7rWhh1B +CxMjidPJC+iKunqjo3M3NYB9Ergzd0A4wPpeMNLytqOx1qKVl4GbUu1pTP+A5FPbVFsDbVRfsbjv +JL1vnxHDx2TCDyhihWZeGnuyt++uNckZM6i4J9szVb9o4XVIRFb7zdNIu0eJOqxp9YDG5ERQL1TE +qkPFMTFYvZbF6nVsmnWxTfj3l/+WFvKXTej28xH5On2KOG4Ey+HTRRWqpdEdnV1j6CTmNhTih60b +WfVEm/vXd3wfAXBioSAaosUaKPQhA+4u2cGA6rnZgtZbdsLLO7XSAPCjDuGtbkD326C00EauFddE +wk01+dIL8hf2rGbVJLJP0RyZwG71fet0BLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/ +vgt2Fl43N+bYdJeimUV5 +-----END CERTIFICATE----- + +Root CA Generalitat Valenciana +============================== +-----BEGIN CERTIFICATE----- +MIIGizCCBXOgAwIBAgIEO0XlaDANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJFUzEfMB0GA1UE +ChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290 +IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwHhcNMDEwNzA2MTYyMjQ3WhcNMjEwNzAxMTUyMjQ3 +WjBoMQswCQYDVQQGEwJFUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UE +CxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGKqtXETcvIorKA3Qdyu0togu8M1JAJke+WmmmO3I2 +F0zo37i7L3bhQEZ0ZQKQUgi0/6iMweDHiVYQOTPvaLRfX9ptI6GJXiKjSgbwJ/BXufjpTjJ3Cj9B +ZPPrZe52/lSqfR0grvPXdMIKX/UIKFIIzFVd0g/bmoGlu6GzwZTNVOAydTGRGmKy3nXiz0+J2ZGQ +D0EbtFpKd71ng+CT516nDOeB0/RSrFOyA8dEJvt55cs0YFAQexvba9dHq198aMpunUEDEO5rmXte +JajCq+TA81yc477OMUxkHl6AovWDfgzWyoxVjr7gvkkHD6MkQXpYHYTqWBLI4bft75PelAgxAgMB +AAGjggM7MIIDNzAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnBraS5n +dmEuZXMwEgYDVR0TAQH/BAgwBgEB/wIBAjCCAjQGA1UdIASCAiswggInMIICIwYKKwYBBAG/VQIB +ADCCAhMwggHoBggrBgEFBQcCAjCCAdoeggHWAEEAdQB0AG8AcgBpAGQAYQBkACAAZABlACAAQwBl +AHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAFIAYQDtAHoAIABkAGUAIABsAGEAIABHAGUAbgBlAHIA +YQBsAGkAdABhAHQAIABWAGEAbABlAG4AYwBpAGEAbgBhAC4ADQAKAEwAYQAgAEQAZQBjAGwAYQBy +AGEAYwBpAPMAbgAgAGQAZQAgAFAAcgDhAGMAdABpAGMAYQBzACAAZABlACAAQwBlAHIAdABpAGYA +aQBjAGEAYwBpAPMAbgAgAHEAdQBlACAAcgBpAGcAZQAgAGUAbAAgAGYAdQBuAGMAaQBvAG4AYQBt +AGkAZQBuAHQAbwAgAGQAZQAgAGwAYQAgAHAAcgBlAHMAZQBuAHQAZQAgAEEAdQB0AG8AcgBpAGQA +YQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAHMAZQAgAGUAbgBjAHUAZQBu +AHQAcgBhACAAZQBuACAAbABhACAAZABpAHIAZQBjAGMAaQDzAG4AIAB3AGUAYgAgAGgAdAB0AHAA +OgAvAC8AdwB3AHcALgBwAGsAaQAuAGcAdgBhAC4AZQBzAC8AYwBwAHMwJQYIKwYBBQUHAgEWGWh0 +dHA6Ly93d3cucGtpLmd2YS5lcy9jcHMwHQYDVR0OBBYEFHs100DSHHgZZu90ECjcPk+yeAT8MIGV +BgNVHSMEgY0wgYqAFHs100DSHHgZZu90ECjcPk+yeAT8oWykajBoMQswCQYDVQQGEwJFUzEfMB0G +A1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScwJQYDVQQDEx5S +b290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmGCBDtF5WgwDQYJKoZIhvcNAQEFBQADggEBACRh +TvW1yEICKrNcda3FbcrnlD+laJWIwVTAEGmiEi8YPyVQqHxK6sYJ2fR1xkDar1CdPaUWu20xxsdz +Ckj+IHLtb8zog2EWRpABlUt9jppSCS/2bxzkoXHPjCpaF3ODR00PNvsETUlR4hTJZGH71BTg9J63 +NI8KJr2XXPR5OkowGcytT6CYirQxlyric21+eLj4iIlPsSKRZEv1UN4D2+XFducTZnV+ZfsBn5OH +iJ35Rld8TWCvmHMTI6QgkYH60GFmuH3Rr9ZvHmw96RH9qfmCIoaZM3Fa6hlXPZHNqcCjbgcTpsnt ++GijnsNacgmHKNHEc8RzGF9QdRYxn7fofMM= +-----END CERTIFICATE----- + +A-Trust-nQual-03 +================ +-----BEGIN CERTIFICATE----- +MIIDzzCCAregAwIBAgIDAWweMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYDVQQGEwJBVDFIMEYGA1UE +Cgw/QS1UcnVzdCBHZXMuIGYuIFNpY2hlcmhlaXRzc3lzdGVtZSBpbSBlbGVrdHIuIERhdGVudmVy +a2VociBHbWJIMRkwFwYDVQQLDBBBLVRydXN0LW5RdWFsLTAzMRkwFwYDVQQDDBBBLVRydXN0LW5R +dWFsLTAzMB4XDTA1MDgxNzIyMDAwMFoXDTE1MDgxNzIyMDAwMFowgY0xCzAJBgNVBAYTAkFUMUgw +RgYDVQQKDD9BLVRydXN0IEdlcy4gZi4gU2ljaGVyaGVpdHNzeXN0ZW1lIGltIGVsZWt0ci4gRGF0 +ZW52ZXJrZWhyIEdtYkgxGTAXBgNVBAsMEEEtVHJ1c3QtblF1YWwtMDMxGTAXBgNVBAMMEEEtVHJ1 +c3QtblF1YWwtMDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtPWFuA/OQO8BBC4SA +zewqo51ru27CQoT3URThoKgtUaNR8t4j8DRE/5TrzAUjlUC5B3ilJfYKvUWG6Nm9wASOhURh73+n +yfrBJcyFLGM/BWBzSQXgYHiVEEvc+RFZznF/QJuKqiTfC0Li21a8StKlDJu3Qz7dg9MmEALP6iPE +SU7l0+m0iKsMrmKS1GWH2WrX9IWf5DMiJaXlyDO6w8dB3F/GaswADm0yqLaHNgBid5seHzTLkDx4 +iHQF63n1k3Flyp3HaxgtPVxO59X4PzF9j4fsCiIvI+n+u33J4PTs63zEsMMtYrWacdaxaujs2e3V +cuy+VwHOBVWf3tFgiBCzAgMBAAGjNjA0MA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0OBAoECERqlWdV +eRFPMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAVdRU0VlIXLOThaq/Yy/kgM40 +ozRiPvbY7meIMQQDbwvUB/tOdQ/TLtPAF8fGKOwGDREkDg6lXb+MshOWcdzUzg4NCmgybLlBMRmr +sQd7TZjTXLDR8KdCoLXEjq/+8T/0709GAHbrAvv5ndJAlseIOrifEXnzgGWovR/TeIGgUUw3tKZd +JXDRZslo+S4RFGjxVJgIrCaSD96JntT6s3kr0qN51OyLrIdTaEJMUVF0HhsnLuP1Hyl0Te2v9+GS +mYHovjrHF1D2t8b8m7CKa9aIA5GPBnc6hQLdmNVDeD/GMBWsm2vLV7eJUYs66MmEDNuxUCAKGkq6 +ahq97BvIxYSazQ== +-----END CERTIFICATE----- + +TWCA Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ +VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG +EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB +IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx +QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC +oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP +4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r +y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG +9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC +mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW +QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY +T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny +Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +Security Communication RootCA2 +============================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc +U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh +dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC +SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy +aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++ ++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R +3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV +spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K +EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8 +QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB +CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj +u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk +3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q +tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29 +mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +EC-ACC +====== +-----BEGIN CERTIFICATE----- +MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB8zELMAkGA1UE +BhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2VydGlmaWNhY2lvIChOSUYgUS0w +ODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYD +VQQLEyxWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UE +CxMsSmVyYXJxdWlhIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMT +BkVDLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQGEwJFUzE7 +MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8gKE5JRiBRLTA4MDExNzYt +SSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBDZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZl +Z2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQubmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJh +cnF1aWEgRW50aXRhdHMgZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUND +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R85iK +w5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm4CgPukLjbo73FCeT +ae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaVHMf5NLWUhdWZXqBIoH7nF2W4onW4 +HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNdQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0a +E9jD2z3Il3rucO2n5nzbcc8tlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw +0JDnJwIDAQABo4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4opvpXY0wfwYD +VR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBodHRwczovL3d3dy5jYXRjZXJ0 +Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5l +dC92ZXJhcnJlbCAwDQYJKoZIhvcNAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJ +lF7W2u++AVtd0x7Y/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNa +Al6kSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhyRp/7SNVe +l+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOSAgu+TGbrIP65y7WZf+a2 +E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xlnJ2lYJU6Un/10asIbvPuW/mIPX64b24D +5EI= +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions RootCA 2011 +======================================================= +-----BEGIN CERTIFICATE----- +MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1IxRDBCBgNVBAoT +O0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9y +aXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z +IFJvb3RDQSAyMDExMB4XDTExMTIwNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYT +AkdSMUQwQgYDVQQKEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z +IENlcnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNo +IEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPzdYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI +1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa +71HFK9+WXesyHgLacEnsbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u +8yBRQlqD75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSPFEDH +3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNVHRMBAf8EBTADAQH/ +MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp5dgTBCPuQSUwRwYDVR0eBEAwPqA8 +MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQub3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQu +b3JnMA0GCSqGSIb3DQEBBQUAA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVt +XdMiKahsog2p6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 +TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7dIsXRSZMFpGD +/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8AcysNnq/onN694/BtZqhFLKPM58N +7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXIl7WdmplNsDz4SgCbZN2fOUvRJ9e4 +-----END CERTIFICATE----- + +Actalis Authentication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM +BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE +AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky +MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz +IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ +wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa +by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6 +zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f +YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2 +oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l +EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7 +hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8 +EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5 +jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY +iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI +WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0 +JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx +K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+ +Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC +4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo +2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz +lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem +OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9 +vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +Trustis FPS Root CA +=================== +-----BEGIN CERTIFICATE----- +MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQG +EwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQLExNUcnVzdGlzIEZQUyBSb290 +IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTExMzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNV +BAoTD1RydXN0aXMgTGltaXRlZDEcMBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQ +RUN+AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihHiTHcDnlk +H5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjjvSkCqPoc4Vu5g6hBSLwa +cY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zt +o3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlBOrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEA +AaNTMFEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAd +BgNVHQ4EFgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01GX2c +GE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmWzaD+vkAMXBJV+JOC +yinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP41BIy+Q7DsdwyhEQsb8tGD+pmQQ9P +8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZEf1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHV +l/9D7S3B2l0pKoU/rGXuhg8FjZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYl +iB6XzCGcKQENZetX2fNXlrtIzYE= +-----END CERTIFICATE----- + +StartCom Certification Authority +================================ +-----BEGIN CERTIFICATE----- +MIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN +U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu +ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0 +NjM3WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk +LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg +U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y +o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/ +Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d +eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt +2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z +6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ +osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/ +untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc +UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT +37uMdBNSSwIDAQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFulF2mHMMo0aEPQ +Qa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCCATgwLgYIKwYBBQUHAgEWImh0 +dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cu +c3RhcnRzc2wuY29tL2ludGVybWVkaWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENv +bW1lcmNpYWwgKFN0YXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0 +aGUgc2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93d3cuc3RhcnRzc2wuY29t +L3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBG +cmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5 +fPGFf59Jb2vKXfuM/gTFwWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWm +N3PH/UvSTa0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst0OcN +Org+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNcpRJvkrKTlMeIFw6T +tn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKlCcWw0bdT82AUuoVpaiF8H3VhFyAX +e2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVFP0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA +2MFrLH9ZXF2RsXAiV+uKa0hK1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBs +HvUwyKMQ5bLmKhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE +JnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ8dCAWZvLMdib +D4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnmfyWl8kgAwKQB2j8= +-----END CERTIFICATE----- + +StartCom Certification Authority G2 +=================================== +-----BEGIN CERTIFICATE----- +MIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMN +U3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +RzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1OTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UE +ChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgRzIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8O +o1XJJZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsDvfOpL9HG +4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnooD/Uefyf3lLE3PbfHkffi +Aez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/Q0kGi4xDuFby2X8hQxfqp0iVAXV16iul +Q5XqFYSdCI0mblWbq9zSOdIxHWDirMxWRST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbs +O+wmETRIjfaAKxojAuuKHDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8H +vKTlXcxNnw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM0D4L +nMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/iUUjXuG+v+E5+M5iS +FGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9Ha90OrInwMEePnWjFqmveiJdnxMa +z6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHgTuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJ +KoZIhvcNAQELBQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K +2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfXUfEpY9Z1zRbk +J4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl6/2o1PXWT6RbdejF0mCy2wl+ +JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG +/+gyRr61M3Z3qAFdlsHB1b6uJcDJHgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTc +nIhT76IxW1hPkWLIwpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/Xld +blhYXzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5lIxKVCCIc +l85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoohdVddLHRDiBYmxOlsGOm +7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulrso8uBtjRkcfGEvRM/TAXw8HaOFvjqerm +obp573PYtlNXLfbQ4ddI +-----END CERTIFICATE----- + +Buypass Class 2 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X +DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1 +g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn +9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b +/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU +CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff +awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI +zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn +Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX +Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs +M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI +osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S +aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd +DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD +LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0 +oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC +wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS +CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN +rJgWVqA= +-----END CERTIFICATE----- + +Buypass Class 3 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X +DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH +sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR +5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh +7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ +ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH +2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV +/afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ +RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA +Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq +j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G +uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG +Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8 +ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2 +KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz +6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug +UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe +eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi +Cp/HuZc= +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 3 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgx +MDAxMTAyOTU2WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN8ELg63iIVl6bmlQdTQyK +9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/RLyTPWGrTs0NvvAgJ1gORH8EGoel15YU +NpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZF +iP0Zf3WHHx+xGwpzJFu5ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W +0eDrXltMEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1A/d2O2GCahKqGFPr +AyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOyWL6ukK2YJ5f+AbGwUgC4TeQbIXQb +fsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzT +ucpH9sry9uetuUg/vBa3wW306gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7h +P0HHRwA11fXT91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4pTpPDpFQUWw== +-----END CERTIFICATE----- + +EE Certification Centre Root CA +=============================== +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQG +EwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEoMCYGA1UEAwwfRUUgQ2Vy +dGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIw +MTAxMDMwMTAxMDMwWhgPMjAzMDEyMTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlB +UyBTZXJ0aWZpdHNlZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRy +ZSBSb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUyeuuOF0+W2Ap7kaJjbMeM +TC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvObntl8jixwKIy72KyaOBhU8E2lf/slLo2 +rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIwWFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw +93X2PaRka9ZP585ArQ/dMtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtN +P2MbRMNE1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/zQas8fElyalL1BSZ +MEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEF +BQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEFBQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+Rj +xY6hUFaTlrg4wCQiZrxTFGGVv9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqM +lIpPnTX/dqQGE5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u +uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIWiAYLtqZLICjU +3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/vGVCJYMzpJJUPwssd8m92kMfM +dcGWxZ0= +-----END CERTIFICATE----- + +TURKTRUST Certificate Services Provider Root 2007 +================================================= +-----BEGIN CERTIFICATE----- +MIIEPTCCAyWgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvzE/MD0GA1UEAww2VMOcUktUUlVTVCBF +bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJUUjEP +MA0GA1UEBwwGQW5rYXJhMV4wXAYDVQQKDFVUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUg +QmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLiAoYykgQXJhbMSxayAyMDA3MB4X +DTA3MTIyNTE4MzcxOVoXDTE3MTIyMjE4MzcxOVowgb8xPzA9BgNVBAMMNlTDnFJLVFJVU1QgRWxl +a3Ryb25payBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTELMAkGA1UEBhMCVFIxDzAN +BgNVBAcMBkFua2FyYTFeMFwGA1UECgxVVMOcUktUUlVTVCBCaWxnaSDEsGxldGnFn2ltIHZlIEJp +bGnFn2ltIEfDvHZlbmxpxJ9pIEhpem1ldGxlcmkgQS7Fni4gKGMpIEFyYWzEsWsgMjAwNzCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKu3PgqMyKVYFeaK7yc9SrToJdPNM8Ig3BnuiD9N +YvDdE3ePYakqtdTyuTFYKTsvP2qcb3N2Je40IIDu6rfwxArNK4aUyeNgsURSsloptJGXg9i3phQv +KUmi8wUG+7RP2qFsmmaf8EMJyupyj+sA1zU511YXRxcw9L6/P8JorzZAwan0qafoEGsIiveGHtya +KhUG9qPw9ODHFNRRf8+0222vR5YXm3dx2KdxnSQM9pQ/hTEST7ruToK4uT6PIzdezKKqdfcYbwnT +rqdUKDT74eA7YH2gvnmJhsifLfkKS8RQouf9eRbHegsYz85M733WB2+Y8a+xwXrXgTW4qhe04MsC +AwEAAaNCMEAwHQYDVR0OBBYEFCnFkKslrxHkYb+j/4hhkeYO/pyBMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAQDdr4Ouwo0RSVgrESLFF6QSU2TJ/s +Px+EnWVUXKgWAkD6bho3hO9ynYYKVZ1WKKxmLNA6VpM0ByWtCLCPyA8JWcqdmBzlVPi5RX9ql2+I +aE1KBiY3iAIOtsbWcpnOa3faYjGkVh+uX4132l32iPwa2Z61gfAyuOOI0JzzaqC5mxRZNTZPz/OO +Xl0XrRWV2N2y1RVuAE6zS89mlOTgzbUF2mNXi+WzqtvALhyQRNsaXRik7r4EW5nVcV9VZWRi1aKb +BFmGyGJ353yCRWo9F7/snXUMrqNvWtMvmDb08PUZqxFdyKbjKlhqQgnDvZImZjINXQhVdP+MmNAK +poRq0Tl9 +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTAe +Fw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NThaME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxE +LVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOAD +ER03UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42tSHKXzlA +BF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9RySPocq60vFYJfxLLHLGv +KZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsMlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7z +p+hnUquVH+BGPtikw8paxTGA6Eian5Rp/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUC +AwEAAaOCARowggEWMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ +4PGEMA4GA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVjdG9y +eS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUyMENBJTIwMiUyMDIw +MDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRlcmV2b2NhdGlvbmxpc3QwQ6BBoD+G +PWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAw +OS5jcmwwDQYJKoZIhvcNAQELBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm +2H6NMLVwMeniacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4KzCUqNQT4YJEV +dT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8PIWmawomDeCTmGCufsYkl4ph +X5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3YJohw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 EV 2009 +================================= +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUwNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfS +egpnljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM03TP1YtHh +zRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6ZqQTMFexgaDbtCHu39b+T +7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lRp75mpoo6Kr3HGrHhFPC+Oh25z1uxav60 +sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure35 +11H3a6UCAwEAAaOCASQwggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyv +cop9NteaHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFwOi8v +ZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xhc3MlMjAzJTIwQ0El +MjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRp +b25saXN0MEagRKBChkBodHRwOi8vd3d3LmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xh +c3NfM19jYV8yX2V2XzIwMDkuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+ +PPoeUSbrh/Yp3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNFCSuGdXzfX2lX +ANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7naxpeG0ILD5EJt/rDiZE4OJudA +NCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqXKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVv +w9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +PSCProcert +========== +-----BEGIN CERTIFICATE----- +MIIJhjCCB26gAwIBAgIBCzANBgkqhkiG9w0BAQsFADCCAR4xPjA8BgNVBAMTNUF1dG9yaWRhZCBk +ZSBDZXJ0aWZpY2FjaW9uIFJhaXogZGVsIEVzdGFkbyBWZW5lem9sYW5vMQswCQYDVQQGEwJWRTEQ +MA4GA1UEBxMHQ2FyYWNhczEZMBcGA1UECBMQRGlzdHJpdG8gQ2FwaXRhbDE2MDQGA1UEChMtU2lz +dGVtYSBOYWNpb25hbCBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9uaWNhMUMwQQYDVQQLEzpTdXBl +cmludGVuZGVuY2lhIGRlIFNlcnZpY2lvcyBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9uaWNhMSUw +IwYJKoZIhvcNAQkBFhZhY3JhaXpAc3VzY2VydGUuZ29iLnZlMB4XDTEwMTIyODE2NTEwMFoXDTIw +MTIyNTIzNTk1OVowgdExJjAkBgkqhkiG9w0BCQEWF2NvbnRhY3RvQHByb2NlcnQubmV0LnZlMQ8w +DQYDVQQHEwZDaGFjYW8xEDAOBgNVBAgTB01pcmFuZGExKjAoBgNVBAsTIVByb3ZlZWRvciBkZSBD +ZXJ0aWZpY2Fkb3MgUFJPQ0VSVDE2MDQGA1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0aWZp +Y2FjaW9uIEVsZWN0cm9uaWNhMQswCQYDVQQGEwJWRTETMBEGA1UEAxMKUFNDUHJvY2VydDCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANW39KOUM6FGqVVhSQ2oh3NekS1wwQYalNo97BVC +wfWMrmoX8Yqt/ICV6oNEolt6Vc5Pp6XVurgfoCfAUFM+jbnADrgV3NZs+J74BCXfgI8Qhd19L3uA +3VcAZCP4bsm+lU/hdezgfl6VzbHvvnpC2Mks0+saGiKLt38GieU89RLAu9MLmV+QfI4tL3czkkoh +RqipCKzx9hEC2ZUWno0vluYC3XXCFCpa1sl9JcLB/KpnheLsvtF8PPqv1W7/U0HU9TI4seJfxPmO +EO8GqQKJ/+MMbpfg353bIdD0PghpbNjU5Db4g7ayNo+c7zo3Fn2/omnXO1ty0K+qP1xmk6wKImG2 +0qCZyFSTXai20b1dCl53lKItwIKOvMoDKjSuc/HUtQy9vmebVOvh+qBa7Dh+PsHMosdEMXXqP+UH +0quhJZb25uSgXTcYOWEAM11G1ADEtMo88aKjPvM6/2kwLkDd9p+cJsmWN63nOaK/6mnbVSKVUyqU +td+tFjiBdWbjxywbk5yqjKPK2Ww8F22c3HxT4CAnQzb5EuE8XL1mv6JpIzi4mWCZDlZTOpx+FIyw +Bm/xhnaQr/2v/pDGj59/i5IjnOcVdo/Vi5QTcmn7K2FjiO/mpF7moxdqWEfLcU8UC17IAggmosvp +r2uKGcfLFFb14dq12fy/czja+eevbqQ34gcnAgMBAAGjggMXMIIDEzASBgNVHRMBAf8ECDAGAQH/ +AgEBMDcGA1UdEgQwMC6CD3N1c2NlcnRlLmdvYi52ZaAbBgVghl4CAqASDBBSSUYtRy0yMDAwNDAz +Ni0wMB0GA1UdDgQWBBRBDxk4qpl/Qguk1yeYVKIXTC1RVDCCAVAGA1UdIwSCAUcwggFDgBStuyId +xuDSAaj9dlBSk+2YwU2u06GCASakggEiMIIBHjE+MDwGA1UEAxM1QXV0b3JpZGFkIGRlIENlcnRp +ZmljYWNpb24gUmFpeiBkZWwgRXN0YWRvIFZlbmV6b2xhbm8xCzAJBgNVBAYTAlZFMRAwDgYDVQQH +EwdDYXJhY2FzMRkwFwYDVQQIExBEaXN0cml0byBDYXBpdGFsMTYwNAYDVQQKEy1TaXN0ZW1hIE5h +Y2lvbmFsIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25pY2ExQzBBBgNVBAsTOlN1cGVyaW50ZW5k +ZW5jaWEgZGUgU2VydmljaW9zIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25pY2ExJTAjBgkqhkiG +9w0BCQEWFmFjcmFpekBzdXNjZXJ0ZS5nb2IudmWCAQowDgYDVR0PAQH/BAQDAgEGME0GA1UdEQRG +MESCDnByb2NlcnQubmV0LnZloBUGBWCGXgIBoAwMClBTQy0wMDAwMDKgGwYFYIZeAgKgEgwQUklG +LUotMzE2MzUzNzMtNzB2BgNVHR8EbzBtMEagRKBChkBodHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52 +ZS9sY3IvQ0VSVElGSUNBRE8tUkFJWi1TSEEzODRDUkxERVIuY3JsMCOgIaAfhh1sZGFwOi8vYWNy +YWl6LnN1c2NlcnRlLmdvYi52ZTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9v +Y3NwLnN1c2NlcnRlLmdvYi52ZTBBBgNVHSAEOjA4MDYGBmCGXgMBAjAsMCoGCCsGAQUFBwIBFh5o +dHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9kcGMwDQYJKoZIhvcNAQELBQADggIBACtZ6yKZu4Sq +T96QxtGGcSOeSwORR3C7wJJg7ODU523G0+1ng3dS1fLld6c2suNUvtm7CpsR72H0xpkzmfWvADmN +g7+mvTV+LFwxNG9s2/NkAZiqlCxB3RWGymspThbASfzXg0gTB1GEMVKIu4YXx2sviiCtxQuPcD4q +uxtxj7mkoP3YldmvWb8lK5jpY5MvYB7Eqvh39YtsL+1+LrVPQA3uvFd359m21D+VJzog1eWuq2w1 +n8GhHVnchIHuTQfiSLaeS5UtQbHh6N5+LwUeaO6/u5BlOsju6rEYNxxik6SgMexxbJHmpHmJWhSn +FFAFTKQAVzAswbVhltw+HoSvOULP5dAssSS830DD7X9jSr3hTxJkhpXzsOfIt+FTvZLm8wyWuevo +5pLtp4EJFAv8lXrPj9Y0TzYS3F7RNHXGRoAvlQSMx4bEqCaJqD8Zm4G7UaRKhqsLEQ+xrmNTbSjq +3TNWOByyrYDT13K9mmyZY+gAu0F2BbdbmRiKw7gSXFbPVgx96OLP7bx0R/vu0xdOIk9W/1DzLuY5 +poLWccret9W6aAjtmcz9opLLabid+Qqkpj5PkygqYWwHJgD/ll9ohri4zspV4KuxPX+Y1zMOWj3Y +eMLEYC/HYvBhkdI4sPaeVdtAgAUSM84dkpvRabP/v/GSCmE1P93+hvS84Bpxs2Km +-----END CERTIFICATE----- + +China Internet Network Information Center EV Certificates Root +============================================================== +-----BEGIN CERTIFICATE----- +MIID9zCCAt+gAwIBAgIESJ8AATANBgkqhkiG9w0BAQUFADCBijELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUNoaW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24gQ2VudGVyMUcwRQYDVQQDDD5D +aGluYSBJbnRlcm5ldCBOZXR3b3JrIEluZm9ybWF0aW9uIENlbnRlciBFViBDZXJ0aWZpY2F0ZXMg +Um9vdDAeFw0xMDA4MzEwNzExMjVaFw0zMDA4MzEwNzExMjVaMIGKMQswCQYDVQQGEwJDTjEyMDAG +A1UECgwpQ2hpbmEgSW50ZXJuZXQgTmV0d29yayBJbmZvcm1hdGlvbiBDZW50ZXIxRzBFBgNVBAMM +PkNoaW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24gQ2VudGVyIEVWIENlcnRpZmljYXRl +cyBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm35z7r07eKpkQ0H1UN+U8i6y +jUqORlTSIRLIOTJCBumD1Z9S7eVnAztUwYyZmczpwA//DdmEEbK40ctb3B75aDFk4Zv6dOtouSCV +98YPjUesWgbdYavi7NifFy2cyjw1l1VxzUOFsUcW9SxTgHbP0wBkvUCZ3czY28Sf1hNfQYOL+Q2H +klY0bBoQCxfVWhyXWIQ8hBouXJE0bhlffxdpxWXvayHG1VA6v2G5BY3vbzQ6sm8UY78WO5upKv23 +KzhmBsUs4qpnHkWnjQRmQvaPK++IIGmPMowUc9orhpFjIpryp9vOiYurXccUwVswah+xt54ugQEC +7c+WXmPbqOY4twIDAQABo2MwYTAfBgNVHSMEGDAWgBR8cks5x8DbYqVPm6oYNJKiyoOCWTAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUfHJLOcfA22KlT5uqGDSSosqD +glkwDQYJKoZIhvcNAQEFBQADggEBACrDx0M3j92tpLIM7twUbY8opJhJywyA6vPtI2Z1fcXTIWd5 +0XPFtQO3WKwMVC/GVhMPMdoG52U7HW8228gd+f2ABsqjPWYWqJ1MFn3AlUa1UeTiH9fqBk1jjZaM +7+czV0I664zBechNdn3e9rG3geCg+aF4RhcaVpjwTj2rHO3sOdwHSPdj/gauwqRcalsyiMXHM4Ws +ZkJHwlgkmeHlPuV1LI5D1l08eB6olYIpUNHRFrrvwb562bTYzB5MRuF3sTGrvSrIzo9uoV1/A3U0 +5K2JRVRevq4opbs/eHnrc7MKDf2+yfdWrPa37S+bISnHOLaVxATywy39FCqQmbkHzJ8= +-----END CERTIFICATE----- + +Swisscom Root CA 2 +================== +-----BEGIN CERTIFICATE----- +MIIF2TCCA8GgAwIBAgIQHp4o6Ejy5e/DfEoeWhhntjANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQG +EwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2VydGlmaWNhdGUgU2Vy +dmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3QgQ0EgMjAeFw0xMTA2MjQwODM4MTRaFw0zMTA2 +MjUwNzM4MTRaMGQxCzAJBgNVBAYTAmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGln +aXRhbCBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAyMIIC +IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlUJOhJ1R5tMJ6HJaI2nbeHCOFvErjw0DzpPM +LgAIe6szjPTpQOYXTKueuEcUMncy3SgM3hhLX3af+Dk7/E6J2HzFZ++r0rk0X2s682Q2zsKwzxNo +ysjL67XiPS4h3+os1OD5cJZM/2pYmLcX5BtS5X4HAB1f2uY+lQS3aYg5oUFgJWFLlTloYhyxCwWJ +wDaCFCE/rtuh/bxvHGCGtlOUSbkrRsVPACu/obvLP+DHVxxX6NZp+MEkUp2IVd3Chy50I9AU/SpH +Wrumnf2U5NGKpV+GY3aFy6//SSj8gO1MedK75MDvAe5QQQg1I3ArqRa0jG6F6bYRzzHdUyYb3y1a +SgJA/MTAtukxGggo5WDDH8SQjhBiYEQN7Aq+VRhxLKX0srwVYv8c474d2h5Xszx+zYIdkeNL6yxS +NLCK/RJOlrDrcH+eOfdmQrGrrFLadkBXeyq96G4DsguAhYidDMfCd7Camlf0uPoTXGiTOmekl9Ab +mbeGMktg2M7v0Ax/lZ9vh0+Hio5fCHyqW/xavqGRn1V9TrALacywlKinh/LTSlDcX3KwFnUey7QY +Ypqwpzmqm59m2I2mbJYV4+by+PGDYmy7Velhk6M99bFXi08jsJvllGov34zflVEpYKELKeRcVVi3 +qPyZ7iVNTA6z00yPhOgpD/0QVAKFyPnlw4vP5w8CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYw +HQYDVR0hBBYwFDASBgdghXQBUwIBBgdghXQBUwIBMBIGA1UdEwEB/wQIMAYBAf8CAQcwHQYDVR0O +BBYEFE0mICKJS9PVpAqhb97iEoHF8TwuMB8GA1UdIwQYMBaAFE0mICKJS9PVpAqhb97iEoHF8Twu +MA0GCSqGSIb3DQEBCwUAA4ICAQAyCrKkG8t9voJXiblqf/P0wS4RfbgZPnm3qKhyN2abGu2sEzsO +v2LwnN+ee6FTSA5BesogpxcbtnjsQJHzQq0Qw1zv/2BZf82Fo4s9SBwlAjxnffUy6S8w5X2lejjQ +82YqZh6NM4OKb3xuqFp1mrjX2lhIREeoTPpMSQpKwhI3qEAMw8jh0FcNlzKVxzqfl9NX+Ave5XLz +o9v/tdhZsnPdTSpxsrpJ9csc1fV5yJmz/MFMdOO0vSk3FQQoHt5FRnDsr7p4DooqzgB53MBfGWcs +a0vvaGgLQ+OswWIJ76bdZWGgr4RVSJFSHMYlkSrQwSIjYVmvRRGFHQEkNI/Ps/8XciATwoCqISxx +OQ7Qj1zB09GOInJGTB2Wrk9xseEFKZZZ9LuedT3PDTcNYtsmjGOpI99nBjx8Oto0QuFmtEYE3saW +mA9LSHokMnWRn6z3aOkquVVlzl1h0ydw2Df+n7mvoC5Wt6NlUe07qxS/TFED6F+KBZvuim6c779o ++sjaC+NCydAXFJy3SuCvkychVSa1ZC+N8f+mQAWFBVzKBxlcCxMoTFh/wqXvRdpg065lYZ1Tg3TC +rvJcwhbtkj6EPnNgiLx29CzP0H1907he0ZESEOnN3col49XtmS++dYFLJPlFRpTJKSFTnCZFqhMX +5OfNeOI5wSsSnqaeG8XmDtkx2Q== +-----END CERTIFICATE----- + +Swisscom Root EV CA 2 +===================== +-----BEGIN CERTIFICATE----- +MIIF4DCCA8igAwIBAgIRAPL6ZOJ0Y9ON/RAdBB92ylgwDQYJKoZIhvcNAQELBQAwZzELMAkGA1UE +BhMCY2gxETAPBgNVBAoTCFN3aXNzY29tMSUwIwYDVQQLExxEaWdpdGFsIENlcnRpZmljYXRlIFNl +cnZpY2VzMR4wHAYDVQQDExVTd2lzc2NvbSBSb290IEVWIENBIDIwHhcNMTEwNjI0MDk0NTA4WhcN +MzEwNjI1MDg0NTA4WjBnMQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsT +HERpZ2l0YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxHjAcBgNVBAMTFVN3aXNzY29tIFJvb3QgRVYg +Q0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMT3HS9X6lds93BdY7BxUglgRCgz +o3pOCvrY6myLURYaVa5UJsTMRQdBTxB5f3HSek4/OE6zAMaVylvNwSqD1ycfMQ4jFrclyxy0uYAy +Xhqdk/HoPGAsp15XGVhRXrwsVgu42O+LgrQ8uMIkqBPHoCE2G3pXKSinLr9xJZDzRINpUKTk4Rti +GZQJo/PDvO/0vezbE53PnUgJUmfANykRHvvSEaeFGHR55E+FFOtSN+KxRdjMDUN/rhPSays/p8Li +qG12W0OfvrSdsyaGOx9/5fLoZigWJdBLlzin5M8J0TbDC77aO0RYjb7xnglrPvMyxyuHxuxenPaH +Za0zKcQvidm5y8kDnftslFGXEBuGCxobP/YCfnvUxVFkKJ3106yDgYjTdLRZncHrYTNaRdHLOdAG +alNgHa/2+2m8atwBz735j9m9W8E6X47aD0upm50qKGsaCnw8qyIL5XctcfaCNYGu+HuB5ur+rPQa +m3Rc6I8k9l2dRsQs0h4rIWqDJ2dVSqTjyDKXZpBy2uPUZC5f46Fq9mDU5zXNysRojddxyNMkM3Ox +bPlq4SjbX8Y96L5V5jcb7STZDxmPX2MYWFCBUWVv8p9+agTnNCRxunZLWB4ZvRVgRaoMEkABnRDi +xzgHcgplwLa7JSnaFp6LNYth7eVxV4O1PHGf40+/fh6Bn0GXAgMBAAGjgYYwgYMwDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdIQQWMBQwEgYHYIV0AVMCAgYHYIV0AVMCAjASBgNVHRMBAf8ECDAGAQH/AgED +MB0GA1UdDgQWBBRF2aWBbj2ITY1x0kbBbkUe88SAnTAfBgNVHSMEGDAWgBRF2aWBbj2ITY1x0kbB +bkUe88SAnTANBgkqhkiG9w0BAQsFAAOCAgEAlDpzBp9SSzBc1P6xXCX5145v9Ydkn+0UjrgEjihL +j6p7jjm02Vj2e6E1CqGdivdj5eu9OYLU43otb98TPLr+flaYC/NUn81ETm484T4VvwYmneTwkLbU +wp4wLh/vx3rEUMfqe9pQy3omywC0Wqu1kx+AiYQElY2NfwmTv9SoqORjbdlk5LgpWgi/UOGED1V7 +XwgiG/W9mR4U9s70WBCCswo9GcG/W6uqmdjyMb3lOGbcWAXH7WMaLgqXfIeTK7KK4/HsGOV1timH +59yLGn602MnTihdsfSlEvoqq9X46Lmgxk7lq2prg2+kupYTNHAq4Sgj5nPFhJpiTt3tm7JFe3VE/ +23MPrQRYCd0EApUKPtN236YQHoA96M2kZNEzx5LH4k5E4wnJTsJdhw4Snr8PyQUQ3nqjsTzyP6Wq +J3mtMX0f/fwZacXduT98zca0wjAefm6S139hdlqP65VNvBFuIXxZN5nQBrz5Bm0yFqXZaajh3DyA +HmBR3NdUIR7KYndP+tiPsys6DXhyyWhBWkdKwqPrGtcKqzwyVcgKEZzfdNbwQBUdyLmPtTbFr/gi +uMod89a2GQ+fYWVq6nTIfI/DT11lgh/ZDYnadXL77/FHZxOzyNEZiCcmmpl5fx7kLD977vHeTYuW +l8PVP3wbI+2ksx0WckNLIOFZfsLorSa/ovc= +-----END CERTIFICATE----- + +CA Disig Root R1 +================ +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAMMDmu5QkG4oMA0GCSqGSIb3DQEBBQUAMFIxCzAJBgNVBAYTAlNLMRMw +EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMuMRkwFwYDVQQDExBDQSBEaXNp +ZyBSb290IFIxMB4XDTEyMDcxOTA5MDY1NloXDTQyMDcxOTA5MDY1NlowUjELMAkGA1UEBhMCU0sx +EzARBgNVBAcTCkJyYXRpc2xhdmExEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERp +c2lnIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqw3j33Jijp1pedxiy +3QRkD2P9m5YJgNXoqqXinCaUOuiZc4yd39ffg/N4T0Dhf9Kn0uXKE5Pn7cZ3Xza1lK/oOI7bm+V8 +u8yN63Vz4STN5qctGS7Y1oprFOsIYgrY3LMATcMjfF9DCCMyEtztDK3AfQ+lekLZWnDZv6fXARz2 +m6uOt0qGeKAeVjGu74IKgEH3G8muqzIm1Cxr7X1r5OJeIgpFy4QxTaz+29FHuvlglzmxZcfe+5nk +CiKxLU3lSCZpq+Kq8/v8kiky6bM+TR8noc2OuRf7JT7JbvN32g0S9l3HuzYQ1VTW8+DiR0jm3hTa +YVKvJrT1cU/J19IG32PK/yHoWQbgCNWEFVP3Q+V8xaCJmGtzxmjOZd69fwX3se72V6FglcXM6pM6 +vpmumwKjrckWtc7dXpl4fho5frLABaTAgqWjR56M6ly2vGfb5ipN0gTco65F97yLnByn1tUD3AjL +LhbKXEAz6GfDLuemROoRRRw1ZS0eRWEkG4IupZ0zXWX4Qfkuy5Q/H6MMMSRE7cderVC6xkGbrPAX +ZcD4XW9boAo0PO7X6oifmPmvTiT6l7Jkdtqr9O3jw2Dv1fkCyC2fg69naQanMVXVz0tv/wQFx1is +XxYb5dKj6zHbHzMVTdDypVP1y+E9Tmgt2BLdqvLmTZtJ5cUoobqwWsagtQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUiQq0OJMa5qvum5EY+fU8PjXQ +04IwDQYJKoZIhvcNAQEFBQADggIBADKL9p1Kyb4U5YysOMo6CdQbzoaz3evUuii+Eq5FLAR0rBNR +xVgYZk2C2tXck8An4b58n1KeElb21Zyp9HWc+jcSjxyT7Ff+Bw+r1RL3D65hXlaASfX8MPWbTx9B +LxyE04nH4toCdu0Jz2zBuByDHBb6lM19oMgY0sidbvW9adRtPTXoHqJPYNcHKfyyo6SdbhWSVhlM +CrDpfNIZTUJG7L399ldb3Zh+pE3McgODWF3vkzpBemOqfDqo9ayk0d2iLbYq/J8BjuIQscTK5Gfb +VSUZP/3oNn6z4eGBrxEWi1CXYBmCAMBrTXO40RMHPuq2MU/wQppt4hF05ZSsjYSVPCGvxdpHyN85 +YmLLW1AL14FABZyb7bq2ix4Eb5YgOe2kfSnbSM6C3NQCjR0EMVrHS/BsYVLXtFHCgWzN4funodKS +ds+xDzdYpPJScWc/DIh4gInByLUfkmO+p3qKViwaqKactV2zY9ATIKHrkWzQjX2v3wvkF7mGnjix +lAxYjOBVqjtjbZqJYLhkKpLGN/R+Q0O3c+gB53+XD9fyexn9GtePyfqFa3qdnom2piiZk4hA9z7N +UaPK6u95RyG1/jLix8NRb76AdPCkwzryT+lf3xkK8jsTQ6wxpLPn6/wY1gGp8yqPNg7rtLG8t0zJ +a7+h89n07eLw4+1knj0vllJPgFOL +-----END CERTIFICATE----- + +CA Disig Root R2 +================ +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNVBAYTAlNLMRMw +EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMuMRkwFwYDVQQDExBDQSBEaXNp +ZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQyMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sx +EzARBgNVBAcTCkJyYXRpc2xhdmExEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERp +c2lnIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbC +w3OeNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNHPWSb6Wia +xswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3Ix2ymrdMxp7zo5eFm1tL7 +A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbeQTg06ov80egEFGEtQX6sx3dOy1FU+16S +GBsEWmjGycT6txOgmLcRK7fWV8x8nhfRyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqV +g8NTEQxzHQuyRpDRQjrOQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa +5Beny912H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJQfYE +koopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUDi/ZnWejBBhG93c+A +Ak9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORsnLMOPReisjQS1n6yqEm70XooQL6i +Fh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5u +Qu0wDQYJKoZIhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqfGopTpti72TVV +sRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkblvdhuDvEK7Z4bLQjb/D907Je +dR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W8 +1k/BfDxujRNt+3vrMNDcTa/F1balTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjx +mHHEt38OFdAlab0inSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01 +utI3gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18DrG5gPcFw0 +sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3OszMOl6W8KjptlwlCFtaOg +UxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8xL4ysEr3vQCj8KWefshNPZiTEUxnpHikV +7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +ACCVRAIZ1 +========= +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UEAwwJQUNDVlJB +SVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQswCQYDVQQGEwJFUzAeFw0xMTA1 +MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwH +UEtJQUNDVjENMAsGA1UECgwEQUNDVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCbqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gM +jmoYHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWoG2ioPej0 +RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpAlHPrzg5XPAOBOp0KoVdD +aaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhrIA8wKFSVf+DuzgpmndFALW4ir50awQUZ +0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDG +WuzndN9wrqODJerWx5eHk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs7 +8yM2x/474KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMOm3WR +5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpacXpkatcnYGMN285J +9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPluUsXQA+xtrn13k/c4LOsOxFwYIRK +Q26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYIKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRw +Oi8vd3d3LmFjY3YuZXMvZmlsZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEu +Y3J0MB8GCCsGAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeTVfZW6oHlNsyM +Hj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIGCCsGAQUFBwICMIIBFB6CARAA +QQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUAcgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBh +AO0AegAgAGQAZQAgAGwAYQAgAEEAQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUA +YwBuAG8AbABvAGcA7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBj +AHQAcgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAAQwBQAFMA +IABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUAczAwBggrBgEFBQcCARYk +aHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2MuaHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0 +dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRtaW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2 +MV9kZXIuY3JsMA4GA1UdDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZI +hvcNAQEFBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdpD70E +R9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gUJyCpZET/LtZ1qmxN +YEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+mAM/EKXMRNt6GGT6d7hmKG9Ww7Y49 +nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepDvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJ +TS+xJlsndQAJxGJ3KQhfnlmstn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3 +sCPdK6jT2iWH7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szAh1xA2syVP1Xg +Nce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xFd3+YJ5oyXSrjhO7FmGYvliAd +3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2HpPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3p +EfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +TWCA Global Root CA +=================== +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcxEjAQBgNVBAoT +CVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMTVFdDQSBHbG9iYWwgUm9vdCBD +QTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQK +EwlUQUlXQU4tQ0ExEDAOBgNVBAsTB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2C +nJfF10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz0ALfUPZV +r2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfChMBwqoJimFb3u/Rk28OKR +Q4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbHzIh1HrtsBv+baz4X7GGqcXzGHaL3SekV +tTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1W +KKD+u4ZqyPpcC1jcxkt2yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99 +sy2sbZCilaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYPoA/p +yJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQABDzfuBSO6N+pjWxn +kjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcEqYSjMq+u7msXi7Kx/mzhkIyIqJdI +zshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6g +cFGn90xHNcgL1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WFH6vPNOw/KP4M +8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNoRI2T9GRwoD2dKAXDOXC4Ynsg +/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlg +lPx4mI88k1HtQJAH32RjJMtOcQWh15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryP +A9gK8kxkRr05YuWW6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3m +i4TWnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5jwa19hAM8 +EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWzaGHQRiapIVJpLesux+t3 +zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0= +-----END CERTIFICATE----- + +TeliaSonera Root CA v1 +====================== +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAwNzEUMBIGA1UE +CgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJvb3QgQ0EgdjEwHhcNMDcxMDE4 +MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYDVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwW +VGVsaWFTb25lcmEgUm9vdCBDQSB2MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+ +6yfwIaPzaSZVfp3FVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA +3GV17CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+XZ75Ljo1k +B1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+/jXh7VB7qTCNGdMJjmhn +Xb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxH +oLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkmdtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3 +F0fUTPHSiXk+TT2YqGHeOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJ +oWjiUIMusDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4pgd7 +gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fsslESl1MpWtTwEhDc +TwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQarMCpgKIv7NHfirZ1fpoeDVNAgMB +AAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qW +DNXr+nuqF+gTEjANBgkqhkiG9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNm +zqjMDfz1mgbldxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1TjTQpgcmLNkQfW +pb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBedY2gea+zDTYa4EzAvXUYNR0PV +G6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpc +c41teyWRyu5FrgZLAMzTsVlQ2jqIOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOT +JsjrDNYmiLbAJM+7vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2 +qReWt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcnHL/EVlP6 +Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVxSK236thZiNSQvxaz2ems +WWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +E-Tugra Certification Authority +=============================== +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNVBAYTAlRSMQ8w +DQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamls +ZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN +ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMw +NTEyMDk0OFoXDTIzMDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmEx +QDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxl +cmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQD +DB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEA4vU/kwVRHoViVF56C/UYB4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vd +hQd2h8y/L5VMzH2nPbxHD5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5K +CKpbknSFQ9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEoq1+g +ElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3Dk14opz8n8Y4e0ypQ +BaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcHfC425lAcP9tDJMW/hkd5s3kc91r0 +E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsutdEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gz +rt48Ue7LE3wBf4QOXVGUnhMMti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAq +jqFGOjGY5RH8zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn +rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUXU8u3Zg5mTPj5 +dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6Jyr+zE7S6E5UMA8GA1UdEwEB +/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEG +MA0GCSqGSIb3DQEBCwUAA4ICAQAFNzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAK +kEh47U6YA5n+KGCRHTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jO +XKqYGwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c77NCR807 +VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3+GbHeJAAFS6LrVE1Uweo +a2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WKvJUawSg5TB9D0pH0clmKuVb8P7Sd2nCc +dlqMQ1DujjByTd//SffGqWfZbawCEeI6FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEV +KV0jq9BgoRJP3vQXzTLlyb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gT +Dx4JnW2PAJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpDy4Q0 +8ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8dNL/+I5c30jn6PQ0G +C7TbO6Orb1wdtn7os4I07QZcJA== +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 2 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgx +MDAxMTA0MDE0WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUdAqSzm1nzHoqvNK38DcLZ +SBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiCFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/F +vudocP05l03Sx5iRUKrERLMjfTlH6VJi1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx970 +2cu+fjOlbpSD8DT6IavqjnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGV +WOHAD3bZwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/WSA2AHmgoCJrjNXy +YdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhyNsZt+U2e+iKo4YFWz827n+qrkRk4 +r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPACuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNf +vNoBYimipidx5joifsFvHZVwIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR +3p1m0IvVVGb6g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlPBSeOE6Fuwg== +-----END CERTIFICATE----- + +Atos TrustedRoot 2011 +===================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UEAwwVQXRvcyBU +cnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0xMTA3MDcxNDU4 +MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMMFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsG +A1UECgwEQXRvczELMAkGA1UEBhMCREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV +hTuXbyo7LjvPpvMpNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr +54rMVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+SZFhyBH+ +DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ4J7sVaE3IqKHBAUsR320 +HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0Lcp2AMBYHlT8oDv3FdU9T1nSatCQujgKR +z3bFmx5VdJx4IbHwLfELn8LVlhgf8FQieowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7R +l+lwrrw7GWzbITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZ +bNshMBgGA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +CwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8jvZfza1zv7v1Apt+h +k6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kPDpFrdRbhIfzYJsdHt6bPWHJxfrrh +TZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pcmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a9 +61qn8FYiqTxlVMYVqL2Gns2Dlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G +3mB/ufNPRJLvKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +QuoVadis Root CA 1 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakE +PBtVwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWerNrwU8lm +PNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF34168Xfuw6cwI2H44g4hWf6 +Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh4Pw5qlPafX7PGglTvF0FBM+hSo+LdoIN +ofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXpUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/l +g6AnhF4EwfWQvTA9xO+oabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV +7qJZjqlc3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/GKubX +9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSthfbZxbGL0eUQMk1f +iyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KOTk0k+17kBL5yG6YnLUlamXrXXAkg +t3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOtzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZI +hvcNAQELBQADggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2cDMT/uFPpiN3 +GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUNqXsCHKnQO18LwIE6PWThv6ct +Tr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP ++V04ikkwj+3x6xn0dxoxGE1nVGwvb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh +3jRJjehZrJ3ydlo28hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fa +wx/kNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNjZgKAvQU6 +O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhpq1467HxpvMc7hU6eFbm0 +FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFtnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOV +hMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +QuoVadis Root CA 2 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFh +ZiFfqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduY +NM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ymc5GQYaYDFCDy54ejiK2t +oIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+o +MiwMzAkd056OXbxMmO7FGmh77FOm6RQ1o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+l +V0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZo +L1NesNKqIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQ +sSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43ehvNURG3YBZwjgQQvD +6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxh +lRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALGcC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZI +hvcNAQELBQADggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RCroijQ1h5fq7K +pVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9 +x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgz +dWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6X +U/IyAgkwo1jwDQHVcsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+Nw +mNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNgKCLjsZWD +zYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKN +JeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4WSr2Rz0ZiC3oheGe7IUIarFsNMkd7Egr +O3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +QuoVadis Root CA 3 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286 +IxSR/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNuFoM7pmRL +Mon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXRU7Ox7sWTaYI+FrUoRqHe +6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+cra1AdHkrAj80//ogaX3T7mH1urPnMNA3 +I4ZyYUUpSFlob3emLoG+B01vr87ERRORFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3U +VDmrJqMz6nWB2i3ND0/kA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f7 +5li59wzweyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634RylsSqi +Md5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBpVzgeAVuNVejH38DM +dyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0QA4XN8f+MFrXBsj6IbGB/kE+V9/Yt +rQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZI +hvcNAQELBQADggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnIFUBhynLWcKzS +t/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5WvvoxXqA/4Ti2Tk08HS6IT7SdEQ +TXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFgu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9Du +DcpmvJRPpq3t/O5jrFc/ZSXPsoaP0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGib +Ih6BJpsQBJFxwAYf3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmD +hPbl8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+DhcI00iX +0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HNPlopNLk9hM6xZdRZkZFW +dSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ywaZWWDYWGWVjUTR939+J399roD1B0y2 +PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +DigiCert Assured ID Root G2 +=========================== +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgw +MTE1MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSAn61UQbVH +35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4HteccbiJVMWWXvdMX0h5i89vq +bFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9HpEgjAALAcKxHad3A2m67OeYfcgnDmCXRw +VWmvo2ifv922ebPynXApVfSr/5Vh88lAbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OP +YLfykqGxvYmJHzDNw6YuYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+Rn +lTGNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTO +w0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPIQW5pJ6d1Ee88hjZv +0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I0jJmwYrA8y8678Dj1JGG0VDjA9tz +d29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4GnilmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAW +hsI6yLETcDbYz+70CjTVW0z9B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0M +jomZmWzwPDCvON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +DigiCert Assured ID Root G3 +=========================== +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYD +VQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJfZn4f5dwb +RXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17QRSAPWXYQ1qAk8C3eNvJs +KTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgF +UaFNN6KDec6NHSrkhDAKBggqhkjOPQQDAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5Fy +YZ5eEJJZVrmDxxDnOOlYJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy +1vUhZscv6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +DigiCert Global Root G2 +======================= +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUx +MjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJ +kTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO +3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauV +BJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyM +UNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu +5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsr +F9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0U +WTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBH +QRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/ +iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +DigiCert Global Root G3 +======================= +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYD +VQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAw +MDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k +aWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FGfp4tn+6O +YwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPOZ9wj/wMco+I+o0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNp +Yim8S8YwCgYIKoZIzj0EAwMDaAAwZQIxAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y +3maTD/HMsQmP3Wyr+mt/oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34 +VOKa5Vt8sycX +-----END CERTIFICATE----- + +DigiCert Trusted Root G4 +======================== +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw +HwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEp +pz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o +k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7Fsa +vOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY +QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6 +MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtm +mnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7 +f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH +dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8 +oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY +ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdNOj6PWTkiU0Tr +yF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy +7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iah +ixTXTBmyUEFxPT9NcCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN +5r5N0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb +/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP0oUA51Aa +5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tK +G48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP +82Z+ +-----END CERTIFICATE----- + +WoSign +====== +-----BEGIN CERTIFICATE----- +MIIFdjCCA16gAwIBAgIQXmjWEXGUY1BWAGjzPsnFkTANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQG +EwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxKjAoBgNVBAMTIUNlcnRpZmljYXRpb24g +QXV0aG9yaXR5IG9mIFdvU2lnbjAeFw0wOTA4MDgwMTAwMDFaFw0zOTA4MDgwMTAwMDFaMFUxCzAJ +BgNVBAYTAkNOMRowGAYDVQQKExFXb1NpZ24gQ0EgTGltaXRlZDEqMCgGA1UEAxMhQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkgb2YgV29TaWduMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +vcqNrLiRFVaXe2tcesLea9mhsMMQI/qnobLMMfo+2aYpbxY94Gv4uEBf2zmoAHqLoE1UfcIiePyO +CbiohdfMlZdLdNiefvAA5A6JrkkoRBoQmTIPJYhTpA2zDxIIFgsDcSccf+Hb0v1naMQFXQoOXXDX +2JegvFNBmpGN9J42Znp+VsGQX+axaCA2pIwkLCxHC1l2ZjC1vt7tj/id07sBMOby8w7gLJKA84X5 +KIq0VC6a7fd2/BVoFutKbOsuEo/Uz/4Mx1wdC34FMr5esAkqQtXJTpCzWQ27en7N1QhatH/YHGkR ++ScPewavVIMYe+HdVHpRaG53/Ma/UkpmRqGyZxq7o093oL5d//xWC0Nyd5DKnvnyOfUNqfTq1+ez +EC8wQjchzDBwyYaYD8xYTYO7feUapTeNtqwylwA6Y3EkHp43xP901DfA4v6IRmAR3Qg/UDaruHqk +lWJqbrDKaiFaafPz+x1wOZXzp26mgYmhiMU7ccqjUu6Du/2gd/Tkb+dC221KmYo0SLwX3OSACCK2 +8jHAPwQ+658geda4BmRkAjHXqc1S+4RFaQkAKtxVi8QGRkvASh0JWzko/amrzgD5LkhLJuYwTKVY +yrREgk/nkR4zw7CT/xH8gdLKH3Ep3XZPkiWvHYG3Dy+MwwbMLyejSuQOmbp8HkUff6oZRZb9/D0C +AwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOFmzw7R +8bNLtwYgFP6HEtX2/vs+MA0GCSqGSIb3DQEBBQUAA4ICAQCoy3JAsnbBfnv8rWTjMnvMPLZdRtP1 +LOJwXcgu2AZ9mNELIaCJWSQBnfmvCX0KI4I01fx8cpm5o9dU9OpScA7F9dY74ToJMuYhOZO9sxXq +T2r09Ys/L3yNWC7F4TmgPsc9SnOeQHrAK2GpZ8nzJLmzbVUsWh2eJXLOC62qx1ViC777Y7NhRCOj +y+EaDveaBk3e1CNOIZZbOVtXHS9dCF4Jef98l7VNg64N1uajeeAz0JmWAjCnPv/So0M/BVoG6kQC +2nz4SNAzqfkHx5Xh9T71XXG68pWpdIhhWeO/yloTunK0jF02h+mmxTwTv97QRCbut+wucPrXnbes +5cVAWubXbHssw1abR80LzvobtCHXt2a49CUwi1wNuepnsvRtrtWhnk/Yn+knArAdBtaP4/tIEp9/ +EaEQPkxROpaw0RPxx9gmrjrKkcRpnd8BKWRRb2jaFOwIQZeQjdCygPLPwj2/kWjFgGcexGATVdVh +mVd8upUPYUk6ynW8yQqTP2cOEvIo4jEbwFcW3wh8GcF+Dx+FHgo2fFt+J7x6v+Db9NpSvd4MVHAx +kUOVyLzwPt0JfjBkUO1/AaQzZ01oT74V77D2AhGiGxMlOtzCWfHjXEa7ZywCRuoeSKbmW9m1vFGi +kpbbqsY3Iqb+zCB0oy2pLmvLwIIRIbWTee5Ehr7XHuQe+w== +-----END CERTIFICATE----- + +WoSign China +============ +-----BEGIN CERTIFICATE----- +MIIFWDCCA0CgAwIBAgIQUHBrzdgT/BtOOzNy0hFIjTANBgkqhkiG9w0BAQsFADBGMQswCQYDVQQG +EwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxGzAZBgNVBAMMEkNBIOayg+mAmuagueiv +geS5pjAeFw0wOTA4MDgwMTAwMDFaFw0zOTA4MDgwMTAwMDFaMEYxCzAJBgNVBAYTAkNOMRowGAYD +VQQKExFXb1NpZ24gQ0EgTGltaXRlZDEbMBkGA1UEAwwSQ0Eg5rKD6YCa5qC56K+B5LmmMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0EkhHiX8h8EqwqzbdoYGTufQdDTc7WU1/FDWiD+k +8H/rD195L4mx/bxjWDeTmzj4t1up+thxx7S8gJeNbEvxUNUqKaqoGXqW5pWOdO2XCld19AXbbQs5 +uQF/qvbW2mzmBeCkTVL829B0txGMe41P/4eDrv8FAxNXUDf+jJZSEExfv5RxadmWPgxDT74wwJ85 +dE8GRV2j1lY5aAfMh09Qd5Nx2UQIsYo06Yms25tO4dnkUkWMLhQfkWsZHWgpLFbE4h4TV2TwYeO5 +Ed+w4VegG63XX9Gv2ystP9Bojg/qnw+LNVgbExz03jWhCl3W6t8Sb8D7aQdGctyB9gQjF+BNdeFy +b7Ao65vh4YOhn0pdr8yb+gIgthhid5E7o9Vlrdx8kHccREGkSovrlXLp9glk3Kgtn3R46MGiCWOc +76DbT52VqyBPt7D3h1ymoOQ3OMdc4zUPLK2jgKLsLl3Az+2LBcLmc272idX10kaO6m1jGx6KyX2m ++Jzr5dVjhU1zZmkR/sgO9MHHZklTfuQZa/HpelmjbX7FF+Ynxu8b22/8DU0GAbQOXDBGVWCvOGU6 +yke6rCzMRh+yRpY/8+0mBe53oWprfi1tWFxK1I5nuPHa1UaKJ/kR8slC/k7e3x9cxKSGhxYzoacX +GKUN5AXlK8IrC6KVkLn9YDxOiT7nnO4fuwECAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFOBNv9ybQV0T6GTwp+kVpOGBwboxMA0GCSqGSIb3DQEBCwUA +A4ICAQBqinA4WbbaixjIvirTthnVZil6Xc1bL3McJk6jfW+rtylNpumlEYOnOXOvEESS5iVdT2H6 +yAa+Tkvv/vMx/sZ8cApBWNromUuWyXi8mHwCKe0JgOYKOoICKuLJL8hWGSbueBwj/feTZU7n85iY +r83d2Z5AiDEoOqsuC7CsDCT6eiaY8xJhEPRdF/d+4niXVOKM6Cm6jBAyvd0zaziGfjk9DgNyp115 +j0WKWa5bIW4xRtVZjc8VX90xJc/bYNaBRHIpAlf2ltTW/+op2znFuCyKGo3Oy+dCMYYFaA6eFN0A +kLppRQjbbpCBhqcqBT/mhDn4t/lXX0ykeVoQDF7Va/81XwVRHmyjdanPUIPTfPRm94KNPQx96N97 +qA4bLJyuQHCH2u2nFoJavjVsIE4iYdm8UXrNemHcSxH5/mc0zy4EZmFcV5cjjPOGG0jfKq+nwf/Y +jj4Du9gqsPoUJbJRa4ZDhS4HIxaAjUz7tGM7zMN07RujHv41D198HRaG9Q7DlfEvr10lO1Hm13ZB +ONFLAzkopR6RctR9q5czxNM+4Gm2KHmgCY0c0f9BckgG/Jou5yD5m6Leie2uPAmvylezkolwQOQv +T8Jwg0DXJCxr5wkf09XHwQj02w47HAcLQxGEIYbpgNR12KvxAmLBsX5VYc8T1yaw15zLKYs4SgsO +kI26oQ== +-----END CERTIFICATE----- + +COMODO RSA Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCBhTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMTE5MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR6FSS0gpWsawNJN3Fz0Rn +dJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8Xpz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZ +FGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+ +5eNu/Nio5JIk2kNrYrhV/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pG +x8cgoLEfZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z+pUX +2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7wqP/0uK3pN/u6uPQL +OvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZahSL0896+1DSJMwBGB7FY79tOi4lu3 +sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVICu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+C +GCe01a60y1Dma/RMhnEw6abfFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5 +WdYgGq/yapiqcrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvlwFTPoCWOAvn9sKIN9SCYPBMt +rFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+ +nq6PK7o9mfjYcwlYRm6mnPTXJ9OV2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSg +tZx8jb8uk2IntznaFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwW +sRqZCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiKboHGhfKp +pC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmckejkk9u+UJueBPSZI9FoJA +zMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yLS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHq +ZJx64SIDqZxubw5lT2yHh17zbqD5daWbQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk52 +7RH89elWsn2/x20Kk4yl0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7I +LaZRfyHBNVOFBkpdn627G190 +-----END CERTIFICATE----- + +USERTrust RSA Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAEmUXNg7D2wiz +0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2j +Y0K2dvKpOyuR+OJv0OwWIJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFn +RghRy4YUVD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O ++T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT79uq +/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6c0Plfg6lZrEpfDKE +Y1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJM +lXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8 +yexDJtC/QV9AqURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+ +eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPFUp/L+M+ZBn8b2kMVn54CVVeW +FPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KOVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ +7l8wXEskEVX/JJpuXior7gtNn3/3ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQ +Eg9zKC7F4iRO/Fjs8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM +8WcRiQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYzeSf7dNXGi +FSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZXHlKYC6SQK5MNyosycdi +yA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9c +J2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRBVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGw +sAvgnEzDHNb842m1R0aBL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gx +Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +USERTrust ECC Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2 +0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez +nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV +HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB +HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu +9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R4 +=========================== +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprl +OQcJFspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAwDgYDVR0P +AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61FuOJAf/sKbvu+M8k8o4TV +MAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGXkPoUVy0D7O48027KqGx2vKLeuwIgJ6iF +JzWbVsaj8kfSt24bAgAXqmemFZHe+pTsewv4n4Q= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R5 +=========================== +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6 +SFkc8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8kehOvRnkmS +h5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYIKoZIzj0EAwMDaAAwZQIxAOVpEslu28Yx +uglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7 +yFz9SO8NdCKoCOJuxUnOxwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +Staat der Nederlanden Root CA - G3 +================================== +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJOTDEeMBwGA1UE +CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFhdCBkZXIgTmVkZXJsYW5kZW4g +Um9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloXDTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMC +TkwxHjAcBgNVBAoMFVN0YWF0IGRlciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5l +ZGVybGFuZGVuIFJvb3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4y +olQPcPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WWIkYFsO2t +x1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqXxz8ecAgwoNzFs21v0IJy +EavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFyKJLZWyNtZrVtB0LrpjPOktvA9mxjeM3K +Tj215VKb8b475lRgsGYeCasH/lSJEULR9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUur +mkVLoR9BvUhTFXFkC4az5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU5 +1nus6+N86U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7Ngzp +07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHPbMk7ccHViLVlvMDo +FxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXtBznaqB16nzaeErAMZRKQFWDZJkBE +41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTtXUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleu +yjWcLhL75LpdINyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwpLiniyMMB8jPq +KqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8Ipf3YF3qKS9Ysr1YvY2WTxB1 +v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixpgZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA +8KCWAg8zxXHzniN9lLf9OtMJgwYh/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b +8KKaa8MFSu1BYBQw0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0r +mj1AfsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq4BZ+Extq +1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR1VmiiXTTn74eS9fGbbeI +JG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/QFH1T/U67cjF68IeHRaVesd+QnGTbksV +tzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM94B7IWcnMFk= +-----END CERTIFICATE----- + +Staat der Nederlanden EV Root CA +================================ +-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJOTDEeMBwGA1UE +CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFhdCBkZXIgTmVkZXJsYW5kZW4g +RVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0yMjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5M +MR4wHAYDVQQKDBVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRl +cmxhbmRlbiBFViBSb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkk +SzrSM4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nCUiY4iKTW +O0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3dZ//BYY1jTw+bbRcwJu+r +0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46prfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8 +Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13lpJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gV +XJrm0w912fxBmJc+qiXbj5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr +08C+eKxCKFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS/ZbV +0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0XcgOPvZuM5l5Tnrmd +74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH1vI4gnPah1vlPNOePqc7nvQDs/nx +fRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrPpx9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwa +ivsnuL8wbqg7MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI +eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u2dfOWBfoqSmu +c0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHSv4ilf0X8rLiltTMMgsT7B/Zq +5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTCwPTxGfARKbalGAKb12NMcIxHowNDXLldRqAN +b/9Zjr7dn3LDWyvfjFvO5QxGbJKyCqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tN +f1zuacpzEPuKqf2evTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi +5Dp6Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIaGl6I6lD4 +WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeLeG9QgkRQP2YGiqtDhFZK +DyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGy +eUN51q1veieQA6TqJIc/2b3Z6fJfUEkc7uzXLg== +-----END CERTIFICATE----- + +IdenTrust Commercial Root CA 1 +============================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBS +b290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQwMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzES +MBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENB +IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ld +hNlT3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU+ehcCuz/ +mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gpS0l4PJNgiCL8mdo2yMKi +1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1bVoE/c40yiTcdCMbXTMTEl3EASX2MN0C +XZ/g1Ue9tOsbobtJSdifWwLziuQkkORiT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl +3ZBWzvurpWCdxJ35UrCLvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzy +NeVJSQjKVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZKdHzV +WYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHTc+XvvqDtMwt0viAg +xGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hvl7yTmvmcEpB4eoCHFddydJxVdHix +uuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5NiGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZI +hvcNAQELBQADggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwtLRvM7Kqas6pg +ghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93nAbowacYXVKV7cndJZ5t+qnt +ozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmV +YjzlVYA211QC//G5Xc7UI2/YRYRKW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUX +feu+h1sXIFRRk0pTAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/ro +kTLql1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG4iZZRHUe +2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZmUlO+KWA2yUPHGNiiskz +Z2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7R +cGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +IdenTrust Public Sector Root CA 1 +================================= +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3Rv +ciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcNMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJV +UzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBS +b290IENBIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTy +P4o7ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGyRBb06tD6 +Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlSbdsHyo+1W/CD80/HLaXI +rcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF/YTLNiCBWS2ab21ISGHKTN9T0a9SvESf +qy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoS +mJxZZoY+rfGwyj4GD3vwEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFn +ol57plzy9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9VGxyh +LrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ2fjXctscvG29ZV/v +iDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsVWaFHVCkugyhfHMKiq3IXAAaOReyL +4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gDW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8B +Af8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMw +DQYJKoZIhvcNAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHVDRDtfULAj+7A +mgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9TaDKQGXSc3z1i9kKlT/YPyNt +GtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8GlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFt +m6/n6J91eEyrRjuazr8FGF1NFTwWmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMx +NRF4eKLg6TCMf4DfWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4 +Mhn5+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJtshquDDI +ajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhAGaQdp/lLQzfcaFpPz+vC +ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ +3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +Entrust Root Certification Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVy +bXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ug +b25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIw +HhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoT +DUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMx +OTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP +/vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXz +HHfV1IWNcCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKU +s/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4y +TGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRx +AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ6 +0B7vfec7aVHUbI2fkBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5Z +iXMRrEPR9RP/jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDgi +nWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+ +vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xO +e4pIb4tF9g== +-----END CERTIFICATE----- + +Entrust Root Certification Authority - EC1 +========================================== +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMCVVMx +FjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn +YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYw +FAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2Fs +LXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQg +dXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHy +AsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef +9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3h +vxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8 +kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +CFCA EV ROOT +============ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4GA1UE +CgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNB +IEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkxMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEw +MC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQD +DAxDRkNBIEVWIFJPT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnV +BU03sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpLTIpTUnrD +7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5/ZOkVIBMUtRSqy5J35DN +uF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp7hZZLDRJGqgG16iI0gNyejLi6mhNbiyW +ZXvKWfry4t3uMCz7zEasxGPrb382KzRzEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7 +xzbh72fROdOXW3NiGUgthxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9f +py25IGvPa931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqotaK8K +gWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNgTnYGmE69g60dWIol +hdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfVPKPtl8MeNPo4+QgO48BdK4PRVmrJ +tqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hvcWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAf +BgNVHSMEGDAWgBTj/i39KNALtbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObTej/tUxPQ4i9q +ecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdLjOztUmCypAbqTuv0axn96/Ua +4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBSESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sG +E5uPhnEFtC+NiWYzKXZUmhH4J/qyP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfX +BDrDMlI1Dlb4pd19xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjn +aH9dCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN5mydLIhy +PDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe/v5WOaHIz16eGWRGENoX +kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C +ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H5 +========================================================= +-----BEGIN CERTIFICATE----- +MIIEJzCCAw+gAwIBAgIHAI4X/iQggTANBgkqhkiG9w0BAQsFADCBsTELMAkGA1UEBhMCVFIxDzAN +BgNVBAcMBkFua2FyYTFNMEsGA1UECgxEVMOcUktUUlVTVCBCaWxnaSDEsGxldGnFn2ltIHZlIEJp +bGnFn2ltIEfDvHZlbmxpxJ9pIEhpem1ldGxlcmkgQS7Fni4xQjBABgNVBAMMOVTDnFJLVFJVU1Qg +RWxla3Ryb25payBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSBINTAeFw0xMzA0MzAw +ODA3MDFaFw0yMzA0MjgwODA3MDFaMIGxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMU0w +SwYDVQQKDERUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnE +n2kgSGl6bWV0bGVyaSBBLsWeLjFCMEAGA1UEAww5VMOcUktUUlVTVCBFbGVrdHJvbmlrIFNlcnRp +ZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIEg1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEApCUZ4WWe60ghUEoI5RHwWrom/4NZzkQqL/7hzmAD/I0Dpe3/a6i6zDQGn1k19uwsu537 +jVJp45wnEFPzpALFp/kRGml1bsMdi9GYjZOHp3GXDSHHmflS0yxjXVW86B8BSLlg/kJK9siArs1m +ep5Fimh34khon6La8eHBEJ/rPCmBp+EyCNSgBbGM+42WAA4+Jd9ThiI7/PS98wl+d+yG6w8z5UNP +9FR1bSmZLmZaQ9/LXMrI5Tjxfjs1nQ/0xVqhzPMggCTTV+wVunUlm+hkS7M0hO8EuPbJbKoCPrZV +4jI3X/xml1/N1p7HIL9Nxqw/dV8c7TKcfGkAaZHjIxhT6QIDAQABo0IwQDAdBgNVHQ4EFgQUVpkH +HtOsDGlktAxQR95DLL4gwPswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggEBAJ5FdnsXSDLyOIspve6WSk6BGLFRRyDN0GSxDsnZAdkJzsiZ3GglE9Rc8qPo +BP5yCccLqh0lVX6Wmle3usURehnmp349hQ71+S4pL+f5bFgWV1Al9j4uPqrtd3GqqpmWRgqujuwq +URawXs3qZwQcWDD1YIq9pr1N5Za0/EKJAWv2cMhQOQwt1WbZyNKzMrcbGW3LM/nfpeYVhDfwwvJl +lpKQd/Ct9JDpEXjXk4nAPQu6KfTomZ1yju2dL+6SfaHx/126M2CFYv4HAqGEVka+lgqaE9chTLd8 +B59OTj+RdPsnnRHM3eaxynFNExc5JsUpISuTKWqW+qtB4Uu2NQvAmxU= +-----END CERTIFICATE----- + +TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı H6 +========================================================= +-----BEGIN CERTIFICATE----- +MIIEJjCCAw6gAwIBAgIGfaHyZeyKMA0GCSqGSIb3DQEBCwUAMIGxMQswCQYDVQQGEwJUUjEPMA0G +A1UEBwwGQW5rYXJhMU0wSwYDVQQKDERUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmls +acWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjFCMEAGA1UEAww5VMOcUktUUlVTVCBF +bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIEg2MB4XDTEzMTIxODA5 +MDQxMFoXDTIzMTIxNjA5MDQxMFowgbExCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExTTBL +BgNVBAoMRFTDnFJLVFJVU1QgQmlsZ2kgxLBsZXRpxZ9pbSB2ZSBCaWxpxZ9pbSBHw7x2ZW5sacSf +aSBIaXptZXRsZXJpIEEuxZ4uMUIwQAYDVQQDDDlUw5xSS1RSVVNUIEVsZWt0cm9uaWsgU2VydGlm +aWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLEgSDYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCdsGjW6L0UlqMACprx9MfMkU1xeHe59yEmFXNRFpQJRwXiM/VomjX/3EsvMsew7eKC5W/a +2uqsxgbPJQ1BgfbBOCK9+bGlprMBvD9QFyv26WZV1DOzXPhDIHiTVRZwGTLmiddk671IUP320EED +wnS3/faAz1vFq6TWlRKb55cTMgPp1KtDWxbtMyJkKbbSk60vbNg9tvYdDjTu0n2pVQ8g9P0pu5Fb +HH3GQjhtQiht1AH7zYiXSX6484P4tZgvsycLSF5W506jM7NE1qXyGJTtHB6plVxiSvgNZ1GpryHV ++DKdeboaX+UEVU0TRv/yz3THGmNtwx8XEsMeED5gCLMxAgMBAAGjQjBAMB0GA1UdDgQWBBTdVRcT +9qzoSCHK77Wv0QAy7Z6MtTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQsFAAOCAQEAb1gNl0OqFlQ+v6nfkkU/hQu7VtMMUszIv3ZnXuaqs6fvuay0EBQNdH49ba3R +fdCaqaXKGDsCQC4qnFAUi/5XfldcEQlLNkVS9z2sFP1E34uXI9TDwe7UU5X+LEr+DXCqu4svLcsy +o4LyVN/Y8t3XSHLuSqMplsNEzm61kod2pLv0kmzOLBQJZo6NrRa1xxsJYTvjIKIDgI6tflEATseW +hvtDmHd9KMeP2Cpu54Rvl0EpABZeTeIT6lnAY2c6RPuY/ATTMHKm9ocJV612ph1jmv3XZch4gyt1 +O6VbuA1df74jrlZVlFjvH4GMKrLN5ptjnhi85WsGtAuYSyher4hYyw== +-----END CERTIFICATE----- + +Certinomis - Root CA +==================== +-----BEGIN CERTIFICATE----- +MIIFkjCCA3qgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJGUjETMBEGA1UEChMK +Q2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxHTAbBgNVBAMTFENlcnRpbm9taXMg +LSBSb290IENBMB4XDTEzMTAyMTA5MTcxOFoXDTMzMTAyMTA5MTcxOFowWjELMAkGA1UEBhMCRlIx +EzARBgNVBAoTCkNlcnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMR0wGwYDVQQDExRD +ZXJ0aW5vbWlzIC0gUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTMCQos +P5L2fxSeC5yaah1AMGT9qt8OHgZbn1CF6s2Nq0Nn3rD6foCWnoR4kkjW4znuzuRZWJflLieY6pOo +d5tK8O90gC3rMB+12ceAnGInkYjwSond3IjmFPnVAy//ldu9n+ws+hQVWZUKxkd8aRi5pwP5ynap +z8dvtF4F/u7BUrJ1Mofs7SlmO/NKFoL21prbcpjp3vDFTKWrteoB4owuZH9kb/2jJZOLyKIOSY00 +8B/sWEUuNKqEUL3nskoTuLAPrjhdsKkb5nPJWqHZZkCqqU2mNAKthH6yI8H7KsZn9DS2sJVqM09x +RLWtwHkziOC/7aOgFLScCbAK42C++PhmiM1b8XcF4LVzbsF9Ri6OSyemzTUK/eVNfaoqoynHWmgE +6OXWk6RiwsXm9E/G+Z8ajYJJGYrKWUM66A0ywfRMEwNvbqY/kXPLynNvEiCL7sCCeN5LLsJJwx3t +FvYk9CcbXFcx3FXuqB5vbKziRcxXV4p1VxngtViZSTYxPDMBbRZKzbgqg4SGm/lg0h9tkQPTYKbV +PZrdd5A9NaSfD171UkRpucC63M9933zZxKyGIjK8e2uR73r4F2iw4lNVYC2vPsKD2NkJK/DAZNuH +i5HMkesE/Xa0lZrmFAYb1TQdvtj/dBxThZngWVJKYe2InmtJiUZ+IFrZ50rlau7SZRFDAgMBAAGj +YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTvkUz1pcMw6C8I +6tNxIqSSaHh02TAfBgNVHSMEGDAWgBTvkUz1pcMw6C8I6tNxIqSSaHh02TANBgkqhkiG9w0BAQsF +AAOCAgEAfj1U2iJdGlg+O1QnurrMyOMaauo++RLrVl89UM7g6kgmJs95Vn6RHJk/0KGRHCwPT5iV +WVO90CLYiF2cN/z7ZMF4jIuaYAnq1fohX9B0ZedQxb8uuQsLrbWwF6YSjNRieOpWauwK0kDDPAUw +Pk2Ut59KA9N9J0u2/kTO+hkzGm2kQtHdzMjI1xZSg081lLMSVX3l4kLr5JyTCcBMWwerx20RoFAX +lCOotQqSD7J6wWAsOMwaplv/8gzjqh8c3LigkyfeY+N/IZ865Z764BNqdeuWXGKRlI5nU7aJ+BIJ +y29SWwNyhlCVCNSNh4YVH5Uk2KRvms6knZtt0rJ2BobGVgjF6wnaNsIbW0G+YSrjcOa4pvi2WsS9 +Iff/ql+hbHY5ZtbqTFXhADObE5hjyW/QASAJN1LnDE8+zbz1X5YnpyACleAu6AdBBR8Vbtaw5Bng +DwKTACdyxYvRVB9dSsNAl35VpnzBMwQUAR1JIGkLGZOdblgi90AMRgwjY/M50n92Uaf0yKHxDHYi +I0ZSKS3io0EHVmmY0gUJvGnHWmHNj4FgFU2A3ZDifcRQ8ow7bkrHxuaAKzyBvBGAFhAn1/DNP3nM +cyrDflOR1m749fPH0FFNjkulW+YZFzvWgQncItzujrnEj1PhZ7szuIgVRs/taTX/dQ1G885x4cVr +hkIGuUE= +-----END CERTIFICATE----- diff --git a/www/analytics/core/DataTable.php b/www/analytics/core/DataTable.php index b70ef4bd..221af531 100644 --- a/www/analytics/core/DataTable.php +++ b/www/analytics/core/DataTable.php @@ -1,6 +1,6 @@ * ### The Basics - * + * * DataTables consist of rows and each row consists of columns. A column value can be * a numeric, a string or an array. - * + * * Every row has an ID. The ID is either the index of the row or {@link ID_SUMMARY_ROW}. - * + * * DataTables are hierarchical data structures. Each row can also contain an additional * nested sub-DataTable (commonly referred to as a 'subtable'). - * + * * Both DataTables and DataTable rows can hold **metadata**. _DataTable metadata_ is information * regarding all the data, such as the site or period that the data is for. _Row metadata_ * is information regarding that row, such as a browser logo or website URL. - * + * * Finally, all DataTables contain a special _summary_ row. This row, if it exists, is * always at the end of the DataTable. - * + * * ### Populating DataTables - * + * * Data can be added to DataTables in three different ways. You can either: - * + * * 1. create rows one by one and add them through {@link addRow()} then truncate if desired, * 2. create an array of DataTable\Row instances or an array of arrays and add them using * {@link addRowsFromArray()} or {@link addRowsFromSimpleArray()} * then truncate if desired, * 3. or set the maximum number of allowed rows (with {@link setMaximumAllowedRows()}) * and add rows one by one. - * + * * If you want to eventually truncate your data (standard practice for all Piwik plugins), * the third method is the most memory efficient. It is, unfortunately, not always possible * to use since it requires that the data be sorted before adding. - * + * * ### Manipulating DataTables - * + * * There are two ways to manipulate a DataTable. You can either: - * + * * 1. manually iterate through each row and manipulate the data, * 2. or you can use predefined filters. - * + * * A filter is a class that has a 'filter' method which will manipulate a DataTable in * some way. There are several predefined Filters that allow you to do common things, * such as, - * + * * - add a new column to each row, * - add new metadata to each row, * - modify an existing column value for each row, * - sort an entire DataTable, * - and more. - * + * * Using these filters instead of writing your own code will increase code clarity and * reduce code redundancy. Additionally, filters have the advantage that they can be * applied to DataTable\Map instances. So you can visit every DataTable in a {@link DataTable\Map} * without having to write a recursive visiting function. - * + * * All predefined filters exist in the **Piwik\DataTable\BaseFilter** namespace. - * + * * _Note: For convenience, [anonymous functions](http://www.php.net/manual/en/functions.anonymous.php) * can be used as DataTable filters._ - * + * * ### Applying Filters - * + * * Filters can be applied now (via {@link filter()}), or they can be applied later (via * {@link queueFilter()}). - * + * * Filters that sort rows or manipulate the number of rows should be applied right away. * Non-essential, presentation filters should be queued. - * + * * ### Learn more - * + * * - See **{@link ArchiveProcessor}** to learn how DataTables are persisted. - * + * * ### Examples - * + * * **Populating a DataTable** - * + * * // adding one row at a time * $dataTable = new DataTable(); * $dataTable->addRow(new Row(array( @@ -115,7 +114,7 @@ require_once PIWIK_INCLUDE_PATH . '/core/Common.php'; * Row::COLUMNS => array('label' => 'thing2', 'nb_visits' => 2, 'nb_actions' => 2), * Row::METADATA => array('url' => 'http://thing2.com') * ))); - * + * * // using an array of rows * $dataTable = new DataTable(); * $dataTable->addRowsFromArray(array( @@ -128,32 +127,32 @@ require_once PIWIK_INCLUDE_PATH . '/core/Common.php'; * Row::METADATA => array('url' => 'http://thing2.com') * ) * )); - * + * * // using a "simple" array * $dataTable->addRowsFromSimpleArray(array( * array('label' => 'thing1', 'nb_visits' => 1, 'nb_actions' => 1), * array('label' => 'thing2', 'nb_visits' => 2, 'nb_actions' => 2) * )); - * + * * **Getting & setting metadata** - * + * * $dataTable = \Piwik\Plugins\Referrers\API::getInstance()->getSearchEngines($idSite = 1, $period = 'day', $date = '2007-07-24'); * $oldPeriod = $dataTable->metadata['period']; - * $dataTable->metadata['period'] = Period::factory('week', Date::factory('2013-10-18')); - * + * $dataTable->metadata['period'] = Period\Factory::build('week', Date::factory('2013-10-18')); + * * **Serializing & unserializing** - * + * * $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); - * + * * $serializedDataTable = $serializedData[0]; * $serailizedSubTable = $serializedData[$idSubtable]; - * + * * **Filtering for an API method** - * + * * public function getMyReport($idSite, $period, $date, $segment = false, $expanded = false) * { * $dataTable = Archive::getDataTableFromArchive('MyPlugin_MyReport', $idSite, $period, $date, $segment, $expanded); @@ -162,16 +161,16 @@ require_once PIWIK_INCLUDE_PATH . '/core/Common.php'; * $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', __NAMESPACE__ . '\getUrlFromLabelForMyReport')); * return $dataTable; * } - * + * * * @api */ -class DataTable implements DataTableInterface +class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess { const MAX_DEPTH_DEFAULT = 15; /** Name for metadata that describes when a report was archived. */ - const ARCHIVED_DATE_METADATA_NAME = 'archived_date'; + const ARCHIVED_DATE_METADATA_NAME = 'ts_archived'; /** Name for metadata that describes which columns are empty and should not be shown. */ const EMPTY_COLUMNS_METADATA_NAME = 'empty_column'; @@ -182,14 +181,14 @@ class DataTable implements DataTableInterface /** * Name for metadata that describes how individual columns should be aggregated when {@link addDataTable()} * or {@link Piwik\DataTable\Row::sumRow()} is called. - * + * * This metadata value must be an array that maps column names with valid operations. Valid aggregation operations are: - * + * * - `'skip'`: do nothing * - `'max'`: does `max($column1, $column2)` * - `'min'`: does `min($column1, $column2)` * - `'sum'`: does `$column1 + $column2` - * + * * See {@link addDataTable()} and {@link DataTable\Row::sumRow()} for more information. */ const COLUMN_AGGREGATION_OPS_METADATA_NAME = 'column_aggregation_ops'; @@ -200,6 +199,13 @@ class DataTable implements DataTableInterface /** The original label of the Summary Row. */ const LABEL_SUMMARY_ROW = -1; + /** + * Name for metadata that contains extra {@link Piwik\Plugin\ProcessedMetric}s for a DataTable. + * These metrics will be added in addition to the ones specified in the table's associated + * {@link Piwik\Plugin\Report} class. + */ + const EXTRA_PROCESSED_METRICS_METADATA_NAME = 'extra_processed_metrics'; + /** * Maximum nesting level. */ @@ -258,6 +264,13 @@ class DataTable implements DataTableInterface */ protected $queuedFilters = array(); + /** + * List of disabled filter names eg 'Limit' or 'Sort' + * + * @var array + */ + protected $disabledFilters = array(); + /** * We keep track of the number of rows before applying the LIMIT filter that deletes some rows * @@ -291,7 +304,7 @@ class DataTable implements DataTableInterface /** * Table metadata. Read [this](#class-desc-the-basics) to learn more. - * + * * Any data that describes the data held in the table's rows should go here. * * @var array @@ -326,15 +339,44 @@ class DataTable implements DataTableInterface && isset($this->rows) ) { $depth++; - foreach ($this->getRows() as $row) { + foreach ($this->rows as $row) { Common::destroy($row); } + if (isset($this->summaryRow)) { + Common::destroy($this->summaryRow); + } unset($this->rows); - Manager::getInstance()->setTableDeleted($this->getId()); + Manager::getInstance()->setTableDeleted($this->currentId); $depth--; } } + /** + * Clone. Called when cloning the datatable. We need to make sure to create a new datatableId. + * If we do not increase tableId it can result in segmentation faults when destructing a datatable. + */ + public function __clone() + { + // registers this instance to the manager + $this->currentId = Manager::getInstance()->addTable($this); + } + + public function setLabelsHaveChanged() + { + $this->indexNotUpToDate = true; + } + + /** + * @ignore + * does not update the summary row! + */ + public function setRows($rows) + { + unset($this->rows); + $this->rows = $rows; + $this->indexNotUpToDate = true; + } + /** * Sorts the DataTable rows using the supplied callback function. * @@ -344,16 +386,16 @@ class DataTable implements DataTableInterface */ public function sort($functionCallback, $columnSortedBy) { - $this->indexNotUpToDate = true; - $this->tableSortedBy = $columnSortedBy; + $this->setTableSortedBy($columnSortedBy); + usort($this->rows, $functionCallback); - if ($this->enableRecursiveSort === true) { - foreach ($this->getRows() as $row) { - if (($idSubtable = $row->getIdSubDataTable()) !== null) { - $table = Manager::getInstance()->getTable($idSubtable); - $table->enableRecursiveSort(); - $table->sort($functionCallback, $columnSortedBy); + if ($this->isSortRecursiveEnabled()) { + foreach ($this->getRowsWithoutSummaryRow() as $row) { + $subTable = $row->getSubtable(); + if ($subTable) { + $subTable->enableRecursiveSort(); + $subTable->sort($functionCallback, $columnSortedBy); } } } @@ -380,6 +422,23 @@ class DataTable implements DataTableInterface $this->enableRecursiveSort = true; } + /** + * @ignore + */ + public function isSortRecursiveEnabled() + { + return $this->enableRecursiveSort === true; + } + + /** + * @ignore + */ + public function setTableSortedBy($column) + { + $this->indexNotUpToDate = true; + $this->tableSortedBy = $column; + } + /** * Enables recursive filtering. If this method is called then the {@link filter()} method * will apply filters to every subtable in addition to this instance. @@ -389,9 +448,17 @@ class DataTable implements DataTableInterface $this->enableRecursiveFilters = true; } + /** + * @ignore + */ + public function disableRecursiveFilters() + { + $this->enableRecursiveFilters = false; + } + /** * Applies a filter to this datatable. - * + * * If {@link enableRecursiveFilters()} was called, the filter will be applied * to all subtables as well. * @@ -410,6 +477,10 @@ class DataTable implements DataTableInterface return; } + if (in_array($className, $this->disabledFilters)) { + return; + } + if (!class_exists($className, true)) { $className = 'Piwik\DataTable\Filter\\' . $className; } @@ -426,10 +497,50 @@ class DataTable implements DataTableInterface $filter->filter($this); } + /** + * Applies a filter to all subtables but not to this datatable. + * + * @param string|Closure $className Class name, eg. `"Sort"` or "Piwik\DataTable\Filters\Sort"`. If no + * namespace is supplied, `Piwik\DataTable\BaseFilter` is assumed. This parameter + * can also be a closure that takes a DataTable as its first parameter. + * @param array $parameters Array of extra parameters to pass to the filter. + */ + public function filterSubtables($className, $parameters = array()) + { + foreach ($this->getRowsWithoutSummaryRow() as $row) { + $subtable = $row->getSubtable(); + if ($subtable) { + $subtable->filter($className, $parameters); + $subtable->filterSubtables($className, $parameters); + } + } + } + + /** + * Adds a filter and a list of parameters to the list of queued filters of all subtables. These filters will be + * executed when {@link applyQueuedFilters()} is called. + * + * Filters that prettify the column values or don't need the full set of rows should be queued. This + * way they will be run after the table is truncated which will result in better performance. + * + * @param string|Closure $className The class name of the filter, eg. `'Limit'`. + * @param array $parameters The parameters to give to the filter, eg. `array($offset, $limit)` for the Limit filter. + */ + public function queueFilterSubtables($className, $parameters = array()) + { + foreach ($this->getRowsWithoutSummaryRow() as $row) { + $subtable = $row->getSubtable(); + if ($subtable) { + $subtable->queueFilter($className, $parameters); + $subtable->queueFilterSubtables($className, $parameters); + } + } + } + /** * Adds a filter and a list of parameters to the list of queued filters. These filters will be * executed when {@link applyQueuedFilters()} is called. - * + * * Filters that prettify the column values or don't need the full set of rows should be queued. This * way they will be run after the table is truncated which will result in better performance. * @@ -444,6 +555,23 @@ class DataTable implements DataTableInterface $this->queuedFilters[] = array('className' => $className, 'parameters' => $parameters); } + /** + * Disable a specific filter to run on this DataTable in case you have already applied this filter or if you will + * handle this filter manually by using a custom filter. Be aware if you disable a given filter, that filter won't + * be ever executed. Even if another filter calls this filter on the DataTable. + * + * @param string $className eg 'Limit' or 'Sort'. Passing a `Closure` or an `array($class, $methodName)` is not + * supported yet. We check for exact match. So if you disable 'Limit' and + * call `->filter('Limit')` this filter won't be executed. If you call + * `->filter('Piwik\DataTable\Filter\Limit')` that filter will be executed. See it as a + * feature. + * @ignore + */ + public function disableFilter($className) + { + $this->disabledFilters[] = $className; + } + /** * Applies all filters that were previously queued to the table. See {@link queueFilter()} * for more information. @@ -453,54 +581,59 @@ class DataTable implements DataTableInterface foreach ($this->queuedFilters as $filter) { $this->filter($filter['className'], $filter['parameters']); } - $this->queuedFilters = array(); + $this->clearQueuedFilters(); } /** * Sums a DataTable to this one. - * + * * This method will sum rows that have the same label. If a row is found in `$tableToSum` whose * label is not found in `$this`, the row will be added to `$this`. - * + * * If the subtables for this table are loaded, they will be summed as well. - * + * * Rows are summed together by summing individual columns. By default columns are summed by * adding one column value to another. Some columns cannot be aggregated this way. In these * cases, the {@link COLUMN_AGGREGATION_OPS_METADATA_NAME} * metadata can be used to specify a different type of operation. - * + * * @param \Piwik\DataTable $tableToSum + * @throws Exception */ - public function addDataTable(DataTable $tableToSum, $doAggregateSubTables = true) + public function addDataTable(DataTable $tableToSum) { - if($tableToSum instanceof Simple) { - if($tableToSum->getRowsCount() > 1) { + if ($tableToSum instanceof Simple) { + if ($tableToSum->getRowsCount() > 1) { throw new Exception("Did not expect a Simple table with more than one row in addDataTable()"); } $row = $tableToSum->getFirstRow(); $this->aggregateRowFromSimpleTable($row); } else { - foreach ($tableToSum->getRows() as $row) { - $this->aggregateRowWithLabel($row, $doAggregateSubTables); + $columnAggregationOps = $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME); + foreach ($tableToSum->getRowsWithoutSummaryRow() as $row) { + $this->aggregateRowWithLabel($row, $columnAggregationOps); + } + // we do not use getRows() as this method might get called 100k times when aggregating many datatables and + // this takes a lot of time. + $row = $tableToSum->getRowFromId(DataTable::ID_SUMMARY_ROW); + if ($row) { + $this->aggregateRowWithLabel($row, $columnAggregationOps); } } } /** * Returns the Row whose `'label'` column is equal to `$label`. - * + * * This method executes in constant time except for the first call which caches row * label => row ID mappings. - * + * * @param string $label `'label'` column value to look for. * @return Row|false The row if found, `false` if otherwise. */ public function getRowFromLabel($label) { $rowId = $this->getRowIdFromLabel($label); - if ($rowId instanceof Row) { - return $rowId; - } if (is_int($rowId) && isset($this->rows[$rowId])) { return $this->rows[$rowId]; } @@ -509,6 +642,9 @@ class DataTable implements DataTableInterface ) { return $this->summaryRow; } + if ($rowId instanceof Row) { + return $rowId; + } return false; } @@ -517,13 +653,12 @@ class DataTable implements DataTableInterface * * This method executes in constant time except for the first call which caches row * label => row ID mappings. - * + * * @param string $label `'label'` column value to look for. * @return int The row ID. */ public function getRowIdFromLabel($label) { - $this->rebuildIndexContinuously = true; if ($this->indexNotUpToDate) { $this->rebuildIndex(); } @@ -534,7 +669,8 @@ class DataTable implements DataTableInterface return self::ID_SUMMARY_ROW; } - $label = (string)$label; + $label = (string) $label; + if (!isset($this->rowsIndexByLabel[$label])) { return false; } @@ -559,15 +695,26 @@ class DataTable implements DataTableInterface /** * Rebuilds the index used to lookup a row by label + * @internal */ - private function rebuildIndex() + public function rebuildIndex() { - foreach ($this->getRows() as $id => $row) { + $this->rebuildIndexContinuously = true; + + foreach ($this->rows as $id => $row) { $label = $row->getColumn('label'); if ($label !== false) { $this->rowsIndexByLabel[$label] = $id; } } + + if ($this->summaryRow) { + $label = $this->summaryRow->getColumn('label'); + if ($label !== false) { + $this->rowsIndexByLabel[$label] = DataTable::ID_SUMMARY_ROW; + } + } + $this->indexNotUpToDate = false; } @@ -592,7 +739,7 @@ class DataTable implements DataTableInterface /** * Returns the row that has a subtable with ID matching `$idSubtable`. - * + * * @param int $idSubTable The subtable ID. * @return Row|false The row or false if not found */ @@ -609,7 +756,7 @@ class DataTable implements DataTableInterface /** * Adds a row to this table. - * + * * If {@link setMaximumAllowedRows()} was called and the current row count is * at the maximum, the new row will be summed to the summary row. If there is no summary row, * this row is set as the summary row. @@ -624,8 +771,9 @@ class DataTable implements DataTableInterface if ($this->maximumAllowedRows > 0 && $this->getRowsCount() >= $this->maximumAllowedRows - 1 ) { - if ($this->summaryRow === null) // create the summary row if necessary - { + if ($this->summaryRow === null) { + // create the summary row if necessary + $columns = array('label' => self::LABEL_SUMMARY_ROW) + $row->getColumns(); $this->addSummaryRow(new Row(array(Row::COLUMNS => $columns))); } else { @@ -649,7 +797,7 @@ class DataTable implements DataTableInterface /** * Sets the summary row. - * + * * _Note: A DataTable can have only one summary row._ * * @param Row $row @@ -684,7 +832,7 @@ class DataTable implements DataTableInterface /** * Adds a new row from an array. - * + * * You can add row metadata with this method. * * @param array $row eg. `array(Row::COLUMNS => array('visits' => 13, 'test' => 'toto'), @@ -697,7 +845,7 @@ class DataTable implements DataTableInterface /** * Adds a new row a from an array of column values. - * + * * Row metadata cannot be added with this method. * * @param array $row eg. `array('name' => 'google analytics', 'license' => 'commercial')` @@ -721,6 +869,14 @@ class DataTable implements DataTableInterface } } + /** + * @ignore + */ + public function getRowsWithoutSummaryRow() + { + return $this->rows; + } + /** * Returns an array containing all column values for the requested column. * @@ -739,7 +895,7 @@ class DataTable implements DataTableInterface /** * Returns an array containing all column values of columns whose name starts with `$name`. * - * @param $namePrefix The column name prefix. + * @param string $namePrefix The column name prefix. * @return array The array of column values. */ public function getColumnsStartingWith($namePrefix) @@ -759,10 +915,10 @@ class DataTable implements DataTableInterface /** * Returns the names of every column this DataTable contains. This method will return the * columns of the first row with data and will assume they occur in every other row as well. - * + * *_ Note: If column names still use their in-database INDEX values (@see Metrics), they * will be converted to their string name in the array result._ - * + * * @return array Array of string column names. */ public function getColumns() @@ -788,7 +944,7 @@ class DataTable implements DataTableInterface /** * Returns an array containing the requested metadata value of each row. - * + * * @param string $name The metadata column to return. * @return array */ @@ -803,7 +959,7 @@ class DataTable implements DataTableInterface /** * Returns the number of rows in the table including the summary row. - * + * * @return int */ public function getRowsCount() @@ -860,8 +1016,8 @@ class DataTable implements DataTableInterface { $totalCount = 0; foreach ($this->rows as $row) { - if (($idSubTable = $row->getIdSubDataTable()) !== null) { - $subTable = Manager::getInstance()->getTable($idSubTable); + $subTable = $row->getSubtable(); + if ($subTable) { $count = $subTable->getRowsCountRecursive(); $totalCount += $count; } @@ -893,15 +1049,14 @@ class DataTable implements DataTableInterface * @param string $oldName Old column name. * @param string $newName New column name. */ - public function renameColumn($oldName, $newName, $doRenameColumnsOfSubTables = true) + public function renameColumn($oldName, $newName) { - foreach ($this->getRows() as $row) { + foreach ($this->rows as $row) { $row->renameColumn($oldName, $newName); - if($doRenameColumnsOfSubTables) { - if (($idSubDataTable = $row->getIdSubDataTable()) !== null) { - Manager::getInstance()->getTable($idSubDataTable)->renameColumn($oldName, $newName); - } + $subTable = $row->getSubtable(); + if ($subTable) { + $subTable->renameColumn($oldName, $newName); } } if (!is_null($this->summaryRow)) { @@ -917,12 +1072,13 @@ class DataTable implements DataTableInterface */ public function deleteColumns($names, $deleteRecursiveInSubtables = false) { - foreach ($this->getRows() as $row) { + foreach ($this->rows as $row) { foreach ($names as $name) { $row->deleteColumn($name); } - if (($idSubDataTable = $row->getIdSubDataTable()) !== null) { - Manager::getInstance()->getTable($idSubDataTable)->deleteColumns($names, $deleteRecursiveInSubtables); + $subTable = $row->getSubtable(); + if ($subTable) { + $subTable->deleteColumns($names, $deleteRecursiveInSubtables); } } if (!is_null($this->summaryRow)) { @@ -977,12 +1133,12 @@ class DataTable implements DataTableInterface } if (is_null($limit)) { - $spliced = array_splice($this->rows, $offset); + array_splice($this->rows, $offset); } else { - $spliced = array_splice($this->rows, $offset, $limit); + array_splice($this->rows, $offset, $limit); } - $countDeleted = count($spliced); - return $countDeleted; + + return $count - $this->getRowsCount(); } /** @@ -1000,7 +1156,7 @@ class DataTable implements DataTableInterface /** * Returns a string representation of this DataTable for convenient viewing. - * + * * _Note: This uses the **html** DataTable renderer._ * * @return string @@ -1019,16 +1175,13 @@ class DataTable implements DataTableInterface * each row has a label that exists in the other table, and if each row * is equal to the row in the other table with the same label. The order * of rows is not important. - * + * * @param \Piwik\DataTable $table1 * @param \Piwik\DataTable $table2 * @return bool */ public static function isEqual(DataTable $table1, DataTable $table2) { - $rows1 = $table1->getRows(); - $rows2 = $table2->getRows(); - $table1->rebuildIndex(); $table2->rebuildIndex(); @@ -1036,6 +1189,8 @@ class DataTable implements DataTableInterface return false; } + $rows1 = $table1->getRows(); + foreach ($rows1 as $row1) { $row2 = $table2->getRowFromLabel($row1->getColumn('label')); if ($row2 === false @@ -1050,10 +1205,10 @@ class DataTable implements DataTableInterface /** * Serializes an entire DataTable hierarchy and returns the array of serialized DataTables. - * + * * The first element in the returned array will be the serialized representation of this DataTable. * Every subsequent element will be a serialized subtable. - * + * * This DataTable and subtables can optionally be truncated before being serialized. In most * cases where DataTables can become quite large, they should be truncated before being persisted * in an archive. @@ -1065,7 +1220,7 @@ class DataTable implements DataTableInterface * @param int $maximumRowsInSubDataTable If not null, defines the maximum number of rows allowed in serialized subtables. * @param string $columnToSortByBeforeTruncation The column to sort by before truncating, eg, `Metrics::INDEX_NB_VISITS`. * @return array The array of serialized DataTables: - * + * * array( * // this DataTable (the root) * 0 => 'eghuighahgaueytae78yaet7yaetae', @@ -1075,7 +1230,7 @@ class DataTable implements DataTableInterface * * // another subtable * 2 => 'gqegJHUIGHEQjkgneqjgnqeugUGEQHGUHQE', - * + * * // etc. * ); */ @@ -1084,9 +1239,12 @@ class DataTable implements DataTableInterface $columnToSortByBeforeTruncation = null) { static $depth = 0; + // make sure subtableIds are consecutive from 1 to N + static $subtableId = 0; if ($depth > self::$maximumDepthLevelAllowed) { $depth = 0; + $subtableId = 0; throw new Exception("Maximum recursion level of " . self::$maximumDepthLevelAllowed . " reached. Maybe you have set a DataTable\Row with an associated DataTable belonging already to one of its parent tables?"); } if (!is_null($maximumRowsInDataTable)) { @@ -1098,75 +1256,121 @@ class DataTable implements DataTableInterface ); } + $consecutiveSubtableIds = array(); + $forcedId = $subtableId; + // For each row, get the serialized row // If it is associated to a sub table, get the serialized table recursively ; // but returns all serialized tables and subtable in an array of 1 dimension $aSerializedDataTable = array(); - foreach ($this->rows as $row) { - if (($idSubTable = $row->getIdSubDataTable()) !== null) { - $subTable = null; - try { - $subTable = Manager::getInstance()->getTable($idSubTable); - } catch(TableNotFoundException $e) { - // This occurs is an unknown & random data issue. Catch Exception and remove subtable from the row. - $row->removeSubtable(); - // Go to next row - continue; - } - + foreach ($this->rows as $id => $row) { + $subTable = $row->getSubtable(); + if ($subTable) { + $consecutiveSubtableIds[$id] = ++$subtableId; $depth++; $aSerializedDataTable = $aSerializedDataTable + $subTable->getSerialized($maximumRowsInSubDataTable, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation); $depth--; + } else { + $row->removeSubtable(); } } - // we load the current Id of the DataTable - $forcedId = $this->getId(); // if the datatable is the parent we force the Id at 0 (this is part of the specification) if ($depth == 0) { $forcedId = 0; + $subtableId = 0; } // we then serialize the rows and store them in the serialized dataTable - $addToRows = array(self::ID_SUMMARY_ROW => $this->summaryRow); - - $aSerializedDataTable[$forcedId] = serialize($this->rows + $addToRows); - foreach ($this->rows as &$row) { - $row->cleanPostSerialize(); + $rows = array(); + foreach ($this->rows as $id => $row) { + if (array_key_exists($id, $consecutiveSubtableIds)) { + $backup = $row->subtableId; + $row->subtableId = $consecutiveSubtableIds[$id]; + $rows[$id] = $row->export(); + $row->subtableId = $backup; + } else { + $rows[$id] = $row->export(); + } } + if (isset($this->summaryRow)) { + $rows[self::ID_SUMMARY_ROW] = $this->summaryRow->export(); + } + + $aSerializedDataTable[$forcedId] = serialize($rows); + unset($rows); + return $aSerializedDataTable; } + private static $previousRowClasses = array('O:39:"Piwik\DataTable\Row\DataTableSummaryRow"', 'O:19:"Piwik\DataTable\Row"', 'O:36:"Piwik_DataTable_Row_DataTableSummary"', 'O:19:"Piwik_DataTable_Row"'); + private static $rowClassToUseForUnserialize = 'O:29:"Piwik_DataTable_SerializedRow"'; + + /** + * It is faster to unserialize existing serialized Row instances to "Piwik_DataTable_SerializedRow" and access the + * `$row->c` property than implementing a "__wakeup" method in the Row instance to map the "$row->c" to $row->columns + * etc. We're talking here about 15% faster reports aggregation in some cases. To be concrete: We have a test where + * Archiving a year takes 1700 seconds with "__wakeup" and 1400 seconds with this method. Yes, it takes 300 seconds + * to wake up millions of rows. We should be able to remove this code here end 2015 and use the "__wakeup" way by then. + * Why? By then most new archives will have only arrays serialized anyway and therefore this mapping is rather an overhead. + * + * @param string $serialized + * @return array + * @throws Exception In case the unserialize fails + */ + private function unserializeRows($serialized) + { + $serialized = str_replace(self::$previousRowClasses, self::$rowClassToUseForUnserialize, $serialized); + $rows = unserialize($serialized); + + if ($rows === false) { + throw new Exception("The unserialization has failed!"); + } + + return $rows; + } + /** * Adds a set of rows from a serialized DataTable string. * * See {@link serialize()}. - * + * * _Note: This function will successfully load DataTables serialized by Piwik 1.X._ - * - * @param string $stringSerialized A string with the format of a string in the array returned by + * + * @param string $serialized A string with the format of a string in the array returned by * {@link serialize()}. - * @throws Exception if `$stringSerialized` is invalid. + * @throws Exception if `$serialized` is invalid. */ - public function addRowsFromSerializedArray($stringSerialized) + public function addRowsFromSerializedArray($serialized) { - require_once PIWIK_INCLUDE_PATH . "/core/DataTable/Bridges.php"; - - $serialized = unserialize($stringSerialized); - if ($serialized === false) { - throw new Exception("The unserialization has failed!"); + $rows = $this->unserializeRows($serialized); + + if (array_key_exists(self::ID_SUMMARY_ROW, $rows)) { + if (is_array($rows[self::ID_SUMMARY_ROW])) { + $this->summaryRow = new Row($rows[self::ID_SUMMARY_ROW]); + } elseif (isset($rows[self::ID_SUMMARY_ROW]->c)) { + $this->summaryRow = new Row($rows[self::ID_SUMMARY_ROW]->c); // Pre Piwik 2.13 + } + unset($rows[self::ID_SUMMARY_ROW]); + } + + foreach ($rows as $id => $row) { + if (isset($row->c)) { + $this->addRow(new Row($row->c)); // Pre Piwik 2.13 + } else { + $this->addRow(new Row($row)); + } } - $this->addRowsFromArray($serialized); } /** * Adds multiple rows from an array. - * + * * You can add row metadata with this method. * * @param array $array Array with the following structure - * + * * array( * // row1 * array( @@ -1183,6 +1387,7 @@ class DataTable implements DataTableInterface if (is_array($row)) { $row = new Row($row); } + if ($id == self::ID_SUMMARY_ROW) { $this->summaryRow = $row; } else { @@ -1193,11 +1398,11 @@ class DataTable implements DataTableInterface /** * Adds multiple rows from an array containing arrays of column values. - * + * * Row metadata cannot be added with this method. * * @param array $array Array with the following structure: - * + * * array( * array( col1_name => valueA, col2_name => valueC, ...), * array( col1_name => valueB, col2_name => valueD, ...), @@ -1210,11 +1415,10 @@ class DataTable implements DataTableInterface return; } - // we define an exception we may throw if at one point we notice that we cannot handle the data structure - $e = new Exception(" Data structure returned is not convertible in the requested format." . + $exceptionText = " Data structure returned is not convertible in the requested format." . " Try to call this method with the parameters '&format=original&serialize=1'" . "; you will get the original php data structure serialized." . - " The data structure looks like this: \n \$data = " . var_export($array, true) . "; "); + " The data structure looks like this: \n \$data = %s; "; // first pass to see if the array has the structure // array(col1_name => val1, col2_name => val2, etc.) @@ -1255,12 +1459,13 @@ class DataTable implements DataTableInterface // it cannot be lost during the conversion. Because we are not able to handle properly // this key, we throw an explicit exception. if (is_string($key)) { - throw $e; + // we define an exception we may throw if at one point we notice that we cannot handle the data structure + throw new Exception(sprintf($exceptionText, var_export($array, true))); } // if any of the sub elements of row is an array we cannot handle this data structure... foreach ($row as $subRow) { if (is_array($subRow)) { - throw $e; + throw new Exception(sprintf($exceptionText, var_export($array, true))); } } $row = new Row(array(Row::COLUMNS => $row)); @@ -1274,28 +1479,28 @@ class DataTable implements DataTableInterface /** * Rewrites the input `$array` - * + * * array ( * LABEL => array(col1 => X, col2 => Y), * LABEL2 => array(col1 => X, col2 => Y), * ) - * + * * to a DataTable with rows that look like: - * + * * array ( * array( Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y)), * array( Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)), * ) * - * Will also convert arrays like: - * + * Will also convert arrays like: + * * array ( * LABEL => X, * LABEL2 => Y, * ) - * + * * to: - * + * * array ( * array( Row::COLUMNS => array('label' => LABEL, 'value' => X)), * array( Row::COLUMNS => array('label' => LABEL2, 'value' => Y)), @@ -1329,11 +1534,11 @@ class DataTable implements DataTableInterface /** * Sets the maximum depth level to at least a certain value. If the current value is * greater than `$atLeastLevel`, the maximum nesting level is not changed. - * + * * The maximum depth level determines the maximum number of subtable levels in the * DataTable tree. For example, if it is set to `2`, this DataTable is allowed to * have subtables, but the subtables are not. - * + * * @param int $atLeastLevel */ public static function setMaximumDepthLevelAllowedAtLeast($atLeastLevel) @@ -1381,7 +1586,7 @@ class DataTable implements DataTableInterface /** * Sets several metadata values by name. - * + * * @param array $values Array mapping metadata names with metadata values. */ public function setMetadataValues($values) @@ -1393,7 +1598,7 @@ class DataTable implements DataTableInterface /** * Sets metadata, erasing existing values. - * + * * @param array $values Array mapping metadata names with metadata values. */ public function setAllTableMetadata($metadata) @@ -1417,14 +1622,14 @@ class DataTable implements DataTableInterface * Traverses a DataTable tree using an array of labels and returns the row * it finds or `false` if it cannot find one. The number of path segments that * were successfully walked is also returned. - * + * * If `$missingRowColumns` is supplied, the specified path is created. When * a subtable is encountered w/o the required label, a new row is created * with the label, and a new subtable is added to the row. * * Read [http://en.wikipedia.org/wiki/Tree_(data_structure)#Traversal_methods](http://en.wikipedia.org/wiki/Tree_(data_structure)#Traversal_methods) * for more information about tree walking. - * + * * @param array $path The path to walk. An array of label values. The first element * refers to a row in this DataTable, the second in a subtable of * the first row, the third a subtable of the second row, etc. @@ -1452,15 +1657,17 @@ class DataTable implements DataTableInterface // if there is no table to advance to, and we're not adding missing rows, return false if ($missingRowColumns === false) { return array(false, $i); - } else // if we're adding missing rows, add a new row - { + } else { + // if we're adding missing rows, add a new row + $row = new DataTableSummaryRow(); $row->setColumns(array('label' => $segment) + $missingRowColumns); $next = $table->addRow($row); - if ($next !== $row) // if the row wasn't added, the table is full - { + if ($next !== $row) { + // if the row wasn't added, the table is full + // Summary row, has no metadata $next->deleteMetadata(); return array($next, $i); @@ -1474,8 +1681,9 @@ class DataTable implements DataTableInterface // missing rows, return false if ($missingRowColumns === false) { return array(false, $i); - } else if ($i != $pathLength - 1) // create subtable if missing, but only if not on the last segment - { + } elseif ($i != $pathLength - 1) { + // create subtable if missing, but only if not on the last segment + $table = new DataTable(); $table->setMaximumAllowedRows($maxSubtableRows); $table->metadata[self::COLUMN_AGGREGATION_OPS_METADATA_NAME] @@ -1495,7 +1703,7 @@ class DataTable implements DataTableInterface * * @param string|bool $labelColumn If supplied the label of the parent row will be added to * a new column in each subtable row. - * + * * If set to, `'label'` each subtable row's label will be prepended * w/ the parent row's label. So `'child_label'` becomes * `'parent_label - child_label'`. @@ -1506,7 +1714,7 @@ class DataTable implements DataTableInterface public function mergeSubtables($labelColumn = false, $useMetadataColumn = false) { $result = new DataTable(); - foreach ($this->getRows() as $row) { + foreach ($this->getRowsWithoutSummaryRow() as $row) { $subtable = $row->getSubtable(); if ($subtable !== false) { $parentLabel = $row->getColumn('label'); @@ -1551,9 +1759,9 @@ class DataTable implements DataTableInterface /** * Returns a new DataTable created with data from a 'simple' array. - * + * * See {@link addRowsFromSimpleArray()}. - * + * * @param array $array * @return \Piwik\DataTable */ @@ -1566,7 +1774,7 @@ class DataTable implements DataTableInterface /** * Creates a new DataTable instance from a serialized DataTable string. - * + * * See {@link getSerialized()} and {@link addRowsFromSerializedArray()} * for more information on DataTable serialization. * @@ -1586,9 +1794,10 @@ class DataTable implements DataTableInterface * $row must have a column "label". The $row will be summed to this table's row with the same label. * * @param $row + * @params null|array $columnAggregationOps * @throws \Exception */ - protected function aggregateRowWithLabel(Row $row, $doAggregateSubTables = true) + protected function aggregateRowWithLabel(Row $row, $columnAggregationOps) { $labelToLookFor = $row->getColumn('label'); if ($labelToLookFor === false) { @@ -1602,19 +1811,16 @@ class DataTable implements DataTableInterface $this->addRow($row); } } else { - $rowFound->sumRow($row, $copyMeta = true, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME)); + $rowFound->sumRow($row, $copyMeta = true, $columnAggregationOps); - if($doAggregateSubTables) { - // if the row to add has a subtable whereas the current row doesn't - // we simply add it (cloning the subtable) - // if the row has the subtable already - // then we have to recursively sum the subtables - if (($idSubTable = $row->getIdSubDataTable()) !== null) { - $subTable = Manager::getInstance()->getTable($idSubTable); - $subTable->metadata[self::COLUMN_AGGREGATION_OPS_METADATA_NAME] - = $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME); - $rowFound->sumSubtable($subTable); - } + // if the row to add has a subtable whereas the current row doesn't + // we simply add it (cloning the subtable) + // if the row has the subtable already + // then we have to recursively sum the subtables + $subTable = $row->getSubtable(); + if ($subTable) { + $subTable->metadata[self::COLUMN_AGGREGATION_OPS_METADATA_NAME] = $columnAggregationOps; + $rowFound->sumSubtable($subTable); } } } @@ -1626,7 +1832,6 @@ class DataTable implements DataTableInterface { if ($row === false) { return; - } $thisRow = $this->getFirstRow(); if ($thisRow === false) { @@ -1635,4 +1840,42 @@ class DataTable implements DataTableInterface } $thisRow->sumRow($row, $copyMeta = true, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME)); } -} \ No newline at end of file + + /** + * Unsets all queued filters. + */ + public function clearQueuedFilters() + { + $this->queuedFilters = array(); + } + + /** + * @return \ArrayIterator|Row[] + */ + public function getIterator() + { + return new \ArrayIterator($this->getRows()); + } + + public function offsetExists($offset) + { + $row = $this->getRowFromId($offset); + + return false !== $row; + } + + public function offsetGet($offset) + { + return $this->getRowFromId($offset); + } + + public function offsetSet($offset, $value) + { + $this->rows[$offset] = $value; + } + + public function offsetUnset($offset) + { + $this->deleteRow($offset); + } +} diff --git a/www/analytics/core/DataTable/BaseFilter.php b/www/analytics/core/DataTable/BaseFilter.php index 22c926e5..ce423112 100644 --- a/www/analytics/core/DataTable/BaseFilter.php +++ b/www/analytics/core/DataTable/BaseFilter.php @@ -1,6 +1,6 @@ enableRecursive) { return; } - if ($row->isSubtableLoaded()) { - $subTable = Manager::getInstance()->getTable($row->getIdSubDataTable()); + $subTable = $row->getSubtable(); + if ($subTable) { $this->filter($subTable); } } diff --git a/www/analytics/core/DataTable/Bridges.php b/www/analytics/core/DataTable/Bridges.php index cf924e03..ccc25736 100644 --- a/www/analytics/core/DataTable/Bridges.php +++ b/www/analytics/core/DataTable/Bridges.php @@ -1,6 +1,6 @@ filter('AddColumnsProcessedMetrics'); - * + * * @api */ class AddColumnsProcessedMetrics extends BaseFilter @@ -43,7 +46,7 @@ class AddColumnsProcessedMetrics extends BaseFilter /** * Constructor. - * + * * @param DataTable $table The table to eventually filter. * @param bool $deleteRowsWithNoVisit Whether to delete rows with no visits or not. */ @@ -61,89 +64,32 @@ class AddColumnsProcessedMetrics extends BaseFilter */ public function filter($table) { - $rowsIdToDelete = array(); + if ($this->deleteRowsWithNoVisit) { + $this->deleteRowsWithNoVisit($table); + } + + $extraProcessedMetrics = $table->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME); + + $extraProcessedMetrics[] = new ConversionRate(); + $extraProcessedMetrics[] = new ActionsPerVisit(); + $extraProcessedMetrics[] = new AverageTimeOnSite(); + $extraProcessedMetrics[] = new BounceRate(); + + $table->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, $extraProcessedMetrics); + } + + private function deleteRowsWithNoVisit(DataTable $table) + { foreach ($table->getRows() as $key => $row) { - $nbVisits = $this->getColumn($row, Metrics::INDEX_NB_VISITS); - $nbActions = $this->getColumn($row, Metrics::INDEX_NB_ACTIONS); + $nbVisits = Metric::getMetric($row, 'nb_visits'); + $nbActions = Metric::getMetric($row, 'nb_actions'); + if ($nbVisits == 0 && $nbActions == 0 - && $this->deleteRowsWithNoVisit ) { - // case of keyword/website/campaign with a conversion for this day, - // but no visit, we don't show it - $rowsIdToDelete[] = $key; - continue; + // case of keyword/website/campaign with a conversion for this day, but no visit, we don't show it + $table->deleteRow($key); } - - $nbVisitsConverted = (int)$this->getColumn($row, Metrics::INDEX_NB_VISITS_CONVERTED); - if ($nbVisitsConverted > 0) { - $conversionRate = round(100 * $nbVisitsConverted / $nbVisits, $this->roundPrecision); - try { - $row->addColumn('conversion_rate', $conversionRate . "%"); - } catch (\Exception $e) { - // conversion_rate can be defined upstream apparently? FIXME - } - } - - if ($nbVisits == 0) { - $actionsPerVisit = $averageTimeOnSite = $bounceRate = $this->invalidDivision; - } else { - // nb_actions / nb_visits => Actions/visit - // sum_visit_length / nb_visits => Avg. Time on Site - // bounce_count / nb_visits => Bounce Rate - $actionsPerVisit = round($nbActions / $nbVisits, $this->roundPrecision); - $visitLength = $this->getColumn($row, Metrics::INDEX_SUM_VISIT_LENGTH); - $averageTimeOnSite = round($visitLength / $nbVisits, $rounding = 0); - $bounceRate = round(100 * $this->getColumn($row, Metrics::INDEX_BOUNCE_COUNT) / $nbVisits, $this->roundPrecision); - } - try { - $row->addColumn('nb_actions_per_visit', $actionsPerVisit); - $row->addColumn('avg_time_on_site', $averageTimeOnSite); - // It could be useful for API users to have raw sum length value. - //$row->addMetadata('sum_visit_length', $visitLength); - } catch (\Exception $e) { - } - - try { - $row->addColumn('bounce_rate', $bounceRate . "%"); - } catch (\Exception $e) { - } - - $this->filterSubTable($row); } - $table->deleteRows($rowsIdToDelete); - } - - /** - * Returns column from a given row. - * Will work with 2 types of datatable - * - raw datatables coming from the archive DB, which columns are int indexed - * - datatables processed resulting of API calls, which columns have human readable english names - * - * @param Row|array $row - * @param int $columnIdRaw see consts in Archive:: - * @param bool|array $mappingIdToName - * @return mixed Value of column, false if not found - */ - protected function getColumn($row, $columnIdRaw, $mappingIdToName = false) - { - if (empty($mappingIdToName)) { - $mappingIdToName = Metrics::$mappingFromIdToName; - } - $columnIdReadable = $mappingIdToName[$columnIdRaw]; - if ($row instanceof Row) { - $raw = $row->getColumn($columnIdRaw); - if ($raw !== false) { - return $raw; - } - return $row->getColumn($columnIdReadable); - } - if (isset($row[$columnIdRaw])) { - return $row[$columnIdRaw]; - } - if (isset($row[$columnIdReadable])) { - return $row[$columnIdReadable]; - } - return false; } } diff --git a/www/analytics/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php b/www/analytics/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php index 05a16631..7bdb7c79 100644 --- a/www/analytics/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php +++ b/www/analytics/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php @@ -1,6 +1,6 @@ filter('AddColumnsProcessedMetricsGoal', * array($enable = true, $idGoal = Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER)); - * + * * @api */ class AddColumnsProcessedMetricsGoal extends AddColumnsProcessedMetrics @@ -68,7 +75,7 @@ class AddColumnsProcessedMetricsGoal extends AddColumnsProcessedMetrics /** * Constructor. - * + * * @param DataTable $table The table that will eventually filtered. * @param bool $enable Always set to true. * @param string $processOnlyIdGoal Defines what metrics to add (don't process metrics when you don't display them). @@ -76,13 +83,14 @@ class AddColumnsProcessedMetricsGoal extends AddColumnsProcessedMetrics * If self::GOALS_OVERVIEW, only the main goal metrics will be added. * If an int > 0, then will process only metrics for this specific Goal. */ - public function __construct($table, $enable = true, $processOnlyIdGoal) + public function __construct($table, $enable = true, $processOnlyIdGoal, $goalsToProcess = null) { $this->processOnlyIdGoal = $processOnlyIdGoal; $this->isEcommerce = $this->processOnlyIdGoal == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER || $this->processOnlyIdGoal == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART; parent::__construct($table); // Ensure that all rows with no visit but conversions will be displayed $this->deleteRowsWithNoVisit = false; + $this->goalsToProcess = $goalsToProcess; } /** @@ -95,132 +103,63 @@ class AddColumnsProcessedMetricsGoal extends AddColumnsProcessedMetrics { // Add standard processed metrics parent::filter($table); - $roundingPrecision = GoalManager::REVENUE_PRECISION; - $expectedColumns = array(); - foreach ($table->getRows() as $key => $row) { - $currentColumns = $row->getColumns(); - $newColumns = array(); - // visits could be undefined when there is a conversion but no visit - $nbVisits = (int)$this->getColumn($row, Metrics::INDEX_NB_VISITS); - $conversions = (int)$this->getColumn($row, Metrics::INDEX_NB_CONVERSIONS); - $goals = $this->getColumn($currentColumns, Metrics::INDEX_GOALS); - if ($goals) { - $revenue = 0; - foreach ($goals as $goalId => $goalMetrics) { - if ($goalId == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART) { - continue; - } - if ($goalId >= GoalManager::IDGOAL_ORDER - || $goalId == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER - ) { - $revenue += (int)$this->getColumn($goalMetrics, Metrics::INDEX_GOAL_REVENUE, Metrics::$mappingFromIdToNameGoal); - } - } + $goals = $this->getGoalsInTable($table); + if (!empty($this->goalsToProcess)) { + $goals = array_unique(array_merge($goals, $this->goalsToProcess)); + sort($goals); + } - if ($revenue == 0) { - $revenue = (int)$this->getColumn($currentColumns, Metrics::INDEX_REVENUE); - } - if (!isset($currentColumns['revenue_per_visit'])) { - // If no visit for this metric, but some conversions, we still want to display some kind of "revenue per visit" - // even though it will actually be in this edge case "Revenue per conversion" - $revenuePerVisit = $this->invalidDivision; - if ($nbVisits > 0 - || $conversions > 0 - ) { - $revenuePerVisit = round($revenue / ($nbVisits == 0 ? $conversions : $nbVisits), $roundingPrecision); - } - $newColumns['revenue_per_visit'] = $revenuePerVisit; - } - if ($this->processOnlyIdGoal == self::GOALS_MINIMAL_REPORT) { - $row->addColumns($newColumns); + $idSite = DataTableFactory::getSiteIdFromMetadata($table); + + $extraProcessedMetrics = $table->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME); + + $extraProcessedMetrics[] = new RevenuePerVisit(); + if ($this->processOnlyIdGoal != self::GOALS_MINIMAL_REPORT) { + foreach ($goals as $idGoal) { + if (($this->processOnlyIdGoal > self::GOALS_FULL_TABLE + || $this->isEcommerce) + && $this->processOnlyIdGoal != $idGoal + ) { continue; } - // Display per goal metrics - // - conversion rate - // - conversions - // - revenue per visit - foreach ($goals as $goalId => $goalMetrics) { - $goalId = str_replace("idgoal=", "", $goalId); - if (($this->processOnlyIdGoal > self::GOALS_FULL_TABLE - || $this->isEcommerce) - && $this->processOnlyIdGoal != $goalId - ) { - continue; - } - $conversions = (int)$this->getColumn($goalMetrics, Metrics::INDEX_GOAL_NB_CONVERSIONS, Metrics::$mappingFromIdToNameGoal); - // Goal Conversion rate - $name = 'goal_' . $goalId . '_conversion_rate'; - if ($nbVisits == 0) { - $value = $this->invalidDivision; - } else { - $value = round(100 * $conversions / $nbVisits, $roundingPrecision); - } - $newColumns[$name] = $value . "%"; - $expectedColumns[$name] = true; + $extraProcessedMetrics[] = new ConversionRate($idSite, $idGoal); // PerGoal\ConversionRate - // When the table is displayed by clicking on the flag icon, we only display the columns - // Visits, Conversions, Per goal conversion rate, Revenue - if ($this->processOnlyIdGoal == self::GOALS_OVERVIEW) { - continue; - } - - // Goal Conversions - $name = 'goal_' . $goalId . '_nb_conversions'; - $newColumns[$name] = $conversions; - $expectedColumns[$name] = true; - - // Goal Revenue per visit - $name = 'goal_' . $goalId . '_revenue_per_visit'; - // See comment above for $revenuePerVisit - $goalRevenue = (float)$this->getColumn($goalMetrics, Metrics::INDEX_GOAL_REVENUE, Metrics::$mappingFromIdToNameGoal); - $revenuePerVisit = round($goalRevenue / ($nbVisits == 0 ? $conversions : $nbVisits), $roundingPrecision); - $newColumns[$name] = $revenuePerVisit; - $expectedColumns[$name] = true; - - // Total revenue - $name = 'goal_' . $goalId . '_revenue'; - $newColumns[$name] = $goalRevenue; - $expectedColumns[$name] = true; - - if ($this->isEcommerce) { - - // AOV Average Order Value - $name = 'goal_' . $goalId . '_avg_order_revenue'; - $newColumns[$name] = $goalRevenue / $conversions; - $expectedColumns[$name] = true; - - // Items qty - $name = 'goal_' . $goalId . '_items'; - $newColumns[$name] = $this->getColumn($goalMetrics, Metrics::INDEX_GOAL_ECOMMERCE_ITEMS, Metrics::$mappingFromIdToNameGoal); - $expectedColumns[$name] = true; - } + // When the table is displayed by clicking on the flag icon, we only display the columns + // Visits, Conversions, Per goal conversion rate, Revenue + if ($this->processOnlyIdGoal == self::GOALS_OVERVIEW) { + continue; } - } - // conversion_rate can be defined upstream apparently? FIXME - try { - $row->addColumns($newColumns); - } catch (Exception $e) { - } - } - $expectedColumns['revenue_per_visit'] = true; + $extraProcessedMetrics[] = new Conversions($idSite, $idGoal); // PerGoal\Conversions or GoalSpecific\ + $extraProcessedMetrics[] = new GoalSpecificRevenuePerVisit($idSite, $idGoal); // PerGoal\Revenue + $extraProcessedMetrics[] = new Revenue($idSite, $idGoal); // PerGoal\Revenue - // make sure all goals values are set, 0 by default - // if no value then sorting would put at the end - $expectedColumns = array_keys($expectedColumns); - $rows = $table->getRows(); - foreach ($rows as &$row) { - foreach ($expectedColumns as $name) { - if (false === $row->getColumn($name)) { - $value = 0; - if (strpos($name, 'conversion_rate') !== false) { - $value = '0%'; - } - $row->addColumn($name, $value); + if ($this->isEcommerce) { + $extraProcessedMetrics[] = new AverageOrderRevenue($idSite, $idGoal); + $extraProcessedMetrics[] = new ItemsCount($idSite, $idGoal); } } } + + $table->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, $extraProcessedMetrics); + } + + private function getGoalsInTable(DataTable $table) + { + $result = array(); + foreach ($table->getRows() as $row) { + $goals = Metric::getMetric($row, 'goals'); + if (!$goals) { + continue; + } + + foreach ($goals as $goalId => $goalMetrics) { + $goalId = str_replace("idgoal=", "", $goalId); + $result[] = $goalId; + } + } + return array_unique($result); } } diff --git a/www/analytics/core/DataTable/Filter/AddSegmentByLabel.php b/www/analytics/core/DataTable/Filter/AddSegmentByLabel.php new file mode 100644 index 00000000..1eacd4f7 --- /dev/null +++ b/www/analytics/core/DataTable/Filter/AddSegmentByLabel.php @@ -0,0 +1,99 @@ +filter('AddSegmentByLabel', array('segmentName')); + * $dataTable->filter('AddSegmentByLabel', array(array('segmentName1', 'segment2'), ';'); + * + * @api + */ +class AddSegmentByLabel extends BaseFilter +{ + private $segments; + private $delimiter; + + /** + * Generates a segment filter based on the label column and the given segment names + * + * @param DataTable $table + * @param string|array $segmentOrSegments Either one segment or an array of segments. + * If more than one segment is given a delimter has to be defined. + * @param string $delimiter The delimiter by which the label should be splitted. + */ + public function __construct($table, $segmentOrSegments, $delimiter = '') + { + parent::__construct($table); + + if (!is_array($segmentOrSegments)) { + $segmentOrSegments = array($segmentOrSegments); + } + + $this->segments = $segmentOrSegments; + $this->delimiter = $delimiter; + } + + /** + * See {@link AddSegmentByLabel}. + * + * @param DataTable $table + */ + public function filter($table) + { + if (empty($this->segments)) { + $msg = 'AddSegmentByLabel is called without having any segments defined'; + Development::error($msg); + return; + } + + if (count($this->segments) === 1) { + $segment = reset($this->segments); + + foreach ($table->getRowsWithoutSummaryRow() as $key => $row) { + $label = $row->getColumn('label'); + + if (!empty($label)) { + $row->setMetadata('segment', $segment . '==' . urlencode($label)); + } + } + } elseif (!empty($this->delimiter)) { + $numSegments = count($this->segments); + $conditionAnd = ';'; + + foreach ($table->getRowsWithoutSummaryRow() as $key => $row) { + $label = $row->getColumn('label'); + if (!empty($label)) { + $parts = explode($this->delimiter, $label); + + if (count($parts) === $numSegments) { + $filter = array(); + foreach ($this->segments as $index => $segment) { + if (!empty($segment)) { + $filter[] = $segment . '==' . urlencode($parts[$index]); + } + } + $row->setMetadata('segment', implode($conditionAnd, $filter)); + } + } + } + } else { + $names = implode(', ', $this->segments); + $msg = 'Multiple segments are given but no delimiter defined. Segments: ' . $names; + Development::error($msg); + } + } +} diff --git a/www/analytics/core/DataTable/Filter/AddSegmentByLabelMapping.php b/www/analytics/core/DataTable/Filter/AddSegmentByLabelMapping.php new file mode 100644 index 00000000..29fbdced --- /dev/null +++ b/www/analytics/core/DataTable/Filter/AddSegmentByLabelMapping.php @@ -0,0 +1,63 @@ +filter('AddSegmentByLabelMapping', array('segmentName', array('1' => 'smartphone, '2' => 'desktop'))); + * + * @api + */ +class AddSegmentByLabelMapping extends BaseFilter +{ + private $segment; + private $mapping; + + /** + * @param DataTable $table + * @param string $segment + * @param array $mapping + */ + public function __construct($table, $segment, $mapping) + { + parent::__construct($table); + + $this->segment = $segment; + $this->mapping = $mapping; + } + + /** + * See {@link AddSegmentByLabelMapping}. + * + * @param DataTable $table + */ + public function filter($table) + { + if (empty($this->segment) || empty($this->mapping)) { + return; + } + + foreach ($table->getRows() as $row) { + $label = $row->getColumn('label'); + + if (!empty($this->mapping[$label])) { + $label = $this->mapping[$label]; + $row->setMetadata('segment', $this->segment . '==' . urlencode($label)); + } + } + } +} diff --git a/www/analytics/core/DataTable/Filter/AddSegmentBySegmentValue.php b/www/analytics/core/DataTable/Filter/AddSegmentBySegmentValue.php new file mode 100644 index 00000000..7417a48c --- /dev/null +++ b/www/analytics/core/DataTable/Filter/AddSegmentBySegmentValue.php @@ -0,0 +1,78 @@ +filter('AddSegmentBySegmentValue', array($reportInstance)); + * + * @api + */ +class AddSegmentBySegmentValue extends BaseFilter +{ + /** + * @var \Piwik\Plugin\Report + */ + private $report; + + /** + * @param DataTable $table + * @param $report + */ + public function __construct($table, $report) + { + parent::__construct($table); + $this->report = $report; + } + + /** + * See {@link AddSegmentBySegmentValue}. + * + * @param DataTable $table + * @return int The number of deleted rows. + */ + public function filter($table) + { + if (empty($this->report) || !$table->getRowsCount()) { + return; + } + + $dimension = $this->report->getDimension(); + + if (empty($dimension)) { + return; + } + + $segments = $dimension->getSegments(); + + if (empty($segments)) { + return; + } + + /** @var \Piwik\Plugin\Segment $segment */ + $segment = reset($segments); + $segmentName = $segment->getSegment(); + + foreach ($table->getRows() as $row) { + $value = $row->getMetadata('segmentValue'); + $filter = $row->getMetadata('segment'); + + if ($value !== false && $filter === false) { + $row->setMetadata('segment', sprintf('%s==%s', $segmentName, urlencode($value))); + } + } + } +} diff --git a/www/analytics/core/DataTable/Filter/AddSegmentValue.php b/www/analytics/core/DataTable/Filter/AddSegmentValue.php new file mode 100644 index 00000000..886db60d --- /dev/null +++ b/www/analytics/core/DataTable/Filter/AddSegmentValue.php @@ -0,0 +1,32 @@ +filter('AddSegmentValue', array()); + * $dataTable->filter('AddSegmentValue', array(function ($label) { + * $transformedValue = urldecode($transformedValue); + * return $transformedValue; + * }); + * + * @api + */ +class AddSegmentValue extends ColumnCallbackAddMetadata +{ + public function __construct($table, $callback = null) + { + parent::__construct($table, 'label', 'segmentValue', $callback, null, false); + } +} diff --git a/www/analytics/core/DataTable/Filter/AddSummaryRow.php b/www/analytics/core/DataTable/Filter/AddSummaryRow.php index 20de397a..9cb4b3e2 100644 --- a/www/analytics/core/DataTable/Filter/AddSummaryRow.php +++ b/www/analytics/core/DataTable/Filter/AddSummaryRow.php @@ -1,6 +1,6 @@ filter('AddSummaryRow'); - * + * * // use a human readable label for the summary row (instead of '-1') * $dataTable->filter('AddSummaryRow', array($labelSummaryRow = Piwik::translate('General_Total'))); - * + * * @api */ class AddSummaryRow extends BaseFilter diff --git a/www/analytics/core/DataTable/Filter/BeautifyRangeLabels.php b/www/analytics/core/DataTable/Filter/BeautifyRangeLabels.php index dac3aad2..bf5e8d65 100644 --- a/www/analytics/core/DataTable/Filter/BeautifyRangeLabels.php +++ b/www/analytics/core/DataTable/Filter/BeautifyRangeLabels.php @@ -1,6 +1,6 @@ queueFilter('BeautifyRangeLabels', array("1 visit", "%s visits")); - * + * * @api */ class BeautifyRangeLabels extends ColumnCallbackReplace @@ -65,7 +65,7 @@ class BeautifyRangeLabels extends ColumnCallbackReplace parent::__construct($table, 'label', array($this, 'beautify'), array()); $this->labelSingular = $labelSingular; - $this->labelPlural = $labelPlural; + $this->labelPlural = $labelPlural; } /** diff --git a/www/analytics/core/DataTable/Filter/BeautifyTimeRangeLabels.php b/www/analytics/core/DataTable/Filter/BeautifyTimeRangeLabels.php index fc4416fb..7cc21699 100644 --- a/www/analytics/core/DataTable/Filter/BeautifyTimeRangeLabels.php +++ b/www/analytics/core/DataTable/Filter/BeautifyTimeRangeLabels.php @@ -1,6 +1,6 @@ filter('BeautifyTimeRangeLabels', array("%1$s-%2$s min", "1 min", "%s min")); - * + * * @api */ class BeautifyTimeRangeLabels extends BeautifyRangeLabels @@ -70,7 +70,7 @@ class BeautifyTimeRangeLabels extends BeautifyRangeLabels { if ($lowerBound < 60) { return sprintf($this->labelSecondsPlural, $lowerBound, $lowerBound); - } else if ($lowerBound == 60) { + } elseif ($lowerBound == 60) { return $this->labelSingular; } else { return sprintf($this->labelPlural, ceil($lowerBound / 60)); diff --git a/www/analytics/core/DataTable/Filter/CalculateEvolutionFilter.php b/www/analytics/core/DataTable/Filter/CalculateEvolutionFilter.php index 24c86121..73274c53 100755 --- a/www/analytics/core/DataTable/Filter/CalculateEvolutionFilter.php +++ b/www/analytics/core/DataTable/Filter/CalculateEvolutionFilter.php @@ -1,6 +1,6 @@ getPastRowFromCurrent($row); - if (!$pastRow) return 0; + if (!$pastRow) { + return 0; + } return $pastRow->getColumn($this->columnNameUsedAsDivisor); } @@ -121,6 +126,9 @@ class CalculateEvolutionFilter extends ColumnCallbackAddColumnPercentage { $value = self::getPercentageValue($value, $divisor, $this->quotientPrecision); $value = self::appendPercentSign($value); + + $value = Common::forceDotAsSeparatorForDecimalPoint($value); + return $value; } @@ -150,9 +158,10 @@ class CalculateEvolutionFilter extends ColumnCallbackAddColumnPercentage { $number = self::getPercentageValue($currentValue - $pastValue, $pastValue, $quotientPrecision); if ($appendPercentSign) { - $number = self::appendPercentSign($number); + return NumberFormatter::getInstance()->formatPercent($number, $quotientPrecision); } - return $number; + + return NumberFormatter::getInstance()->format($number, $quotientPrecision); } public static function appendPercentSign($number) @@ -165,6 +174,7 @@ class CalculateEvolutionFilter extends ColumnCallbackAddColumnPercentage if ($number > 0) { $number = '+' . $number; } + return $number; } diff --git a/www/analytics/core/DataTable/Filter/ColumnCallbackAddColumn.php b/www/analytics/core/DataTable/Filter/ColumnCallbackAddColumn.php index 56ad449f..1d81f189 100755 --- a/www/analytics/core/DataTable/Filter/ColumnCallbackAddColumn.php +++ b/www/analytics/core/DataTable/Filter/ColumnCallbackAddColumn.php @@ -1,6 +1,6 @@ filter('ColumnCallbackAddColumn', array(array('nb_visits', 'sum_time_spent'), 'avg_time_on_site', $callback)); * * @api @@ -79,17 +80,33 @@ class ColumnCallbackAddColumn extends BaseFilter */ public function filter($table) { - foreach ($table->getRows() as $row) { + $columns = $this->columns; + $functionParams = $this->functionParameters; + $functionToApply = $this->functionToApply; + + $extraProcessedMetrics = $table->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME); + + if (empty($extraProcessedMetrics)) { + $extraProcessedMetrics = array(); + } + + $metric = new CallableProcessedMetric($this->columnToAdd, function (DataTable\Row $row) use ($columns, $functionParams, $functionToApply) { + $columnValues = array(); - foreach ($this->columns as $column) { + foreach ($columns as $column) { $columnValues[] = $row->getColumn($column); } - $parameters = array_merge($columnValues, $this->functionParameters); - $value = call_user_func_array($this->functionToApply, $parameters); + $parameters = array_merge($columnValues, $functionParams); - $row->setColumn($this->columnToAdd, $value); + return call_user_func_array($functionToApply, $parameters); + }, $columns); + $extraProcessedMetrics[] = $metric; + $table->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, $extraProcessedMetrics); + + foreach ($table->getRows() as $row) { + $row->setColumn($this->columnToAdd, $metric->compute($row)); $this->filterSubTable($row); } } diff --git a/www/analytics/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php b/www/analytics/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php index 78841bcb..056c1294 100644 --- a/www/analytics/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php +++ b/www/analytics/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php @@ -1,6 +1,6 @@ queueFilter('ColumnCallbackAddColumnPercentage', array('nb_visits', 'nb_visits_percentage', $nbVisits, 1)); * diff --git a/www/analytics/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php b/www/analytics/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php index 10d53024..57451675 100644 --- a/www/analytics/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php +++ b/www/analytics/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php @@ -1,6 +1,6 @@ queueFilter('ColumnCallbackAddColumnQuotient', array('bounce_rate', 'bounce_count', 'nb_visits', $precision = 2)); - * + * * @api */ class ColumnCallbackAddColumnQuotient extends BaseFilter @@ -38,7 +38,7 @@ class ColumnCallbackAddColumnQuotient extends BaseFilter /** * Constructor. - * + * * @param DataTable $table The DataTable that will eventually be filtered. * @param string $columnNameToAdd The name of the column to add the quotient value to. * @param string $columnValueToRead The name of the column that holds the dividend. @@ -75,7 +75,7 @@ class ColumnCallbackAddColumnQuotient extends BaseFilter */ public function filter($table) { - foreach ($table->getRows() as $key => $row) { + foreach ($table->getRows() as $row) { $value = $this->getDividend($row); if ($value === false && $this->shouldSkipRows) { continue; @@ -109,6 +109,7 @@ class ColumnCallbackAddColumnQuotient extends BaseFilter if ($divisor > 0 && $value > 0) { $quotient = round($value / $divisor, $this->quotientPrecision); } + return $quotient; } @@ -135,7 +136,7 @@ class ColumnCallbackAddColumnQuotient extends BaseFilter { if (!is_null($this->totalValueUsedAsDivisor)) { return $this->totalValueUsedAsDivisor; - } else if ($this->getDivisorFromSummaryRow) { + } elseif ($this->getDivisorFromSummaryRow) { $summaryRow = $this->table->getRowFromId(DataTable::ID_SUMMARY_ROW); return $summaryRow->getColumn($this->columnNameUsedAsDivisor); } else { diff --git a/www/analytics/core/DataTable/Filter/ColumnCallbackAddMetadata.php b/www/analytics/core/DataTable/Filter/ColumnCallbackAddMetadata.php index 34b36c4d..d302e641 100644 --- a/www/analytics/core/DataTable/Filter/ColumnCallbackAddMetadata.php +++ b/www/analytics/core/DataTable/Filter/ColumnCallbackAddMetadata.php @@ -1,6 +1,6 @@ filter('ColumnCallbackAddMetadata', array('label', 'logo', 'Piwik\Plugins\MyPlugin\getLogoFromLabel')); - * + * * @api */ class ColumnCallbackAddMetadata extends BaseFilter @@ -31,7 +31,7 @@ class ColumnCallbackAddMetadata extends BaseFilter /** * Constructor. - * + * * @param DataTable $table The DataTable instance that will be filtered. * @param string|array $columnsToRead The columns to read from each row and pass on to the callback. * @param string $metadataToAdd The name of the metadata field that will be added to each row. @@ -48,12 +48,12 @@ class ColumnCallbackAddMetadata extends BaseFilter if (!is_array($columnsToRead)) { $columnsToRead = array($columnsToRead); } - $this->columnsToRead = $columnsToRead; - $this->functionToApply = $functionToApply; + $this->columnsToRead = $columnsToRead; + $this->functionToApply = $functionToApply; $this->functionParameters = $functionParameters; - $this->metadataToAdd = $metadataToAdd; - $this->applyToSummaryRow = $applyToSummaryRow; + $this->metadataToAdd = $metadataToAdd; + $this->applyToSummaryRow = $applyToSummaryRow; } /** @@ -63,11 +63,13 @@ class ColumnCallbackAddMetadata extends BaseFilter */ public function filter($table) { - foreach ($table->getRows() as $key => $row) { - if (!$this->applyToSummaryRow && $key == DataTable::ID_SUMMARY_ROW) { - continue; - } + if ($this->applyToSummaryRow) { + $rows = $table->getRows(); + } else { + $rows = $table->getRowsWithoutSummaryRow(); + } + foreach ($rows as $key => $row) { $parameters = array(); foreach ($this->columnsToRead as $columnsToRead) { $parameters[] = $row->getColumn($columnsToRead); diff --git a/www/analytics/core/DataTable/Filter/ColumnCallbackDeleteMetadata.php b/www/analytics/core/DataTable/Filter/ColumnCallbackDeleteMetadata.php new file mode 100644 index 00000000..bb8618a6 --- /dev/null +++ b/www/analytics/core/DataTable/Filter/ColumnCallbackDeleteMetadata.php @@ -0,0 +1,55 @@ +filter('ColumnCallbackDeleteMetadata', array('segmentValue')); + * + * @api + */ +class ColumnCallbackDeleteMetadata extends BaseFilter +{ + private $metadataToRemove; + + /** + * Constructor. + * + * @param DataTable $table The DataTable instance that will be filtered. + * @param string $metadataToRemove The name of the metadata field that will be removed from each row. + */ + public function __construct($table, $metadataToRemove) + { + parent::__construct($table); + + $this->metadataToRemove = $metadataToRemove; + } + + /** + * See {@link ColumnCallbackDeleteMetadata}. + * + * @param DataTable $table + */ + public function filter($table) + { + $this->enableRecursive(true); + + foreach ($table->getRows() as $row) { + $row->deleteMetadata($this->metadataToRemove); + + $this->filterSubTable($row); + } + } +} diff --git a/www/analytics/core/DataTable/Filter/ColumnCallbackDeleteRow.php b/www/analytics/core/DataTable/Filter/ColumnCallbackDeleteRow.php index a51e1df8..414bf71e 100644 --- a/www/analytics/core/DataTable/Filter/ColumnCallbackDeleteRow.php +++ b/www/analytics/core/DataTable/Filter/ColumnCallbackDeleteRow.php @@ -1,6 +1,6 @@ filter('ColumnCallbackDeleteRow', array('label', function ($label) use ($labelsToRemove) { * return in_array($label, $labelsToRemove); * })); - * + * * @api */ class ColumnCallbackDeleteRow extends BaseFilter { - private $columnToFilter; private $function; private $functionParams; /** * Constructor. - * + * * @param DataTable $table The DataTable that will be filtered eventually. * @param array|string $columnsToFilter The column or array of columns that should be * passed to the callback. diff --git a/www/analytics/core/DataTable/Filter/ColumnCallbackReplace.php b/www/analytics/core/DataTable/Filter/ColumnCallbackReplace.php index 0b16aedb..b850b7a6 100644 --- a/www/analytics/core/DataTable/Filter/ColumnCallbackReplace.php +++ b/www/analytics/core/DataTable/Filter/ColumnCallbackReplace.php @@ -1,6 +1,6 @@ $truncateLength) { * return substr(0, $truncateLength); @@ -25,10 +25,11 @@ use Piwik\DataTable\Row; * return $value; * } * }; - * + * * // label, url and truncate_length are columns in $dataTable * $dataTable->filter('ColumnCallbackReplace', array('label', 'url'), $truncateString, null, array('truncate_length')); - * + * + * @api */ class ColumnCallbackReplace extends BaseFilter { @@ -39,7 +40,7 @@ class ColumnCallbackReplace extends BaseFilter /** * Constructor. - * + * * @param DataTable $table The DataTable to filter. * @param array|string $columnsToFilter The columns whose values should be passed to the callback * and then replaced with the callback's result. @@ -54,14 +55,14 @@ class ColumnCallbackReplace extends BaseFilter $extraColumnParameters = array()) { parent::__construct($table); - $this->functionToApply = $functionToApply; + $this->functionToApply = $functionToApply; $this->functionParameters = $functionParameters; if (!is_array($columnsToFilter)) { $columnsToFilter = array($columnsToFilter); } - $this->columnsToFilter = $columnsToFilter; + $this->columnsToFilter = $columnsToFilter; $this->extraColumnParameters = $extraColumnParameters; } @@ -72,13 +73,14 @@ class ColumnCallbackReplace extends BaseFilter */ public function filter($table) { - foreach ($table->getRows() as $key => $row) { + foreach ($table->getRows() as $row) { $extraColumnParameters = array(); foreach ($this->extraColumnParameters as $columnName) { $extraColumnParameters[] = $row->getColumn($columnName); } foreach ($this->columnsToFilter as $column) { + // when a value is not defined, we set it to zero by default (rather than displaying '-') $value = $this->getElementToReplace($row, $column); if ($value === false) { @@ -86,14 +88,21 @@ class ColumnCallbackReplace extends BaseFilter } $parameters = array_merge(array($value), $extraColumnParameters); + if (!is_null($this->functionParameters)) { $parameters = array_merge($parameters, $this->functionParameters); } + $newValue = call_user_func_array($this->functionToApply, $parameters); $this->setElementToReplace($row, $column, $newValue); $this->filterSubTable($row); } } + + if (in_array('label', $this->columnsToFilter)) { + // we need to force rebuilding the index + $table->setLabelsHaveChanged(); + } } /** diff --git a/www/analytics/core/DataTable/Filter/ColumnDelete.php b/www/analytics/core/DataTable/Filter/ColumnDelete.php index 1b724109..a0fba5a9 100644 --- a/www/analytics/core/DataTable/Filter/ColumnDelete.php +++ b/www/analytics/core/DataTable/Filter/ColumnDelete.php @@ -1,6 +1,6 @@ filter('ColumnDelete', array($columnsToRemove)); - * + * * $columnsToKeep = array('nb_visits'); * $dataTable->filter('ColumnDelete', array(array(), $columnsToKeep)); - * + * * @api */ class ColumnDelete extends BaseFilter @@ -91,6 +91,7 @@ class ColumnDelete extends BaseFilter * See {@link ColumnDelete}. * * @param DataTable $table + * @return DataTable */ public function filter($table) { @@ -100,15 +101,16 @@ class ColumnDelete extends BaseFilter // remove columns specified in $this->columnsToRemove if (!empty($this->columnsToRemove)) { - foreach ($table->getRows() as $row) { + foreach ($table as $index => $row) { foreach ($this->columnsToRemove as $column) { if ($this->deleteIfZeroOnly) { - $value = $row->getColumn($column); + $value = $row[$column]; if ($value === false || !empty($value)) { continue; } } - $row->deleteColumn($column); + + unset($table[$index][$column]); } } @@ -117,9 +119,8 @@ class ColumnDelete extends BaseFilter // remove columns not specified in $columnsToKeep if (!empty($this->columnsToKeep)) { - foreach ($table->getRows() as $row) { - foreach ($row->getColumns() as $name => $value) { - + foreach ($table as $index => $row) { + foreach ($row as $name => $value) { $keep = false; // @see self::APPEND_TO_COLUMN_NAME_TO_KEEP foreach ($this->columnsToKeep as $nameKeep => $true) { @@ -132,7 +133,7 @@ class ColumnDelete extends BaseFilter && $name != 'label' // label cannot be removed via whitelisting && !isset($this->columnsToKeep[$name]) ) { - $row->deleteColumn($name); + unset($table[$index][$name]); } } } @@ -141,10 +142,12 @@ class ColumnDelete extends BaseFilter } // recurse - if ($recurse) { - foreach ($table->getRows() as $row) { + if ($recurse && !is_array($table)) { + foreach ($table as $row) { $this->filterSubTable($row); } } + + return $table; } } diff --git a/www/analytics/core/DataTable/Filter/ExcludeLowPopulation.php b/www/analytics/core/DataTable/Filter/ExcludeLowPopulation.php index 6e6fbf33..e6a423ef 100644 --- a/www/analytics/core/DataTable/Filter/ExcludeLowPopulation.php +++ b/www/analytics/core/DataTable/Filter/ExcludeLowPopulation.php @@ -1,6 +1,6 @@ filter('ExcludeLowPopulation', array('nb_visits', 3)); - * + * * // remove all countries from UserCountry.getCountry whose percent of total visits is less than 5% * $dataTable = // ... get a DataTable whose queued filters have been run ... * $dataTable->filter('ExcludeLowPopulation', array('nb_visits', false, 0.05)); - * + * * // remove all countries from UserCountry.getCountry whose bounce rate is less than 10% * $dataTable = // ... get a DataTable that has a numerical bounce_rate column ... * $dataTable->filter('ExcludeLowPopulation', array('bounce_rate', 0.10)); @@ -50,7 +51,7 @@ class ExcludeLowPopulation extends BaseFilter * @param string $columnToFilter The name of the column whose value will determine whether * a row is deleted or not. * @param number|false $minimumValue The minimum column value. Rows with column values < - * this number will be deleted. If false, + * this number will be deleted. If false, * `$minimumPercentageThreshold` is used. * @param bool|float $minimumPercentageThreshold If supplied, column values must be a greater * percentage of the sum of all column values than @@ -59,7 +60,13 @@ class ExcludeLowPopulation extends BaseFilter public function __construct($table, $columnToFilter, $minimumValue, $minimumPercentageThreshold = false) { parent::__construct($table); - $this->columnToFilter = $columnToFilter; + + $row = $table->getFirstRow(); + if ($row === false) { + return; + } + + $this->columnToFilter = $this->selectColumnToExclude($columnToFilter, $row); if ($minimumValue == 0) { if ($minimumPercentageThreshold === false) { @@ -80,6 +87,9 @@ class ExcludeLowPopulation extends BaseFilter */ public function filter($table) { + if(empty($this->columnToFilter)) { + return; + } $minimumValue = $this->minimumValue; $isValueLowPopulation = function ($value) use ($minimumValue) { return $value < $minimumValue; @@ -87,4 +97,29 @@ class ExcludeLowPopulation extends BaseFilter $table->filter('ColumnCallbackDeleteRow', array($this->columnToFilter, $isValueLowPopulation)); } + + /** + * Sets the column to be used for Excluding low population + * + * @param DataTable\Row $row + * @return int + */ + private function selectColumnToExclude($columnToFilter, $row) + { + if ($row->hasColumn($columnToFilter)) { + return $columnToFilter; + } + + // filter_excludelowpop=nb_visits but the column name is still Metrics::INDEX_NB_VISITS in the table + $columnIdToName = Metrics::getMappingFromNameToId(); + if (isset($columnIdToName[$columnToFilter])) { + $column = $columnIdToName[$columnToFilter]; + + if ($row->hasColumn($column)) { + return $column; + } + } + + return $columnToFilter; + } } diff --git a/www/analytics/core/DataTable/Filter/GroupBy.php b/www/analytics/core/DataTable/Filter/GroupBy.php index 093aee0a..b00c6614 100755 --- a/www/analytics/core/DataTable/Filter/GroupBy.php +++ b/www/analytics/core/DataTable/Filter/GroupBy.php @@ -1,6 +1,6 @@ filter('GroupBy', array('label', function ($labelUrl) { * return parse_url($labelUrl, PHP_URL_HOST); * })); - * + * * @api */ class GroupBy extends BaseFilter @@ -51,17 +52,17 @@ class GroupBy extends BaseFilter * @param DataTable $table The DataTable to filter. * @param string $groupByColumn The column name to reduce. * @param callable $reduceFunction The reduce function. This must alter the `$groupByColumn` - * columng in some way. + * columng in some way. If not set then the filter will group by the raw column value. * @param array $parameters deprecated - use an [anonymous function](http://php.net/manual/en/functions.anonymous.php) * instead. */ - public function __construct($table, $groupByColumn, $reduceFunction, $parameters = array()) + public function __construct($table, $groupByColumn, $reduceFunction = null, $parameters = array()) { parent::__construct($table); - $this->groupByColumn = $groupByColumn; + $this->groupByColumn = $groupByColumn; $this->reduceFunction = $reduceFunction; - $this->parameters = $parameters; + $this->parameters = $parameters; } /** @@ -71,19 +72,19 @@ class GroupBy extends BaseFilter */ public function filter($table) { + /** @var Row[] $groupByRows */ $groupByRows = array(); $nonGroupByRowIds = array(); - foreach ($table->getRows() as $rowId => $row) { - // skip the summary row - if ($rowId == DataTable::ID_SUMMARY_ROW) { - continue; - } + foreach ($table->getRowsWithoutSummaryRow() as $rowId => $row) { + $groupByColumnValue = $row->getColumn($this->groupByColumn); + $groupByValue = $groupByColumnValue; // reduce the group by column of this row - $groupByColumnValue = $row->getColumn($this->groupByColumn); - $parameters = array_merge(array($groupByColumnValue), $this->parameters); - $groupByValue = call_user_func_array($this->reduceFunction, $parameters); + if ($this->reduceFunction) { + $parameters = array_merge(array($groupByColumnValue), $this->parameters); + $groupByValue = call_user_func_array($this->reduceFunction, $parameters); + } if (!isset($groupByRows[$groupByValue])) { // if we haven't encountered this group by value before, we mark this row as a @@ -98,6 +99,10 @@ class GroupBy extends BaseFilter } } + if ($this->groupByColumn === 'label') { + $table->setLabelsHaveChanged(); + } + // delete the unneeded rows. $table->deleteRows($nonGroupByRowIds); } diff --git a/www/analytics/core/DataTable/Filter/Limit.php b/www/analytics/core/DataTable/Filter/Limit.php index d1174293..415be296 100644 --- a/www/analytics/core/DataTable/Filter/Limit.php +++ b/www/analytics/core/DataTable/Filter/Limit.php @@ -1,6 +1,6 @@ 15 * $dataTable->filter('Limit', array(5, 10)); * @@ -34,9 +34,9 @@ class Limit extends BaseFilter public function __construct($table, $offset, $limit = -1, $keepSummaryRow = false) { parent::__construct($table); - $this->offset = $offset; - $this->limit = $limit; + $this->offset = $offset; + $this->limit = $limit; $this->keepSummaryRow = $keepSummaryRow; } diff --git a/www/analytics/core/DataTable/Filter/MetadataCallbackAddMetadata.php b/www/analytics/core/DataTable/Filter/MetadataCallbackAddMetadata.php index 767ecc4f..414f9391 100644 --- a/www/analytics/core/DataTable/Filter/MetadataCallbackAddMetadata.php +++ b/www/analytics/core/DataTable/Filter/MetadataCallbackAddMetadata.php @@ -1,6 +1,6 @@ filter('MetadataCallbackAddMetadata', array('url', 'logo', 'Piwik\Plugins\MyPlugin\getLogoFromUrl')); * @@ -31,7 +31,7 @@ class MetadataCallbackAddMetadata extends BaseFilter /** * Constructor. - * + * * @param DataTable $table The DataTable that will eventually be filtered. * @param string|array $metadataToRead The metadata to read from each row and pass to the callback. * @param string $metadataToAdd The name of the metadata to add. @@ -57,16 +57,18 @@ class MetadataCallbackAddMetadata extends BaseFilter /** * See {@link MetadataCallbackAddMetadata}. - * + * * @param DataTable $table */ public function filter($table) { - foreach ($table->getRows() as $key => $row) { - if (!$this->applyToSummaryRow && $key == DataTable::ID_SUMMARY_ROW) { - continue; - } + if ($this->applyToSummaryRow) { + $rows = $table->getRows(); + } else { + $rows = $table->getRowsWithoutSummaryRow(); + } + foreach ($rows as $key => $row) { $params = array(); foreach ($this->metadataToRead as $name) { $params[] = $row->getMetadata($name); diff --git a/www/analytics/core/DataTable/Filter/MetadataCallbackReplace.php b/www/analytics/core/DataTable/Filter/MetadataCallbackReplace.php index 2d472c9c..cb04cdb1 100644 --- a/www/analytics/core/DataTable/Filter/MetadataCallbackReplace.php +++ b/www/analytics/core/DataTable/Filter/MetadataCallbackReplace.php @@ -1,6 +1,6 @@ filter('MetadataCallbackReplace', array('url', function ($url) { * return $url . '#index'; * })); @@ -27,7 +27,7 @@ class MetadataCallbackReplace extends ColumnCallbackReplace { /** * Constructor. - * + * * @param DataTable $table The DataTable that will eventually be filtered. * @param array|string $metadataToFilter The metadata whose values should be passed to the callback * and then replaced with the callback's result. diff --git a/www/analytics/core/DataTable/Filter/Pattern.php b/www/analytics/core/DataTable/Filter/Pattern.php index 30a7907b..1327b4f4 100644 --- a/www/analytics/core/DataTable/Filter/Pattern.php +++ b/www/analytics/core/DataTable/Filter/Pattern.php @@ -1,6 +1,6 @@ filter('Pattern', array('label', '^piwik')); * @@ -23,6 +23,9 @@ use Piwik\DataTable\BaseFilter; */ class Pattern extends BaseFilter { + /** + * @var string|array + */ private $columnToFilter; private $patternToSearch; private $patternToSearchQuoted; @@ -30,7 +33,7 @@ class Pattern extends BaseFilter /** * Constructor. - * + * * @param DataTable $table The table to eventually filter. * @param string $columnToFilter The column to match with the `$patternToSearch` pattern. * @param string $patternToSearch The regex pattern to use. @@ -53,7 +56,7 @@ class Pattern extends BaseFilter * @return string * @ignore */ - static public function getPatternQuoted($pattern) + public static function getPatternQuoted($pattern) { return '/' . str_replace('/', '\/', $pattern) . '/'; } @@ -67,14 +70,14 @@ class Pattern extends BaseFilter * @return int * @ignore */ - static public function match($patternQuoted, $string, $invertedMatch = false) + public static function match($patternQuoted, $string, $invertedMatch = false) { return preg_match($patternQuoted . "i", $string) == 1 ^ $invertedMatch; } /** * See {@link Pattern}. - * + * * @param DataTable $table */ public function filter($table) @@ -93,4 +96,30 @@ class Pattern extends BaseFilter } } } + + /** + * See {@link Pattern}. + * + * @param array $array + * @return array + */ + public function filterArray($array) + { + $newArray = array(); + + foreach ($array as $key => $row) { + foreach ($this->columnToFilter as $column) { + if (!array_key_exists($column, $row)) { + continue; + } + + if (self::match($this->patternToSearchQuoted, $row[$column], $this->invertedMatch)) { + $newArray[$key] = $row; + continue 2; + } + } + } + + return $newArray; + } } diff --git a/www/analytics/core/DataTable/Filter/PatternRecursive.php b/www/analytics/core/DataTable/Filter/PatternRecursive.php index ed4c137f..62a8b26b 100644 --- a/www/analytics/core/DataTable/Filter/PatternRecursive.php +++ b/www/analytics/core/DataTable/Filter/PatternRecursive.php @@ -1,6 +1,6 @@ filter('PatternRecursive', array('label', 'index')); * @@ -32,7 +30,7 @@ class PatternRecursive extends BaseFilter /** * Constructor. - * + * * @param DataTable $table The table to eventually filter. * @param string $columnToFilter The column to match with the `$patternToSearch` pattern. * @param string $patternToSearch The regex pattern to use. @@ -48,7 +46,7 @@ class PatternRecursive extends BaseFilter /** * See {@link PatternRecursive}. - * + * * @param DataTable $table * @return int The number of deleted rows. */ @@ -62,18 +60,15 @@ class PatternRecursive extends BaseFilter // AND 2 - the label is not found in the children $patternNotFoundInChildren = false; - try { - $idSubTable = $row->getIdSubDataTable(); - $subTable = Manager::getInstance()->getTable($idSubTable); - + $subTable = $row->getSubtable(); + if (!$subTable) { + $patternNotFoundInChildren = true; + } else { // we delete the row if we couldn't find the pattern in any row in the // children hierarchy if ($this->filter($subTable) == 0) { $patternNotFoundInChildren = true; } - } catch (Exception $e) { - // there is no subtable loaded for example - $patternNotFoundInChildren = true; } if ($patternNotFoundInChildren diff --git a/www/analytics/core/DataTable/Filter/PivotByDimension.php b/www/analytics/core/DataTable/Filter/PivotByDimension.php new file mode 100644 index 00000000..b7f5ca1c --- /dev/null +++ b/www/analytics/core/DataTable/Filter/PivotByDimension.php @@ -0,0 +1,550 @@ +pivotColumn = $pivotColumn; + $this->pivotByColumnLimit = $pivotByColumnLimit ?: self::getDefaultColumnLimit(); + $this->isFetchingBySegmentEnabled = $isFetchingBySegmentEnabled; + + $namesToId = Metrics::getMappingFromNameToId(); + $this->metricIndexValue = isset($namesToId[$this->pivotColumn]) ? $namesToId[$this->pivotColumn] : null; + + $this->setPivotByDimension($pivotByDimension); + $this->setThisReportMetadata($report); + + $this->checkSupportedPivot(); + } + + /** + * Pivots to table. + * + * @param DataTable $table The table to manipulate. + */ + public function filter($table) + { + // set of all column names in the pivoted table mapped with the sum of all column + // values. used later in truncating and ordering the pivoted table's columns. + $columnSet = array(); + + // if no pivot column was set, use the first one found in the row + if (empty($this->pivotColumn)) { + $this->pivotColumn = $this->getNameOfFirstNonLabelColumnInTable($table); + } + + Log::debug("PivotByDimension::%s: pivoting table with pivot column = %s", __FUNCTION__, $this->pivotColumn); + + foreach ($table->getRows() as $row) { + $row->setColumns(array('label' => $row->getColumn('label'))); + + $associatedTable = $this->getIntersectedTable($table, $row); + if (!empty($associatedTable)) { + foreach ($associatedTable->getRows() as $columnRow) { + $pivotTableColumn = $columnRow->getColumn('label'); + + $columnValue = $this->getColumnValue($columnRow, $this->pivotColumn); + + if (isset($columnSet[$pivotTableColumn])) { + $columnSet[$pivotTableColumn] += $columnValue; + } else { + $columnSet[$pivotTableColumn] = $columnValue; + } + + $row->setColumn($pivotTableColumn, $columnValue); + } + + Common::destroy($associatedTable); + unset($associatedTable); + } + } + + Log::debug("PivotByDimension::%s: pivoted columns set: %s", __FUNCTION__, $columnSet); + + $others = Piwik::translate('General_Others'); + $defaultRow = $this->getPivotTableDefaultRowFromColumnSummary($columnSet, $others); + + Log::debug("PivotByDimension::%s: un-prepended default row: %s", __FUNCTION__, $defaultRow); + + // post process pivoted datatable + foreach ($table->getRows() as $row) { + // remove subtables from rows + $row->removeSubtable(); + $row->deleteMetadata('idsubdatatable_in_db'); + + // use default row to ensure column ordering and add missing columns/aggregate cut-off columns + $orderedColumns = $defaultRow; + foreach ($row->getColumns() as $name => $value) { + if (isset($orderedColumns[$name])) { + $orderedColumns[$name] = $value; + } else { + $orderedColumns[$others] += $value; + } + } + $row->setColumns($orderedColumns); + } + + $table->clearQueuedFilters(); // TODO: shouldn't clear queued filters, but we can't wait for them to be run + // since generic filters are run before them. remove after refactoring + // processed metrics. + + // prepend numerals to columns in a queued filter (this way, disable_queued_filters can be used + // to get machine readable data from the API if needed) + $prependedColumnNames = $this->getOrderedColumnsWithPrependedNumerals($defaultRow, $others); + + Log::debug("PivotByDimension::%s: prepended column name mapping: %s", __FUNCTION__, $prependedColumnNames); + + $table->queueFilter(function (DataTable $table) use ($prependedColumnNames) { + foreach ($table->getRows() as $row) { + $row->setColumns(array_combine($prependedColumnNames, $row->getColumns())); + } + }); + } + + /** + * An intersected table is a table that describes visits by a certain dimension for the visits + * represented by a row in another table. This method fetches intersected tables either via + * subtable or by using a segment. Read the class docs for more info. + */ + private function getIntersectedTable(DataTable $table, Row $row) + { + if ($this->isPivotDimensionSubtable()) { + return $this->loadSubtable($table, $row); + } + + if ($this->isFetchingBySegmentEnabled) { + $segmentValue = $row->getColumn('label'); + return $this->fetchIntersectedWithThisBySegment($table, $segmentValue); + } + + // should never occur, unless checkSupportedPivot() fails to catch an unsupported pivot + throw new Exception("Unexpected error, cannot fetch intersected table."); + } + + private function isPivotDimensionSubtable() + { + return self::areDimensionsEqualAndNotNull($this->subtableDimension, $this->pivotByDimension); + } + + private function loadSubtable(DataTable $table, Row $row) + { + $idSubtable = $row->getIdSubDataTable(); + if ($idSubtable === null) { + return null; + } + + $subtable = $row->getSubtable(); + if (!$subtable) { + $subtable = $this->thisReport->fetchSubtable($idSubtable, $this->getRequestParamOverride($table)); + } + + if (!$subtable) { // sanity check + throw new Exception("Unexpected error: could not load subtable '$idSubtable'."); + } + + return $subtable; + } + + private function fetchIntersectedWithThisBySegment(DataTable $table, $segmentValue) + { + $segmentStr = $this->thisReportDimensionSegment->getSegment() . "==" . urlencode($segmentValue); + + // TODO: segment + report API method query params should be stored in DataTable metadata so we don't have to access it here + $originalSegment = Common::getRequestVar('segment', false); + if (!empty($originalSegment)) { + $segmentStr = $originalSegment . ';' . $segmentStr; + } + + Log::debug("PivotByDimension: Fetching intersected with segment '%s'", $segmentStr); + + $params = array('segment' => $segmentStr) + $this->getRequestParamOverride($table); + return $this->pivotDimensionReport->fetch($params); + } + + private function setPivotByDimension($pivotByDimension) + { + $this->pivotByDimension = Dimension::factory($pivotByDimension); + if (empty($this->pivotByDimension)) { + throw new Exception("Invalid dimension '$pivotByDimension'."); + } + + $this->pivotDimensionReport = Report::getForDimension($this->pivotByDimension); + } + + private function setThisReportMetadata($report) + { + list($module, $method) = explode('.', $report); + + $this->thisReport = Report::factory($module, $method); + if (empty($this->thisReport)) { + throw new Exception("Unable to find report '$report'."); + } + + $this->subtableDimension = $this->thisReport->getSubtableDimension(); + + $thisReportDimension = $this->thisReport->getDimension(); + if ($thisReportDimension !== null) { + $segments = $thisReportDimension->getSegments(); + $this->thisReportDimensionSegment = reset($segments); + } + } + + private function checkSupportedPivot() + { + $reportId = $this->thisReport->getModule() . '.' . $this->thisReport->getName(); + + if (!$this->isFetchingBySegmentEnabled) { + // if fetching by segment is disabled, then there must be a subtable for the current report and + // subtable's dimension must be the pivot dimension + + if (empty($this->subtableDimension)) { + throw new Exception("Unsupported pivot: report '$reportId' has no subtable dimension."); + } + + if (!$this->isPivotDimensionSubtable()) { + throw new Exception("Unsupported pivot: the subtable dimension for '$reportId' does not match the " + . "requested pivotBy dimension. [subtable dimension = {$this->subtableDimension->getId()}, " + . "pivot by dimension = {$this->pivotByDimension->getId()}]"); + } + } else { + $canFetchBySubtable = !empty($this->subtableDimension) + && $this->subtableDimension->getId() === $this->pivotByDimension->getId(); + if ($canFetchBySubtable) { + return; + } + + // if fetching by segment is enabled, and we cannot fetch by subtable, then there has to be a report + // for the pivot dimension (so we can fetch the report), and there has to be a segment for this report's + // dimension (so we can use it when fetching) + + if (empty($this->pivotDimensionReport)) { + throw new Exception("Unsupported pivot: No report for pivot dimension '{$this->pivotByDimension->getId()}'" + . " (report required for fetching intersected tables by segment)."); + } + + if (empty($this->thisReportDimensionSegment)) { + throw new Exception("Unsupported pivot: No segment for dimension of report '$reportId'." + . " (segment required for fetching intersected tables by segment)."); + } + } + } + + /** + * @param $columnRow + * @param $pivotColumn + * @return false|mixed + */ + private function getColumnValue(Row $columnRow, $pivotColumn) + { + $value = $columnRow->getColumn($pivotColumn); + if (empty($value) + && !empty($this->metricIndexValue) + ) { + $value = $columnRow->getColumn($this->metricIndexValue); + } + return $value; + } + + private function getNameOfFirstNonLabelColumnInTable(DataTable $table) + { + foreach ($table->getRows() as $row) { + foreach ($row->getColumns() as $columnName => $ignore) { + if ($columnName != 'label') { + return $columnName; + } + } + } + } + + private function getRequestParamOverride(DataTable $table) + { + $params = array( + 'pivotBy' => '', + 'column' => '', + 'flat' => 0, + 'totals' => 0, + 'disable_queued_filters' => 1, + 'disable_generic_filters' => 1, + 'showColumns' => '', + 'hideColumns' => '' + ); + + /** @var Site $site */ + $site = $table->getMetadata('site'); + if (!empty($site)) { + $params['idSite'] = $site->getId(); + } + + /** @var Period $period */ + $period = $table->getMetadata('period'); + if (!empty($period)) { + $params['period'] = $period->getLabel(); + + if ($params['period'] == 'range') { + $params['date'] = $period->getRangeString(); + } else { + $params['date'] = $period->getDateStart()->toString(); + } + } + + return $params; + } + + private function getPivotTableDefaultRowFromColumnSummary($columnSet, $othersRowLabel) + { + // sort columns by sum (to ensure deterministic ordering) + arsort($columnSet); + + // limit columns if necessary (adding aggregate Others column at end) + if ($this->pivotByColumnLimit > 0 + && count($columnSet) > $this->pivotByColumnLimit + ) { + $columnSet = array_slice($columnSet, 0, $this->pivotByColumnLimit - 1, $preserveKeys = true); + $columnSet[$othersRowLabel] = 0; + } + + // remove column sums from array so it can be used as a default row + $columnSet = array_map(function () { return false; }, $columnSet); + + // make sure label column is first + $columnSet = array('label' => false) + $columnSet; + + return $columnSet; + } + + private function getOrderedColumnsWithPrependedNumerals($defaultRow, $othersRowLabel) + { + $flags = ENT_COMPAT; + if (defined('ENT_HTML401')) { + $flags |= ENT_HTML401; // part of default flags for 5.4, but not 5.3 + } + + // must use decoded character otherwise sort later will fail + // (sort column will be set to decoded but columns will have  ) + $nbsp = html_entity_decode(' ', $flags, 'utf-8'); + + $result = array(); + + $currentIndex = 1; + foreach ($defaultRow as $columnName => $ignore) { + if ($columnName === $othersRowLabel + || $columnName === 'label' + ) { + $result[] = $columnName; + } else { + $modifiedColumnName = $currentIndex . '.' . $nbsp . $columnName; + $result[] = $modifiedColumnName; + + ++$currentIndex; + } + } + + return $result; + } + + /** + * Returns true if pivoting by subtable is supported for a report. Will return true if the report + * has a subtable dimension and if the subtable dimension is different than the report's dimension. + * + * @param Report $report + * @return bool + */ + public static function isPivotingReportBySubtableSupported(Report $report) + { + return self::areDimensionsNotEqualAndNotNull($report->getSubtableDimension(), $report->getDimension()); + } + + /** + * Returns true if fetching intersected tables by segment is enabled in the INI config, false if otherwise. + * + * @return bool + */ + public static function isSegmentFetchingEnabledInConfig() + { + return Config::getInstance()->General['pivot_by_filter_enable_fetch_by_segment']; + } + + /** + * Returns the default maximum number of columns to allow in a pivot table from the INI config. + * Uses the **pivot_by_filter_default_column_limit** INI config option. + * + * @return int + */ + public static function getDefaultColumnLimit() + { + return Config::getInstance()->General['pivot_by_filter_default_column_limit']; + } + + /** + * @param Dimension|null $lhs + * @param Dimension|null $rhs + * @return bool + */ + private static function areDimensionsEqualAndNotNull($lhs, $rhs) + { + return !empty($lhs) && !empty($rhs) && $lhs->getId() == $rhs->getId(); + } + + /** + * @param Dimension|null $lhs + * @param Dimension|null $rhs + * @return bool + */ + private static function areDimensionsNotEqualAndNotNull($lhs, $rhs) + { + return !empty($lhs) && !empty($rhs) && $lhs->getId() != $rhs->getId(); + } +} diff --git a/www/analytics/core/DataTable/Filter/PrependSegment.php b/www/analytics/core/DataTable/Filter/PrependSegment.php new file mode 100644 index 00000000..34d14e6f --- /dev/null +++ b/www/analytics/core/DataTable/Filter/PrependSegment.php @@ -0,0 +1,34 @@ +filter('PrependSegment', array('segmentName==segmentValue;')); + * + * @api + */ +class PrependSegment extends PrependValueToMetadata +{ + /** + * @param DataTable $table + * @param string $prependSegment The segment to prepend if a segment is already defined. Make sure to include + * A condition, eg the segment should end with ';' or ',' + */ + public function __construct($table, $prependSegment = '') + { + parent::__construct($table, 'segment', $prependSegment); + } +} diff --git a/www/analytics/core/DataTable/Filter/PrependValueToMetadata.php b/www/analytics/core/DataTable/Filter/PrependValueToMetadata.php new file mode 100644 index 00000000..3e2e0ceb --- /dev/null +++ b/www/analytics/core/DataTable/Filter/PrependValueToMetadata.php @@ -0,0 +1,65 @@ +filter('PrependValueToMetadata', array('segment', 'segmentName==segmentValue')); + * + * @api + */ +class PrependValueToMetadata extends BaseFilter +{ + private $metadataColumn; + private $valueToPrepend; + + /** + * @param DataTable $table + * @param string $metadataName The name of the metadata that should be prepended + * @param string $valueToPrepend The value to prepend if the metadata entry exists + */ + public function __construct($table, $metadataName, $valueToPrepend) + { + parent::__construct($table); + + $this->metadataColumn = $metadataName; + $this->valueToPrepend = $valueToPrepend; + } + + /** + * See {@link PrependValueToMetadata}. + * + * @param DataTable $table + */ + public function filter($table) + { + if (empty($this->metadataColumn) || empty($this->valueToPrepend)) { + return; + } + + $metadataColumn = $this->metadataColumn; + $valueToPrepend = $this->valueToPrepend; + + $table->filter(function (DataTable $dataTable) use ($metadataColumn, $valueToPrepend) { + foreach ($dataTable->getRows() as $row) { + $filter = $row->getMetadata($metadataColumn); + if ($filter !== false) { + $row->setMetadata($metadataColumn, $valueToPrepend . $filter); + } + } + }); + } +} diff --git a/www/analytics/core/DataTable/Filter/RangeCheck.php b/www/analytics/core/DataTable/Filter/RangeCheck.php index d8229a4d..211638d3 100644 --- a/www/analytics/core/DataTable/Filter/RangeCheck.php +++ b/www/analytics/core/DataTable/Filter/RangeCheck.php @@ -1,6 +1,6 @@ columnToFilter = $columnToFilter; - if ($minimumValue < $maximumValue) { + if ((float) $minimumValue < (float) $maximumValue) { self::$minimumValue = $minimumValue; self::$maximumValue = $maximumValue; } @@ -47,10 +47,23 @@ class RangeCheck extends BaseFilter { foreach ($table->getRows() as $row) { $value = $row->getColumn($this->columnToFilter); + + if ($value === false) { + $value = $row->getMetadata($this->columnToFilter); + if ($value !== false) { + if ($value < (float) self::$minimumValue) { + $row->setMetadata($this->columnToFilter, self::$minimumValue); + } elseif ($value > (float) self::$maximumValue) { + $row->setMetadata($this->columnToFilter, self::$maximumValue); + } + } + continue; + } + if ($value !== false) { - if ($value < self::$minimumValue) { + if ($value < (float) self::$minimumValue) { $row->setColumn($this->columnToFilter, self::$minimumValue); - } elseif ($value > self::$maximumValue) { + } elseif ($value > (float) self::$maximumValue) { $row->setColumn($this->columnToFilter, self::$maximumValue); } } diff --git a/www/analytics/core/DataTable/Filter/ReplaceColumnNames.php b/www/analytics/core/DataTable/Filter/ReplaceColumnNames.php index fd842ad0..cbf4bbb3 100644 --- a/www/analytics/core/DataTable/Filter/ReplaceColumnNames.php +++ b/www/analytics/core/DataTable/Filter/ReplaceColumnNames.php @@ -1,6 +1,6 @@ queueFilter('ReplaceColumnNames'); * return $dataTable; * } - * + * * @api */ class ReplaceColumnNames extends BaseFilter @@ -43,14 +43,14 @@ class ReplaceColumnNames extends BaseFilter /** * Constructor. - * + * * @param DataTable $table The table that will be eventually filtered. * @param array|null $mappingToApply The name mapping to apply. Must map old column names * with new ones, eg, - * + * * array('OLD_COLUMN_NAME' => 'NEW_COLUMN NAME', * 'OLD_COLUMN_NAME2' => 'NEW_COLUMN NAME2') - * + * * If null, {@link Piwik\Metrics::$mappingFromIdToName} is used. */ public function __construct($table, $mappingToApply = null) @@ -81,9 +81,8 @@ class ReplaceColumnNames extends BaseFilter */ protected function filterTable($table) { - foreach ($table->getRows() as $key => $row) { - $oldColumns = $row->getColumns(); - $newColumns = $this->getRenamedColumns($oldColumns); + foreach ($table->getRows() as $row) { + $newColumns = $this->getRenamedColumns($row->getColumns()); $row->setColumns($newColumns); $this->filterSubTable($row); } diff --git a/www/analytics/core/DataTable/Filter/ReplaceSummaryRowLabel.php b/www/analytics/core/DataTable/Filter/ReplaceSummaryRowLabel.php index b89a306a..3256fe96 100644 --- a/www/analytics/core/DataTable/Filter/ReplaceSummaryRowLabel.php +++ b/www/analytics/core/DataTable/Filter/ReplaceSummaryRowLabel.php @@ -1,6 +1,6 @@ queueFilter('ReplaceSummaryRowLabel', array(Piwik::translate('General_Others'))); - * + * * @api */ class ReplaceSummaryRowLabel extends BaseFilter { /** * Constructor. - * + * * @param DataTable $table The table that will eventually be filtered. * @param string|null $newLabel The new label for summary row. If null, defaults to * `Piwik::translate('General_Others')`. @@ -53,20 +52,20 @@ class ReplaceSummaryRowLabel extends BaseFilter */ public function filter($table) { - $rows = $table->getRows(); - foreach ($rows as $id => $row) { - if ($row->getColumn('label') == DataTable::LABEL_SUMMARY_ROW - || $id == DataTable::ID_SUMMARY_ROW - ) { + $row = $table->getRowFromId(DataTable::ID_SUMMARY_ROW); + if ($row) { + $row->setColumn('label', $this->newLabel); + } else { + $row = $table->getRowFromLabel(DataTable::LABEL_SUMMARY_ROW); + if ($row) { $row->setColumn('label', $this->newLabel); - break; } } // recurse - foreach ($rows as $row) { - if ($row->isSubtableLoaded()) { - $subTable = Manager::getInstance()->getTable($row->getIdSubDataTable()); + foreach ($table->getRowsWithoutSummaryRow() as $row) { + $subTable = $row->getSubtable(); + if ($subTable) { $this->filter($subTable); } } diff --git a/www/analytics/core/DataTable/Filter/SafeDecodeLabel.php b/www/analytics/core/DataTable/Filter/SafeDecodeLabel.php index eb44c8d7..f2629618 100644 --- a/www/analytics/core/DataTable/Filter/SafeDecodeLabel.php +++ b/www/analytics/core/DataTable/Filter/SafeDecodeLabel.php @@ -1,6 +1,6 @@ enableRecursiveSort(); } + $this->columnToSort = $columnToSort; - $this->naturalSort = $naturalSort; + $this->naturalSort = $naturalSort; $this->setOrder($order); } @@ -55,67 +61,56 @@ class Sort extends BaseFilter { if ($order == 'asc') { $this->order = 'asc'; - $this->sign = 1; + $this->sign = 1; } else { $this->order = 'desc'; - $this->sign = -1; + $this->sign = -1; } } /** * Sorting method used for sorting numbers * - * @param number $a - * @param number $b + * @param array $rowA array[0 => value of column to sort, 1 => label] + * @param array $rowB array[0 => value of column to sort, 1 => label] * @return int */ - public function numberSort($a, $b) + public function numberSort($rowA, $rowB) { - return !isset($a->c[Row::COLUMNS][$this->columnToSort]) - && !isset($b->c[Row::COLUMNS][$this->columnToSort]) + if (isset($rowA[0]) && isset($rowB[0])) { + if ($rowA[0] != $rowB[0] || !isset($rowA[1])) { + return $this->sign * ($rowA[0] < $rowB[0] ? -1 : 1); + } else { + return -1 * $this->sign * strnatcasecmp($rowA[1], $rowB[1]); + } + } elseif (!isset($rowB[0]) && !isset($rowA[0])) { + return -1 * $this->sign * strnatcasecmp($rowA[1], $rowB[1]); + } elseif (!isset($rowA[0])) { + return 1; + } - ? 0 - : ( - !isset($a->c[Row::COLUMNS][$this->columnToSort]) - ? 1 - : ( - !isset($b->c[Row::COLUMNS][$this->columnToSort]) - ? -1 - : (($a->c[Row::COLUMNS][$this->columnToSort] != $b->c[Row::COLUMNS][$this->columnToSort] - || !isset($a->c[Row::COLUMNS]['label'])) - ? ($this->sign * ( - $a->c[Row::COLUMNS][$this->columnToSort] - < $b->c[Row::COLUMNS][$this->columnToSort] - ? -1 - : 1) - ) - : -1 * $this->sign * strnatcasecmp( - $a->c[Row::COLUMNS]['label'], - $b->c[Row::COLUMNS]['label']) - ) - ) - ); + return -1; } /** * Sorting method used for sorting values natural * - * @param mixed $a - * @param mixed $b + * @param mixed $valA + * @param mixed $valB * @return int */ - function naturalSort($a, $b) + public function naturalSort($valA, $valB) { - return !isset($a->c[Row::COLUMNS][$this->columnToSort]) - && !isset($b->c[Row::COLUMNS][$this->columnToSort]) + return !isset($valA) + && !isset($valB) ? 0 - : (!isset($a->c[Row::COLUMNS][$this->columnToSort]) + : (!isset($valA) ? 1 - : (!isset($b->c[Row::COLUMNS][$this->columnToSort]) + : (!isset($valB) ? -1 : $this->sign * strnatcasecmp( - $a->c[Row::COLUMNS][$this->columnToSort], - $b->c[Row::COLUMNS][$this->columnToSort] + $valA, + $valB ) ) ); @@ -124,27 +119,38 @@ class Sort extends BaseFilter /** * Sorting method used for sorting values * - * @param mixed $a - * @param mixed $b + * @param mixed $valA + * @param mixed $valB * @return int */ - function sortString($a, $b) + public function sortString($valA, $valB) { - return !isset($a->c[Row::COLUMNS][$this->columnToSort]) - && !isset($b->c[Row::COLUMNS][$this->columnToSort]) + return !isset($valA) + && !isset($valB) ? 0 - : (!isset($a->c[Row::COLUMNS][$this->columnToSort]) + : (!isset($valA) ? 1 - : (!isset($b->c[Row::COLUMNS][$this->columnToSort]) + : (!isset($valB) ? -1 : $this->sign * - strcasecmp($a->c[Row::COLUMNS][$this->columnToSort], - $b->c[Row::COLUMNS][$this->columnToSort] + strcasecmp($valA, + $valB ) ) ); } + protected function getColumnValue(Row $row) + { + $value = $row->getColumn($this->columnToSort); + + if ($value === false || is_array($value)) { + return null; + } + + return $value; + } + /** * Sets the column to be used for sorting * @@ -153,18 +159,18 @@ class Sort extends BaseFilter */ protected function selectColumnToSort($row) { - $value = $row->getColumn($this->columnToSort); - if ($value !== false) { + $value = $row->hasColumn($this->columnToSort); + if ($value) { return $this->columnToSort; } - $columnIdToName = Metrics::getMappingFromIdToName(); + $columnIdToName = Metrics::getMappingFromNameToId(); // sorting by "nb_visits" but the index is Metrics::INDEX_NB_VISITS in the table if (isset($columnIdToName[$this->columnToSort])) { $column = $columnIdToName[$this->columnToSort]; - $value = $row->getColumn($column); + $value = $row->hasColumn($column); - if ($value !== false) { + if ($value) { return $column; } } @@ -172,8 +178,8 @@ class Sort extends BaseFilter // eg. was previously sorted by revenue_per_visit, but this table // doesn't have this column; defaults with nb_visits $column = Metrics::INDEX_NB_VISITS; - $value = $row->getColumn($column); - if ($value !== false) { + $value = $row->hasColumn($column); + if ($value) { return $column; } @@ -193,21 +199,25 @@ class Sort extends BaseFilter if ($table instanceof Simple) { return; } + if (empty($this->columnToSort)) { return; } - $rows = $table->getRows(); - if (count($rows) == 0) { + + if (!$table->getRowsCount()) { return; } - $row = current($rows); + + $row = $table->getFirstRow(); if ($row === false) { return; } + $this->columnToSort = $this->selectColumnToSort($row); - $value = $row->getColumn($this->columnToSort); - if (is_numeric($value)) { + $value = $this->getFirstValueFromDataTable($table); + + if (is_numeric($value) && $this->columnToSort !== 'label') { $methodToUse = "numberSort"; } else { if ($this->naturalSort) { @@ -216,6 +226,65 @@ class Sort extends BaseFilter $methodToUse = "sortString"; } } - $table->sort(array($this, $methodToUse), $this->columnToSort); + + $this->sort($table, $methodToUse); + } + + private function getFirstValueFromDataTable($table) + { + foreach ($table->getRowsWithoutSummaryRow() as $row) { + $value = $this->getColumnValue($row); + if (!is_null($value)) { + return $value; + } + } + } + + /** + * Sorts the DataTable rows using the supplied callback function. + * + * @param string $functionCallback A comparison callback compatible with {@link usort}. + * @param string $columnSortedBy The column name `$functionCallback` sorts by. This is stored + * so we can determine how the DataTable was sorted in the future. + */ + private function sort(DataTable $table, $functionCallback) + { + $table->setTableSortedBy($this->columnToSort); + + $rows = $table->getRowsWithoutSummaryRow(); + + // get column value and label only once for performance tweak + $values = array(); + if ($functionCallback === 'numberSort') { + foreach ($rows as $key => $row) { + $values[$key] = array($this->getColumnValue($row), $row->getColumn('label')); + } + } else { + foreach ($rows as $key => $row) { + $values[$key] = $this->getColumnValue($row); + } + } + + uasort($values, array($this, $functionCallback)); + + $sortedRows = array(); + foreach ($values as $key => $value) { + $sortedRows[] = $rows[$key]; + } + + $table->setRows($sortedRows); + + unset($rows); + unset($sortedRows); + + if ($table->isSortRecursiveEnabled()) { + foreach ($table->getRowsWithoutSummaryRow() as $row) { + $subTable = $row->getSubtable(); + if ($subTable) { + $subTable->enableRecursiveSort(); + $this->sort($subTable, $functionCallback); + } + } + } } } diff --git a/www/analytics/core/DataTable/Filter/Truncate.php b/www/analytics/core/DataTable/Filter/Truncate.php index fa538965..ec95811c 100644 --- a/www/analytics/core/DataTable/Filter/Truncate.php +++ b/www/analytics/core/DataTable/Filter/Truncate.php @@ -1,6 +1,6 @@ filter('Truncate', array($truncateAfter = 500)); - * + * * **Using a custom summary row label** - * + * * $dataTable->filter('Truncate', array($truncateAfter = 500, $summaryRowLabel = Piwik::translate('General_Total'))); - * + * * @api */ class Truncate extends BaseFilter { /** * Constructor. - * + * * @param DataTable $table The table that will be filtered eventually. * @param int $truncateAfter The row index to truncate at. All rows passed this index will * be removed. @@ -69,11 +69,15 @@ class Truncate extends BaseFilter */ public function filter($table) { + if ($this->truncateAfter < 0) { + return; + } + $this->addSummaryRow($table); $table->queueFilter('ReplaceSummaryRowLabel', array($this->labelSummaryRow)); if ($this->filterRecursive) { - foreach ($table->getRows() as $row) { + foreach ($table->getRowsWithoutSummaryRow() as $row) { if ($row->isSubtableLoaded()) { $this->filter($row->getSubtable()); } @@ -81,17 +85,23 @@ class Truncate extends BaseFilter } } + /** + * @param DataTable $table + */ private function addSummaryRow($table) { - $table->filter('Sort', array($this->columnToSortByBeforeTruncating, 'desc')); - if ($table->getRowsCount() <= $this->truncateAfter + 1) { return; } - $rows = $table->getRows(); - $count = $table->getRowsCount(); + $table->filter('Sort', array($this->columnToSortByBeforeTruncating, 'desc', $naturalSort = true, $recursiveSort = false)); + + $rows = array_values($table->getRows()); + $count = $table->getRowsCount(); $newRow = new Row(array(Row::COLUMNS => array('label' => DataTable::LABEL_SUMMARY_ROW))); + + $aggregationOps = $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME); + for ($i = $this->truncateAfter; $i < $count; $i++) { if (!isset($rows[$i])) { // case when the last row is a summary row, it is not indexed by $cout but by DataTable::ID_SUMMARY_ROW @@ -99,10 +109,10 @@ class Truncate extends BaseFilter //FIXME: I'm not sure why it could return false, but it was reported in: http://forum.piwik.org/read.php?2,89324,page=1#msg-89442 if ($summaryRow) { - $newRow->sumRow($summaryRow, $enableCopyMetadata = false, $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME)); + $newRow->sumRow($summaryRow, $enableCopyMetadata = false, $aggregationOps); } } else { - $newRow->sumRow($rows[$i], $enableCopyMetadata = false, $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME)); + $newRow->sumRow($rows[$i], $enableCopyMetadata = false, $aggregationOps); } } diff --git a/www/analytics/core/DataTable/Manager.php b/www/analytics/core/DataTable/Manager.php index b9703c14..228f13da 100644 --- a/www/analytics/core/DataTable/Manager.php +++ b/www/analytics/core/DataTable/Manager.php @@ -1,6 +1,6 @@ tables[$this->nextTableId] = $table; $this->nextTableId++; - return $this->nextTableId - 1; + $this[$this->nextTableId] = $table; + return $this->nextTableId; } /** @@ -60,10 +61,11 @@ class Manager extends Singleton */ public function getTable($idTable) { - if (!isset($this->tables[$idTable])) { - throw new TableNotFoundException(sprintf("This report has been reprocessed since your last click. To see this error less often, please increase the timeout value in seconds in Settings > General Settings. (error: id %s not found).", $idTable)); + if (!isset($this[$idTable])) { + throw new TableNotFoundException(sprintf("Error: table id %s not found in memory. (If this error is causing you problems in production, please report it in Piwik issue tracker.)", $idTable)); } - return $this->tables[$idTable]; + + return $this[$idTable]; } /** @@ -73,7 +75,7 @@ class Manager extends Singleton */ public function getMostRecentTableId() { - return $this->nextTableId - 1; + return $this->nextTableId; } /** @@ -81,14 +83,15 @@ class Manager extends Singleton */ public function deleteAll($deleteWhenIdTableGreaterThan = 0) { - foreach ($this->tables as $id => $table) { + foreach ($this as $id => $table) { if ($id > $deleteWhenIdTableGreaterThan) { $this->deleteTable($id); } } + if ($deleteWhenIdTableGreaterThan == 0) { - $this->tables = array(); - $this->nextTableId = 1; + $this->exchangeArray(array()); + $this->nextTableId = 0; } } @@ -100,8 +103,8 @@ class Manager extends Singleton */ public function deleteTable($id) { - if (isset($this->tables[$id])) { - Common::destroy($this->tables[$id]); + if (isset($this[$id])) { + Common::destroy($this[$id]); $this->setTableDeleted($id); } } @@ -131,7 +134,7 @@ class Manager extends Singleton */ public function setTableDeleted($id) { - $this->tables[$id] = null; + $this[$id] = null; } /** @@ -140,7 +143,7 @@ class Manager extends Singleton public function dumpAllTables() { echo "
Manager->dumpAllTables()
"; - foreach ($this->tables as $id => $table) { + foreach ($this as $id => $table) { if (!($table instanceof DataTable)) { echo "Error table $id is not instance of datatable
"; var_export($table); diff --git a/www/analytics/core/DataTable/Map.php b/www/analytics/core/DataTable/Map.php index 7985afb1..8f9d259f 100644 --- a/www/analytics/core/DataTable/Map.php +++ b/www/analytics/core/DataTable/Map.php @@ -1,6 +1,6 @@ getDataTables() as $id => $table) { + foreach ($this->getDataTables() as $table) { $table->filter($className, $parameters); } } + /** + * Apply a filter to all subtables contained by this instance. + * + * @param string|Closure $className Name of filter class or a Closure. + * @param array $parameters Parameters to pass to the filter. + */ + public function filterSubtables($className, $parameters = array()) + { + foreach ($this->getDataTables() as $table) { + $table->filterSubtables($className, $parameters); + } + } + + /** + * Apply a queued filter to all subtables contained by this instance. + * + * @param string|Closure $className Name of filter class or a Closure. + * @param array $parameters Parameters to pass to the filter. + */ + public function queueFilterSubtables($className, $parameters = array()) + { + foreach ($this->getDataTables() as $table) { + $table->queueFilterSubtables($className, $parameters); + } + } + /** * Returns the array of DataTables contained by this class. * @@ -142,7 +169,7 @@ class Map implements DataTableInterface /** * Returns the last element in the Map's array. - * + * * @return DataTable|Map|false */ public function getLastRow() @@ -161,6 +188,22 @@ class Map implements DataTableInterface $this->array[$label] = $table; } + public function getRowFromIdSubDataTable($idSubtable) + { + $dataTables = $this->getDataTables(); + + // find first datatable containing data + foreach ($dataTables as $subTable) { + $subTableRow = $subTable->getRowFromIdSubDataTable($idSubtable); + + if (!empty($subTableRow)) { + return $subTableRow; + } + } + + return null; + } + /** * Returns a string output of this DataTable\Map (applying the default renderer to every {@link DataTable} * of this DataTable\Map). @@ -184,11 +227,31 @@ class Map implements DataTableInterface } } + /** + * @ignore + */ + public function disableRecursiveFilters() + { + foreach ($this->getDataTables() as $table) { + $table->disableRecursiveFilters(); + } + } + + /** + * @ignore + */ + public function enableRecursiveFilters() + { + foreach ($this->getDataTables() as $table) { + $table->enableRecursiveFilters(); + } + } + /** * Renames the given column in each contained {@link DataTable}. * * See {@link DataTable::renameColumn()}. - * + * * @param string $oldName * @param string $newName */ @@ -203,7 +266,7 @@ class Map implements DataTableInterface * Deletes the specified columns in each contained {@link DataTable}. * * See {@link DataTable::deleteColumns()}. - * + * * @param array $columns The columns to delete. * @param bool $deleteRecursiveInSubtables This param is currently not used. */ @@ -216,7 +279,7 @@ class Map implements DataTableInterface /** * Deletes a table from the array of DataTables. - * + * * @param string $id The label associated with {@link DataTable}. */ public function deleteRow($id) @@ -246,12 +309,14 @@ class Map implements DataTableInterface public function getColumn($name) { $values = array(); + foreach ($this->getDataTables() as $table) { $moreValues = $table->getColumn($name); foreach ($moreValues as &$value) { $values[] = $value; } } + return $values; } @@ -263,19 +328,19 @@ class Map implements DataTableInterface * The result of this function is determined by the type of DataTable * this instance holds. If this DataTable\Map instance holds an array * of DataTables, this function will transform it from: - * + * * Label 0: * DataTable(row1) * Label 1: * DataTable(row2) - * + * * to: - * + * * DataTable(row1[label = 'Label 0'], row2[label = 'Label 1']) * * If this instance holds an array of DataTable\Maps, this function will * transform it from: - * + * * Outer Label 0: // the outer DataTable\Map * Inner Label 0: // one of the inner DataTable\Maps * DataTable(row1) @@ -286,9 +351,9 @@ class Map implements DataTableInterface * DataTable(row3) * Inner Label 1: * DataTable(row4) - * + * * to: - * + * * Inner Label 0: * DataTable(row1[label = 'Outer Label 0'], row3[label = 'Outer Label 1']) * Inner Label 1: @@ -366,11 +431,11 @@ class Map implements DataTableInterface /** * Sums a DataTable to all the tables in this array. - * + * * _Note: Will only add `$tableToSum` if the childTable has some rows._ * * See {@link Piwik\DataTable::addDataTable()}. - * + * * @param DataTable $tableToSum */ public function addDataTable(DataTable $tableToSum) diff --git a/www/analytics/core/DataTable/Renderer.php b/www/analytics/core/DataTable/Renderer.php index 7820f3e5..e8796aea 100644 --- a/www/analytics/core/DataTable/Renderer.php +++ b/www/analytics/core/DataTable/Renderer.php @@ -1,6 +1,6 @@ setTable($dataTable); * echo $render; */ -abstract class Renderer +abstract class Renderer extends BaseFactory { protected $table; @@ -100,7 +101,7 @@ abstract class Renderer */ protected function renderHeader() { - @header('Content-Type: text/plain; charset=utf-8'); + Common::sendHeader('Content-Type: text/plain; charset=utf-8'); } /** @@ -110,22 +111,6 @@ abstract class Renderer */ abstract public function render(); - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - abstract public function renderException(); - - protected function getExceptionMessage() - { - $message = $this->exception->getMessage(); - if (\Piwik_ShouldPrintBackTraceWithMessage()) { - $message .= "\n" . $this->exception->getTraceAsString(); - } - return self::renderHtmlEntities($message); - } - /** * @see render() * @return string @@ -144,32 +129,17 @@ abstract class Renderer public function setTable($table) { if (!is_array($table) - && !($table instanceof DataTable) - && !($table instanceof DataTable\Map) + && !($table instanceof DataTableInterface) ) { - throw new Exception("DataTable renderers renderer accepts only DataTable and Map instances, and arrays."); + throw new Exception("DataTable renderers renderer accepts only DataTable, Simple and Map instances, and arrays."); } $this->table = $table; } - /** - * Set the Exception to be rendered - * - * @param Exception $exception to be rendered - * @throws Exception - */ - public function setException($exception) - { - if (!($exception instanceof Exception)) { - throw new Exception("The exception renderer accepts only an Exception object."); - } - $this->exception = $exception; - } - /** * @var array */ - static protected $availableRenderers = array('xml', + protected static $availableRenderers = array('xml', 'json', 'csv', 'tsv', @@ -182,41 +152,25 @@ abstract class Renderer * * @return array */ - static public function getRenderers() + public static function getRenderers() { return self::$availableRenderers; } - /** - * Returns the DataTable associated to the output format $name - * - * @param string $name - * @throws Exception If the renderer is unknown - * @return \Piwik\DataTable\Renderer - */ - static public function factory($name) + protected static function getClassNameFromClassId($id) { - $className = ucfirst(strtolower($name)); + $className = ucfirst(strtolower($id)); $className = 'Piwik\DataTable\Renderer\\' . $className; - try { - Loader::loadClass($className); - return new $className; - } catch (Exception $e) { - $availableRenderers = implode(', ', self::getRenderers()); - @header('Content-Type: text/plain; charset=utf-8'); - throw new Exception(Piwik::translate('General_ExceptionInvalidRendererFormat', array($className, $availableRenderers))); - } + + return $className; } - /** - * Returns $rawData after all applicable characters have been converted to HTML entities. - * - * @param String $rawData data to be converted - * @return String - */ - static protected function renderHtmlEntities($rawData) + protected static function getInvalidClassIdExceptionMessage($id) { - return self::formatValueXml($rawData); + $availableRenderers = implode(', ', self::getRenderers()); + $klassName = self::getClassNameFromClassId($id); + + return Piwik::translate('General_ExceptionInvalidRendererFormat', array($klassName, $availableRenderers)); } /** @@ -236,12 +190,14 @@ abstract class Renderer $value = @mb_convert_encoding($value, 'UTF-8', 'UTF-8'); } $value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); + $htmlentities = array(" ", "¡", "¢", "£", "¤", "¥", "¦", "§", "¨", "©", "ª", "«", "¬", "­", "®", "¯", "°", "±", "²", "³", "´", "µ", "¶", "·", "¸", "¹", "º", "»", "¼", "½", "¾", "¿", "À", "Á", "Â", "Ã", "Ä", "Å", "Æ", "Ç", "È", "É", "Ê", "Ë", "Ì", "Í", "Î", "Ï", "Ð", "Ñ", "Ò", "Ó", "Ô", "Õ", "Ö", "×", "Ø", "Ù", "Ú", "Û", "Ü", "Ý", "Þ", "ß", "à", "á", "â", "ã", "ä", "å", "æ", "ç", "è", "é", "ê", "ë", "ì", "í", "î", "ï", "ð", "ñ", "ò", "ó", "ô", "õ", "ö", "÷", "ø", "ù", "ú", "û", "ü", "ý", "þ", "ÿ", "€"); - $xmlentities = array("¢", "£", "¤", "¥", "¦", "§", "¨", "©", "ª", "«", "¬", "­", "®", "¯", "°", "±", "²", "³", "´", "µ", "¶", "·", "¸", "¹", "º", "»", "¼", "½", "¾", "¿", "À", "Á", "Â", "Ã", "Ä", "Å", "Æ", "Ç", "È", "É", "Ê", "Ë", "Ì", "Í", "Î", "Ï", "Ð", "Ñ", "Ò", "Ó", "Ô", "Õ", "Ö", "×", "Ø", "Ù", "Ú", "Û", "Ü", "Ý", "Þ", "ß", "à", "á", "â", "ã", "ä", "å", "æ", "ç", "è", "é", "ê", "ë", "ì", "í", "î", "ï", "ð", "ñ", "ò", "ó", "ô", "õ", "ö", "÷", "ø", "ù", "ú", "û", "ü", "ý", "þ", "ÿ", "€"); - $value = str_replace($htmlentities, $xmlentities, $value); + $xmlentities = array("¢", "£", "¤", "¥", "¦", "§", "¨", "©", "ª", "«", "¬", "­", "®", "¯", "°", "±", "²", "³", "´", "µ", "¶", "·", "¸", "¹", "º", "»", "¼", "½", "¾", "¿", "À", "Á", "Â", "Ã", "Ä", "Å", "Æ", "Ç", "È", "É", "Ê", "Ë", "Ì", "Í", "Î", "Ï", "Ð", "Ñ", "Ò", "Ó", "Ô", "Õ", "Ö", "×", "Ø", "Ù", "Ú", "Û", "Ü", "Ý", "Þ", "ß", "à", "á", "â", "ã", "ä", "å", "æ", "ç", "è", "é", "ê", "ë", "ì", "í", "î", "ï", "ð", "ñ", "ò", "ó", "ô", "õ", "ö", "÷", "ø", "ù", "ú", "û", "ü", "ý", "þ", "ÿ", "€"); + $value = str_replace($htmlentities, $xmlentities, $value); } elseif ($value === false) { $value = 0; } + return $value; } diff --git a/www/analytics/core/DataTable/Renderer/Console.php b/www/analytics/core/DataTable/Renderer/Console.php index 7959c644..bd16e93c 100644 --- a/www/analytics/core/DataTable/Renderer/Console.php +++ b/www/analytics/core/DataTable/Renderer/Console.php @@ -1,6 +1,6 @@ renderHeader(); return $this->renderTable($this->table); } - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - public function renderException() - { - $this->renderHeader(); - $exceptionMessage = $this->getExceptionMessage(); - return 'Error: ' . $exceptionMessage; - } - /** * Sets the prefix to be used * @@ -85,8 +71,9 @@ class Console extends Renderer */ protected function renderTable($table, $prefix = "") { - if (is_array($table)) // convert array to DataTable - { + if (is_array($table)) { + // convert array to DataTable + $table = DataTable::makeFromSimpleArray($table); } @@ -110,8 +97,11 @@ class Console extends Renderer $dataTableMapBreak = true; break; } - if (is_string($value)) $value = "'$value'"; - elseif (is_array($value)) $value = var_export($value, true); + if (is_string($value)) { + $value = "'$value'"; + } elseif (is_array($value)) { + $value = var_export($value, true); + } $columns[] = "'$column' => $value"; } @@ -122,8 +112,11 @@ class Console extends Renderer $metadata = array(); foreach ($row->getMetadata() as $name => $value) { - if (is_string($value)) $value = "'$value'"; - elseif (is_array($value)) $value = var_export($value, true); + if (is_string($value)) { + $value = "'$value'"; + } elseif (is_array($value)) { + $value = var_export($value, true); + } $metadata[] = "'$name' => $value"; } $metadata = implode(", ", $metadata); @@ -133,14 +126,10 @@ class Console extends Renderer . $row->getIdSubDataTable() . "]
\n"; if (!is_null($row->getIdSubDataTable())) { - if ($row->isSubtableLoaded()) { + $subTable = $row->getSubtable(); + if ($subTable) { $depth++; - $output .= $this->renderTable( - Manager::getInstance()->getTable( - $row->getIdSubDataTable() - ), - $prefix . '      ' - ); + $output .= $this->renderTable($subTable, $prefix . '      '); $depth--; } else { $output .= "-- Sub DataTable not loaded
\n"; @@ -155,7 +144,7 @@ class Console extends Renderer foreach ($metadata as $id => $metadataIn) { $output .= "
"; $output .= $prefix . " $id
"; - if(is_array($metadataIn)) { + if (is_array($metadataIn)) { foreach ($metadataIn as $name => $value) { $output .= $prefix . $prefix . "$name => $value"; } diff --git a/www/analytics/core/DataTable/Renderer/Csv.php b/www/analytics/core/DataTable/Renderer/Csv.php index cc9030d4..c3fb08b1 100644 --- a/www/analytics/core/DataTable/Renderer/Csv.php +++ b/www/analytics/core/DataTable/Renderer/Csv.php @@ -1,6 +1,6 @@ renderHeader(); - if ($this->convertToUnicode - && function_exists('mb_convert_encoding') - ) { - $str = chr(255) . chr(254) . mb_convert_encoding($str, 'UTF-16LE', 'UTF-8'); - } + $str = $this->convertToUnicode($str); return $str; } - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - function renderException() - { - @header('Content-Type: text/html; charset=utf-8'); - $exceptionMessage = $this->getExceptionMessage(); - return 'Error: ' . $exceptionMessage; - } - /** * Enables / Disables unicode converting * @@ -133,8 +119,9 @@ class Csv extends Renderer */ protected function renderTable($table, &$allColumns = array()) { - if (is_array($table)) // convert array to DataTable - { + if (is_array($table)) { + // convert array to DataTable + $table = DataTable::makeFromSimpleArray($table); } @@ -205,42 +192,7 @@ class Csv extends Renderer } } - $csv = array(); - foreach ($table->getRows() as $row) { - $csvRow = $this->flattenColumnArray($row->getColumns()); - - if ($this->exportMetadata) { - $metadata = $row->getMetadata(); - foreach ($metadata as $name => $value) { - if ($name == 'idsubdatatable_in_db') { - continue; - } - //if a metadata and a column have the same name make sure they dont overwrite - if ($this->translateColumnNames) { - $name = Piwik::translate('General_Metadata') . ': ' . $name; - } else { - $name = 'metadata_' . $name; - } - - $csvRow[$name] = $value; - } - } - - foreach ($csvRow as $name => $value) { - $allColumns[$name] = true; - } - - if ($this->exportIdSubtable) { - $idsubdatatable = $row->getIdSubDataTable(); - if ($idsubdatatable !== false - && $this->hideIdSubDatatable === false - ) { - $csvRow['idsubdatatable'] = $idsubdatatable; - } - } - - $csv[] = $csvRow; - } + $csv = $this->makeArrayFromDataTable($table, $allColumns); // now we make sure that all the rows in the CSV array have all the columns foreach ($csv as &$row) { @@ -251,31 +203,7 @@ class Csv extends Renderer } } - $str = ''; - - // specific case, we have only one column and this column wasn't named properly (indexed by a number) - // we don't print anything in the CSV file => an empty line - if (sizeof($allColumns) == 1 - && reset($allColumns) - && !is_string(key($allColumns)) - ) { - $str .= ''; - } else { - // render row names - $str .= $this->getHeaderLine(array_keys($allColumns)) . $this->lineEnd; - } - - // we render the CSV - foreach ($csv as $theRow) { - $rowStr = ''; - foreach ($allColumns as $columnName => $true) { - $rowStr .= $this->formatValue($theRow[$columnName]) . $this->separator; - } - // remove the last separator - $rowStr = substr_replace($rowStr, "", -strlen($this->separator)); - $str .= $rowStr . $this->lineEnd; - } - $str = substr($str, 0, -strlen($this->lineEnd)); + $str = $this->buildCsvString($allColumns, $csv); return $str; } @@ -287,9 +215,20 @@ class Csv extends Renderer */ private function getHeaderLine($columnMetrics) { + foreach ($columnMetrics as $index => $value) { + if (in_array($value, $this->unsupportedColumns)) { + unset($columnMetrics[$index]); + } + } + if ($this->translateColumnNames) { $columnMetrics = $this->translateColumnNames($columnMetrics); } + + foreach ($columnMetrics as &$value) { + $value = $this->formatValue($value); + } + return implode($this->separator, $columnMetrics); } @@ -334,14 +273,15 @@ class Csv extends Renderer $period = Common::getRequestVar('period', false); $date = Common::getRequestVar('date', false); - if ($period || $date) // in test cases, there are no request params set - { + if ($period || $date) { + // in test cases, there are no request params set + if ($period == 'range') { $period = new Range($period, $date); - } else if (strpos($date, ',') !== false) { + } elseif (strpos($date, ',') !== false) { $period = new Range('range', $date); } else { - $period = Period::factory($period, Date::factory($date)); + $period = Period\Factory::build($period, Date::factory($date)); } $prettyDate = $period->getLocalizedLongString(); @@ -353,8 +293,7 @@ class Csv extends Renderer } // silent fail otherwise unit tests fail - @header('Content-Type: application/vnd.ms-excel'); - @header('Content-Disposition: attachment; filename="' . $fileName . '"'); + Common::sendHeader('Content-Disposition: attachment; filename="' . $fileName . '"', true); ProxyHttp::overrideCacheControlHeaders(); } @@ -400,4 +339,119 @@ class Csv extends Renderer return $name; } } + + /** + * @param $allColumns + * @param $csv + * @return array + */ + private function buildCsvString($allColumns, $csv) + { + $str = ''; + + // specific case, we have only one column and this column wasn't named properly (indexed by a number) + // we don't print anything in the CSV file => an empty line + if (sizeof($allColumns) == 1 + && reset($allColumns) + && !is_string(key($allColumns)) + ) { + $str .= ''; + } else { + // render row names + $str .= $this->getHeaderLine(array_keys($allColumns)) . $this->lineEnd; + } + + // we render the CSV + foreach ($csv as $theRow) { + $rowStr = ''; + foreach ($allColumns as $columnName => $true) { + $rowStr .= $this->formatValue($theRow[$columnName]) . $this->separator; + } + // remove the last separator + $rowStr = substr_replace($rowStr, "", -strlen($this->separator)); + $str .= $rowStr . $this->lineEnd; + } + $str = substr($str, 0, -strlen($this->lineEnd)); + return $str; + } + + /** + * @param $table + * @param $allColumns + * @return array of csv data + */ + private function makeArrayFromDataTable($table, &$allColumns) + { + $csv = array(); + foreach ($table->getRows() as $row) { + $csvRow = $this->flattenColumnArray($row->getColumns()); + + if ($this->exportMetadata) { + $metadata = $row->getMetadata(); + foreach ($metadata as $name => $value) { + if ($name == 'idsubdatatable_in_db') { + continue; + } + //if a metadata and a column have the same name make sure they dont overwrite + if ($this->translateColumnNames) { + $name = Piwik::translate('General_Metadata') . ': ' . $name; + } else { + $name = 'metadata_' . $name; + } + + if (is_array($value)) { + if (!in_array($name, $this->unsupportedColumns)) { + $this->unsupportedColumns[] = $name; + } + } else { + $csvRow[$name] = $value; + } + + } + } + + foreach ($csvRow as $name => $value) { + if (in_array($name, $this->unsupportedColumns)) { + unset($allColumns[$name]); + } else { + $allColumns[$name] = true; + } + } + + if ($this->exportIdSubtable) { + $idsubdatatable = $row->getIdSubDataTable(); + if ($idsubdatatable !== false + && $this->hideIdSubDatatable === false + ) { + $csvRow['idsubdatatable'] = $idsubdatatable; + } + } + + $csv[] = $csvRow; + } + + if (!empty($this->unsupportedColumns)) { + foreach ($this->unsupportedColumns as $unsupportedColumn) { + foreach ($csv as $index => $row) { + unset($row[$index][$unsupportedColumn]); + } + } + } + + return $csv; + } + + /** + * @param $str + * @return string + */ + private function convertToUnicode($str) + { + if ($this->convertToUnicode + && function_exists('mb_convert_encoding') + ) { + $str = chr(255) . chr(254) . mb_convert_encoding($str, 'UTF-16LE', 'UTF-8'); + } + return $str; + } } diff --git a/www/analytics/core/DataTable/Renderer/Html.php b/www/analytics/core/DataTable/Renderer/Html.php index ca4cd653..f27dc517 100644 --- a/www/analytics/core/DataTable/Renderer/Html.php +++ b/www/analytics/core/DataTable/Renderer/Html.php @@ -1,6 +1,6 @@ tableId = str_replace('.', '_', $id); } - /** - * Output HTTP Content-Type header - */ - protected function renderHeader() - { - @header('Content-Type: text/html; charset=utf-8'); - } - /** * Computes the dataTable output and returns the string/binary * * @return string */ - function render() + public function render() { - $this->renderHeader(); $this->tableStructure = array(); $this->allColumns = array(); $this->i = 0; @@ -57,18 +48,6 @@ class Html extends Renderer return $this->renderTable($this->table); } - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - function renderException() - { - $this->renderHeader(); - $exceptionMessage = $this->getExceptionMessage(); - return nl2br($exceptionMessage); - } - /** * Computes the output for the given data table * @@ -77,8 +56,9 @@ class Html extends Renderer */ protected function renderTable($table) { - if (is_array($table)) // convert array to DataTable - { + if (is_array($table)) { + // convert array to DataTable + $table = DataTable::makeFromSimpleArray($table); } @@ -88,8 +68,9 @@ class Html extends Renderer $this->buildTableStructure($subtable, '_' . $table->getKeyName(), $date); } } - } else // Simple - { + } else { + // Simple + if ($table->getRowsCount()) { $this->buildTableStructure($table); } @@ -134,7 +115,9 @@ class Html extends Renderer $metadata = array(); foreach ($row->getMetadata() as $name => $value) { - if (is_string($value)) $value = "'$value'"; + if (is_string($value)) { + $value = "'$value'"; + } $metadata[] = "'$name' => $value"; } diff --git a/www/analytics/core/DataTable/Renderer/Json.php b/www/analytics/core/DataTable/Renderer/Json.php index 664f14f4..36079aad 100644 --- a/www/analytics/core/DataTable/Renderer/Json.php +++ b/www/analytics/core/DataTable/Renderer/Json.php @@ -1,6 +1,6 @@ renderHeader(); return $this->renderTable($this->table); } - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - function renderException() - { - $this->renderHeader(); - - $exceptionMessage = $this->getExceptionMessage(); - $exceptionMessage = str_replace(array("\r\n", "\n"), "", $exceptionMessage); - - $result = json_encode(array('result' => 'error', 'message' => $exceptionMessage)); - - return $this->jsonpWrap($result); - } - /** * Computes the output for the given data table * @@ -73,7 +54,6 @@ class Json extends Renderer } } } - } else { $array = $this->convertDataTableToArray($table); } @@ -90,40 +70,15 @@ class Json extends Renderer }; array_walk_recursive($array, $callback); - $str = json_encode($array); - - return $this->jsonpWrap($str); - } - - /** - * @param $str - * @return string - */ - protected function jsonpWrap($str) - { - if (($jsonCallback = Common::getRequestVar('callback', false)) === false) - $jsonCallback = Common::getRequestVar('jsoncallback', false); - if ($jsonCallback !== false) { - if (preg_match('/^[0-9a-zA-Z_.]*$/D', $jsonCallback) > 0) { - $str = $jsonCallback . "(" . $str . ")"; - } - } + // silence "Warning: json_encode(): Invalid UTF-8 sequence in argument" + $str = @json_encode($array); return $str; } - /** - * Sends the http header for json file - */ - protected function renderHeader() - { - self::sendHeaderJSON(); - ProxyHttp::overrideCacheControlHeaders(); - } - public static function sendHeaderJSON() { - @header('Content-Type: application/json; charset=utf-8'); + Common::sendHeader('Content-Type: application/json; charset=utf-8'); } private function convertDataTableToArray($table) diff --git a/www/analytics/core/DataTable/Renderer/Php.php b/www/analytics/core/DataTable/Renderer/Php.php index 80e49bc2..9a121bc6 100644 --- a/www/analytics/core/DataTable/Renderer/Php.php +++ b/www/analytics/core/DataTable/Renderer/Php.php @@ -1,6 +1,6 @@ renderHeader(); - if (is_null($dataTable)) { $dataTable = $this->table; } @@ -87,26 +84,6 @@ class Php extends Renderer return $toReturn; } - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - public function renderException() - { - $this->renderHeader(); - - $exceptionMessage = $this->getExceptionMessage(); - - $return = array('result' => 'error', 'message' => $exceptionMessage); - - if ($this->serialize) { - $return = serialize($return); - } - - return $return; - } - /** * Produces a flat php array from the DataTable, putting "columns" and "metadata" on the same level. * @@ -133,7 +110,7 @@ class Php extends Renderer if (self::shouldWrapArrayBeforeRendering($flatArray)) { $flatArray = array($flatArray); } - } else if ($dataTable instanceof DataTable\Map) { + } elseif ($dataTable instanceof DataTable\Map) { $flatArray = array(); foreach ($dataTable->getDataTables() as $keyName => $table) { $serializeSave = $this->serialize; @@ -141,7 +118,7 @@ class Php extends Renderer $flatArray[$keyName] = $this->flatRender($table); $this->serialize = $serializeSave; } - } else if ($dataTable instanceof Simple) { + } elseif ($dataTable instanceof Simple) { $flatArray = $this->renderSimpleTable($dataTable); // if we return only one numeric value then we print out the result in a simple tag @@ -228,10 +205,11 @@ class Php extends Renderer $newRow['issummaryrow'] = true; } + $subTable = $row->getSubtable(); if ($this->isRenderSubtables() - && $row->isSubtableLoaded() + && $subTable ) { - $subTable = $this->renderTable(Manager::getInstance()->getTable($row->getIdSubDataTable())); + $subTable = $this->renderTable($subTable); $newRow['subtable'] = $subTable; if ($this->hideIdSubDatatable === false && isset($newRow['metadata']['idsubdatatable_in_db']) diff --git a/www/analytics/core/DataTable/Renderer/Rss.php b/www/analytics/core/DataTable/Renderer/Rss.php index 2c07e00b..fd18b443 100644 --- a/www/analytics/core/DataTable/Renderer/Rss.php +++ b/www/analytics/core/DataTable/Renderer/Rss.php @@ -1,6 +1,6 @@ renderHeader(); return $this->renderTable($this->table); } - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - function renderException() - { - header('Content-type: text/plain'); - $exceptionMessage = $this->getExceptionMessage(); - return 'Error: ' . $exceptionMessage; - } - /** * Computes the output for the given data table * @@ -101,14 +87,6 @@ class Rss extends Renderer return $header . $out . $footer; } - /** - * Sends the xml file http header - */ - protected function renderHeader() - { - @header('Content-Type: text/xml; charset=utf-8'); - } - /** * Returns the RSS file footer * @@ -185,7 +163,6 @@ class Rss extends Renderer } } $html .= "\n"; - $colspan = count($allColumns); foreach ($tableStructure as $row) { $html .= "\n\n"; diff --git a/www/analytics/core/DataTable/Renderer/Tsv.php b/www/analytics/core/DataTable/Renderer/Tsv.php index 949874da..af45a62d 100644 --- a/www/analytics/core/DataTable/Renderer/Tsv.php +++ b/www/analytics/core/DataTable/Renderer/Tsv.php @@ -1,6 +1,6 @@ setSeparator("\t"); @@ -32,7 +31,7 @@ class Tsv extends Csv * * @return string */ - function render() + public function render() { return parent::render(); } diff --git a/www/analytics/core/DataTable/Renderer/Xml.php b/www/analytics/core/DataTable/Renderer/Xml.php index 15f4f52e..b01f5596 100644 --- a/www/analytics/core/DataTable/Renderer/Xml.php +++ b/www/analytics/core/DataTable/Renderer/Xml.php @@ -1,6 +1,6 @@ renderHeader(); return '' . "\n" . $this->renderTable($this->table); } - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - function renderException() - { - $this->renderHeader(); - - $exceptionMessage = $this->getExceptionMessage(); - - $return = '' . "\n" . - "\n" . - "\t\n" . - ""; - - return $return; - } - /** * Converts the given data table to an array * @@ -174,17 +154,16 @@ class Xml extends Renderer foreach ($array as $key => $value) { // based on the type of array & the key, determine how this node will look if ($isAssociativeArray) { - $keyIsInvalidXmlElement = is_numeric($key) || is_numeric($key[0]); - if ($keyIsInvalidXmlElement) { - $prefix = ""; - $suffix = ""; - $emptyNode = ""; - } else if (strpos($key, '=') !== false) { + if (strpos($key, '=') !== false) { list($keyAttributeName, $key) = explode('=', $key, 2); $prefix = ""; $suffix = ""; $emptyNode = ""; + } elseif (!self::isValidXmlTagName($key)) { + $prefix = ""; + $suffix = ""; + $emptyNode = ""; } else { $prefix = "<$key>"; $suffix = ""; @@ -201,7 +180,7 @@ class Xml extends Renderer $result .= $prefixLines . $prefix . "\n"; $result .= $this->renderArray($value, $prefixLines . "\t"); $result .= $prefixLines . $suffix . "\n"; - } else if ($value instanceof DataTable + } elseif ($value instanceof DataTable || $value instanceof Map ) { if ($value->getRowsCount() == 0) { @@ -210,7 +189,7 @@ class Xml extends Renderer $result .= $prefixLines . $prefix . "\n"; if ($value instanceof Map) { $result .= $this->renderDataTableMap($value, $this->getArrayFromDataTable($value), $prefixLines); - } else if ($value instanceof Simple) { + } elseif ($value instanceof Simple) { $result .= $this->renderDataTableSimple($this->getArrayFromDataTable($value), $prefixLines); } else { $result .= $this->renderDataTable($this->getArrayFromDataTable($value), $prefixLines); @@ -358,6 +337,8 @@ class Xml extends Renderer */ protected function renderDataTable($array, $prefixLine = "") { + $columnsHaveInvalidChars = $this->areTableLabelsInvalidXmlTagNames(reset($array)); + $out = ''; foreach ($array as $rowId => $row) { if (!is_array($row)) { @@ -370,10 +351,9 @@ class Xml extends Renderer continue; } - // Handing case idgoal=7, creating a new array for that one $rowAttribute = ''; - if (($equalFound = strstr($rowId, '=')) !== false) { + if (strstr($rowId, '=') !== false) { $rowAttribute = explode('=', $rowId); $rowAttribute = " " . $rowAttribute[0] . "='" . $rowAttribute[1] . "'"; } @@ -394,10 +374,13 @@ class Xml extends Renderer } else { $value = self::formatValueXml($value); } + + list($tagStart, $tagEnd) = $this->getTagStartAndEndFor($name, $columnsHaveInvalidChars); + if (strlen($value) == 0) { - $out .= $prefixLine . "\t\t<$name />\n"; + $out .= $prefixLine . "\t\t<$tagStart />\n"; } else { - $out .= $prefixLine . "\t\t<$name>" . $value . "\n"; + $out .= $prefixLine . "\t\t<$tagStart>" . $value . "\n"; } } $out .= "\t"; @@ -420,24 +403,62 @@ class Xml extends Renderer $array = array('value' => $array); } + $columnsHaveInvalidChars = $this->areTableLabelsInvalidXmlTagNames($array); + $out = ''; foreach ($array as $keyName => $value) { $xmlValue = self::formatValueXml($value); + list($tagStart, $tagEnd) = $this->getTagStartAndEndFor($keyName, $columnsHaveInvalidChars); if (strlen($xmlValue) == 0) { - $out .= $prefixLine . "\t<$keyName />\n"; + $out .= $prefixLine . "\t<$tagStart />\n"; } else { - $out .= $prefixLine . "\t<$keyName>" . $xmlValue . "\n"; + $out .= $prefixLine . "\t<$tagStart>" . $xmlValue . "\n"; } } return $out; } /** - * Sends the XML headers + * Returns true if a string is a valid XML tag name, false if otherwise. + * + * @param string $str + * @return bool */ - protected function renderHeader() + private static function isValidXmlTagName($str) { - // silent fail because otherwise it throws an exception in the unit tests - @header('Content-Type: text/xml; charset=utf-8'); + static $validTagRegex = null; + + if ($validTagRegex === null) { + $invalidTagChars = "!\"#$%&'()*+,\\/;<=>?@[\\]\\\\^`{|}~"; + $invalidTagStartChars = $invalidTagChars . "\\-.0123456789"; + $validTagRegex = "/^[^" . $invalidTagStartChars . "][^" . $invalidTagChars . "]*$/"; + } + + $result = preg_match($validTagRegex, $str); + return !empty($result); + } + + private function areTableLabelsInvalidXmlTagNames($rowArray) + { + if (!empty($rowArray)) { + foreach ($rowArray as $name => $value) { + if (!self::isValidXmlTagName($name)) { + return true; + } + } + } + return false; + } + + private function getTagStartAndEndFor($keyName, $columnsHaveInvalidChars) + { + if ($columnsHaveInvalidChars) { + $tagStart = "col name=\"" . self::formatValueXml($keyName) . "\""; + $tagEnd = "col"; + } else { + $tagStart = $tagEnd = $keyName; + } + + return array($tagStart, $tagEnd); } } diff --git a/www/analytics/core/DataTable/Row.php b/www/analytics/core/DataTable/Row.php index 2137da25..ad8a850c 100644 --- a/www/analytics/core/DataTable/Row.php +++ b/www/analytics/core/DataTable/Row.php @@ -1,6 +1,6 @@ value mappings. * - * * @api */ -class Row +class Row implements \ArrayAccess, \IteratorAggregate { /** * List of columns that cannot be summed. An associative array for speed. @@ -30,27 +30,21 @@ class Row */ private static $unsummableColumns = array( 'label' => true, - 'full_url' => true // column used w/ old Piwik versions + 'full_url' => true // column used w/ old Piwik versions, ); - /** - * This array contains the row information: - * - array indexed by self::COLUMNS contains the columns, pairs of (column names, value) - * - (optional) array indexed by self::METADATA contains the metadata, pairs of (metadata name, value) - * - (optional) integer indexed by self::DATATABLE_ASSOCIATED contains the ID of the DataTable associated to this row. - * This ID can be used to read the DataTable from the DataTable_Manager. - * - * @var array - * @see constructor for more information - * @ignore - */ - public $c = array(); - - private $subtableIdWasNegativeBeforeSerialize = false; - // @see sumRow - implementation detail public $maxVisitsSummed = 0; + private $columns = array(); + private $metadata = array(); + private $isSubtableLoaded = false; + + /** + * @internal + */ + public $subtableId = null; + const COLUMNS = 0; const METADATA = 1; const DATATABLE_ASSOCIATED = 3; @@ -59,7 +53,7 @@ class Row * Constructor. * * @param array $row An array with the following structure: - * + * * array( * Row::COLUMNS => array('label' => 'Piwik', * 'column1' => 42, @@ -72,51 +66,33 @@ class Row */ public function __construct($row = array()) { - $this->c[self::COLUMNS] = array(); - $this->c[self::METADATA] = array(); - $this->c[self::DATATABLE_ASSOCIATED] = null; - if (isset($row[self::COLUMNS])) { - $this->c[self::COLUMNS] = $row[self::COLUMNS]; + $this->columns = $row[self::COLUMNS]; } if (isset($row[self::METADATA])) { - $this->c[self::METADATA] = $row[self::METADATA]; + $this->metadata = $row[self::METADATA]; } - if (isset($row[self::DATATABLE_ASSOCIATED]) - && $row[self::DATATABLE_ASSOCIATED] instanceof DataTable - ) { - $this->setSubtable($row[self::DATATABLE_ASSOCIATED]); + if (isset($row[self::DATATABLE_ASSOCIATED])) { + if ($row[self::DATATABLE_ASSOCIATED] instanceof DataTable) { + $this->setSubtable($row[self::DATATABLE_ASSOCIATED]); + } else { + $this->subtableId = $row[self::DATATABLE_ASSOCIATED]; + } } } /** - * Because $this->c[self::DATATABLE_ASSOCIATED] is negative when the table is in memory, - * we must prior to serialize() call, make sure the ID is saved as positive integer - * - * Only serialize the "c" member + * Used when archiving to serialize the Row's properties. + * @return array * @ignore */ - public function __sleep() + public function export() { - if (!empty($this->c[self::DATATABLE_ASSOCIATED]) - && $this->c[self::DATATABLE_ASSOCIATED] < 0 - ) { - $this->c[self::DATATABLE_ASSOCIATED] = -1 * $this->c[self::DATATABLE_ASSOCIATED]; - $this->subtableIdWasNegativeBeforeSerialize = true; - } - return array('c'); - } - - /** - * Must be called after the row was serialized and __sleep was called. - * @ignore - */ - public function cleanPostSerialize() - { - if ($this->subtableIdWasNegativeBeforeSerialize) { - $this->c[self::DATATABLE_ASSOCIATED] = -1 * $this->c[self::DATATABLE_ASSOCIATED]; - $this->subtableIdWasNegativeBeforeSerialize = false; - } + return array( + self::COLUMNS => $this->columns, + self::METADATA => $this->metadata, + self::DATATABLE_ASSOCIATED => $this->subtableId, + ); } /** @@ -125,9 +101,10 @@ class Row */ public function __destruct() { - if ($this->isSubtableLoaded()) { - Manager::getInstance()->deleteTable($this->getIdSubDataTable()); - $this->c[self::DATATABLE_ASSOCIATED] = null; + if ($this->isSubtableLoaded) { + Manager::getInstance()->deleteTable($this->subtableId); + $this->subtableId = null; + $this->isSubtableLoaded = false; } } @@ -141,15 +118,21 @@ class Row { $columns = array(); foreach ($this->getColumns() as $column => $value) { - if (is_string($value)) $value = "'$value'"; - elseif (is_array($value)) $value = var_export($value, true); + if (is_string($value)) { + $value = "'$value'"; + } elseif (is_array($value)) { + $value = var_export($value, true); + } $columns[] = "'$column' => $value"; } $columns = implode(", ", $columns); $metadata = array(); foreach ($this->getMetadata() as $name => $value) { - if (is_string($value)) $value = "'$value'"; - elseif (is_array($value)) $value = var_export($value, true); + if (is_string($value)) { + $value = "'$value'"; + } elseif (is_array($value)) { + $value = var_export($value, true); + } $metadata[] = "'$name' => $value"; } $metadata = implode(", ", $metadata); @@ -165,10 +148,11 @@ class Row */ public function deleteColumn($name) { - if (!array_key_exists($name, $this->c[self::COLUMNS])) { + if (!array_key_exists($name, $this->columns)) { return false; } - unset($this->c[self::COLUMNS][$name]); + + unset($this->columns[$name]); return true; } @@ -180,11 +164,12 @@ class Row */ public function renameColumn($oldName, $newName) { - if (isset($this->c[self::COLUMNS][$oldName])) { - $this->c[self::COLUMNS][$newName] = $this->c[self::COLUMNS][$oldName]; + if (isset($this->columns[$oldName])) { + $this->columns[$newName] = $this->columns[$oldName]; } - // outside the if() since we want to delete nulled columns - unset($this->c[self::COLUMNS][$oldName]); + + // outside the if () since we want to delete nulled columns + unset($this->columns[$oldName]); } /** @@ -195,10 +180,11 @@ class Row */ public function getColumn($name) { - if (!isset($this->c[self::COLUMNS][$name])) { + if (!isset($this->columns[$name])) { return false; } - return $this->c[self::COLUMNS][$name]; + + return $this->columns[$name]; } /** @@ -210,19 +196,31 @@ class Row public function getMetadata($name = null) { if (is_null($name)) { - return $this->c[self::METADATA]; + return $this->metadata; } - if (!isset($this->c[self::METADATA][$name])) { + if (!isset($this->metadata[$name])) { return false; } - return $this->c[self::METADATA][$name]; + return $this->metadata[$name]; + } + + /** + * Returns true if a column having the given name is already registered. The value will not be evaluated, it will + * just check whether a column exists independent of its value. + * + * @param string $name + * @return bool + */ + public function hasColumn($name) + { + return array_key_exists($name, $this->columns); } /** * Returns the array containing all the columns. * * @return array Example: - * + * * array( * 'column1' => VALUE, * 'label' => 'www.php.net' @@ -231,7 +229,7 @@ class Row */ public function getColumns() { - return $this->c[self::COLUMNS]; + return $this->columns; } /** @@ -242,10 +240,7 @@ class Row */ public function getIdSubDataTable() { - return !is_null($this->c[self::DATATABLE_ASSOCIATED]) - // abs() is to ensure we return a positive int, @see isSubtableLoaded() - ? abs($this->c[self::DATATABLE_ASSOCIATED]) - : null; + return $this->subtableId; } /** @@ -255,48 +250,49 @@ class Row */ public function getSubtable() { - if ($this->isSubtableLoaded()) { - return Manager::getInstance()->getTable($this->getIdSubDataTable()); + if ($this->isSubtableLoaded) { + try { + return Manager::getInstance()->getTable($this->subtableId); + } catch (TableNotFoundException $e) { + // edge case + } } return false; } + /** + * @param int $subtableId + * @ignore + */ + public function setNonLoadedSubtableId($subtableId) + { + $this->subtableId = $subtableId; + $this->isSubtableLoaded = false; + } + /** * Sums a DataTable to this row's subtable. If this row has no subtable a new * one is created. - * + * * See {@link Piwik\DataTable::addDataTable()} to learn how DataTables are summed. - * + * * @param DataTable $subTable Table to sum to this row's subtable. */ public function sumSubtable(DataTable $subTable) { - if ($this->isSubtableLoaded()) { + if ($this->isSubtableLoaded) { $thisSubTable = $this->getSubtable(); } else { + $this->warnIfSubtableAlreadyExists(); + $thisSubTable = new DataTable(); - $this->addSubtable($thisSubTable); + $this->setSubtable($thisSubTable); } $columnOps = $subTable->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME); $thisSubTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnOps); $thisSubTable->addDataTable($subTable); } - /** - * Attaches a subtable to this row. - * - * @param DataTable $subTable DataTable to associate to this row. - * @return DataTable Returns `$subTable`. - * @throws Exception if a subtable already exists for this row. - */ - public function addSubtable(DataTable $subTable) - { - if (!is_null($this->c[self::DATATABLE_ASSOCIATED])) { - throw new Exception("Adding a subtable to the row, but it already has a subtable associated."); - } - return $this->setSubtable($subTable); - } - /** * Attaches a subtable to this row, overwriting the existing subtable, * if any. @@ -306,9 +302,9 @@ class Row */ public function setSubtable(DataTable $subTable) { - // Hacking -1 to ensure value is negative, so we know the table was loaded - // @see isSubtableLoaded() - $this->c[self::DATATABLE_ASSOCIATED] = -1 * $subTable->getId(); + $this->subtableId = $subTable->getId(); + $this->isSubtableLoaded = true; + return $subTable; } @@ -321,8 +317,7 @@ class Row { // self::DATATABLE_ASSOCIATED are set as negative values, // as a flag to signify that the subtable is loaded in memory - return !is_null($this->c[self::DATATABLE_ASSOCIATED]) - && $this->c[self::DATATABLE_ASSOCIATED] < 0; + return $this->isSubtableLoaded; } /** @@ -330,17 +325,18 @@ class Row */ public function removeSubtable() { - $this->c[self::DATATABLE_ASSOCIATED] = null; + $this->subtableId = null; + $this->isSubtableLoaded = false; } /** * Set all the columns at once. Overwrites **all** previously set columns. * - * @param array eg, `array('label' => 'www.php.net', 'nb_visits' => 15894)` + * @param array $columns eg, `array('label' => 'www.php.net', 'nb_visits' => 15894)` */ public function setColumns($columns) { - $this->c[self::COLUMNS] = $columns; + $this->columns = $columns; } /** @@ -351,7 +347,7 @@ class Row */ public function setColumn($name, $value) { - $this->c[self::COLUMNS][$name] = $value; + $this->columns[$name] = $value; } /** @@ -362,7 +358,7 @@ class Row */ public function setMetadata($name, $value) { - $this->c[self::METADATA][$name] = $value; + $this->metadata[$name] = $value; } /** @@ -374,13 +370,13 @@ class Row public function deleteMetadata($name = false) { if ($name === false) { - $this->c[self::METADATA] = array(); + $this->metadata = array(); return true; } - if (!isset($this->c[self::METADATA][$name])) { + if (!isset($this->metadata[$name])) { return false; } - unset($this->c[self::METADATA][$name]); + unset($this->metadata[$name]); return true; } @@ -388,15 +384,15 @@ class Row * Add a new column to the row. If the column already exists, throws an exception. * * @param string $name name of the column to add. - * @param mixed $value value of the column to set. + * @param mixed $value value of the column to set or a PHP callable. * @throws Exception if the column already exists. */ public function addColumn($name, $value) { - if (isset($this->c[self::COLUMNS][$name])) { + if (isset($this->columns[$name])) { throw new Exception("Column $name already in the array!"); } - $this->c[self::COLUMNS][$name] = $value; + $this->setColumn($name, $value); } /** @@ -429,51 +425,61 @@ class Row */ public function addMetadata($name, $value) { - if (isset($this->c[self::METADATA][$name])) { + if (isset($this->metadata[$name])) { throw new Exception("Metadata $name already in the array!"); } - $this->c[self::METADATA][$name] = $value; + $this->setMetadata($name, $value); + } + + private function isSummableColumn($columnName) + { + return empty(self::$unsummableColumns[$columnName]); } /** * Sums the given `$rowToSum` columns values to the existing row column values. * Only the int or float values will be summed. Label columns will be ignored * even if they have a numeric value. - * + * * Columns in `$rowToSum` that don't exist in `$this` are added to `$this`. * * @param \Piwik\DataTable\Row $rowToSum The row to sum to this row. * @param bool $enableCopyMetadata Whether metadata should be copied or not. - * @param array $aggregationOperations for columns that should not be summed, determine which + * @param array|bool $aggregationOperations for columns that should not be summed, determine which * aggregation should be used (min, max). format: * `array('column name' => 'function name')` + * @throws Exception */ public function sumRow(Row $rowToSum, $enableCopyMetadata = true, $aggregationOperations = false) { foreach ($rowToSum->getColumns() as $columnToSumName => $columnToSumValue) { - if (!isset(self::$unsummableColumns[$columnToSumName])) // make sure we can add this column - { - $thisColumnValue = $this->getColumn($columnToSumName); - - $operation = (is_array($aggregationOperations) && isset($aggregationOperations[$columnToSumName]) ? - strtolower($aggregationOperations[$columnToSumName]) : 'sum'); - - // max_actions is a core metric that is generated in ArchiveProcess_Day. Therefore, it can be - // present in any data table and is not part of the $aggregationOperations mechanism. - if ($columnToSumName == Metrics::INDEX_MAX_ACTIONS) { - $operation = 'max'; - } - if(empty($operation)) { - throw new Exception("Unknown aggregation operation for column $columnToSumName."); - } - $newValue = $this->getColumnValuesMerged($operation, $thisColumnValue, $columnToSumValue); - - $this->setColumn($columnToSumName, $newValue); + if (!$this->isSummableColumn($columnToSumName)) { + continue; } + + $thisColumnValue = $this->getColumn($columnToSumName); + + $operation = 'sum'; + if (is_array($aggregationOperations) && isset($aggregationOperations[$columnToSumName])) { + $operation = strtolower($aggregationOperations[$columnToSumName]); + } + + // max_actions is a core metric that is generated in ArchiveProcess_Day. Therefore, it can be + // present in any data table and is not part of the $aggregationOperations mechanism. + if ($columnToSumName == Metrics::INDEX_MAX_ACTIONS) { + $operation = 'max'; + } + if (empty($operation)) { + throw new Exception("Unknown aggregation operation for column $columnToSumName."); + } + + $newValue = $this->getColumnValuesMerged($operation, $thisColumnValue, $columnToSumValue); + + $this->setColumn($columnToSumName, $newValue); } if ($enableCopyMetadata) { - $this->sumRowMetadata($rowToSum); + $this->sumRowMetadata($rowToSum, $aggregationOperations); } } @@ -491,7 +497,7 @@ class Row case 'min': if (!$thisColumnValue) { $newValue = $columnToSumValue; - } else if (!$columnToSumValue) { + } elseif (!$columnToSumValue) { $newValue = $thisColumnValue; } else { $newValue = min($thisColumnValue, $columnToSumValue); @@ -500,6 +506,19 @@ class Row case 'sum': $newValue = $this->sumRowArray($thisColumnValue, $columnToSumValue); break; + case 'uniquearraymerge': + if (is_array($thisColumnValue) && is_array($columnToSumValue)) { + foreach ($columnToSumValue as $columnSum) { + if (!in_array($columnSum, $thisColumnValue)) { + $thisColumnValue[] = $columnSum; + } + } + } elseif (!is_array($thisColumnValue) && is_array($columnToSumValue)) { + $thisColumnValue = $columnToSumValue; + } + + $newValue = $thisColumnValue; + break; default: throw new Exception("Unknown operation '$operation'."); } @@ -508,23 +527,45 @@ class Row /** * Sums the metadata in `$rowToSum` with the metadata in `$this` row. - * + * * @param Row $rowToSum + * @param array $aggregationOperations */ - public function sumRowMetadata($rowToSum) + public function sumRowMetadata($rowToSum, $aggregationOperations = array()) { - if (!empty($rowToSum->c[self::METADATA]) + if (!empty($rowToSum->metadata) && !$this->isSummaryRow() ) { + $aggregatedMetadata = array(); + + if (is_array($aggregationOperations)) { + // we need to aggregate value before value is overwritten by maybe another row + foreach ($aggregationOperations as $columnn => $operation) { + $thisMetadata = $this->getMetadata($columnn); + $sumMetadata = $rowToSum->getMetadata($columnn); + + if ($thisMetadata === false && $sumMetadata === false) { + continue; + } + + $aggregatedMetadata[$columnn] = $this->getColumnValuesMerged($operation, $thisMetadata, $sumMetadata); + } + } + // We shall update metadata, and keep the metadata with the _most visits or pageviews_, rather than first or last seen $visits = max($rowToSum->getColumn(Metrics::INDEX_PAGE_NB_HITS) || $rowToSum->getColumn(Metrics::INDEX_NB_VISITS), // Old format pre-1.2, @see also method doSumVisitsMetrics() $rowToSum->getColumn('nb_actions') || $rowToSum->getColumn('nb_visits')); if (($visits && $visits > $this->maxVisitsSummed) - || empty($this->c[self::METADATA]) + || empty($this->metadata) ) { $this->maxVisitsSummed = $visits; - $this->c[self::METADATA] = $rowToSum->c[self::METADATA]; + $this->metadata = $rowToSum->metadata; + } + + foreach ($aggregatedMetadata as $column => $value) { + // we need to make sure aggregated value is used, and not metadata from $rowToSum + $this->setMetadata($column, $value); } } } @@ -532,7 +573,7 @@ class Row /** * Returns `true` if this row is the summary row, `false` if otherwise. This function * depends on the label of the row, and so, is not 100% accurate. - * + * * @return bool */ public function isSummaryRow() @@ -558,10 +599,15 @@ class Row return $thisColumnValue + $columnToSumValue; } + if ($columnToSumValue === false) { + return $thisColumnValue; + } + + if ($thisColumnValue === false) { + return $columnToSumValue; + } + if (is_array($columnToSumValue)) { - if ($thisColumnValue == false) { - return $columnToSumValue; - } $newValue = $thisColumnValue; foreach ($columnToSumValue as $arrayIndex => $arrayValue) { if (!isset($newValue[$arrayIndex])) { @@ -572,16 +618,7 @@ class Row return $newValue; } - if (is_string($columnToSumValue)) { - if ($thisColumnValue === false) { - return $columnToSumValue; - } else if ($columnToSumValue === false) { - return $thisColumnValue; - } else { - throw new Exception("Trying to add two strings values in DataTable\Row::sumRowArray: " - . "'$thisColumnValue' + '$columnToSumValue'"); - } - } + $this->warnWhenSummingTwoStrings($thisColumnValue, $columnToSumValue); return 0; } @@ -594,7 +631,7 @@ class Row * @return bool * @ignore */ - static public function compareElements($elem1, $elem2) + public static function compareElements($elem1, $elem2) { if (is_array($elem1)) { if (is_array($elem2)) { @@ -602,11 +639,13 @@ class Row } return 1; } - if (is_array($elem2)) + if (is_array($elem2)) { return -1; + } - if ((string)$elem1 === (string)$elem2) + if ((string)$elem1 === (string)$elem2) { return 0; + } return ((string)$elem1 > (string)$elem2) ? 1 : -1; } @@ -615,17 +654,17 @@ class Row * Helper function that tests if two rows are equal. * * Two rows are equal if: - * + * * - they have exactly the same columns / metadata * - they have a subDataTable associated, then we check that both of them are the same. - * + * * Column order is not important. * * @param \Piwik\DataTable\Row $row1 first to compare * @param \Piwik\DataTable\Row $row2 second to compare * @return bool */ - static public function isEqual(Row $row1, Row $row2) + public static function isEqual(Row $row1, Row $row2) { //same columns $cols1 = $row1->getColumns(); @@ -662,4 +701,53 @@ class Row } return true; } + + public function offsetExists($offset) + { + return $this->hasColumn($offset); + } + + public function offsetGet($offset) + { + return $this->getColumn($offset); + } + + public function offsetSet($offset, $value) + { + $this->setColumn($offset, $value); + } + + public function offsetUnset($offset) + { + $this->deleteColumn($offset); + } + + public function getIterator() + { + return new \ArrayIterator($this->columns); + } + + private function warnIfSubtableAlreadyExists() + { + if (!is_null($this->subtableId)) { + Log::warning( + "Row with label '%s' (columns = %s) has already a subtable id=%s but it was not loaded - overwriting the existing sub-table.", + $this->getColumn('label'), + implode(", ", $this->getColumns()), + $this->getIdSubDataTable() + ); + } + } + + protected function warnWhenSummingTwoStrings($thisColumnValue, $columnToSumValue) + { + if (is_string($columnToSumValue)) { + Log::warning( + "Trying to add two strings in DataTable\Row::sumRowArray: %s + %s for row %s", + $thisColumnValue, + $columnToSumValue, + $this->__toString() + ); + } + } } diff --git a/www/analytics/core/DataTable/Row/DataTableSummaryRow.php b/www/analytics/core/DataTable/Row/DataTableSummaryRow.php index 479ac745..64e45d49 100644 --- a/www/analytics/core/DataTable/Row/DataTableSummaryRow.php +++ b/www/analytics/core/DataTable/Row/DataTableSummaryRow.php @@ -1,6 +1,6 @@ sumTable($subTable); } } @@ -47,9 +44,8 @@ class DataTableSummaryRow extends Row */ public function recalculate() { - $id = $this->getIdSubDataTable(); - if ($id !== null) { - $subTable = Manager::getInstance()->getTable($id); + $subTable = $this->getSubtable(); + if ($subTable) { $this->sumTable($subTable); } } @@ -61,8 +57,17 @@ class DataTableSummaryRow extends Row */ private function sumTable($table) { - foreach ($table->getRows() as $row) { - $this->sumRow($row, $enableCopyMetadata = false, $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME)); + $metadata = $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME); + $enableCopyMetadata = false; + + foreach ($table->getRowsWithoutSummaryRow() as $row) { + $this->sumRow($row, $enableCopyMetadata, $metadata); + } + + $summaryRow = $table->getRowFromId(DataTable::ID_SUMMARY_ROW); + + if ($summaryRow) { + $this->sumRow($summaryRow, $enableCopyMetadata, $metadata); } } } diff --git a/www/analytics/core/DataTable/Simple.php b/www/analytics/core/DataTable/Simple.php index 010e6586..018818c2 100644 --- a/www/analytics/core/DataTable/Simple.php +++ b/www/analytics/core/DataTable/Simple.php @@ -1,6 +1,6 @@ $value1, * 'Label row 2' => $value2, diff --git a/www/analytics/core/DataTable/TableNotFoundException.php b/www/analytics/core/DataTable/TableNotFoundException.php index 6ccd5e12..4286405b 100644 --- a/www/analytics/core/DataTable/TableNotFoundException.php +++ b/www/analytics/core/DataTable/TableNotFoundException.php @@ -1,6 +1,6 @@ addHour(5); - * echo $date->getLocalized("%longDay% the %day% of %longMonth% at %time%"); - * + * echo $date->getLocalized("EEE, d. MMM y 'at' HH:mm:ss"); + * * @api */ class Date @@ -40,6 +42,36 @@ class Date /** The default date time string format. */ const DATE_TIME_FORMAT = 'Y-m-d H:i:s'; + const DATETIME_FORMAT_LONG = DateTimeFormatProvider::DATE_FORMAT_LONG; + const DATETIME_FORMAT_SHORT = DateTimeFormatProvider::DATETIME_FORMAT_SHORT; + const DATE_FORMAT_LONG = DateTimeFormatProvider::DATE_FORMAT_LONG; + const DATE_FORMAT_DAY_MONTH = DateTimeFormatProvider::DATE_FORMAT_DAY_MONTH; + const DATE_FORMAT_SHORT = DateTimeFormatProvider::DATE_FORMAT_SHORT; + const DATE_FORMAT_MONTH_SHORT = DateTimeFormatProvider::DATE_FORMAT_MONTH_SHORT; + const DATE_FORMAT_MONTH_LONG = DateTimeFormatProvider::DATE_FORMAT_MONTH_LONG; + const DATE_FORMAT_YEAR = DateTimeFormatProvider::DATE_FORMAT_YEAR; + const TIME_FORMAT = DateTimeFormatProvider::TIME_FORMAT; + + /** + * Max days for months (non-leap-year). See {@link addPeriod()} implementation. + * + * @var int[] + */ + private static $maxDaysInMonth = array( + '1' => 31, + '2' => 28, + '3' => 31, + '4' => 30, + '5' => 31, + '6' => 30, + '7' => 31, + '8' => 31, + '9' => 30, + '10' => 31, + '11' => 30, + '12' => 31 + ); + /** * The stored timestamp is always UTC based. * The returned timestamp via getTimestamp() will have the conversion applied @@ -84,7 +116,6 @@ class Date */ public static function factory($dateString, $timezone = null) { - $invalidDateException = new Exception(Piwik::translate('General_ExceptionInvalidDateFormat', array("YYYY-MM-DD, or 'today' or 'yesterday'", "strtotime", "http://php.net/strtotime")) . ": $dateString"); if ($dateString instanceof self) { $dateString = $dateString->toString(); } @@ -105,7 +136,7 @@ class Date ($dateString = strtotime($dateString)) === false ) ) { - throw $invalidDateException; + throw self::getInvalidDateFormatException($dateString); } else { $date = new Date($dateString); } @@ -113,7 +144,7 @@ class Date // can't be doing web analytics before the 1st website // Tue, 06 Aug 1991 00:00:00 GMT if ($timestamp < 681436800) { - throw $invalidDateException; + throw self::getInvalidDateFormatException($dateString); } if (empty($timezone)) { return $date; @@ -133,6 +164,19 @@ class Date return $this->toString(self::DATE_TIME_FORMAT); } + /** + * Returns the current hour in UTC timezone. + * @return string + * @throws Exception + */ + public function getHourUTC() + { + $dateTime = $this->getDatetime(); + $hourInTz = Date::factory($dateTime, 'UTC')->toString('G'); + + return $hourInTz; + } + /** * Returns the start of the day of the current timestamp in UTC. For example, * if the current timestamp is `'2007-07-24 14:04:24'` in UTC, the result will @@ -164,7 +208,7 @@ class Date /** * Returns a new date object with the same timestamp as `$this` but with a new * timezone. - * + * * See {@link getTimestamp()} to see how the timezone is used. * * @param string $timezone eg, `'UTC'`, `'Europe/London'`, etc. @@ -222,6 +266,17 @@ class Date return strtotime($datetime); } + /** + * Returns the date in the "Y-m-d H:i:s" PHP format + * + * @param int $timestamp + * @return string + */ + public static function getDatetimeFromTimestamp($timestamp) + { + return date("Y-m-d H:i:s", $timestamp); + } + /** * Returns the Unix timestamp of the date in UTC. * @@ -253,15 +308,16 @@ class Date // Unit tests pass (@see Date.test.php) but I'm pretty sure this is not the right way to do it date_default_timezone_set($this->timezone); $dtzone = timezone_open('UTC'); - $time = date('r', $this->timestamp); - $dtime = date_create($time); + $time = date('r', $this->timestamp); + $dtime = date_create($time); + date_timezone_set($dtime, $dtzone); - $dateWithTimezone = date_format($dtime, 'r'); + $dateWithTimezone = date_format($dtime, 'r'); $dateWithoutTimezone = substr($dateWithTimezone, 0, -6); - $timestamp = strtotime($dateWithoutTimezone); + $timestamp = strtotime($dateWithoutTimezone); date_default_timezone_set('UTC'); - return (int)$timestamp; + return (int) $timestamp; } /** @@ -375,7 +431,6 @@ class Date return 0; } if ($currentYear < $toCompareYear) { - return -1; } return 1; @@ -383,7 +438,7 @@ class Date /** * Returns `true` if current date is today. - * + * * @return bool */ public function isToday() @@ -560,9 +615,39 @@ class Date /** * Returns a localized date string using the given template. * The template should contain tags that will be replaced with localized date strings. - * - * Allowed tags include: - * + * + * @param string $template eg. `"MMM y"` + * @return string eg. `"Aug 2009"` + */ + public function getLocalized($template) + { + $template = $this->replaceLegacyPlaceholders($template); + + $dateTimeFormatProvider = StaticContainer::get('Piwik\Intl\Data\Provider\DateTimeFormatProvider'); + + $template = $dateTimeFormatProvider->getFormatPattern($template); + + $tokens = self::parseFormat($template); + + $out = ''; + + foreach ($tokens AS $token) { + if (is_array($token)) { + $out .= $this->formatToken(array_shift($token)); + + } else { + $out .= $token; + } + } + + return $out; + } + + /** + * Replaces legacy placeholders + * + * @deprecated should be removed in Piwik 3.0.0 or later + * * - **%day%**: replaced with the day of the month without leading zeros, eg, **1** or **20**. * - **%shortMonth%**: the short month in the current language, eg, **Jan**, **Feb**. * - **%longMonth%**: the whole month name in the current language, eg, **January**, **February**. @@ -571,27 +656,183 @@ class Date * - **%longYear%**: the four digit year, eg, **2007**, **2013**. * - **%shortYear%**: the two digit year, eg, **07**, **13**. * - **%time%**: the time of day, eg, **07:35:00**, or **15:45:00**. - * - * @param string $template eg. `"%shortMonth% %longYear%"` - * @return string eg. `"Aug 2009"` */ - public function getLocalized($template) + protected function replaceLegacyPlaceholders($template) + { + if (strpos($template, '%') === false) { + return $template; + } + + $mapping = array( + '%day%' => 'd', + '%shortMonth%' => 'MMM', + '%longMonth%' => 'MMMM', + '%shortDay%' => 'EEE', + '%longDay%' => 'EEEE', + '%longYear%' => 'y', + '%shortYear%' => 'yy', + '%time%' => 'HH:mm:ss' + ); + + return str_replace(array_keys($mapping), array_values($mapping), $template); + } + + protected function formatToken($token) { - $day = $this->toString('j'); $dayOfWeek = $this->toString('N'); $monthOfYear = $this->toString('n'); - $patternToValue = array( - "%day%" => $day, - "%shortMonth%" => Piwik::translate('General_ShortMonth_' . $monthOfYear), - "%longMonth%" => Piwik::translate('General_LongMonth_' . $monthOfYear), - "%shortDay%" => Piwik::translate('General_ShortDay_' . $dayOfWeek), - "%longDay%" => Piwik::translate('General_LongDay_' . $dayOfWeek), - "%longYear%" => $this->toString('Y'), - "%shortYear%" => $this->toString('y'), - "%time%" => $this->toString('H:i:s') - ); - $out = str_replace(array_keys($patternToValue), array_values($patternToValue), $template); - return $out; + $translator = StaticContainer::get('Piwik\Translation\Translator'); + + switch ($token) { + // year + case "yyyy": + case "y": + return $this->toString('Y'); + case "yy": + return $this->toString('y'); + // month + case "MMMM": + return $translator->translate('Intl_Month_Long_' . $monthOfYear); + case "MMM": + return $translator->translate('Intl_Month_Short_' . $monthOfYear); + case "MM": + return $this->toString('n'); + case "M": + return $this->toString('m'); + case "LLLL": + return $translator->translate('Intl_Month_Long_StandAlone_' . $monthOfYear); + case "LLL": + return $translator->translate('Intl_Month_Short_StandAlone_' . $monthOfYear); + case "LL": + return $this->toString('n'); + case "L": + return $this->toString('m'); + // day + case "dd": + return $this->toString('d'); + case "d": + return $this->toString('j'); + case "EEEE": + return $translator->translate('Intl_Day_Long_' . $dayOfWeek); + case "EEE": + case "EE": + case "E": + return $translator->translate('Intl_Day_Short_' . $dayOfWeek); + case "CCCC": + return $translator->translate('Intl_Day_Long_StandAlone_' . $dayOfWeek); + case "CCC": + case "CC": + case "C": + return $translator->translate('Intl_Day_Short_StandAlone_' . $dayOfWeek); + case "D": + return 1 + (int)$this->toString('z'); // 1 - 366 + case "F": + return (int)(((int)$this->toString('j') + 6) / 7); + // week in month + case "w": + $weekDay = date('N', mktime(0, 0, 0, $this->toString('m'), 1, $this->toString('y'))); + return floor(($weekDay + (int)$this->toString('m') - 2) / 7) + 1; + // week in year + case "W": + return $this->toString('N'); + // hour + case "HH": + return $this->toString('H'); + case "H": + return $this->toString('G'); + case "hh": + return $this->toString('h'); + case "h": + return $this->toString('g'); + // minute + case "mm": + case "m": + return $this->toString('i'); + // second + case "ss": + case "s": + return $this->toString('s'); + // am / pm + case "a": + return $this->toString('a') == 'am' ? $translator->translate('Intl_Time_AM') : $translator->translate('Intl_Time_PM'); + + // currently not implemented: + case "G": + case "GG": + case "GGG": + case "GGGG": + case "GGGGG": + return ''; // era + case "z": + case "Z": + case "v": + return ''; // time zone + + } + + return ''; + } + + protected static $tokens = array( + 'G', 'y', 'M', 'L', 'd', 'h', 'H', 'm', 's', 'E', 'c', 'e', 'D', 'F', 'w', 'W', 'a', 'z', 'Z', 'v', + ); + + /** + * Parses the datetime format pattern and returns a tokenized result array + * + * Examples: + * Input Output + * 'dd.mm.yyyy' array(array('dd'), '.', array('mm'), '.', array('yyyy')) + * 'y?M?d?EEEE ah:mm:ss' array(array('y'), '?', array('M'), '?', array('d'), '?', array('EEEE'), ' ', array('a'), array('h'), ':', array('mm'), ':', array('ss')) + * + * @param string $pattern the pattern to be parsed + * @return array tokenized parsing result + */ + protected static function parseFormat($pattern) + { + static $formats = array(); // cache + if (isset($formats[$pattern])) { + return $formats[$pattern]; + } + $tokens = array(); + $n = strlen($pattern); + $isLiteral = false; + $literal = ''; + for ($i = 0; $i < $n; ++$i) { + $c = $pattern[$i]; + if ($c === "'") { + if ($i < $n - 1 && $pattern[$i + 1] === "'") { + $tokens[] = "'"; + $i++; + } elseif ($isLiteral) { + $tokens[] = $literal; + $literal = ''; + $isLiteral = false; + } else { + $isLiteral = true; + $literal = ''; + } + } elseif ($isLiteral) { + $literal .= $c; + } else { + for ($j = $i + 1; $j < $n; ++$j) { + if ($pattern[$j] !== $c) { + break; + } + } + $p = str_repeat($c, $j - $i); + if (in_array($c, self::$tokens)) { + $tokens[] = array($p); + } else { + $tokens[] = $p; + } + $i = $j - 1; + } + } + if ($literal !== '') { + $tokens[] = $literal; + } + return $formats[$pattern] = $tokens; } /** @@ -664,6 +905,17 @@ class Date return $this->addHour(-$n); } + /** + * Subtracts `$n` seconds from `$this` date and returns the result in a new Date. + * + * @param int $n Number of seconds to subtract. Can be less than 0. + * @return \Piwik\Date + */ + public function subSeconds($n) + { + return new Date($this->timestamp - $n, $this->timezone); + } + /** * Adds a period to `$this` date and returns the result in a new Date instance. * @@ -673,14 +925,41 @@ class Date */ public function addPeriod($n, $period) { - if ($n < 0) { - $ts = strtotime("$n $period", $this->timestamp); + if (strtolower($period) == 'month') { // TODO: comments + $dateInfo = getdate($this->timestamp); + + $ts = mktime( + $dateInfo['hours'], + $dateInfo['minutes'], + $dateInfo['seconds'], + $dateInfo['mon'] + (int)$n, + 1, + $dateInfo['year'] + ); + + $daysToAdd = min($dateInfo['mday'], self::getMaxDaysInMonth($ts)) - 1; + $ts += self::NUM_SECONDS_IN_DAY * $daysToAdd; } else { - $ts = strtotime("+$n $period", $this->timestamp); + $time = $n < 0 ? "$n $period" : "+$n $period"; + + $ts = strtotime($time, $this->timestamp); } + return new Date($ts, $this->timezone); } + private static function getMaxDaysInMonth($timestamp) + { + $month = (int)date('m', $timestamp); + if (date('L', $timestamp) == 1 + && $month == 2 + ) { + return 29; + } else { + return self::$maxDaysInMonth[$month]; + } + } + /** * Subtracts a period from `$this` date and returns the result in a new Date instance. * @@ -703,4 +982,10 @@ class Date { return $secs / self::NUM_SECONDS_IN_DAY; } + + private static function getInvalidDateFormatException($dateString) + { + $message = Piwik::translate('General_ExceptionInvalidDateFormat', array("YYYY-MM-DD, or 'today' or 'yesterday'", "strtotime", "http://php.net/strtotime")); + return new Exception($message . ": $dateString"); + } } diff --git a/www/analytics/core/Db.php b/www/analytics/core/Db.php index a418e490..a4b114d6 100644 --- a/www/analytics/core/Db.php +++ b/www/analytics/core/Db.php @@ -1,6 +1,6 @@ Debug['enable_sql_profiler']; + $dbConfig['profiler'] = @$config->Debug['enable_sql_profiler']; return $dbConfig; } + /** + * For tests only. + * @param $connection + * @ignore + * @internal + */ + public static function setDatabaseObject($connection) + { + self::$connection = $connection; + } + /** * Connects to the database. - * + * * Shouldn't be called directly, use {@link get()} instead. - * + * * @param array|null $dbConfig Connection parameters in an array. Defaults to the `[database]` * INI config section. */ @@ -103,6 +118,16 @@ class Db self::$connection = $db; } + /** + * Detect whether a database object is initialized / created or not. + * + * @internal + */ + public static function hasDatabaseObject() + { + return isset(self::$connection); + } + /** * Disconnects and destroys the database connection. * @@ -124,7 +149,7 @@ class Db * @throws \Exception If there is an error in the SQL. * @return integer|\Zend_Db_Statement */ - static public function exec($sql) + public static function exec($sql) { /** @var \Zend_Db_Adapter_Abstract $db */ $db = self::get(); @@ -132,6 +157,8 @@ class Db $q = $profiler->queryStart($sql, \Zend_Db_Profiler::INSERT); try { + self::logSql(__FUNCTION__, $sql); + $return = self::get()->exec($sql); } catch (Exception $ex) { self::logExtraInfoIfDeadlock($ex); @@ -139,13 +166,14 @@ class Db } $profiler->queryEnd($q); + return $return; } /** * Executes an SQL query and returns the [Zend_Db_Statement](http://framework.zend.com/manual/1.12/en/zend.db.statement.html) * for the query. - * + * * This method is meant for non-query SQL statements like `INSERT` and `UPDATE. If you want to fetch * data from the DB you should use one of the fetch... functions. * @@ -154,9 +182,11 @@ class Db * @throws \Exception If there is a problem with the SQL or bind parameters. * @return \Zend_Db_Statement */ - static public function query($sql, $parameters = array()) + public static function query($sql, $parameters = array()) { try { + self::logSql(__FUNCTION__, $sql, $parameters); + return self::get()->query($sql, $parameters); } catch (Exception $ex) { self::logExtraInfoIfDeadlock($ex); @@ -173,9 +203,11 @@ class Db * @return array The fetched rows, each element is an associative array mapping column names * with column values. */ - static public function fetchAll($sql, $parameters = array()) + public static function fetchAll($sql, $parameters = array()) { try { + self::logSql(__FUNCTION__, $sql, $parameters); + return self::get()->fetchAll($sql, $parameters); } catch (Exception $ex) { self::logExtraInfoIfDeadlock($ex); @@ -192,9 +224,11 @@ class Db * @return array The fetched row, each element is an associative array mapping column names * with column values. */ - static public function fetchRow($sql, $parameters = array()) + public static function fetchRow($sql, $parameters = array()) { try { + self::logSql(__FUNCTION__, $sql, $parameters); + return self::get()->fetchRow($sql, $parameters); } catch (Exception $ex) { self::logExtraInfoIfDeadlock($ex); @@ -211,9 +245,11 @@ class Db * @throws \Exception If there is a problem with the SQL or bind parameters. * @return string */ - static public function fetchOne($sql, $parameters = array()) + public static function fetchOne($sql, $parameters = array()) { try { + self::logSql(__FUNCTION__, $sql, $parameters); + return self::get()->fetchOne($sql, $parameters); } catch (Exception $ex) { self::logExtraInfoIfDeadlock($ex); @@ -234,9 +270,11 @@ class Db * 'col1value2' => array('col2' => '...', 'col3' => ...)) * ``` */ - static public function fetchAssoc($sql, $parameters = array()) + public static function fetchAssoc($sql, $parameters = array()) { try { + self::logSql(__FUNCTION__, $sql, $parameters); + return self::get()->fetchAssoc($sql, $parameters); } catch (Exception $ex) { self::logExtraInfoIfDeadlock($ex); @@ -247,33 +285,33 @@ class Db /** * Deletes all desired rows in a table, while using a limit. This function will execute many * DELETE queries until there are no more rows to delete. - * + * * Use this function when you need to delete many thousands of rows from a table without * locking the table for too long. - * + * * **Example** - * + * * // delete all visit rows whose ID is less than a certain value, 100000 rows at a time * $idVisit = // ... * Db::deleteAllRows(Common::prefixTable('log_visit'), "WHERE idvisit <= ?", "idvisit ASC", 100000, array($idVisit)); - * + * * @param string $table The name of the table to delete from. Must be prefixed (see {@link Piwik\Common::prefixTable()}). * @param string $where The where clause of the query. Must include the WHERE keyword. - * @param $orderBy The column to order by and the order by direction, eg, `idvisit ASC`. + * @param string $orderBy The column to order by and the order by direction, eg, `idvisit ASC`. * @param int $maxRowsPerQuery The maximum number of rows to delete per `DELETE` query. * @param array $parameters Parameters to bind for each query. * @return int The total number of rows deleted. */ - static public function deleteAllRows($table, $where, $orderBy, $maxRowsPerQuery = 100000, $parameters = array()) + public static function deleteAllRows($table, $where, $orderBy, $maxRowsPerQuery = 100000, $parameters = array()) { $orderByClause = $orderBy ? "ORDER BY $orderBy" : ""; - $sql = "DELETE FROM $table - $where - $orderByClause + + $sql = "DELETE FROM $table $where $orderByClause LIMIT " . (int)$maxRowsPerQuery; // delete rows w/ a limit $totalRowsDeleted = 0; + do { $rowsDeleted = self::query($sql, $parameters)->rowCount(); @@ -285,44 +323,60 @@ class Db /** * Runs an `OPTIMIZE TABLE` query on the supplied table or tables. - * + * * Tables will only be optimized if the `[General] enable_sql_optimize_queries` INI config option is * set to **1**. * * @param string|array $tables The name of the table to optimize or an array of tables to optimize. * Table names must be prefixed (see {@link Piwik\Common::prefixTable()}). + * @param bool $force If true, the `OPTIMIZE TABLE` query will be run even if InnoDB tables are being used. * @return \Zend_Db_Statement */ - static public function optimizeTables($tables) + public static function optimizeTables($tables, $force = false) { $optimize = Config::getInstance()->General['enable_sql_optimize_queries']; - if (empty($optimize)) { - return; + + if (empty($optimize) + && !$force + ) { + return false; } if (empty($tables)) { return false; } + if (!is_array($tables)) { $tables = array($tables); } - // filter out all InnoDB tables - $myisamDbTables = array(); - foreach (Db::fetchAll("SHOW TABLE STATUS") as $row) { - if (strtolower($row['Engine']) == 'myisam' - && in_array($row['Name'], $tables) - ) { - $myisamDbTables[] = $row['Name']; + if (!self::isOptimizeInnoDBSupported() + && !$force + ) { + // filter out all InnoDB tables + $myisamDbTables = array(); + foreach (self::getTableStatus() as $row) { + if (strtolower($row['Engine']) == 'myisam' + && in_array($row['Name'], $tables) + ) { + $myisamDbTables[] = $row['Name']; + } } + + $tables = $myisamDbTables; } - if (empty($myisamDbTables)) { + if (empty($tables)) { return false; } // optimize the tables - return self::query("OPTIMIZE TABLE " . implode(',', $myisamDbTables)); + return self::query("OPTIMIZE TABLE " . implode(',', $tables)); + } + + private static function getTableStatus() + { + return Db::fetchAll("SHOW TABLE STATUS"); } /** @@ -332,19 +386,19 @@ class Db * Table names must be prefixed (see {@link Piwik\Common::prefixTable()}). * @return \Zend_Db_Statement */ - static public function dropTables($tables) + public static function dropTables($tables) { if (!is_array($tables)) { $tables = array($tables); } - return self::query("DROP TABLE " . implode(',', $tables)); + return self::query("DROP TABLE `" . implode('`,`', $tables) . "`"); } /** * Drops all tables */ - static public function dropAllTables() + public static function dropAllTables() { $tablesAlreadyInstalled = DbHelper::getTablesInstalled(); self::dropTables($tablesAlreadyInstalled); @@ -355,36 +409,32 @@ class Db * * @param string|array $table The name of the table you want to get the columns definition for. * @return \Zend_Db_Statement + * @deprecated since 2.11.0 */ - static public function getColumnNamesFromTable($table) + public static function getColumnNamesFromTable($table) { - $columns = self::fetchAll("SHOW COLUMNS FROM " . $table); - - $columnNames = array(); - foreach ($columns as $column) { - $columnNames[] = $column['Field']; - } - - return $columnNames; + $tableMetadataAccess = new TableMetadata(); + return $tableMetadataAccess->getColumns($table); } /** * Locks the supplied table or tables. - * + * * **NOTE:** Piwik does not require the `LOCK TABLES` privilege to be available. Piwik * should still work if it has not been granted. - * + * * @param string|array $tablesToRead The table or tables to obtain 'read' locks on. Table names must * be prefixed (see {@link Piwik\Common::prefixTable()}). * @param string|array $tablesToWrite The table or tables to obtain 'write' locks on. Table names must * be prefixed (see {@link Piwik\Common::prefixTable()}). * @return \Zend_Db_Statement */ - static public function lockTables($tablesToRead, $tablesToWrite = array()) + public static function lockTables($tablesToRead, $tablesToWrite = array()) { if (!is_array($tablesToRead)) { $tablesToRead = array($tablesToRead); } + if (!is_array($tablesToWrite)) { $tablesToWrite = array($tablesToWrite); } @@ -393,6 +443,7 @@ class Db foreach ($tablesToWrite as $table) { $lockExprs[] = $table . " WRITE"; } + foreach ($tablesToRead as $table) { $lockExprs[] = $table . " READ"; } @@ -405,10 +456,10 @@ class Db * * **NOTE:** Piwik does not require the `LOCK TABLES` privilege to be available. Piwik * should still work if it has not been granted. - * + * * @return \Zend_Db_Statement */ - static public function unlockAllTables() + public static function unlockAllTables() { return self::exec("UNLOCK TABLES"); } @@ -416,7 +467,7 @@ class Db /** * Performs a `SELECT` statement on a table one chunk at a time and returns the first * successfully fetched value. - * + * * This function will execute a query on one set of rows in a table. If nothing * is fetched, it will execute the query on the next set of rows and so on until * the query returns a value. @@ -425,10 +476,10 @@ class Db * should be used when performing a `SELECT` that can take a long time to finish. * Using several smaller `SELECT`s will ensure that the table will not be locked * for too long. - * + * * **Example** - * - * // find the most recent visit that is older than a certain date + * + * // find the most recent visit that is older than a certain date * $dateStart = // ... * $sql = "SELECT idvisit * FROM $logVisit @@ -451,9 +502,10 @@ class Db * * @return string */ - static public function segmentedFetchFirst($sql, $first, $last, $step, $params = array()) + public static function segmentedFetchFirst($sql, $first, $last, $step, $params = array()) { $result = false; + if ($step > 0) { for ($i = $first; $result === false && $i <= $last; $i += $step) { $result = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); @@ -463,17 +515,18 @@ class Db $result = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); } } + return $result; } /** * Performs a `SELECT` on a table one chunk at a time and returns an array * of every fetched value. - * + * * This function will break up a `SELECT` query into several smaller queries by * using only a limited number of rows at a time. It will accumulate the results * of each smaller query and return the result. - * + * * This function should be used when performing a `SELECT` that can * take a long time to finish. Using several smaller queries will ensure that * the table will not be locked for too long. @@ -487,9 +540,10 @@ class Db * @param array $params Parameters to bind in the query, `array(param1 => value1, param2 => value2)` * @return array An array of primitive values. */ - static public function segmentedFetchOne($sql, $first, $last, $step, $params = array()) + public static function segmentedFetchOne($sql, $first, $last, $step, $params = array()) { $result = array(); + if ($step > 0) { for ($i = $first; $i <= $last; $i += $step) { $result[] = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); @@ -499,6 +553,7 @@ class Db $result[] = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); } } + return $result; } @@ -509,11 +564,11 @@ class Db * This function will break up a `SELECT` query into several smaller queries by * using only a limited number of rows at a time. It will accumulate the results * of each smaller query and return the result. - * + * * This function should be used when performing a `SELECT` that can * take a long time to finish. Using several smaller queries will ensure that * the table will not be locked for too long. - * + * * @param string $sql The SQL to perform. The last two conditions of the `WHERE` * expression must be as follows: `'id >= ? AND id < ?'` where * **id** is the int id of the table. @@ -524,33 +579,35 @@ class Db * @return array An array of rows that includes the result set of every smaller * query. */ - static public function segmentedFetchAll($sql, $first, $last, $step, $params = array()) + public static function segmentedFetchAll($sql, $first, $last, $step, $params = array()) { $result = array(); + if ($step > 0) { for ($i = $first; $i <= $last; $i += $step) { $currentParams = array_merge($params, array($i, $i + $step)); - $result = array_merge($result, self::fetchAll($sql, $currentParams)); + $result = array_merge($result, self::fetchAll($sql, $currentParams)); } } else { for ($i = $first; $i >= $last; $i += $step) { $currentParams = array_merge($params, array($i, $i + $step)); - $result = array_merge($result, self::fetchAll($sql, $currentParams)); + $result = array_merge($result, self::fetchAll($sql, $currentParams)); } } + return $result; } /** * Performs a `UPDATE` or `DELETE` statement on a table one chunk at a time. - * + * * This function will break up a query into several smaller queries by * using only a limited number of rows at a time. - * + * * This function should be used when executing a non-query statement will * take a long time to finish. Using several smaller queries will ensure that * the table will not be locked for too long. - * + * * @param string $sql The SQL to perform. The last two conditions of the `WHERE` * expression must be as follows: `'id >= ? AND id < ?'` where * **id** is the int id of the table. @@ -559,7 +616,7 @@ class Db * @param int $step The maximum number of rows to scan in one query. * @param array $params Parameters to bind in the query, `array(param1 => value1, param2 => value2)` */ - static public function segmentedQuery($sql, $first, $last, $step, $params = array()) + public static function segmentedQuery($sql, $first, $last, $step, $params = array()) { if ($step > 0) { for ($i = $first; $i <= $last; $i += $step) { @@ -574,17 +631,6 @@ class Db } } - /** - * Returns `true` if a table in the database, `false` if otherwise. - * - * @param string $tableName The name of the table to check for. Must be prefixed. - * @return bool - */ - static public function tableExists($tableName) - { - return self::query("SHOW TABLES LIKE ?", $tableName)->rowCount() > 0; - } - /** * Attempts to get a named lock. This function uses a timeout of 1s, but will * retry a set number of times. @@ -592,9 +638,14 @@ class Db * @param string $lockName The lock name. * @param int $maxRetries The max number of times to retry. * @return bool `true` if the lock was obtained, `false` if otherwise. + * @throws \Exception if Lock name is too long */ - static public function getDbLock($lockName, $maxRetries = 30) + public static function getDbLock($lockName, $maxRetries = 30) { + if (strlen($lockName) > 64) { + throw new \Exception('DB lock name has to be 64 characters or less for MySQL 5.7 compatibility.'); + } + /* * the server (e.g., shared hosting) may have a low wait timeout * so instead of a single GET_LOCK() with a 30 second timeout, @@ -606,11 +657,13 @@ class Db $db = self::get(); while ($maxRetries > 0) { - if ($db->fetchOne($sql, array($lockName)) == '1') { + $result = $db->fetchOne($sql, array($lockName)); + if ($result == '1') { return true; } $maxRetries--; } + return false; } @@ -620,7 +673,7 @@ class Db * @param string $lockName The lock name. * @return bool `true` if the lock was released, `false` if otherwise. */ - static public function releaseDbLock($lockName) + public static function releaseDbLock($lockName) { $sql = 'SELECT RELEASE_LOCK(?)'; @@ -661,11 +714,62 @@ class Db private static function logExtraInfoIfDeadlock($ex) { - if (self::get()->isErrNo($ex, 1213)) { + if (!self::get()->isErrNo($ex, 1213)) { + return; + } + + try { $deadlockInfo = self::fetchAll("SHOW ENGINE INNODB STATUS"); // log using exception so backtrace appears in log output Log::debug(new Exception("Encountered deadlock: " . print_r($deadlockInfo, true))); + + } catch(\Exception $e) { + // 1227 Access denied; you need (at least one of) the PROCESS privilege(s) for this operation } } + + private static function logSql($functionName, $sql, $parameters = array()) + { + if (self::$logQueries === false + || @Config::getInstance()->Debug['log_sql_queries'] != 1 + ) { + return; + } + + // NOTE: at the moment we don't log parameters in order to avoid sensitive information leaks + Log::debug("Db::%s() executing SQL: %s", $functionName, $sql); + } + + /** + * @param bool $enable + */ + public static function enableQueryLog($enable) + { + self::$logQueries = $enable; + } + + /** + * @return boolean + */ + public static function isQueryLogEnabled() + { + return self::$logQueries; + } + + public static function isOptimizeInnoDBSupported($version = null) + { + if ($version === null) { + $version = Db::fetchOne("SELECT VERSION()"); + } + + $version = strtolower($version); + + if (strpos($version, "mariadb") === false) { + return false; + } + + $semanticVersion = strstr($version, '-', $beforeNeedle = true); + return version_compare($semanticVersion, '10.1.1', '>='); + } } diff --git a/www/analytics/core/Db/Adapter.php b/www/analytics/core/Db/Adapter.php index ea017733..bea69e1d 100644 --- a/www/analytics/core/Db/Adapter.php +++ b/www/analytics/core/Db/Adapter.php @@ -1,6 +1,6 @@ $val) { + $infos[$key] = $val; + } + + $adapter = new $className($infos); if ($connect) { $adapter->getConnection(); @@ -60,10 +63,15 @@ class Adapter * * @param string $adapterName * @return string + * @throws \Exception */ private static function getAdapterClassName($adapterName) { - return 'Piwik\Db\Adapter\\' . str_replace(' ', '\\', ucwords(str_replace(array('_', '\\'), ' ', strtolower($adapterName)))); + $className = 'Piwik\Db\Adapter\\' . str_replace(' ', '\\', ucwords(str_replace(array('_', '\\'), ' ', strtolower($adapterName)))); + if (!class_exists($className)) { + throw new \Exception("Adapter $adapterName is not valid."); + } + return $className; } /** diff --git a/www/analytics/core/Db/Adapter/Mysqli.php b/www/analytics/core/Db/Adapter/Mysqli.php index c3990bfe..c06c8440 100644 --- a/www/analytics/core/Db/Adapter/Mysqli.php +++ b/www/analytics/core/Db/Adapter/Mysqli.php @@ -1,6 +1,6 @@ _connection) { + return; + } + + parent::_connect(); + + $this->_connection->query('SET sql_mode = "' . Db::SQL_MODE . '"'); + } + /** * Check MySQL version * @@ -56,8 +68,9 @@ class Mysqli extends Zend_Db_Adapter_Mysqli implements AdapterInterface */ public function checkServerVersion() { - $serverVersion = $this->getServerVersion(); + $serverVersion = $this->getServerVersion(); $requiredVersion = Config::getInstance()->General['minimum_mysql_version']; + if (version_compare($serverVersion, $requiredVersion) === -1) { throw new Exception(Piwik::translate('General_ExceptionDatabaseVersion', array('MySQL', $serverVersion, $requiredVersion))); } @@ -72,6 +85,7 @@ class Mysqli extends Zend_Db_Adapter_Mysqli implements AdapterInterface { $serverVersion = $this->getServerVersion(); $clientVersion = $this->getClientVersion(); + // incompatible change to DECIMAL implementation in 5.0.3 if (version_compare($serverVersion, '5.0.3') >= 0 && version_compare($clientVersion, '5.0.3') < 0 @@ -168,10 +182,12 @@ class Mysqli extends Zend_Db_Adapter_Mysqli implements AdapterInterface public function getClientVersion() { $this->_connect(); - $version = $this->_connection->server_version; - $major = (int)($version / 10000); - $minor = (int)($version % 10000 / 100); + + $version = $this->_connection->server_version; + $major = (int)($version / 10000); + $minor = (int)($version % 10000 / 100); $revision = (int)($version % 100); + return $major . '.' . $minor . '.' . $revision; } } diff --git a/www/analytics/core/Db/Adapter/Pdo/Mssql.php b/www/analytics/core/Db/Adapter/Pdo/Mssql.php index cca882b3..96e1c729 100644 --- a/www/analytics/core/Db/Adapter/Pdo/Mssql.php +++ b/www/analytics/core/Db/Adapter/Pdo/Mssql.php @@ -1,6 +1,6 @@ _config["host"]; - $database = $this->_config["dbname"]; + $database = $this->_config["dbname"]; if (is_null($database)) { $database = 'master'; } @@ -134,8 +134,9 @@ class Mssql extends Zend_Db_Adapter_Pdo_Mssql implements AdapterInterface */ public function checkServerVersion() { - $serverVersion = $this->getServerVersion(); + $serverVersion = $this->getServerVersion(); $requiredVersion = Config::getInstance()->General['minimum_mssql_version']; + if (version_compare($serverVersion, $requiredVersion) === -1) { throw new Exception(Piwik::translate('General_ExceptionDatabaseVersion', array('MSSQL', $serverVersion, $requiredVersion))); } @@ -149,7 +150,7 @@ class Mssql extends Zend_Db_Adapter_Pdo_Mssql implements AdapterInterface public function getServerVersion() { try { - $stmt = $this->query("SELECT CAST(SERVERPROPERTY('productversion') as VARCHAR) as productversion"); + $stmt = $this->query("SELECT CAST(SERVERPROPERTY('productversion') as VARCHAR) as productversion"); $result = $stmt->fetchAll(Zend_Db::FETCH_NUM); if (count($result)) { return $result[0][0]; @@ -169,6 +170,7 @@ class Mssql extends Zend_Db_Adapter_Pdo_Mssql implements AdapterInterface { $serverVersion = $this->getServerVersion(); $clientVersion = $this->getClientVersion(); + if (version_compare($serverVersion, '10') >= 0 && version_compare($clientVersion, '10') < 0 ) { @@ -183,8 +185,7 @@ class Mssql extends Zend_Db_Adapter_Pdo_Mssql implements AdapterInterface */ public static function isEnabled() { - $extensions = @get_loaded_extensions(); - return in_array('PDO', $extensions) && in_array('pdo_sqlsrv', $extensions); + return extension_loaded('PDO') && extension_loaded('pdo_sqlsrv'); } /** @@ -224,6 +225,7 @@ class Mssql extends Zend_Db_Adapter_Pdo_Mssql implements AdapterInterface if (preg_match('/(?:\[|\s)([0-9]{4})(?:\]|\s)/', $e->getMessage(), $match)) { return $match[1] == $errno; } + return false; } diff --git a/www/analytics/core/Db/Adapter/Pdo/Mysql.php b/www/analytics/core/Db/Adapter/Pdo/Mysql.php index 368c7810..9e26fb7a 100644 --- a/www/analytics/core/Db/Adapter/Pdo/Mysql.php +++ b/www/analytics/core/Db/Adapter/Pdo/Mysql.php @@ -1,6 +1,6 @@ _connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + return $this->_connection; + } + + protected function _connect() + { + if ($this->_connection) { + return; + } + + parent::_connect(); + // MYSQL_ATTR_USE_BUFFERED_QUERY will use more memory when enabled // $this->_connection->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); - return $this->_connection; + $this->_connection->exec('SET sql_mode = "' . Db::SQL_MODE . '"'); } /** @@ -92,8 +104,9 @@ class Mysql extends Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface */ public function checkServerVersion() { - $serverVersion = $this->getServerVersion(); + $serverVersion = $this->getServerVersion(); $requiredVersion = Config::getInstance()->General['minimum_mysql_version']; + if (version_compare($serverVersion, $requiredVersion) === -1) { throw new Exception(Piwik::translate('General_ExceptionDatabaseVersion', array('MySQL', $serverVersion, $requiredVersion))); } @@ -108,6 +121,7 @@ class Mysql extends Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface { $serverVersion = $this->getServerVersion(); $clientVersion = $this->getClientVersion(); + // incompatible change to DECIMAL implementation in 5.0.3 if (version_compare($serverVersion, '5.0.3') >= 0 && version_compare($clientVersion, '5.0.3') < 0 @@ -123,8 +137,7 @@ class Mysql extends Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface */ public static function isEnabled() { - $extensions = @get_loaded_extensions(); - return in_array('PDO', $extensions) && in_array('pdo_mysql', $extensions) && in_array('mysql', PDO::getAvailableDrivers()); + return extension_loaded('PDO') && extension_loaded('pdo_mysql') && in_array('mysql', PDO::getAvailableDrivers()); } /** @@ -159,6 +172,7 @@ class Mysql extends Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface if (preg_match('/(?:\[|\s)([0-9]{4})(?:\]|\s)/', $e->getMessage(), $match)) { return $match[1] == $errno; } + return false; } @@ -170,9 +184,11 @@ class Mysql extends Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface public function isConnectionUTF8() { $charsetInfo = $this->fetchAll('SHOW VARIABLES LIKE ?', array('character_set_connection')); + if (empty($charsetInfo)) { return false; } + $charset = $charsetInfo[0]['Value']; return $charset === 'utf8'; } @@ -230,4 +246,19 @@ class Mysql extends Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface $this->cachePreparedStatement[$sql] = $stmt; return $stmt; } + + /** + * Override _dsn() to ensure host and port to not be passed along + * if unix_socket is set since setting both causes unexpected behaviour + * @see http://php.net/manual/en/ref.pdo-mysql.connection.php + */ + protected function _dsn() + { + if (!empty($this->_config['unix_socket'])) { + unset($this->_config['host']); + unset($this->_config['port']); + } + + return parent::_dsn(); + } } diff --git a/www/analytics/core/Db/Adapter/Pdo/Pgsql.php b/www/analytics/core/Db/Adapter/Pdo/Pgsql.php index b93d5191..109c881c 100644 --- a/www/analytics/core/Db/Adapter/Pdo/Pgsql.php +++ b/www/analytics/core/Db/Adapter/Pdo/Pgsql.php @@ -1,6 +1,6 @@ getServerVersion(); $requiredVersion = Config::getInstance()->General['minimum_pgsql_version']; + if (version_compare($databaseVersion, $requiredVersion) === -1) { throw new Exception(Piwik::translate('General_ExceptionDatabaseVersion', array('PostgreSQL', $databaseVersion, $requiredVersion))); } @@ -66,8 +67,7 @@ class Pgsql extends Zend_Db_Adapter_Pdo_Pgsql implements AdapterInterface */ public static function isEnabled() { - $extensions = @get_loaded_extensions(); - return in_array('PDO', $extensions) && in_array('pdo_pgsql', $extensions); + return extension_loaded('PDO') && extension_loaded('pdo_pgsql'); } /** @@ -146,6 +146,7 @@ class Pgsql extends Zend_Db_Adapter_Pdo_Pgsql implements AdapterInterface if (preg_match('/([0-9]{2}[0-9P][0-9]{2})/', $e->getMessage(), $match)) { return $match[1] == $map[$errno]; } + return false; } diff --git a/www/analytics/core/Db/AdapterInterface.php b/www/analytics/core/Db/AdapterInterface.php index 38a81a9e..cd0e4cde 100644 --- a/www/analytics/core/Db/AdapterInterface.php +++ b/www/analytics/core/Db/AdapterInterface.php @@ -1,6 +1,6 @@ General['enable_load_data_infile']; if ($loadDataInfileEnabled && Db::get()->hasBulkLoader()) { + + $path = self::getBestPathForLoadData(); + $filePath = $path . $tableName . '-' . Common::generateUniqId() . '.csv'; + try { $fileSpec = array( 'delim' => "\t", @@ -76,13 +75,9 @@ class BatchInsert }, 'eol' => "\r\n", 'null' => 'NULL', + 'charset' => $charset ); - // hack for charset mismatch - if (!DbHelper::isDatabaseConnectionUTF8() && !isset(Config::getInstance()->database['charset'])) { - $fileSpec['charset'] = 'latin1'; - } - self::createCSVFile($filePath, $fileSpec, $values); if (!is_readable($filePath)) { @@ -95,20 +90,40 @@ class BatchInsert return true; } } catch (Exception $e) { - Log::info("LOAD DATA INFILE failed or not supported, falling back to normal INSERTs... Error was: %s", $e->getMessage()); - if ($throwException) { throw $e; } } + + // if all else fails, fallback to a series of INSERTs + if (file_exists($filePath)) { + @unlink($filePath); + } } - // if all else fails, fallback to a series of INSERTs - @unlink($filePath); self::tableInsertBatchIterate($tableName, $fields, $values); + return false; } + private static function getBestPathForLoadData() + { + try { + $path = Db::fetchOne('SELECT @@secure_file_priv'); // was introduced in 5.0.38 + } catch (Exception $e) { + // we do not rethrow exception as an error is expected if MySQL is < 5.0.38 + // in this case tableInsertBatch might still work + } + + if (empty($path) || !is_dir($path) || !is_writable($path)) { + $path = StaticContainer::get('path.tmp') . '/assets/'; + } elseif (!Common::stringEndsWith($path, '/')) { + $path .= '/'; + } + + return $path; + } + /** * Batch insert into table from CSV (or other delimited) file. * @@ -124,7 +139,7 @@ class BatchInsert { // Chroot environment: prefix the path with the absolute chroot path $chrootPath = Config::getInstance()->General['absolute_chroot_path']; - if(!empty($chrootPath)) { + if (!empty($chrootPath)) { $filePath = $chrootPath . $filePath; } @@ -161,19 +176,20 @@ class BatchInsert "; /* - * First attempt: assume web server and MySQL server are on the same machine; - * this requires that the db user have the FILE privilege; however, since this is - * a global privilege, it may not be granted due to security concerns - */ + * First attempt: assume web server and MySQL server are on the same machine; + * this requires that the db user have the FILE privilege; however, since this is + * a global privilege, it may not be granted due to security concerns + */ $keywords = array(''); /* - * Second attempt: using the LOCAL keyword means the client reads the file and sends it to the server; - * the LOCAL keyword may trigger a known PHP PDO_MYSQL bug when MySQL not built with --enable-local-infile - * @see http://bugs.php.net/bug.php?id=54158 - */ + * Second attempt: using the LOCAL keyword means the client reads the file and sends it to the server; + * the LOCAL keyword may trigger a known PHP PDO\MYSQL bug when MySQL not built with --enable-local-infile + * @see http://bugs.php.net/bug.php?id=54158 + */ $openBaseDir = ini_get('open_basedir'); - $safeMode = ini_get('safe_mode'); + $safeMode = ini_get('safe_mode'); + if (empty($openBaseDir) && empty($safeMode)) { // php 5.x - LOAD DATA LOCAL INFILE is disabled if open_basedir restrictions or safe_mode enabled $keywords[] = 'LOCAL '; @@ -191,22 +207,21 @@ class BatchInsert return true; } catch (Exception $e) { -// echo $sql . ' ---- ' . $e->getMessage(); $code = $e->getCode(); $message = $e->getMessage() . ($code ? "[$code]" : ''); - if (!Db::get()->isErrNo($e, '1148')) { - Log::info("LOAD DATA INFILE failed... Error was: %s", $message); - } $exceptions[] = "\n Try #" . (count($exceptions) + 1) . ': ' . $queryStart . ": " . $message; } } + if (count($exceptions)) { - throw new Exception(implode(",", $exceptions)); + $message = "LOAD DATA INFILE failed... Error was: " . implode(",", $exceptions); + Log::info($message); + throw new Exception($message); } + return false; } - /** * Create CSV (or other delimited) files * @@ -215,13 +230,13 @@ class BatchInsert * @param array $rows Array of array corresponding to rows of values * @throws Exception if unable to create or write to file */ - static protected function createCSVFile($filePath, $fileSpec, $rows) + protected static function createCSVFile($filePath, $fileSpec, $rows) { // Set up CSV delimiters, quotes, etc $delim = $fileSpec['delim']; $quote = $fileSpec['quote']; - $eol = $fileSpec['eol']; - $null = $fileSpec['null']; + $eol = $fileSpec['eol']; + $null = $fileSpec['null']; $escapespecial_cb = $fileSpec['escapespecial_cb']; $fp = @fopen($filePath, 'wb'); @@ -248,6 +263,7 @@ class BatchInsert throw new Exception('Error writing to the tmp file ' . $filePath); } } + fclose($fp); @chmod($filePath, 0777); diff --git a/www/analytics/core/Db/Schema.php b/www/analytics/core/Db/Schema.php index b7de26b6..b973af0b 100644 --- a/www/analytics/core/Db/Schema.php +++ b/www/analytics/core/Db/Schema.php @@ -1,6 +1,6 @@ array( - self::DEFAULT_SCHEMA, - // InfiniDB - ), - - // Microsoft SQL Server -// 'MSSQL' => array( 'Mssql' ), - - // PostgreSQL -// 'PDO_PGSQL' => array( 'Pgsql' ), - - // IBM DB2 -// 'IBM' => array( 'Ibm' ), - - // Oracle -// 'OCI' => array( 'Oci' ), - ); - - $adapterName = strtoupper($adapterName); - switch ($adapterName) { - case 'PDO_MYSQL': - case 'MYSQLI': - $adapterName = 'MYSQL'; - break; - - case 'PDO_MSSQL': - case 'SQLSRV': - $adapterName = 'MSSQL'; - break; - - case 'PDO_IBM': - case 'DB2': - $adapterName = 'IBM'; - break; - - case 'PDO_OCI': - case 'ORACLE': - $adapterName = 'OCI'; - break; - } - $schemaNames = $allSchemaNames[$adapterName]; - - $schemas = array(); - - foreach ($schemaNames as $schemaName) { - $className = __NAMESPACE__ . '\\Schema\\' . $schemaName; - if (call_user_func(array($className, 'isAvailable'))) { - $schemas[] = $schemaName; - } - } - - return $schemas; - } /** * Load schema */ private function loadSchema() { - $config = Config::getInstance(); - $dbInfos = $config->database; + $config = Config::getInstance(); + $dbInfos = $config->database; $schemaName = trim($dbInfos['schema']); - $className = self::getSchemaClassName($schemaName); + $className = self::getSchemaClassName($schemaName); $this->schema = new $className(); } @@ -134,6 +71,7 @@ class Schema extends Singleton if ($this->schema === null) { $this->loadSchema(); } + return $this->schema; } @@ -233,6 +171,18 @@ class Schema extends Singleton return $this->getSchema()->getTablesInstalled($forceReload); } + /** + * Get list of installed columns in a table + * + * @param string $tableName The name of a table. + * + * @return array Installed columns indexed by the column name. + */ + public function getTableColumns($tableName) + { + return $this->getSchema()->getTableColumns($tableName); + } + /** * Returns true if Piwik tables exist * diff --git a/www/analytics/core/Db/Schema/Mysql.php b/www/analytics/core/Db/Schema/Mysql.php index bb5cc214..07cb1490 100644 --- a/www/analytics/core/Db/Schema/Mysql.php +++ b/www/analytics/core/Db/Schema/Mysql.php @@ -1,6 +1,6 @@ fetchAssoc('SHOW ENGINES'); - if (array_key_exists($engineName, $allEngines)) { - $support = $allEngines[$engineName]['Support']; - return $support == 'DEFAULT' || $support == 'YES'; - } - return false; - } - - /** - * Is this schema available? - * - * @return bool True if schema is available; false otherwise - */ - static public function isAvailable() - { - return self::hasStorageEngine('InnoDB'); - } + private $tablesInstalled = null; /** * Get the SQL to create Piwik tables @@ -58,284 +33,229 @@ class Mysql implements SchemaInterface $prefixTables = $this->getTablePrefix(); $tables = array( - 'user' => "CREATE TABLE {$prefixTables}user ( - login VARCHAR(100) NOT NULL, - password CHAR(32) NOT NULL, - alias VARCHAR(45) NOT NULL, - email VARCHAR(100) NOT NULL, - token_auth CHAR(32) NOT NULL, - superuser_access TINYINT(2) unsigned NOT NULL DEFAULT '0', - date_registered TIMESTAMP NULL, - PRIMARY KEY(login), - UNIQUE KEY uniq_keytoken(token_auth) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + 'user' => "CREATE TABLE {$prefixTables}user ( + login VARCHAR(100) NOT NULL, + password CHAR(32) NOT NULL, + alias VARCHAR(45) NOT NULL, + email VARCHAR(100) NOT NULL, + token_auth CHAR(32) NOT NULL, + superuser_access TINYINT(2) unsigned NOT NULL DEFAULT '0', + date_registered TIMESTAMP NULL, + PRIMARY KEY(login), + UNIQUE KEY uniq_keytoken(token_auth) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", - 'access' => "CREATE TABLE {$prefixTables}access ( - login VARCHAR(100) NOT NULL, - idsite INTEGER UNSIGNED NOT NULL, - access VARCHAR(10) NULL, - PRIMARY KEY(login, idsite) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + 'access' => "CREATE TABLE {$prefixTables}access ( + login VARCHAR(100) NOT NULL, + idsite INTEGER UNSIGNED NOT NULL, + access VARCHAR(10) NULL, + PRIMARY KEY(login, idsite) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", - 'site' => "CREATE TABLE {$prefixTables}site ( - idsite INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, - name VARCHAR(90) NOT NULL, - main_url VARCHAR(255) NOT NULL, - ts_created TIMESTAMP NULL, - ecommerce TINYINT DEFAULT 0, - sitesearch TINYINT DEFAULT 1, - sitesearch_keyword_parameters TEXT NOT NULL, - sitesearch_category_parameters TEXT NOT NULL, - timezone VARCHAR( 50 ) NOT NULL, - currency CHAR( 3 ) NOT NULL, - excluded_ips TEXT NOT NULL, - excluded_parameters TEXT NOT NULL, - excluded_user_agents TEXT NOT NULL, - `group` VARCHAR(250) NOT NULL, - `type` VARCHAR(255) NOT NULL, - keep_url_fragment TINYINT NOT NULL DEFAULT 0, - PRIMARY KEY(idsite) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + 'site' => "CREATE TABLE {$prefixTables}site ( + idsite INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + name VARCHAR(90) NOT NULL, + main_url VARCHAR(255) NOT NULL, + ts_created TIMESTAMP NULL, + ecommerce TINYINT DEFAULT 0, + sitesearch TINYINT DEFAULT 1, + sitesearch_keyword_parameters TEXT NOT NULL, + sitesearch_category_parameters TEXT NOT NULL, + timezone VARCHAR( 50 ) NOT NULL, + currency CHAR( 3 ) NOT NULL, + exclude_unknown_urls TINYINT(1) DEFAULT 0, + excluded_ips TEXT NOT NULL, + excluded_parameters TEXT NOT NULL, + excluded_user_agents TEXT NOT NULL, + `group` VARCHAR(250) NOT NULL, + `type` VARCHAR(255) NOT NULL, + keep_url_fragment TINYINT NOT NULL DEFAULT 0, + PRIMARY KEY(idsite) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", - 'site_url' => "CREATE TABLE {$prefixTables}site_url ( - idsite INTEGER(10) UNSIGNED NOT NULL, - url VARCHAR(255) NOT NULL, - PRIMARY KEY(idsite, url) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + 'site_setting' => "CREATE TABLE {$prefixTables}site_setting ( + idsite INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `setting_name` VARCHAR(255) NOT NULL, + `setting_value` LONGTEXT NOT NULL, + PRIMARY KEY(idsite, setting_name) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", - 'goal' => " CREATE TABLE `{$prefixTables}goal` ( - `idsite` int(11) NOT NULL, - `idgoal` int(11) NOT NULL, - `name` varchar(50) NOT NULL, - `match_attribute` varchar(20) NOT NULL, - `pattern` varchar(255) NOT NULL, - `pattern_type` varchar(10) NOT NULL, - `case_sensitive` tinyint(4) NOT NULL, - `allow_multiple` tinyint(4) NOT NULL, - `revenue` float NOT NULL, - `deleted` tinyint(4) NOT NULL default '0', - PRIMARY KEY (`idsite`,`idgoal`) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + 'site_url' => "CREATE TABLE {$prefixTables}site_url ( + idsite INTEGER(10) UNSIGNED NOT NULL, + url VARCHAR(255) NOT NULL, + PRIMARY KEY(idsite, url) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", - 'logger_message' => "CREATE TABLE {$prefixTables}logger_message ( - idlogger_message INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, + 'goal' => "CREATE TABLE `{$prefixTables}goal` ( + `idsite` int(11) NOT NULL, + `idgoal` int(11) NOT NULL, + `name` varchar(50) NOT NULL, + `match_attribute` varchar(20) NOT NULL, + `pattern` varchar(255) NOT NULL, + `pattern_type` varchar(10) NOT NULL, + `case_sensitive` tinyint(4) NOT NULL, + `allow_multiple` tinyint(4) NOT NULL, + `revenue` float NOT NULL, + `deleted` tinyint(4) NOT NULL default '0', + PRIMARY KEY (`idsite`,`idgoal`) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + + 'logger_message' => "CREATE TABLE {$prefixTables}logger_message ( + idlogger_message INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, tag VARCHAR(50) NULL, - timestamp TIMESTAMP NULL, + timestamp TIMESTAMP NULL, level VARCHAR(16) NULL, - message TEXT NULL, - PRIMARY KEY(idlogger_message) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + message TEXT NULL, + PRIMARY KEY(idlogger_message) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + 'log_action' => "CREATE TABLE {$prefixTables}log_action ( + idaction INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + name TEXT, + hash INTEGER(10) UNSIGNED NOT NULL, + type TINYINT UNSIGNED NULL, + url_prefix TINYINT(2) NULL, + PRIMARY KEY(idaction), + INDEX index_type_hash (type, hash) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", - 'log_action' => "CREATE TABLE {$prefixTables}log_action ( - idaction INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, - name TEXT, - hash INTEGER(10) UNSIGNED NOT NULL, - type TINYINT UNSIGNED NULL, - url_prefix TINYINT(2) NULL, - PRIMARY KEY(idaction), - INDEX index_type_hash (type, hash) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'log_visit' => "CREATE TABLE {$prefixTables}log_visit ( - idvisit INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, - idsite INTEGER(10) UNSIGNED NOT NULL, - idvisitor BINARY(8) NOT NULL, - visitor_localtime TIME NOT NULL, - visitor_returning TINYINT(1) NOT NULL, - visitor_count_visits SMALLINT(5) UNSIGNED NOT NULL, - visitor_days_since_last SMALLINT(5) UNSIGNED NOT NULL, - visitor_days_since_order SMALLINT(5) UNSIGNED NOT NULL, - visitor_days_since_first SMALLINT(5) UNSIGNED NOT NULL, - visit_first_action_time DATETIME NOT NULL, - visit_last_action_time DATETIME NOT NULL, - visit_exit_idaction_url INTEGER(11) UNSIGNED NULL DEFAULT 0, - visit_exit_idaction_name INTEGER(11) UNSIGNED NOT NULL, - visit_entry_idaction_url INTEGER(11) UNSIGNED NOT NULL, - visit_entry_idaction_name INTEGER(11) UNSIGNED NOT NULL, - visit_total_actions SMALLINT(5) UNSIGNED NOT NULL, - visit_total_searches SMALLINT(5) UNSIGNED NOT NULL, - visit_total_events SMALLINT(5) UNSIGNED NOT NULL, - visit_total_time SMALLINT(5) UNSIGNED NOT NULL, - visit_goal_converted TINYINT(1) NOT NULL, - visit_goal_buyer TINYINT(1) NOT NULL, - referer_type TINYINT(1) UNSIGNED NULL, - referer_name VARCHAR(70) NULL, - referer_url TEXT NOT NULL, - referer_keyword VARCHAR(255) NULL, - config_id BINARY(8) NOT NULL, - config_os CHAR(3) NOT NULL, - config_browser_name VARCHAR(10) NOT NULL, - config_browser_version VARCHAR(20) NOT NULL, - config_resolution VARCHAR(9) NOT NULL, - config_pdf TINYINT(1) NOT NULL, - config_flash TINYINT(1) NOT NULL, - config_java TINYINT(1) NOT NULL, - config_director TINYINT(1) NOT NULL, - config_quicktime TINYINT(1) NOT NULL, - config_realplayer TINYINT(1) NOT NULL, - config_windowsmedia TINYINT(1) NOT NULL, - config_gears TINYINT(1) NOT NULL, - config_silverlight TINYINT(1) NOT NULL, - config_cookie TINYINT(1) NOT NULL, - location_ip VARBINARY(16) NOT NULL, - location_browser_lang VARCHAR(20) NOT NULL, - location_country CHAR(3) NOT NULL, - location_region char(2) DEFAULT NULL, - location_city varchar(255) DEFAULT NULL, - location_latitude float(10, 6) DEFAULT NULL, - location_longitude float(10, 6) DEFAULT NULL, - PRIMARY KEY(idvisit), - INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time), - INDEX index_idsite_datetime (idsite, visit_last_action_time), - INDEX index_idsite_idvisitor (idsite, idvisitor) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + 'log_visit' => "CREATE TABLE {$prefixTables}log_visit ( + idvisit INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + idsite INTEGER(10) UNSIGNED NOT NULL, + idvisitor BINARY(8) NOT NULL, + visit_last_action_time DATETIME NOT NULL, + config_id BINARY(8) NOT NULL, + location_ip VARBINARY(16) NOT NULL, + PRIMARY KEY(idvisit), + INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time), + INDEX index_idsite_datetime (idsite, visit_last_action_time), + INDEX index_idsite_idvisitor (idsite, idvisitor) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", 'log_conversion_item' => "CREATE TABLE `{$prefixTables}log_conversion_item` ( - idsite int(10) UNSIGNED NOT NULL, - idvisitor BINARY(8) NOT NULL, - server_time DATETIME NOT NULL, - idvisit INTEGER(10) UNSIGNED NOT NULL, - idorder varchar(100) NOT NULL, + idsite int(10) UNSIGNED NOT NULL, + idvisitor BINARY(8) NOT NULL, + server_time DATETIME NOT NULL, + idvisit INTEGER(10) UNSIGNED NOT NULL, + idorder varchar(100) NOT NULL, + idaction_sku INTEGER(10) UNSIGNED NOT NULL, + idaction_name INTEGER(10) UNSIGNED NOT NULL, + idaction_category INTEGER(10) UNSIGNED NOT NULL, + idaction_category2 INTEGER(10) UNSIGNED NOT NULL, + idaction_category3 INTEGER(10) UNSIGNED NOT NULL, + idaction_category4 INTEGER(10) UNSIGNED NOT NULL, + idaction_category5 INTEGER(10) UNSIGNED NOT NULL, + price FLOAT NOT NULL, + quantity INTEGER(10) UNSIGNED NOT NULL, + deleted TINYINT(1) UNSIGNED NOT NULL, + PRIMARY KEY(idvisit, idorder, idaction_sku), + INDEX index_idsite_servertime ( idsite, server_time ) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", - idaction_sku INTEGER(10) UNSIGNED NOT NULL, - idaction_name INTEGER(10) UNSIGNED NOT NULL, - idaction_category INTEGER(10) UNSIGNED NOT NULL, - idaction_category2 INTEGER(10) UNSIGNED NOT NULL, - idaction_category3 INTEGER(10) UNSIGNED NOT NULL, - idaction_category4 INTEGER(10) UNSIGNED NOT NULL, - idaction_category5 INTEGER(10) UNSIGNED NOT NULL, - price FLOAT NOT NULL, - quantity INTEGER(10) UNSIGNED NOT NULL, - deleted TINYINT(1) UNSIGNED NOT NULL, - - PRIMARY KEY(idvisit, idorder, idaction_sku), - INDEX index_idsite_servertime ( idsite, server_time ) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'log_conversion' => "CREATE TABLE `{$prefixTables}log_conversion` ( - idvisit int(10) unsigned NOT NULL, - idsite int(10) unsigned NOT NULL, - idvisitor BINARY(8) NOT NULL, - server_time datetime NOT NULL, - idaction_url int(11) default NULL, - idlink_va int(11) default NULL, - referer_visit_server_date date default NULL, - referer_type int(10) unsigned default NULL, - referer_name varchar(70) default NULL, - referer_keyword varchar(255) default NULL, - visitor_returning tinyint(1) NOT NULL, - visitor_count_visits SMALLINT(5) UNSIGNED NOT NULL, - visitor_days_since_first SMALLINT(5) UNSIGNED NOT NULL, - visitor_days_since_order SMALLINT(5) UNSIGNED NOT NULL, - location_country char(3) NOT NULL, - location_region char(2) DEFAULT NULL, - location_city varchar(255) DEFAULT NULL, - location_latitude float(10, 6) DEFAULT NULL, - location_longitude float(10, 6) DEFAULT NULL, - url text NOT NULL, - idgoal int(10) NOT NULL, - buster int unsigned NOT NULL, - - idorder varchar(100) default NULL, - items SMALLINT UNSIGNED DEFAULT NULL, - revenue float default NULL, - revenue_subtotal float default NULL, - revenue_tax float default NULL, - revenue_shipping float default NULL, - revenue_discount float default NULL, - - PRIMARY KEY (idvisit, idgoal, buster), - UNIQUE KEY unique_idsite_idorder (idsite, idorder), - INDEX index_idsite_datetime ( idsite, server_time ) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + 'log_conversion' => "CREATE TABLE `{$prefixTables}log_conversion` ( + idvisit int(10) unsigned NOT NULL, + idsite int(10) unsigned NOT NULL, + idvisitor BINARY(8) NOT NULL, + server_time datetime NOT NULL, + idaction_url int(11) default NULL, + idlink_va int(11) default NULL, + idgoal int(10) NOT NULL, + buster int unsigned NOT NULL, + idorder varchar(100) default NULL, + items SMALLINT UNSIGNED DEFAULT NULL, + url text NOT NULL, + PRIMARY KEY (idvisit, idgoal, buster), + UNIQUE KEY unique_idsite_idorder (idsite, idorder), + INDEX index_idsite_datetime ( idsite, server_time ) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", 'log_link_visit_action' => "CREATE TABLE {$prefixTables}log_link_visit_action ( - idlink_va INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT, - idsite int(10) UNSIGNED NOT NULL, - idvisitor BINARY(8) NOT NULL, - server_time DATETIME NOT NULL, - idvisit INTEGER(10) UNSIGNED NOT NULL, - idaction_url INTEGER(10) UNSIGNED DEFAULT NULL, - idaction_url_ref INTEGER(10) UNSIGNED NULL DEFAULT 0, - idaction_name INTEGER(10) UNSIGNED, - idaction_name_ref INTEGER(10) UNSIGNED NOT NULL, - idaction_event_category INTEGER(10) UNSIGNED DEFAULT NULL, - idaction_event_action INTEGER(10) UNSIGNED DEFAULT NULL, - time_spent_ref_action INTEGER(10) UNSIGNED NOT NULL, + idlink_va INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT, + idsite int(10) UNSIGNED NOT NULL, + idvisitor BINARY(8) NOT NULL, + idvisit INTEGER(10) UNSIGNED NOT NULL, + idaction_url_ref INTEGER(10) UNSIGNED NULL DEFAULT 0, + idaction_name_ref INTEGER(10) UNSIGNED NOT NULL, + custom_float FLOAT NULL DEFAULT NULL, + PRIMARY KEY(idlink_va), + INDEX index_idvisit(idvisit) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", - custom_float FLOAT NULL DEFAULT NULL, - PRIMARY KEY(idlink_va), - INDEX index_idvisit(idvisit), - INDEX index_idsite_servertime ( idsite, server_time ) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + 'log_profiling' => "CREATE TABLE {$prefixTables}log_profiling ( + query TEXT NOT NULL, + count INTEGER UNSIGNED NULL, + sum_time_ms FLOAT NULL, + UNIQUE KEY query(query(100)) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", - 'log_profiling' => "CREATE TABLE {$prefixTables}log_profiling ( - query TEXT NOT NULL, - count INTEGER UNSIGNED NULL, - sum_time_ms FLOAT NULL, - UNIQUE KEY query(query(100)) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + 'option' => "CREATE TABLE `{$prefixTables}option` ( + option_name VARCHAR( 255 ) NOT NULL, + option_value LONGTEXT NOT NULL, + autoload TINYINT NOT NULL DEFAULT '1', + PRIMARY KEY ( option_name ), + INDEX autoload( autoload ) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", - 'option' => "CREATE TABLE `{$prefixTables}option` ( - option_name VARCHAR( 255 ) NOT NULL, - option_value LONGTEXT NOT NULL, - autoload TINYINT NOT NULL DEFAULT '1', - PRIMARY KEY ( option_name ), - INDEX autoload( autoload ) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + 'session' => "CREATE TABLE {$prefixTables}session ( + id VARCHAR( 255 ) NOT NULL, + modified INTEGER, + lifetime INTEGER, + data TEXT, + PRIMARY KEY ( id ) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", - 'session' => "CREATE TABLE {$prefixTables}session ( - id CHAR(32) NOT NULL, - modified INTEGER, - lifetime INTEGER, - data TEXT, - PRIMARY KEY ( id ) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + 'archive_numeric' => "CREATE TABLE {$prefixTables}archive_numeric ( + idarchive INTEGER UNSIGNED NOT NULL, + name VARCHAR(255) NOT NULL, + idsite INTEGER UNSIGNED NULL, + date1 DATE NULL, + date2 DATE NULL, + period TINYINT UNSIGNED NULL, + ts_archived DATETIME NULL, + value DOUBLE NULL, + PRIMARY KEY(idarchive, name), + INDEX index_idsite_dates_period(idsite, date1, date2, period, ts_archived), + INDEX index_period_archived(period, ts_archived) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", - 'archive_numeric' => "CREATE TABLE {$prefixTables}archive_numeric ( - idarchive INTEGER UNSIGNED NOT NULL, - name VARCHAR(255) NOT NULL, - idsite INTEGER UNSIGNED NULL, - date1 DATE NULL, - date2 DATE NULL, - period TINYINT UNSIGNED NULL, - ts_archived DATETIME NULL, - value DOUBLE NULL, - PRIMARY KEY(idarchive, name), - INDEX index_idsite_dates_period(idsite, date1, date2, period, ts_archived), - INDEX index_period_archived(period, ts_archived) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + 'archive_blob' => "CREATE TABLE {$prefixTables}archive_blob ( + idarchive INTEGER UNSIGNED NOT NULL, + name VARCHAR(255) NOT NULL, + idsite INTEGER UNSIGNED NULL, + date1 DATE NULL, + date2 DATE NULL, + period TINYINT UNSIGNED NULL, + ts_archived DATETIME NULL, + value MEDIUMBLOB NULL, + PRIMARY KEY(idarchive, name), + INDEX index_period_archived(period, ts_archived) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", - 'archive_blob' => "CREATE TABLE {$prefixTables}archive_blob ( - idarchive INTEGER UNSIGNED NOT NULL, - name VARCHAR(255) NOT NULL, - idsite INTEGER UNSIGNED NULL, - date1 DATE NULL, - date2 DATE NULL, - period TINYINT UNSIGNED NULL, - ts_archived DATETIME NULL, - value MEDIUMBLOB NULL, - PRIMARY KEY(idarchive, name), - INDEX index_period_archived(period, ts_archived) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + 'sequence' => "CREATE TABLE {$prefixTables}sequence ( + `name` VARCHAR(120) NOT NULL, + `value` BIGINT(20) UNSIGNED NOT NULL , + PRIMARY KEY(`name`) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", ); + return $tables; } @@ -365,16 +285,37 @@ class Mysql implements SchemaInterface */ public function getTablesNames() { - $aTables = array_keys($this->getTablesCreateSql()); + $aTables = array_keys($this->getTablesCreateSql()); $prefixTables = $this->getTablePrefix(); + $return = array(); foreach ($aTables as $table) { $return[] = $prefixTables . $table; } + return $return; } - private $tablesInstalled = null; + /** + * Get list of installed columns in a table + * + * @param string $tableName The name of a table. + * + * @return array Installed columns indexed by the column name. + */ + public function getTableColumns($tableName) + { + $db = $this->getDb(); + + $allColumns = $db->fetchAll("SHOW COLUMNS FROM . $tableName"); + + $fields = array(); + foreach ($allColumns as $column) { + $fields[trim($column['Field'])] = $column; + } + + return $fields; + } /** * Get list of tables installed @@ -387,13 +328,10 @@ class Mysql implements SchemaInterface if (is_null($this->tablesInstalled) || $forceReload === true ) { - $db = Db::get(); - $prefixTables = $this->getTablePrefix(); + $db = $this->getDb(); + $prefixTables = $this->getTablePrefixEscaped(); - // '_' matches any character; force it to be literal - $prefixTables = str_replace('_', '\_', $prefixTables); - - $allTables = $db->fetchCol("SHOW TABLES LIKE '" . $prefixTables . "%'"); + $allTables = $this->getAllExistingTables($prefixTables); // all the tables to be installed $allMyTables = $this->getTablesNames(); @@ -403,12 +341,13 @@ class Mysql implements SchemaInterface // at this point we have the static list of core tables, but let's add the monthly archive tables $allArchiveNumeric = $db->fetchCol("SHOW TABLES LIKE '" . $prefixTables . "archive_numeric%'"); - $allArchiveBlob = $db->fetchCol("SHOW TABLES LIKE '" . $prefixTables . "archive_blob%'"); + $allArchiveBlob = $db->fetchCol("SHOW TABLES LIKE '" . $prefixTables . "archive_blob%'"); $allTablesReallyInstalled = array_merge($tablesInstalled, $allArchiveNumeric, $allArchiveBlob); $this->tablesInstalled = $allTablesReallyInstalled; } + return $this->tablesInstalled; } @@ -432,6 +371,7 @@ class Mysql implements SchemaInterface if (is_null($dbName)) { $dbName = $this->getDbName(); } + Db::exec("CREATE DATABASE IF NOT EXISTS " . $dbName . " DEFAULT CHARACTER SET utf8"); } @@ -454,8 +394,8 @@ class Mysql implements SchemaInterface Db::exec($statement); } catch (Exception $e) { // mysql code error 1050:table already exists - // see bug #153 http://dev.piwik.org/trac/ticket/153 - if (!Db::get()->isErrNo($e, '1050')) { + // see bug #153 https://github.com/piwik/piwik/issues/153 + if (!$this->getDb()->isErrNo($e, '1050')) { throw $e; } } @@ -475,7 +415,7 @@ class Mysql implements SchemaInterface */ public function createTables() { - $db = Db::get(); + $db = $this->getDb(); $prefixTables = $this->getTablePrefix(); $tablesAlreadyInstalled = $this->getTablesInstalled(); @@ -498,9 +438,9 @@ class Mysql implements SchemaInterface { // The anonymous user is the user that is assigned by default // note that the token_auth value is anonymous, which is assigned by default as well in the Login plugin - $db = Db::get(); + $db = $this->getDb(); $db->query("INSERT IGNORE INTO " . Common::prefixTable("user") . " - VALUES ( 'anonymous', '', 'anonymous', 'anonymous@example.org', 'anonymous', 0, '" . Date::factory('now')->getDatetime() . "' );"); + VALUES ( 'anonymous', '', 'anonymous', 'anonymous@example.org', 'anonymous', 0, '" . Date::factory('now')->getDatetime() . "' );"); } /** @@ -508,32 +448,51 @@ class Mysql implements SchemaInterface */ public function truncateAllTables() { - $tablesAlreadyInstalled = $this->getTablesInstalled($forceReload = true); - foreach ($tablesAlreadyInstalled as $table) { + $tables = $this->getAllExistingTables(); + foreach ($tables as $table) { Db::query("TRUNCATE `$table`"); } } private function getTablePrefix() { - $dbInfos = Db::getDatabaseConfig(); - $prefixTables = $dbInfos['tables_prefix']; - - return $prefixTables; + return $this->getDbSettings()->getTablePrefix(); } private function getTableEngine() { - $dbInfos = Db::getDatabaseConfig(); - $engine = $dbInfos['type']; - return $engine; + return $this->getDbSettings()->getEngine(); + } + + private function getDb() + { + return Db::get(); + } + + private function getDbSettings() + { + return new Db\Settings(); } private function getDbName() { - $dbInfos = Db::getDatabaseConfig(); - $dbName = $dbInfos['dbname']; + return $this->getDbSettings()->getDbName(); + } - return $dbName; + private function getAllExistingTables($prefixTables = false) + { + if (empty($prefixTables)) { + $prefixTables = $this->getTablePrefixEscaped(); + } + + return Db::get()->fetchCol("SHOW TABLES LIKE '" . $prefixTables . "%'"); + } + + private function getTablePrefixEscaped() + { + $prefixTables = $this->getTablePrefix(); + // '_' matches any character; force it to be literal + $prefixTables = str_replace('_', '\_', $prefixTables); + return $prefixTables; } } diff --git a/www/analytics/core/Db/SchemaInterface.php b/www/analytics/core/Db/SchemaInterface.php index 71bc1b7d..8c22bb68 100644 --- a/www/analytics/core/Db/SchemaInterface.php +++ b/www/analytics/core/Db/SchemaInterface.php @@ -1,6 +1,6 @@ getDbSetting('type'); + } + + public function getTablePrefix() + { + return $this->getDbSetting('tables_prefix'); + } + + public function getDbName() + { + return $this->getDbSetting('dbname'); + } + + private function getDbSetting($key) + { + $dbInfos = Db::getDatabaseConfig(); + $engine = $dbInfos[$key]; + + return $engine; + } +} diff --git a/www/analytics/core/DbHelper.php b/www/analytics/core/DbHelper.php index 761bce26..25b1f564 100644 --- a/www/analytics/core/DbHelper.php +++ b/www/analytics/core/DbHelper.php @@ -1,6 +1,6 @@ getTablesInstalled($forceReload); } + /** + * Get list of installed columns in a table + * + * @param string $tableName The name of a table. + * + * @return array Installed columns indexed by the column name. + */ + public static function getTableColumns($tableName) + { + return Schema::getInstance()->getTableColumns($tableName); + } + /** * Creates a new table in the database. * @@ -153,7 +165,7 @@ class DbHelper /** * Get the SQL to create a specific Piwik table * - * @param string $tableName + * @param string $tableName Unprefixed table name. * @return string SQL */ public static function getTableCreateSql($tableName) @@ -161,4 +173,17 @@ class DbHelper return Schema::getInstance()->getTableCreateSql($tableName); } + /** + * Deletes archive tables. For use in tests. + */ + public static function deleteArchiveTables() + { + foreach (ArchiveTableCreator::getTablesArchivesInstalled() as $table) { + Log::debug("Dropping table $table"); + + Db::query("DROP TABLE IF EXISTS `$table`"); + } + + ArchiveTableCreator::refreshTableList($forceReload = true); + } } diff --git a/www/analytics/core/Development.php b/www/analytics/core/Development.php new file mode 100644 index 00000000..3b44eba7 --- /dev/null +++ b/www/analytics/core/Development.php @@ -0,0 +1,196 @@ +Development['enabled']; + } + + return self::$isEnabled; + } + + /** + * Verifies whether a className of object implements the given method. It does not check whether the given method + * is actually callable (public). + * + * @param string|object $classOrObject + * @param string $method + * + * @return bool true if the method exists, false otherwise. + */ + public static function methodExists($classOrObject, $method) + { + if (is_string($classOrObject)) { + return class_exists($classOrObject) && method_exists($classOrObject, $method); + } + + return method_exists($classOrObject, $method); + } + + /** + * Formats a method call depending on the given class/object and method name. It does not perform any checks whether + * does actually exists. + * + * @param string|object $classOrObject + * @param string $method + * + * @return string Formatted method call. Example: "MyNamespace\MyClassname::methodName()" + */ + public static function formatMethodCall($classOrObject, $method) + { + if (is_object($classOrObject)) { + $classOrObject = get_class($classOrObject); + } + + return $classOrObject . '::' . $method . '()'; + } + + /** + * Checks whether the given method is actually callable on the given class/object if the development mode is + * enabled. En error will be triggered if the method does not exist or is not callable (public) containing a useful + * error message for the developer. + * + * @param string|object $classOrObject + * @param string $method + * @param string $prefixMessageIfError You can prepend any string to the error message in case the method is not + * callable. + */ + public static function checkMethodIsCallable($classOrObject, $method, $prefixMessageIfError) + { + if (!self::isEnabled()) { + return; + } + + self::checkMethodExists($classOrObject, $method, $prefixMessageIfError); + + if (!self::isCallableMethod($classOrObject, $method)) { + self::error($prefixMessageIfError . ' "' . self::formatMethodCall($classOrObject, $method) . '" is not callable. Please make sure to method is public'); + } + } + + /** + * Checks whether the given method is actually callable on the given class/object if the development mode is + * enabled. En error will be triggered if the method does not exist or is not callable (public) containing a useful + * error message for the developer. + * + * @param string|object $classOrObject + * @param string $method + * @param string $prefixMessageIfError You can prepend any string to the error message in case the method is not + * callable. + */ + public static function checkMethodExists($classOrObject, $method, $prefixMessageIfError) + { + if (!self::isEnabled()) { + return; + } + + if (!self::methodExists($classOrObject, $method)) { + self::error($prefixMessageIfError . ' "' . self::formatMethodCall($classOrObject, $method) . '" does not exist. Please make sure to define such a method.'); + } + } + + /** + * Verify whether the given method actually exists and is callable (public). + * + * @param string|object $classOrObject + * @param string $method + * @return bool + */ + public static function isCallableMethod($classOrObject, $method) + { + if (!self::methodExists($classOrObject, $method)) { + return false; + } + + $reflection = new \ReflectionMethod($classOrObject, $method); + return $reflection->isPublic(); + } + + /** + * Triggers an error if the development mode is enabled. Depending on the current environment / mode it will either + * log the given message or throw an exception to make sure it will be displayed in the Piwik UI. + * + * @param string $message + * @throws Exception + */ + public static function error($message) + { + if (!self::isEnabled()) { + return; + } + + $message .= ' (This error is only shown in development mode)'; + + if (SettingsServer::isTrackerApiRequest() + || Common::isPhpCliMode()) { + Log::error($message); + } else { + throw new Exception($message); + } + } + + public static function getMethodSourceCode($className, $methodName) + { + $method = new \ReflectionMethod($className, $methodName); + + $file = new \SplFileObject($method->getFileName()); + $offset = $method->getStartLine() - 1; + $count = $method->getEndLine() - $method->getStartLine() + 1; + + $fileIterator = new \LimitIterator($file, $offset, $count); + + $methodCode = "\n " . $method->getDocComment() . "\n"; + foreach ($fileIterator as $line) { + $methodCode .= $line; + } + $methodCode .= "\n"; + + return $methodCode; + } + + public static function getUseStatements($className) + { + $class = new \ReflectionClass($className); + + $file = new \SplFileObject($class->getFileName()); + + $fileIterator = new \LimitIterator($file, 0, $class->getStartLine()); + + $uses = array(); + foreach ($fileIterator as $line) { + if (preg_match('/(\s*)use (.+)/', $line, $match)) { + $uses[] = trim($match[2]); + } + } + + return $uses; + } +} diff --git a/www/analytics/core/DeviceDetectorCache.php b/www/analytics/core/DeviceDetectorCache.php new file mode 100644 index 00000000..543769fc --- /dev/null +++ b/www/analytics/core/DeviceDetectorCache.php @@ -0,0 +1,95 @@ +ttl = (int) $ttl; + $this->cache = PiwikCache::getEagerCache(); + } + + /** + * Function to fetch a cache entry + * + * @param string $id The cache entry ID + * @return array|bool False on error, or array the cache content + */ + public function fetch($id) + { + if (empty($id)) { + return false; + } + + if (array_key_exists($id, self::$staticCache)) { + return self::$staticCache[$id]; + } + + if (!$this->cache->contains($id)) { + return false; + } + + return $this->cache->fetch($id); + } + + /** + * A function to store content a cache entry. + * + * @param string $id The cache entry ID + * @param array $content The cache content + * @throws \Exception + * @return bool True if the entry was succesfully stored + */ + public function save($id, $content, $ttl=0) + { + if (empty($id)) { + return false; + } + + self::$staticCache[$id] = $content; + + return $this->cache->save($id, $content, $this->ttl); + } + + public function contains($id) + { + return !empty(self::$staticCache[$id]) && $this->cache->contains($id); + } + + public function delete($id) + { + if (empty($id)) { + return false; + } + + unset(self::$staticCache[$id]); + + return $this->cache->delete($id); + } + + public function flushAll() + { + return $this->cache->flushAll(); + } +} diff --git a/www/analytics/core/DeviceDetectorFactory.php b/www/analytics/core/DeviceDetectorFactory.php new file mode 100644 index 00000000..896132e7 --- /dev/null +++ b/www/analytics/core/DeviceDetectorFactory.php @@ -0,0 +1,37 @@ +discardBotInformation(); + $deviceDetector->setCache(new DeviceDetectorCache(86400)); + $deviceDetector->parse(); + + self::$deviceDetectorInstances[$userAgent] = $deviceDetector; + + return $deviceDetector; + } +} diff --git a/www/analytics/core/Error.php b/www/analytics/core/Error.php deleted file mode 100644 index 486ee7d5..00000000 --- a/www/analytics/core/Error.php +++ /dev/null @@ -1,222 +0,0 @@ -errno = $errno; - $this->errstr = $errstr; - $this->errfile = $errfile; - $this->errline = $errline; - $this->backtrace = $backtrace; - } - - public function getErrNoString() - { - switch ($this->errno) { - case E_ERROR: - return "Error"; - case E_WARNING: - return "Warning"; - case E_PARSE: - return "Parse Error"; - case E_NOTICE: - return "Notice"; - case E_CORE_ERROR: - return "Core Error"; - case E_CORE_WARNING: - return "Core Warning"; - case E_COMPILE_ERROR: - return "Compile Error"; - case E_COMPILE_WARNING: - return "Compile Warning"; - case E_USER_ERROR: - return "User Error"; - case E_USER_WARNING: - return "User Warning"; - case E_USER_NOTICE: - return "User Notice"; - case E_STRICT: - return "Strict Notice"; - case E_RECOVERABLE_ERROR: - return "Recoverable Error"; - case E_DEPRECATED: - return "Deprecated"; - case E_USER_DEPRECATED: - return "User Deprecated"; - default: - return "Unknown error ({$this->errno})"; - } - } - - public static function formatFileAndDBLogMessage(&$message, $level, $tag, $datetime, $log) - { - if ($message instanceof Error) { - $message = $message->errfile . '(' . $message->errline . '): ' . $message->getErrNoString() - . ' - ' . $message->errstr . "\n" . $message->backtrace; - - $message = $log->formatMessage($level, $tag, $datetime, $message); - } - } - - public static function formatScreenMessage(&$message, $level, $tag, $datetime, $log) - { - if ($message instanceof Error) { - $errno = $message->errno & error_reporting(); - - // problem when using error_reporting with the @ silent fail operator - // it gives an errno 0, and in this case the objective is to NOT display anything on the screen! - // is there any other case where the errno is zero at this point? - if ($errno == 0) { - $message = false; - return; - } - - if (!Common::isPhpCliMode()) { - @header('Content-Type: text/html; charset=utf-8'); - } - - $htmlString = ''; - $htmlString .= "\n
- There is an error. Please report the message (Piwik " . (class_exists('Piwik\Version') ? Version::VERSION : '') . ") - and full backtrace in the Piwik forums (please do a Search first as it might have been reported already!).

- "; - $htmlString .= $message->getErrNoString(); - $htmlString .= ":
{$message->errstr} in {$message->errfile}"; - $htmlString .= " on line {$message->errline}\n"; - $htmlString .= "

Backtrace -->

\n"; - $htmlString .= str_replace("\n", "
\n", $message->backtrace); - $htmlString .= "

"; - $htmlString .= "\n

"; - - $message = $htmlString; - } - } - - public static function setErrorHandler() - { - Piwik::addAction('Log.formatFileMessage', array('\\Piwik\\Error', 'formatFileAndDBLogMessage')); - Piwik::addAction('Log.formatDatabaseMessage', array('\\Piwik\\Error', 'formatFileAndDBLogMessage')); - Piwik::addAction('Log.formatScreenMessage', array('\\Piwik\\Error', 'formatScreenMessage')); - - set_error_handler(array('\\Piwik\\Error', 'errorHandler')); - } - - public static function errorHandler($errno, $errstr, $errfile, $errline) - { - // if the error has been suppressed by the @ we don't handle the error - if (error_reporting() == 0) { - return; - } - - $backtrace = ''; - if (empty(self::$debugBacktraceForTests)) { - $bt = @debug_backtrace(); - if ($bt !== null && isset($bt[0])) { - foreach ($bt as $i => $debug) { - $backtrace .= "#$i " - . (isset($debug['class']) ? $debug['class'] : '') - . (isset($debug['type']) ? $debug['type'] : '') - . (isset($debug['function']) ? $debug['function'] : '') - . '(...) called at [' - . (isset($debug['file']) ? $debug['file'] : '') . ':' - . (isset($debug['line']) ? $debug['line'] : '') . ']' . "\n"; - } - } - } else { - $backtrace = self::$debugBacktraceForTests; - } - - $error = new Error($errno, $errstr, $errfile, $errline, $backtrace); - Log::error($error); - - switch ($errno) { - case E_ERROR: - case E_PARSE: - case E_CORE_ERROR: - case E_CORE_WARNING: - case E_COMPILE_ERROR: - case E_COMPILE_WARNING: - case E_USER_ERROR: - exit; - break; - - case E_WARNING: - case E_NOTICE: - case E_USER_WARNING: - case E_USER_NOTICE: - case E_STRICT: - case E_RECOVERABLE_ERROR: - case E_DEPRECATED: - case E_USER_DEPRECATED: - default: - // do not exit - break; - } - } -} diff --git a/www/analytics/core/ErrorHandler.php b/www/analytics/core/ErrorHandler.php new file mode 100644 index 00000000..965257c8 --- /dev/null +++ b/www/analytics/core/ErrorHandler.php @@ -0,0 +1,135 @@ +getTraceAsString()); + throw new ErrorException($message, 0, $errno, $errfile, $errline); + break; + + case E_WARNING: + case E_NOTICE: + case E_USER_WARNING: + case E_USER_NOTICE: + case E_STRICT: + case E_RECOVERABLE_ERROR: + case E_DEPRECATED: + case E_USER_DEPRECATED: + default: + try { + Log::warning(self::createLogMessage($errno, $errstr, $errfile, $errline)); + } catch (\Exception $ex) { + // ignore (it's possible for this to happen if the StaticContainer hasn't been created yet) + } + + break; + } + } + + private static function createLogMessage($errno, $errstr, $errfile, $errline) + { + return sprintf( + "%s(%d): %s - %s - Piwik " . (class_exists('Piwik\Version') ? Version::VERSION : '') . " - Please report this message in the Piwik forums: http://forum.piwik.org (please do a search first as it might have been reported already)", + $errfile, + $errline, + ErrorHandler::getErrNoString($errno), + $errstr + ); + } + + private static function getHtmlMessage($errno, $errstr, $errfile, $errline, $trace) + { + $trace = Log::$debugBacktraceForTests ?: $trace; + + $message = ErrorHandler::getErrNoString($errno) . ' - ' . $errstr; + + $html = "

There is an error. Please report the message (Piwik " . (class_exists('Piwik\Version') ? Version::VERSION : '') . ") + and full backtrace in the Piwik forums (please do a Search firit might have been reported already!).

"; + $html .= "

{$message} in {$errfile}"; + $html .= " on line {$errline}

"; + $html .= "Backtrace:
";
+        $html .= str_replace("\n", "\n", $trace);
+        $html .= "
"; + + return $html; + } +} diff --git a/www/analytics/core/EventDispatcher.php b/www/analytics/core/EventDispatcher.php index 88e84d88..5795200c 100644 --- a/www/analytics/core/EventDispatcher.php +++ b/www/analytics/core/EventDispatcher.php @@ -1,6 +1,6 @@ pluginManager = $pluginManager; + + foreach ($observers as $observerInfo) { + list($eventName, $callback) = $observerInfo; + $this->extraObservers[$eventName][] = $callback; + } + } + /** * Triggers an event, executing all callbacks associated with it. * @@ -63,24 +91,36 @@ class EventDispatcher extends Singleton $this->pendingEvents[] = array($eventName, $params); } + $manager = $this->pluginManager; + if (empty($plugins)) { - $plugins = \Piwik\Plugin\Manager::getInstance()->getPluginsLoadedAndActivated(); + $plugins = $manager->getPluginsLoadedAndActivated(); } $callbacks = array(); // collect all callbacks to execute - foreach ($plugins as $plugin) { - if (is_string($plugin)) { - $plugin = \Piwik\Plugin\Manager::getInstance()->getLoadedPlugin($plugin); + foreach ($plugins as $pluginName) { + if (!is_string($pluginName)) { + $pluginName = $pluginName->getPluginName(); } - $hooks = $plugin->getListHooksRegistered(); + if (!isset($this->pluginHooks[$pluginName])) { + $plugin = $manager->getLoadedPlugin($pluginName); + $this->pluginHooks[$pluginName] = $plugin->getListHooksRegistered(); + } + + $hooks = $this->pluginHooks[$pluginName]; if (isset($hooks[$eventName])) { list($pluginFunction, $callbackGroup) = $this->getCallbackFunctionAndGroupNumber($hooks[$eventName]); - $callbacks[$callbackGroup][] = is_string($pluginFunction) ? array($plugin, $pluginFunction) : $pluginFunction; + if (is_string($pluginFunction)) { + $plugin = $manager->getLoadedPlugin($pluginName); + $callbacks[$callbackGroup][] = array($plugin, $pluginFunction) ; + } else { + $callbacks[$callbackGroup][] = $pluginFunction; + } } } @@ -92,6 +132,9 @@ class EventDispatcher extends Singleton } } + // sort callbacks by their importance + ksort($callbacks); + // execute callbacks in order foreach ($callbacks as $callbackGroup) { foreach ($callbackGroup as $callback) { @@ -125,30 +168,6 @@ class EventDispatcher extends Singleton $this->extraObservers[$eventName][] = $callback; } - /** - * Removes all registered extra observers for an event name. Only used for testing. - * - * @param string $eventName - */ - public function clearObservers($eventName) - { - $this->extraObservers[$eventName] = array(); - } - - /** - * Removes all registered extra observers. Only used for testing. - */ - public function clearAllObservers() - { - foreach ($this->extraObservers as $eventName => $eventObservers) { - if (strpos($eventName, 'Log.format') === 0) { - continue; - } - - $this->extraObservers[$eventName] = array(); - } - } - /** * Re-posts all pending events to the given plugin. * @@ -170,10 +189,10 @@ class EventDispatcher extends Singleton $pluginFunction = $hookInfo['function']; if (!empty($hookInfo['before'])) { $callbackGroup = self::EVENT_CALLBACK_GROUP_FIRST; - } else if (!empty($hookInfo['after'])) { - $callbackGroup = self::EVENT_CALLBACK_GROUP_SECOND; - } else { + } elseif (!empty($hookInfo['after'])) { $callbackGroup = self::EVENT_CALLBACK_GROUP_THIRD; + } else { + $callbackGroup = self::EVENT_CALLBACK_GROUP_SECOND; } } else { $pluginFunction = $hookInfo; @@ -183,4 +202,3 @@ class EventDispatcher extends Singleton return array($pluginFunction, $callbackGroup); } } - diff --git a/www/analytics/core/Exception/AuthenticationFailedException.php b/www/analytics/core/Exception/AuthenticationFailedException.php new file mode 100644 index 00000000..4091979f --- /dev/null +++ b/www/analytics/core/Exception/AuthenticationFailedException.php @@ -0,0 +1,13 @@ + + */ +class ErrorException extends \ErrorException +{ + public function isHtmlMessage() + { + return true; + } +} diff --git a/www/analytics/core/Exception/Exception.php b/www/analytics/core/Exception/Exception.php new file mode 100644 index 00000000..66d95eb4 --- /dev/null +++ b/www/analytics/core/Exception/Exception.php @@ -0,0 +1,30 @@ +isHtmlMessage = true; + } + + public function isHtmlMessage() + { + return $this->isHtmlMessage; + } +} diff --git a/www/analytics/core/Exception/InvalidRequestParameterException.php b/www/analytics/core/Exception/InvalidRequestParameterException.php new file mode 100644 index 00000000..4ea468c3 --- /dev/null +++ b/www/analytics/core/Exception/InvalidRequestParameterException.php @@ -0,0 +1,13 @@ +getFile(), $message->getLine(), $message->getMessage(), - self::$debugBacktraceForTests ? : $message->getTraceAsString()); - - $message = $log->formatMessage($level, $tag, $datetime, $message); + if (Common::isPhpCliMode()) { + self::dieWithCliError($exception); } + + self::dieWithHtmlErrorPage($exception); } - public static function formatScreenMessage(&$message, $level, $tag, $datetime, $log) + /** + * @param Exception|\Throwable $exception + */ + public static function dieWithCliError($exception) { - if ($message instanceof \Exception) { - if (!Common::isPhpCliMode()) { - @header('Content-Type: text/html; charset=utf-8'); - } + $message = $exception->getMessage(); - $outputFormat = strtolower(Common::getRequestVar('format', 'html', 'string')); + if (!method_exists($exception, 'isHtmlMessage') || !$exception->isHtmlMessage()) { + $message = strip_tags(str_replace('
', PHP_EOL, $message)); + } + + $message = sprintf( + "Uncaught exception: %s\nin %s line %d\n%s\n", + $message, + $exception->getFile(), + $exception->getLine(), + $exception->getTraceAsString() + ); + + echo $message; + + exit(1); + } + + /** + * @param Exception|\Throwable $exception + */ + public static function dieWithHtmlErrorPage($exception) + { + Common::sendHeader('Content-Type: text/html; charset=utf-8'); + + echo self::getErrorResponse($exception); + + exit(1); + } + + /** + * @param Exception|\Throwable $ex + */ + private static function getErrorResponse($ex) + { + $debugTrace = $ex->getTraceAsString(); + + $message = $ex->getMessage(); + + $isHtmlMessage = method_exists($ex, 'isHtmlMessage') && $ex->isHtmlMessage(); + + if (!$isHtmlMessage && Request::isApiRequest($_GET)) { + + $outputFormat = strtolower(Common::getRequestVar('format', 'xml', 'string', $_GET + $_POST)); $response = new ResponseBuilder($outputFormat); - $message = $response->getResponseException(new \Exception($message->getMessage())); - } - } + return $response->getResponseException($ex); - public static function logException(\Exception $exception) - { - Log::error($exception); + } elseif (!$isHtmlMessage) { + $message = Common::sanitizeInputValue($message); + } + + $logo = new CustomLogo(); + + $logoHeaderUrl = false; + $logoFaviconUrl = false; + try { + $logoHeaderUrl = $logo->getHeaderLogoUrl(); + $logoFaviconUrl = $logo->getPathUserFavicon(); + } catch (Exception $ex) { + try { + Log::debug($ex); + } catch (\Exception $otherEx) { + // DI container may not be setup at this point + } + } + + $result = Piwik_GetErrorMessagePage($message, $debugTrace, true, true, $logoHeaderUrl, $logoFaviconUrl); + + try { + /** + * Triggered before a Piwik error page is displayed to the user. + * + * This event can be used to modify the content of the error page that is displayed when + * an exception is caught. + * + * @param string &$result The HTML of the error page. + * @param Exception $ex The Exception displayed in the error page. + */ + Piwik::postEvent('FrontController.modifyErrorPage', array(&$result, $ex)); + } catch (ContainerDoesNotExistException $ex) { + // this can happen when an error occurs before the Piwik environment is created + } + + return $result; } } diff --git a/www/analytics/core/Filechecks.php b/www/analytics/core/Filechecks.php index 52cb995b..bf3e36f1 100644 --- a/www/analytics/core/Filechecks.php +++ b/www/analytics/core/Filechecks.php @@ -1,6 +1,6 @@ chown -R www-data:www-data " . $realpath . "
" . $directoryList; + $directoryList = "chown -R ". self::getUserAndGroup() ." " . $realpath . "
" . $directoryList; } - if(function_exists('shell_exec')) { - $currentUser = trim(shell_exec('whoami')); - if(!empty($currentUser)) { + if (function_exists('shell_exec')) { + $currentUser = self::getUser(); + if (!empty($currentUser)) { $optionalUserInfo = " (running as user '" . $currentUser . "')"; } } - $directoryMessage = "

Piwik couldn't write to some directories $optionalUserInfo.

"; + $directoryMessage = "

Piwik couldn't write to some directories $optionalUserInfo.

"; $directoryMessage .= "

Try to Execute the following commands on your server, to allow Write access on these directories" . ":

" . "
$directoryList
" @@ -102,7 +96,10 @@ class Filechecks . "

After applying the modifications, you can refresh the page.

" . "

If you need more help, try Piwik.org.

"; - Piwik_ExitWithMessage($directoryMessage, false, true); + $ex = new MissingFilePermissionException($directoryMessage); + $ex->setIsHtmlMessage(); + + throw $ex; } /** @@ -117,7 +114,6 @@ class Filechecks $manifest = PIWIK_INCLUDE_PATH . '/config/manifest.inc.php'; - if (file_exists($manifest)) { require_once $manifest; } @@ -139,7 +135,7 @@ class Filechecks if (!file_exists($file) || !is_readable($file)) { $messages[] = Piwik::translate('General_ExceptionMissingFile', $file); - } else if (filesize($file) != $props[0]) { + } elseif (filesize($file) != $props[0]) { if (!$hasMd5 || in_array(substr($path, -4), array('.gif', '.ico', '.jpg', '.png', '.swf'))) { // files that contain binary data (e.g., images) must match the file size $messages[] = Piwik::translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file))); @@ -153,7 +149,7 @@ class Filechecks $messages[] = Piwik::translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file))); } } - } else if ($hasMd5file && (@md5_file($file) !== $props[1])) { + } elseif ($hasMd5file && (@md5_file($file) !== $props[1])) { $messages[] = Piwik::translate('General_ExceptionFileIntegrity', $file); } } @@ -178,7 +174,7 @@ class Filechecks { $realpath = Filesystem::realpath(PIWIK_INCLUDE_PATH . '/'); $message = ''; - $message .= "chown -R www-data:www-data " . $realpath . "
"; + $message .= "chown -R ". self::getUserAndGroup() ." " . $realpath . "
"; $message .= "chmod -R 0755 " . $realpath . "
"; $message .= 'After you execute these commands (or change permissions via your FTP software), refresh the page and you should be able to use the "Automatic Update" feature.'; return $message; @@ -198,9 +194,10 @@ class Filechecks $message .= "On Windows, check that the folder is not read only and is writable.\n You can try to execute:
"; } else { - $message .= "For example, on a Linux server if your Apache httpd user - is www-data, you can try to execute:
\n" - . "chown -R www-data:www-data " . $path . "
"; + $message .= "For example, on a GNU/Linux server if your Apache httpd user is " + . self::getUser() + . ", you can try to execute:
\n" + . "chown -R ". self::getUserAndGroup() ." " . $path . "
"; } $message .= self::getMakeWritableCommand($path); @@ -208,6 +205,29 @@ class Filechecks return $message; } + private static function getUserAndGroup() + { + $user = self::getUser(); + if (!function_exists('shell_exec')) { + return $user . ':' . $user; + } + + $group = trim(shell_exec('groups '. $user .' | cut -f3 -d" "')); + + if (empty($group)) { + $group = 'www-data'; + } + return $user . ':' . $group; + } + + private static function getUser() + { + if (!function_exists('shell_exec')) { + return 'www-data'; + } + return trim(shell_exec('whoami')); + } + /** * Returns the help text displayed to suggest which command to run to give writable access to a file or directory * diff --git a/www/analytics/core/Filesystem.php b/www/analytics/core/Filesystem.php index fc7c8e78..c22a4d0b 100644 --- a/www/analytics/core/Filesystem.php +++ b/www/analytics/core/Filesystem.php @@ -1,6 +1,6 @@ removeMergedAssets($pluginName); View::clearCompiledTemplates(); - Cache::deleteTrackerCache(); + TrackerCache::deleteTrackerCache(); + PiwikCache::flushAll(); + self::clearPhpCaches(); } /** @@ -38,25 +42,6 @@ class Filesystem return realpath(dirname(__FILE__) . "/.."); } - /** - * Create .htaccess file in specified directory - * - * Apache-specific; for IIS @see web.config - * - * @param string $path without trailing slash - * @param bool $overwrite whether to overwrite an existing file or not - * @param string $content - */ - public static function createHtAccess($path, $overwrite = true, $content = "\n\nDeny from all\n\n\n\nDeny from all\n\n\n\nDeny from all\n\n\n") - { - if (SettingsServer::isApache()) { - $file = $path . '/.htaccess'; - if ($overwrite || !file_exists($file)) { - @file_put_contents($file, $content); - } - } - } - /** * Returns true if the string is a valid filename * File names that start with a-Z or 0-9 and contain a-Z, 0-9, underscore(_), dash(-), and dot(.) will be accepted. @@ -88,19 +73,17 @@ class Filesystem /** * Attempts to create a new directory. All errors are silenced. - * + * * _Note: This function does **not** create directories recursively._ * * @param string $path The path of the directory to create. - * @param bool $denyAccess Whether to deny browser access to this new folder by - * creating an **.htaccess** file. * @api */ - public static function mkdir($path, $denyAccess = true) + public static function mkdir($path) { if (!is_dir($path)) { // the mode in mkdir is modified by the current umask - @mkdir($path, $mode = 0755, $recursive = true); + @mkdir($path, self::getChmodForPath($path), $recursive = true); } // try to overcome restrictive umask (mis-)configuration @@ -111,10 +94,6 @@ class Filesystem // enough! we're not going to make the directory world-writeable } } - - if ($denyAccess) { - self::createHtAccess($path, $overwrite = false); - } } /** @@ -139,8 +118,9 @@ class Filesystem // and the return code 1. if NFS, it will return 0 and at least 2 lines of text. $command = "df -T -t nfs \"$sessionsPath\" 2>&1"; - if (function_exists('exec')) // use exec - { + if (function_exists('exec')) { + // use exec + $output = $returnCode = null; @exec($command, $output, $returnCode); @@ -150,13 +130,16 @@ class Filesystem ) { return true; } - } else if (function_exists('shell_exec')) // use shell_exec - { + } elseif (function_exists('shell_exec')) { + // use shell_exec + $output = @shell_exec($command); if ($output) { - $output = explode("\n", $output); - if (count($output) > 1) // check if filesystem is NFS - { + $commandFailed = (false !== strpos($output, "no file systems processed")); + $output = explode("\n", trim($output)); + if (!$commandFailed + && count($output) > 1) { + // check if filesystem is NFS return true; } } @@ -167,7 +150,7 @@ class Filesystem /** * Recursively find pathnames that match a pattern. - * + * * See {@link http://php.net/manual/en/function.glob.php glob} for more info. * * @param string $sDir directory The directory to glob in. @@ -228,6 +211,77 @@ class Filesystem return; } + /** + * Removes all files and directories that are present in the target directory but are not in the source directory. + * + * @param string $source Path to the source directory + * @param string $target Path to the target + */ + public static function unlinkTargetFilesNotPresentInSource($source, $target) + { + $diff = self::directoryDiff($source, $target); + $diff = self::sortFilesDescByPathLength($diff); + + foreach ($diff as $file) { + $remove = $target . $file; + + if (is_dir($remove)) { + @rmdir($remove); + } else { + self::deleteFileIfExists($remove); + } + } + } + + /** + * Sort all given paths/filenames by its path length. Long path names will be listed first. This method can be + * useful if you have for instance a bunch of files/directories to delete. By sorting them by lengh you can make + * sure to delete all files within the folders before deleting the actual folder. + * + * @param string[] $files + * @return string[] + */ + public static function sortFilesDescByPathLength($files) + { + usort($files, function ($a, $b) { + // sort by filename length so we kinda make sure to remove files before its directories + if ($a == $b) { + return 0; + } + + return (strlen($a) > strlen($b) ? -1 : 1); + }); + + return $files; + } + + /** + * Computes the difference of directories. Compares $target against $source and returns a relative path to all files + * and directories in $target that are not present in $source. + * + * @param $source + * @param $target + * + * @return string[] + */ + public static function directoryDiff($source, $target) + { + $sourceFiles = self::globr($source, '*'); + $targetFiles = self::globr($target, '*'); + + $sourceFiles = array_map(function ($file) use ($source) { + return str_replace($source, '', $file); + }, $sourceFiles); + + $targetFiles = array_map(function ($file) use ($target) { + return str_replace($target, '', $file); + }, $targetFiles); + + $diff = array_diff($targetFiles, $sourceFiles); + + return array_values($diff); + } + /** * Copies a file from `$source` to `$dest`. * @@ -241,29 +295,42 @@ class Filesystem */ public static function copy($source, $dest, $excludePhp = false) { - static $phpExtensions = array('php', 'tpl', 'twig'); - if ($excludePhp) { - $path_parts = pathinfo($source); - if (in_array($path_parts['extension'], $phpExtensions)) { + if (self::hasPHPExtension($source)) { return true; } } - if (!@copy($source, $dest)) { - @chmod($dest, 0755); - if (!@copy($source, $dest)) { - $message = "Error while creating/copying file to $dest.
" - . Filechecks::getErrorMessageMissingPermissions(self::getPathToPiwikRoot()); - throw new Exception($message); - } + $success = self::tryToCopyFileAndVerifyItWasCopied($source, $dest); + + if (!$success) { + $success = self::tryToCopyFileAndVerifyItWasCopied($source, $dest); } + + if (!$success) { + throw new Exception("Error while creating/copying file from $source to $dest. Content of copied file is different."); + } + return true; } + private static function hasPHPExtension($file) + { + static $phpExtensions = array('php', 'tpl', 'twig'); + + $path_parts = pathinfo($file); + + if (!empty($path_parts['extension']) + && in_array($path_parts['extension'], $phpExtensions)) { + return true; + } + + return false; + } + /** * Copies the contents of a directory recursively from `$source` to `$target`. - * + * * @param string $source A directory or file to copy, eg. './tmp/latest'. * @param string $target A directory to copy to, eg. '.'. * @param bool $excludePhp Whether to avoid copying files if the file is related to PHP @@ -274,7 +341,7 @@ class Filesystem public static function copyRecursive($source, $target, $excludePhp = false) { if (is_dir($source)) { - self::mkdir($target, false); + self::mkdir($target); $d = dir($source); while (false !== ($entry = $d->read())) { if ($entry == '.' || $entry == '..') { @@ -311,4 +378,130 @@ class Filesystem return @unlink($pathToFile); } + + /** + * Get the size of a file in the specified unit. + * + * @param string $pathToFile + * @param string $unit eg 'B' for Byte, 'KB', 'MB', 'GB', 'TB'. + * + * @return float|null Returns null if file does not exist or the size of the file in the specified unit + * + * @throws Exception In case the unit is invalid + */ + public static function getFileSize($pathToFile, $unit = 'B') + { + $unit = strtoupper($unit); + $units = array('TB' => pow(1024, 4), + 'GB' => pow(1024, 3), + 'MB' => pow(1024, 2), + 'KB' => 1024, + 'B' => 1); + + if (!array_key_exists($unit, $units)) { + throw new Exception('Invalid unit given'); + } + + if (!file_exists($pathToFile)) { + return; + } + + $filesize = filesize($pathToFile); + $factor = $units[$unit]; + $converted = $filesize / $factor; + + return $converted; + } + + /** + * Remove a file. + * + * @param string $file + * @param bool $silenceErrors If true, no exception will be thrown in case removing fails. + */ + public static function remove($file, $silenceErrors = false) + { + if (!file_exists($file)) { + return; + } + + $result = @unlink($file); + + // Testing if the file still exist avoids race conditions + if (!$result && file_exists($file)) { + if ($silenceErrors) { + Log::warning('Failed to delete file ' . $file); + } else { + throw new \RuntimeException('Unable to delete file ' . $file); + } + } + } + + /** + * @param $path + * @return int + */ + private static function getChmodForPath($path) + { + $pathIsTmp = StaticContainer::get('path.tmp'); + if (strpos($path, $pathIsTmp) === 0) { + // tmp/* folder + return 0750; + } + // plugins/* and all others + return 0755; + } + + public static function clearPhpCaches() + { + if (function_exists('apc_clear_cache')) { + apc_clear_cache(); // clear the system (aka 'opcode') cache + } + + if (function_exists('opcache_reset')) { + @opcache_reset(); // reset the opcode cache (php 5.5.0+) + } + + if (function_exists('wincache_refresh_if_changed')) { + @wincache_refresh_if_changed(); // reset the wincache + } + + if (function_exists('xcache_clear_cache') && defined('XC_TYPE_VAR')) { + if (ini_get('xcache.admin.enable_auth')) { + // XCache will not be cleared because "xcache.admin.enable_auth" is enabled in php.ini. + } else { + @xcache_clear_cache(XC_TYPE_VAR); + } + } + } + + private static function havePhpFilesSameContent($file1, $file2) + { + if (self::hasPHPExtension($file1)) { + $sourceMd5 = md5_file($file1); + $destMd5 = md5_file($file2); + + return $sourceMd5 === $destMd5; + } + + return true; + } + + private static function tryToCopyFileAndVerifyItWasCopied($source, $dest) + { + if (!@copy($source, $dest)) { + @chmod($dest, 0755); + if (!@copy($source, $dest)) { + $message = "Error while creating/copying file to $dest.
" + . Filechecks::getErrorMessageMissingPermissions(self::getPathToPiwikRoot()); + throw new Exception($message); + } + } + + if (file_exists($source) && file_exists($dest)) { + return self::havePhpFilesSameContent($source, $dest); + } + + return true; + } } diff --git a/www/analytics/core/FrontController.php b/www/analytics/core/FrontController.php index 63e8a9d3..40f8f381 100644 --- a/www/analytics/core/FrontController.php +++ b/www/analytics/core/FrontController.php @@ -1,6 +1,6 @@ dispatch('UserCountryMap', 'realtimeMap'); * } - * + * * **Using other plugin controller actions** - * + * * public function myPopupWithRealtimeMap() * { * $_GET['changeVisitAlpha'] = false; * $_GET['removeOldVisits'] = false; * $_GET['showFooterMessage'] = false; * $realtimeMap = FrontController::getInstance()->fetchDispatch('UserCountryMap', 'realtimeMap'); - * + * * $view = new View('@MyPlugin/myPopupWithRealtimeMap.twig'); * $view->realtimeMap = $realtimeMap; * return $realtimeMap->render(); @@ -54,6 +57,7 @@ use Piwik\Session; class FrontController extends Singleton { const DEFAULT_MODULE = 'CoreHome'; + /** * Set to false and the Front Controller will not dispatch the request * @@ -61,11 +65,14 @@ class FrontController extends Singleton */ public static $enableDispatch = true; + /** + * @var bool + */ + private $initialized = false; + /** * Executes the requested plugin controller method. - * - * See also {@link fetchDispatch()}. - * + * * @throws Exception|\Piwik\PluginDeactivatedException in case the plugin doesn't exist, the action doesn't exist, * there is not enough permission, etc. * @@ -81,67 +88,40 @@ class FrontController extends Singleton return; } + $filter = new Router(); + $redirection = $filter->filterUrl(Url::getCurrentUrl()); + if ($redirection !== null) { + Url::redirectToUrl($redirection); + return; + } + try { $result = $this->doDispatch($module, $action, $parameters); return $result; } catch (NoAccessException $exception) { + Log::debug($exception); /** * Triggered when a user with insufficient access permissions tries to view some resource. - * + * * This event can be used to customize the error that occurs when a user is denied access * (for example, displaying an error message, redirecting to a page other than login, etc.). - * + * * @param \Piwik\NoAccessException $exception The exception that was caught. */ Piwik::postEvent('User.isNotAuthorized', array($exception), $pending = true); - } catch (Exception $e) { - $debugTrace = $e->getTraceAsString(); - $message = Common::sanitizeInputValue($e->getMessage()); - Piwik_ExitWithMessage($message, $debugTrace, true, true); } } - protected function makeController($module, $action) - { - $controllerClassName = $this->getClassNameController($module); - - // FrontController's autoloader - if (!class_exists($controllerClassName, false)) { - $moduleController = PIWIK_INCLUDE_PATH . '/plugins/' . $module . '/Controller.php'; - if (!is_readable($moduleController)) { - throw new Exception("Module controller $moduleController not found!"); - } - require_once $moduleController; // prefixed by PIWIK_INCLUDE_PATH - } - - $class = $this->getClassNameController($module); - /** @var $controller Controller */ - $controller = new $class; - if ($action === false) { - $action = $controller->getDefaultAction(); - } - - if (!is_callable(array($controller, $action))) { - throw new Exception("Action '$action' not found in the controller '$controllerClassName'."); - } - return array($controller, $action); - } - - protected function getClassNameController($module) - { - return "\\Piwik\\Plugins\\$module\\Controller"; - } - /** * Executes the requested plugin controller method and returns the data, capturing anything the * method `echo`s. - * + * * _Note: If the plugin controller returns something, the return value is returned instead * of whatever is in the output buffer._ - * + * * @param string $module The name of the plugin whose controller to execute, eg, `'UserCountryMap'`. - * @param string $action The controller action name, eg, `'realtimeMap'`. + * @param string $actionName The controller action name, eg, `'realtimeMap'`. * @param array $parameters Array of parameters to pass to the controller action method. * @return string The `echo`'d data or the return value of the controller action. * @deprecated @@ -168,13 +148,14 @@ class FrontController extends Singleton { try { if (class_exists('Piwik\\Profiler') - && !SettingsServer::isTrackerApiRequest()) { + && !SettingsServer::isTrackerApiRequest() + ) { // in tracker mode Piwik\Tracker\Db\Pdo\Mysql does currently not implement profiling Profiler::displayDbProfileReport(); Profiler::printQueryCount(); - Log::debug(Registry::get('timer')); } } catch (Exception $e) { + Log::debug($e); } } @@ -184,21 +165,22 @@ class FrontController extends Singleton // If we are in no dispatch mode, eg. a script reusing Piwik libs, // then we should return the exception directly, rather than trigger the event "bad config file" // which load the HTML page of the installer with the error. - // This is at least required for misc/cron/archive.php and useful to all other scripts return (defined('PIWIK_ENABLE_DISPATCH') && !PIWIK_ENABLE_DISPATCH) || Common::isPhpCliMode() || SettingsServer::isArchivePhpTriggered(); } - static public function setUpSafeMode() + public static function setUpSafeMode() { - register_shutdown_function(array('\\Piwik\\FrontController','triggerSafeModeWhenError')); + register_shutdown_function(array('\\Piwik\\FrontController', 'triggerSafeModeWhenError')); } - static public function triggerSafeModeWhenError() + public static function triggerSafeModeWhenError() { $lastError = error_get_last(); if (!empty($lastError) && $lastError['type'] == E_ERROR) { + Common::sendResponseCode(500); + $controller = FrontController::getInstance(); $controller->init(); $message = $controller->dispatch('CorePluginsAdmin', 'safemode', array($lastError)); @@ -207,33 +189,6 @@ class FrontController extends Singleton } } - /** - * Loads the config file and assign to the global registry - * This is overridden in tests to ensure test config file is used - * - * @return Exception - */ - static public function createConfigObject() - { - $exceptionToThrow = false; - try { - Config::getInstance()->database; // access property to check if the local file exists - } catch (Exception $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); - $exceptionToThrow = $exception; - } - return $exceptionToThrow; - } - /** * Must be called before dispatch() * - checks that directories are writable, @@ -247,135 +202,146 @@ class FrontController extends Singleton */ public function init() { - static $initialized = false; - if ($initialized) { + if ($this->initialized) { return; } - $initialized = true; + $this->initialized = true; + + $tmpPath = StaticContainer::get('path.tmp'); + + $directoriesToCheck = array( + $tmpPath, + $tmpPath . '/assets/', + $tmpPath . '/cache/', + $tmpPath . '/logs/', + $tmpPath . '/tcpdf/', + $tmpPath . '/templates_c/', + ); + + Filechecks::dieIfDirectoriesNotWritable($directoriesToCheck); + + $this->handleMaintenanceMode(); + $this->handleProfiler(); + $this->handleSSLRedirection(); + + Plugin\Manager::getInstance()->loadPluginTranslations(); + Plugin\Manager::getInstance()->loadActivatedPlugins(); + + // try to connect to the database try { - Registry::set('timer', new Timer); - - $directoriesToCheck = array( - '/tmp/', - '/tmp/assets/', - '/tmp/cache/', - '/tmp/logs/', - '/tmp/tcpdf/', - '/tmp/templates_c/', - ); - - Filechecks::dieIfDirectoriesNotWritable($directoriesToCheck); - - Translate::loadEnglishTranslation(); - - $exceptionToThrow = self::createConfigObject(); - - if (Session::isFileBasedSessions()) { - Session::start(); - } - - $this->handleMaintenanceMode(); - $this->handleProfiler(); - $this->handleSSLRedirection(); - - Plugin\Manager::getInstance()->loadActivatedPlugins(); - - if ($exceptionToThrow) { - throw $exceptionToThrow; - } - - try { - Db::createDatabaseObject(); - Option::get('TestingIfDatabaseConnectionWorked'); - - } catch (Exception $exception) { - if (self::shouldRethrowException()) { - throw $exception; - } - - /** - * Triggered if the INI config file has the incorrect format or if certain required configuration - * options are absent. - * - * This event can be used to start the installation process or to display a custom error message. - * - * @param Exception $exception The exception thrown from creating and testing the database - * connection. - */ - Piwik::postEvent('Config.badConfigurationFile', array($exception), $pending = true); + Db::createDatabaseObject(); + Db::fetchAll("SELECT DATABASE()"); + } catch (Exception $exception) { + if (self::shouldRethrowException()) { throw $exception; } - // Init the Access object, so that eg. core/Updates/* can enforce Super User and use some APIs - Access::getInstance(); + Log::debug($exception); /** - * Triggered just after the platform is initialized and plugins are loaded. - * - * This event can be used to do early initialization. - * - * _Note: At this point the user is not authenticated yet._ + * Triggered when Piwik cannot connect to the database. + * + * This event can be used to start the installation process or to display a custom error + * message. + * + * @param Exception $exception The exception thrown from creating and testing the database + * connection. */ - Piwik::postEvent('Request.dispatchCoreAndPluginUpdatesScreen'); + Piwik::postEvent('Db.cannotConnectToDb', array($exception), $pending = true); - \Piwik\Plugin\Manager::getInstance()->installLoadedPlugins(); - - // ensure the current Piwik URL is known for later use - if (method_exists('Piwik\SettingsPiwik', 'getPiwikUrl')) { - $host = SettingsPiwik::getPiwikUrl(); - } - - /** - * Triggered before the user is authenticated, when the global authentication object - * should be created. - * - * Plugins that provide their own authentication implementation should use this event - * to set the global authentication object (which must derive from {@link Piwik\Auth}). - * - * **Example** - * - * Piwik::addAction('Request.initAuthenticationObject', function() { - * Piwik\Registry::set('auth', new MyAuthImplementation()); - * }); - */ - Piwik::postEvent('Request.initAuthenticationObject'); - try { - $authAdapter = Registry::get('auth'); - } catch (Exception $e) { - throw new Exception("Authentication object cannot be found in the Registry. Maybe the Login plugin is not activated? -
You can activate the plugin by adding:
- Plugins[] = Login
- under the [Plugins] section in your config/config.ini.php"); - } - Access::getInstance()->reloadAccess($authAdapter); - - // Force the auth to use the token_auth if specified, so that embed dashboard - // and all other non widgetized controller methods works fine - if (($token_auth = Common::getRequestVar('token_auth', false, 'string')) !== false) { - Request::reloadAuthUsingTokenAuth(); - } - SettingsServer::raiseMemoryLimitIfNecessary(); - - Translate::reloadLanguage(); - \Piwik\Plugin\Manager::getInstance()->postLoadPlugins(); - - /** - * Triggered after the platform is initialized and after the user has been authenticated, but - * before the platform has handled the request. - * - * Piwik uses this event to check for updates to Piwik. - */ - Piwik::postEvent('Platform.initialized'); - } catch (Exception $e) { - - if (self::shouldRethrowException()) { - throw $e; - } - - $debugTrace = $e->getTraceAsString(); - Piwik_ExitWithMessage($e->getMessage(), $debugTrace, true); + throw $exception; } + + // try to get an option (to check if data can be queried) + try { + Option::get('TestingIfDatabaseConnectionWorked'); + } catch (Exception $exception) { + if (self::shouldRethrowException()) { + throw $exception; + } + + Log::debug($exception); + + /** + * Triggered when Piwik cannot access database data. + * + * This event can be used to start the installation process or to display a custom error + * message. + * + * @param Exception $exception The exception thrown from trying to get an option value. + */ + Piwik::postEvent('Config.badConfigurationFile', array($exception), $pending = true); + + throw $exception; + } + + // Init the Access object, so that eg. core/Updates/* can enforce Super User and use some APIs + Access::getInstance(); + + /** + * Triggered just after the platform is initialized and plugins are loaded. + * + * This event can be used to do early initialization. + * + * _Note: At this point the user is not authenticated yet._ + */ + Piwik::postEvent('Request.dispatchCoreAndPluginUpdatesScreen'); + + $this->throwIfPiwikVersionIsOlderThanDBSchema(); + + \Piwik\Plugin\Manager::getInstance()->installLoadedPlugins(); + + // ensure the current Piwik URL is known for later use + if (method_exists('Piwik\SettingsPiwik', 'getPiwikUrl')) { + SettingsPiwik::getPiwikUrl(); + } + + /** + * Triggered before the user is authenticated, when the global authentication object + * should be created. + * + * Plugins that provide their own authentication implementation should use this event + * to set the global authentication object (which must derive from {@link Piwik\Auth}). + * + * **Example** + * + * Piwik::addAction('Request.initAuthenticationObject', function() { + * StaticContainer::getContainer()->set('Piwik\Auth', new MyAuthImplementation()); + * }); + */ + Piwik::postEvent('Request.initAuthenticationObject'); + try { + $authAdapter = StaticContainer::get('Piwik\Auth'); + } catch (Exception $e) { + $message = "Authentication object cannot be found in the container. Maybe the Login plugin is not activated? +
You can activate the plugin by adding:
+ Plugins[] = Login
+ under the [Plugins] section in your config/config.ini.php"; + + $ex = new AuthenticationFailedException($message); + $ex->setIsHtmlMessage(); + + throw $ex; + } + Access::getInstance()->reloadAccess($authAdapter); + + // Force the auth to use the token_auth if specified, so that embed dashboard + // and all other non widgetized controller methods works fine + if (Common::getRequestVar('token_auth', false, 'string') !== false) { + Request::reloadAuthUsingTokenAuth(); + } + SettingsServer::raiseMemoryLimitIfNecessary(); + + \Piwik\Plugin\Manager::getInstance()->postLoadPlugins(); + + /** + * Triggered after the platform is initialized and after the user has been authenticated, but + * before the platform has handled the request. + * + * Piwik uses this event to check for updates to Piwik. + */ + Piwik::postEvent('Platform.initialized'); } protected function prepareDispatch($module, $action, $parameters) @@ -388,10 +354,12 @@ class FrontController extends Singleton $action = Common::getRequestVar('action', false); } - if (!Session::isFileBasedSessions() + if (SettingsPiwik::isPiwikInstalled() && ($module !== 'API' || ($action && $action !== 'index')) ) { Session::start(); + + $this->closeSessionEarlyForFasterUI(); } if (is_null($parameters)) { @@ -402,7 +370,7 @@ class FrontController extends Singleton throw new Exception("Invalid module name '$module'"); } - $module = Request::renameModule($module); + list($module, $action) = Request::getRenamedModuleAndAction($module, $action); if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated($module)) { throw new PluginDeactivatedException($module); @@ -413,54 +381,73 @@ class FrontController extends Singleton protected function handleMaintenanceMode() { - if (Config::getInstance()->General['maintenance_mode'] == 1 - && !Common::isPhpCliMode() - ) { - $format = Common::getRequestVar('format', ''); - - $message = "Piwik is in scheduled maintenance. Please come back later." - . " The administrator can disable maintenance by editing the file piwik/config/config.ini.php and removing the following: " - . " maintenance_mode=1 "; - if (Config::getInstance()->Tracker['record_statistics'] == 0) { - $message .= ' and record_statistics=0'; - } - - $exception = new Exception($message); - // extend explain how to re-enable - // show error message when record stats = 0 - if (empty($format)) { - throw $exception; - } - $response = new ResponseBuilder($format); - echo $response->getResponseException($exception); - exit; + if ((Config::getInstance()->General['maintenance_mode'] != 1) || Common::isPhpCliMode()) { + return; } + Common::sendResponseCode(503); + + $logoUrl = null; + $faviconUrl = null; + try { + $logo = new CustomLogo(); + $logoUrl = $logo->getHeaderLogoUrl(); + $faviconUrl = $logo->getPathUserFavicon(); + } catch (Exception $ex) { + } + $logoUrl = $logoUrl ?: 'plugins/Morpheus/images/logo-header.png'; + $faviconUrl = $faviconUrl ?: 'plugins/CoreHome/images/favicon.png'; + + $page = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Morpheus/templates/maintenance.tpl'); + $page = str_replace('%logoUrl%', $logoUrl, $page); + $page = str_replace('%faviconUrl%', $faviconUrl, $page); + $page = str_replace('%piwikTitle%', Piwik::getRandomTitle(), $page); + echo $page; + exit; } protected function handleSSLRedirection() { // Specifically disable for the opt out iframe - if(Piwik::getModule() == 'CoreAdminHome' && Piwik::getAction() == 'optOut') { + if (Piwik::getModule() == 'CoreAdminHome' && Piwik::getAction() == 'optOut') { return; } - if(Common::isPhpCliMode()) { + // Disable Https for VisitorGenerator + if (Piwik::getModule() == 'VisitorGenerator') { return; } - // Only enable this feature after Piwik is already installed - if(!SettingsPiwik::isPiwikInstalled()) { + if (Common::isPhpCliMode()) { return; } // proceed only when force_ssl = 1 - if(!SettingsPiwik::isHttpsForced()) { + if (!SettingsPiwik::isHttpsForced()) { return; } Url::redirectToHttps(); } + private function closeSessionEarlyForFasterUI() + { + $isDashboardReferrer = !empty($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], 'module=CoreHome&action=index') !== false; + $isAllWebsitesReferrer = !empty($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], 'module=MultiSites&action=index') !== false; + + if ($isDashboardReferrer + && !empty($_POST['token_auth']) + && Common::getRequestVar('widget', 0, 'int') === 1 + ) { + Session::close(); + } + + if (($isDashboardReferrer || $isAllWebsitesReferrer) + && Common::getRequestVar('viewDataTable', '', 'string') === 'sparkline' + ) { + Session::close(); + } + } + private function handleProfiler() { if (!empty($_GET['xhprof'])) { - $mainRun = $_GET['xhprof'] == 1; // archive.php sets xhprof=2 + $mainRun = $_GET['xhprof'] == 1; // core:archive command sets xhprof=2 Profiler::setupProfilerXHProf($mainRun); } } @@ -487,7 +474,10 @@ class FrontController extends Singleton */ Piwik::postEvent('Request.dispatch', array(&$module, &$action, &$parameters)); - list($controller, $action) = $this->makeController($module, $action); + /** @var ControllerResolver $controllerResolver */ + $controllerResolver = StaticContainer::get('Piwik\Http\ControllerResolver'); + + $controller = $controllerResolver->getController($module, $action, $parameters); /** * Triggered directly before controller actions are dispatched. @@ -502,7 +492,7 @@ class FrontController extends Singleton */ Piwik::postEvent(sprintf('Controller.%s.%s', $module, $action), array(&$parameters)); - $result = call_user_func_array(array($controller, $action), $parameters); + $result = call_user_func_array($controller, $parameters); /** * Triggered after a controller action is successfully called. @@ -527,19 +517,33 @@ class FrontController extends Singleton * @param mixed &$result The controller action result. * @param array $parameters The arguments passed to the controller action. */ - Piwik::postEvent('Request.dispatch.end', array(&$result, $parameters)); + Piwik::postEvent('Request.dispatch.end', array(&$result, $module, $action, $parameters)); + return $result; } -} - -/** - * Exception thrown when the requested plugin is not activated in the config file - */ -class PluginDeactivatedException extends Exception -{ - public function __construct($module) + /** + * This method ensures that Piwik Platform cannot be running when using a NEWER database. + */ + private function throwIfPiwikVersionIsOlderThanDBSchema() { - parent::__construct("The plugin $module is not enabled. You can activate the plugin on Settings > Plugins page in Piwik."); + // When developing this situation happens often when switching branches + if (Development::isEnabled()) { + return; + } + + $updater = new Updater(); + + $dbSchemaVersion = $updater->getCurrentComponentVersion('core'); + $current = Version::VERSION; + if (-1 === version_compare($current, $dbSchemaVersion)) { + $messages = array( + Piwik::translate('General_ExceptionDatabaseVersionNewerThanCodebase', array($current, $dbSchemaVersion)), + Piwik::translate('General_ExceptionDatabaseVersionNewerThanCodebaseWait'), + // we cannot fill in the Super User emails as we are failing before Authentication was ready + Piwik::translate('General_ExceptionContactSupportGeneric', array('', '')) + ); + throw new DatabaseSchemaIsNewerThanCodebaseException(implode(" ", $messages)); + } } } diff --git a/www/analytics/core/Http.php b/www/analytics/core/Http.php index d6e73769..7e7602c2 100644 --- a/www/analytics/core/Http.php +++ b/www/analytics/core/Http.php @@ -1,6 +1,6 @@ 5) { throw new Exception('Too many redirects (' . $followDepth . ')'); } @@ -136,6 +154,10 @@ class Http $contentLength = 0; $fileLength = 0; + if (!empty($requestBody) && is_array($requestBody)) { + $requestBody = http_build_query($requestBody); + } + // Piwik services behave like a proxy, so we should act like one. $xff = 'X-Forwarded-For: ' . (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] . ',' : '') @@ -156,16 +178,17 @@ class Http $rangeHeader = 'Range: bytes=' . $byteRange[0] . '-' . $byteRange[1] . "\r\n"; } - // proxy configuration - $proxyHost = Config::getInstance()->proxy['host']; - $proxyPort = Config::getInstance()->proxy['port']; - $proxyUser = Config::getInstance()->proxy['username']; - $proxyPassword = Config::getInstance()->proxy['password']; + list($proxyHost, $proxyPort, $proxyUser, $proxyPassword) = self::getProxyConfiguration($aUrl); + + + $aUrl = trim($aUrl); // other result data - $status = null; + $status = null; $headers = array(); + $httpAuthIsUsed = !empty($httpUsername) || !empty($httpPassword); + if ($method == 'socket') { if (!self::isSocketEnabled()) { // can be triggered in tests @@ -177,11 +200,11 @@ class Http throw new Exception('Malformed URL: ' . $aUrl); } - if ($url['scheme'] != 'http') { + if ($url['scheme'] != 'http' && $url['scheme'] != 'https') { throw new Exception('Invalid protocol/scheme: ' . $url['scheme']); } $host = $url['host']; - $port = isset($url['port)']) ? $url['port'] : 80; + $port = isset($url['port']) ? $url['port'] : 80; $path = isset($url['path']) ? $url['path'] : '/'; if (isset($url['query'])) { $path .= '?' . $url['query']; @@ -219,19 +242,32 @@ class Http throw new Exception("Error while connecting to: $host. Please try again later. $errstr"); } + $httpAuth = ''; + if ($httpAuthIsUsed) { + $httpAuth = 'Authorization: Basic ' . base64_encode($httpUsername.':'.$httpPassword) . "\r\n"; + } + // send HTTP request header $requestHeader .= "Host: $host" . ($port != 80 ? ':' . $port : '') . "\r\n" + . ($httpAuth ? $httpAuth : '') . ($proxyAuth ? $proxyAuth : '') . 'User-Agent: ' . $userAgent . "\r\n" . ($acceptLanguage ? $acceptLanguage . "\r\n" : '') . $xff . "\r\n" . $via . "\r\n" . $rangeHeader - . "Connection: close\r\n" - . "\r\n"; + . "Connection: close\r\n"; fwrite($fsock, $requestHeader); + if (strtolower($httpMethod) === 'post' && !empty($requestBody)) { + fwrite($fsock, self::buildHeadersForPost($requestBody)); + fwrite($fsock, "\r\n"); + fwrite($fsock, $requestBody); + } else { + fwrite($fsock, "\r\n"); + } + $streamMetaData = array('timed_out' => false); @stream_set_blocking($fsock, true); @@ -313,7 +349,9 @@ class Http $acceptInvalidSslCertificate = false, $byteRange, $getExtendedInfo, - $httpMethod + $httpMethod, + $httpUsername, + $httpPassword ); } @@ -359,7 +397,7 @@ class Http // determine success or failure @fclose(@$fsock); - } else if ($method == 'fopen') { + } elseif ($method == 'fopen') { $response = false; // we make sure the request takes less than a few seconds to fail @@ -368,11 +406,17 @@ class Http $default_socket_timeout = @ini_get('default_socket_timeout'); @ini_set('default_socket_timeout', $timeout); + $httpAuth = ''; + if ($httpAuthIsUsed) { + $httpAuth = 'Authorization: Basic ' . base64_encode($httpUsername.':'.$httpPassword) . "\r\n"; + } + $ctx = null; if (function_exists('stream_context_create')) { $stream_options = array( 'http' => array( 'header' => 'User-Agent: ' . $userAgent . "\r\n" + . ($httpAuth ? $httpAuth : '') . ($acceptLanguage ? $acceptLanguage . "\r\n" : '') . $xff . "\r\n" . $via . "\r\n" @@ -390,12 +434,22 @@ class Http } } + if (strtolower($httpMethod) === 'post' && !empty($requestBody)) { + $postHeader = self::buildHeadersForPost($requestBody); + $postHeader .= "\r\n"; + $stream_options['http']['method'] = 'POST'; + $stream_options['http']['header'] .= $postHeader; + $stream_options['http']['content'] = $requestBody; + } + $ctx = stream_context_create($stream_options); } // save to file if (is_resource($file)) { - $handle = fopen($aUrl, 'rb', false, $ctx); + if (!($handle = fopen($aUrl, 'rb', false, $ctx))) { + throw new Exception("Unable to open $aUrl"); + } while (!feof($handle)) { $response = fread($handle, 8192); $fileLength += strlen($response); @@ -403,7 +457,17 @@ class Http } fclose($handle); } else { - $response = file_get_contents($aUrl, 0, $ctx); + $response = @file_get_contents($aUrl, 0, $ctx); + + // try to get http status code from response headers + if (isset($http_response_header) && preg_match('~^HTTP/(\d\.\d)\s+(\d+)(\s*.*)?~', implode("\n", $http_response_header), $m)) { + $status = (int)$m[2]; + } + + if (!$status && $response === false) { + $error = error_get_last(); + throw new \Exception($error['message']); + } $fileLength = strlen($response); } @@ -411,7 +475,7 @@ class Http if (!empty($default_socket_timeout)) { @ini_set('default_socket_timeout', $default_socket_timeout); } - } else if ($method == 'curl') { + } elseif ($method == 'curl') { if (!self::isCurlEnabled()) { // can be triggered in tests throw new Exception("CURL is not enabled in php.ini, but is being used."); @@ -442,8 +506,9 @@ class Http // only get header info if not saving directly to file CURLOPT_HEADER => is_resource($file) ? false : true, CURLOPT_CONNECTTIMEOUT => $timeout, + CURLOPT_TIMEOUT => $timeout, ); - // Case archive.php is triggering archiving on https:// and the certificate is not valid + // Case core:archive command is triggering archiving on https:// and the certificate is not valid if ($acceptInvalidSslCertificate) { $curl_options += array( CURLOPT_SSL_VERIFYHOST => false, @@ -455,6 +520,17 @@ class Http @curl_setopt($ch, CURLOPT_NOBODY, true); } + if (strtolower($httpMethod) === 'post' && !empty($requestBody)) { + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $requestBody); + } + + if (!empty($httpUsername) && !empty($httpPassword)) { + $curl_options += array( + CURLOPT_USERPWD => $httpUsername . ':' . $httpPassword, + ); + } + @curl_setopt_array($ch, $curl_options); self::configCurlCertificate($ch); @@ -485,10 +561,11 @@ class Http if ($response === true) { $response = ''; - } else if ($response === false) { + } elseif ($response === false) { $errstr = curl_error($ch); if ($errstr != '') { - throw new Exception('curl_exec: ' . $errstr); + throw new Exception('curl_exec: ' . $errstr + . '. Hostname requested was: ' . UrlHelper::getHostFromUrl($aUrl)); } $response = ''; } else { @@ -496,7 +573,14 @@ class Http // redirects are included in the output html, so we look for the last line that starts w/ HTTP/... // to split the response while (substr($response, 0, 5) == "HTTP/") { - list($header, $response) = explode("\r\n\r\n", $response, 2); + $split = explode("\r\n\r\n", $response, 2); + + if(count($split) == 2) { + list($header, $response) = $split; + } else { + $response = ''; + $header = $split; + } } foreach (explode("\r\n", $header) as $line) { @@ -538,22 +622,30 @@ class Http } } + private static function buildHeadersForPost($requestBody) + { + $postHeader = "Content-Type: application/x-www-form-urlencoded\r\n"; + $postHeader .= "Content-Length: " . strlen($requestBody) . "\r\n"; + + return $postHeader; + } + /** * Downloads the next chunk of a specific file. The next chunk's byte range * is determined by the existing file's size and the expected file size, which * is stored in the piwik_option table before starting a download. The expected * file size is obtained through a `HEAD` HTTP request. - * + * * _Note: this function uses the **Range** HTTP header to accomplish downloading in * parts. Not every server supports this header._ - * + * * The proper use of this function is to call it once per request. The browser * should continue to send requests to Piwik which will in turn call this method * until the file has completely downloaded. In this way, the user can be informed * of a download's progress. - * + * * **Example Usage** - * + * * ``` * // browser JavaScript * var downloadFile = function (isStart) { @@ -571,10 +663,10 @@ class Http * }); * ajax.send(); * } - * + * * downloadFile(true); * ``` - * + * * ``` * // PHP controller action * public function myAction() @@ -584,7 +676,7 @@ class Http * Http::downloadChunk("http://bigfiles.com/averybigfile.zip", $outputPath, $isStart == 1); * } * ``` - * + * * @param string $url The url to download from. * @param string $outputPath The path to the file to save/append to. * @param bool $isContinuation `true` if this is the continuation of a download, @@ -751,4 +843,47 @@ class Http } return $str; } + + /** + * Returns the If-Modified-Since HTTP header if it can be found. If it cannot be + * found, an empty string is returned. + * + * @return string + */ + public static function getModifiedSinceHeader() + { + $modifiedSince = ''; + if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { + $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE']; + + // strip any trailing data appended to header + if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) { + $modifiedSince = substr($modifiedSince, 0, $semicolonPos); + } + } + return $modifiedSince; + } + + /** + * Returns Proxy to use for connecting via HTTP to given URL + * + * @param string $url + * @return array + */ + private static function getProxyConfiguration($url) + { + $hostname = UrlHelper::getHostFromUrl($url); + + if (Url::isLocalHost($hostname)) { + return array(null, null, null, null); + } + + // proxy configuration + $proxyHost = Config::getInstance()->proxy['host']; + $proxyPort = Config::getInstance()->proxy['port']; + $proxyUser = Config::getInstance()->proxy['username']; + $proxyPassword = Config::getInstance()->proxy['password']; + + return array($proxyHost, $proxyPort, $proxyUser, $proxyPassword); + } } diff --git a/www/analytics/core/Http/ControllerResolver.php b/www/analytics/core/Http/ControllerResolver.php new file mode 100644 index 00000000..569fbee4 --- /dev/null +++ b/www/analytics/core/Http/ControllerResolver.php @@ -0,0 +1,141 @@ +abstractFactory = $abstractFactory; + } + + /** + * @param string $module + * @param string|null $action + * @param array $parameters + * @throws Exception Controller not found. + * @return callable The controller is a PHP callable. + */ + public function getController($module, $action, array &$parameters) + { + $controller = $this->createPluginController($module, $action); + if ($controller) { + return $controller; + } + + $controller = $this->createWidgetController($module, $action, $parameters); + if ($controller) { + return $controller; + } + + $controller = $this->createReportController($module, $action, $parameters); + if ($controller) { + return $controller; + } + + $controller = $this->createReportMenuController($module, $action, $parameters); + if ($controller) { + return $controller; + } + + throw new Exception(sprintf("Action '%s' not found in the module '%s'", $action, $module)); + } + + private function createPluginController($module, $action) + { + $controllerClass = "Piwik\\Plugins\\$module\\Controller"; + if (!class_exists($controllerClass)) { + return null; + } + + /** @var $controller Controller */ + $controller = $this->abstractFactory->make($controllerClass); + + $action = $action ?: $controller->getDefaultAction(); + + if (!is_callable(array($controller, $action))) { + return null; + } + + return array($controller, $action); + } + + private function createWidgetController($module, $action, array &$parameters) + { + $widget = Widgets::factory($module, $action); + + if (!$widget) { + return null; + } + + $parameters['widget'] = $widget; + $parameters['method'] = $action; + + return array($this->createCoreHomeController(), 'renderWidget'); + } + + private function createReportController($module, $action, array &$parameters) + { + $report = Report::factory($module, $action); + + if (!$report) { + return null; + } + + $parameters['report'] = $report; + + return array($this->createCoreHomeController(), 'renderReportWidget'); + } + + private function createReportMenuController($module, $action, array &$parameters) + { + if (!$this->isReportMenuAction($action)) { + return null; + } + + $action = lcfirst(substr($action, 4)); // menuGetPageUrls => getPageUrls + $report = Report::factory($module, $action); + + if (!$report) { + return null; + } + + $parameters['report'] = $report; + + return array($this->createCoreHomeController(), 'renderReportMenu'); + } + + private function isReportMenuAction($action) + { + $startsWithMenu = (Report::PREFIX_ACTION_IN_MENU === substr($action, 0, strlen(Report::PREFIX_ACTION_IN_MENU))); + + return !empty($action) && $startsWithMenu; + } + + private function createCoreHomeController() + { + return $this->abstractFactory->make('Piwik\Plugins\CoreHome\Controller'); + } +} diff --git a/www/analytics/core/Http/Router.php b/www/analytics/core/Http/Router.php new file mode 100644 index 00000000..62547c7c --- /dev/null +++ b/www/analytics/core/Http/Router.php @@ -0,0 +1,39 @@ + $posDot) { - $ipString = substr($ipString, 0, $posColon); - } - // else: Dotted quad IPv6 address, A:B:C:D:E:F:G.H.I.J - } else if (strpos($ipString, ':') === $posColon) { - $ipString = substr($ipString, 0, $posColon); - } - // else: IPv6 address, A:B:C:D:E:F:G:H - } - // else: IPv4 address, A.B.C.D - - return $ipString; - } - - /** - * Sanitize human-readable (user-supplied) IP address range. - * - * Accepts the following formats for $ipRange: - * - single IPv4 address, e.g., 127.0.0.1 - * - single IPv6 address, e.g., ::1/128 - * - IPv4 block using CIDR notation, e.g., 192.168.0.0/22 represents the IPv4 addresses from 192.168.0.0 to 192.168.3.255 - * - IPv6 block using CIDR notation, e.g., 2001:DB8::/48 represents the IPv6 addresses from 2001:DB8:0:0:0:0:0:0 to 2001:DB8:0:FFFF:FFFF:FFFF:FFFF:FFFF - * - wildcards, e.g., 192.168.0.* - * - * @param string $ipRangeString IP address range - * @return string|bool IP address range in CIDR notation OR false - */ - public static function sanitizeIpRange($ipRangeString) - { - $ipRangeString = trim($ipRangeString); - if (empty($ipRangeString)) { - return false; - } - - // IPv4 address with wildcards '*' - if (strpos($ipRangeString, '*') !== false) { - if (preg_match('~(^|\.)\*\.\d+(\.|$)~D', $ipRangeString)) { - return false; - } - - $bits = 32 - 8 * substr_count($ipRangeString, '*'); - $ipRangeString = str_replace('*', '0', $ipRangeString); - } - - // CIDR - if (($pos = strpos($ipRangeString, '/')) !== false) { - $bits = substr($ipRangeString, $pos + 1); - $ipRangeString = substr($ipRangeString, 0, $pos); - } - - // single IP - if (($ip = @inet_pton($ipRangeString)) === false) - return false; - - $maxbits = strlen($ip) * 8; - if (!isset($bits)) - $bits = $maxbits; - - if ($bits < 0 || $bits > $maxbits) { - return false; - } - - return "$ipRangeString/$bits"; - } - - /** - * Converts an IP address in presentation format to network address format. - * - * @param string $ipString IP address, either IPv4 or IPv6, e.g., `"127.0.0.1"`. - * @return string Binary-safe string, e.g., `"\x7F\x00\x00\x01"`. - */ - public static function P2N($ipString) - { - // use @inet_pton() because it throws an exception and E_WARNING on invalid input - $ip = @inet_pton($ipString); - return $ip === false ? "\x00\x00\x00\x00" : $ip; - } - - /** - * Convert network address format to presentation format. - * - * See also {@link prettyPrint()}. - * - * @param string $ip IP address in network address format. - * @return string IP address in presentation format. - */ - public static function N2P($ip) - { - // use @inet_ntop() because it throws an exception and E_WARNING on invalid input - $ipStr = @inet_ntop($ip); - return $ipStr === false ? '0.0.0.0' : $ipStr; - } - - /** - * Alias for {@link N2P()}. - * - * @param string $ip IP address in network address format. - * @return string IP address in presentation format. - */ - public static function prettyPrint($ip) - { - return self::N2P($ip); - } - - /** - * Returns true if `$ip` is an IPv4, IPv4-compat, or IPv4-mapped address, false - * if otherwise. - * - * @param string $ip IP address in network address format. - * @return bool True if IPv4, else false. - */ - public static function isIPv4($ip) - { - // in case mbstring overloads strlen and substr functions - $strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen'; - $substr = function_exists('mb_orig_substr') ? 'mb_orig_substr' : 'substr'; - - // IPv4 - if ($strlen($ip) == 4) { - return true; - } - - // IPv6 - transitional address? - if ($strlen($ip) == 16) { - if (substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff", 0, 12) === 0 - || substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 0, 12) === 0 - ) { - return true; - } - } - - return false; - } - - /** - * Converts an IP address (in network address format) to presentation format. - * This is a backward compatibility function for code that only expects - * IPv4 addresses (i.e., doesn't support IPv6). - * - * This function does not support the long (or its string representation) - * returned by the built-in ip2long() function, from Piwik 1.3 and earlier. - * - * @param string $ip IPv4 address in network address format. - * @return string IP address in presentation format. - */ - public static function long2ip($ip) - { - // IPv4 - if (strlen($ip) == 4) { - return self::N2P($ip); - } - - // IPv6 - transitional address? - if (strlen($ip) == 16) { - if (substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff", 0, 12) === 0 - || substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 0, 12) === 0 - ) { - // remap 128-bit IPv4-mapped and IPv4-compat addresses - return self::N2P(substr($ip, 12)); - } - } - - return '0.0.0.0'; - } - - /** - * Returns true if $ip is an IPv6 address, false if otherwise. This function does - * a naive check. It assumes that whatever format $ip is in, it is well-formed. - * - * @param string $ip - * @return bool - */ - public static function isIPv6($ip) - { - return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); - } - - /** - * Returns true if $ip is a IPv4 mapped address, false if otherwise. - * - * @param string $ip - * @return bool - */ - public static function isMappedIPv4($ip) - { - return substr($ip, 0, strlen(self::MAPPED_IPv4_START)) === self::MAPPED_IPv4_START; - } - - /** - * Returns an IPv4 address from a 'mapped' IPv6 address. - * - * @param string $ip eg, `'::ffff:192.0.2.128'` - * @return string eg, `'192.0.2.128'` - */ - public static function getIPv4FromMappedIPv6($ip) - { - return substr($ip, strlen(self::MAPPED_IPv4_START)); - } - - /** - * Get low and high IP addresses for a specified range. - * - * @param array $ipRange An IP address range in presentation format. - * @return array|bool Array `array($lowIp, $highIp)` in network address format, or false on failure. - */ - public static function getIpsForRange($ipRange) - { - if (strpos($ipRange, '/') === false) { - $ipRange = self::sanitizeIpRange($ipRange); - } - $pos = strpos($ipRange, '/'); - - $bits = substr($ipRange, $pos + 1); - $range = substr($ipRange, 0, $pos); - $high = $low = @inet_pton($range); - if ($low === false) { - return false; - } - - $lowLen = strlen($low); - $i = $lowLen - 1; - $bits = $lowLen * 8 - $bits; - - for ($n = (int)($bits / 8); $n > 0; $n--, $i--) { - $low[$i] = chr(0); - $high[$i] = chr(255); - } - - $n = $bits % 8; - if ($n) { - $low[$i] = chr(ord($low[$i]) & ~((1 << $n) - 1)); - $high[$i] = chr(ord($high[$i]) | ((1 << $n) - 1)); - } - - return array($low, $high); - } - - /** - * Determines if an IP address is in a specified IP address range. - * - * An IPv4-mapped address should be range checked with an IPv4-mapped address range. - * - * @param string $ip IP address in network address format - * @param array $ipRanges List of IP address ranges - * @return bool True if in any of the specified IP address ranges; else false. - */ - public static function isIpInRange($ip, $ipRanges) - { - $ipLen = strlen($ip); - if (empty($ip) || empty($ipRanges) || ($ipLen != 4 && $ipLen != 16)) { - return false; - } - - foreach ($ipRanges as $range) { - if (is_array($range)) { - // already split into low/high IP addresses - $range[0] = self::P2N($range[0]); - $range[1] = self::P2N($range[1]); - } else { - // expect CIDR format but handle some variations - $range = self::getIpsForRange($range); - } - if ($range === false) { - continue; - } - - $low = $range[0]; - $high = $range[1]; - if (strlen($low) != $ipLen) { - continue; - } - - // binary-safe string comparison - if ($ip >= $low && $ip <= $high) { - return true; - } - } - - return false; - } - /** * Returns the most accurate IP address availble for the current user, in * IPv4 format. This could be the proxy client's IP address. @@ -346,7 +45,8 @@ class IP */ public static function getIpFromHeader() { - $clientHeaders = @Config::getInstance()->General['proxy_client_headers']; + $general = Config::getInstance()->General; + $clientHeaders = @$general['proxy_client_headers']; if (!is_array($clientHeaders)) { $clientHeaders = array(); } @@ -357,7 +57,7 @@ class IP } $ipString = self::getNonProxyIpFromHeader($default, $clientHeaders); - return self::sanitizeIp($ipString); + return IPUtils::sanitizeIp($ipString); } /** @@ -371,7 +71,7 @@ class IP { $proxyIps = array(); $config = Config::getInstance()->General; - if(isset($config['proxy_ips'])) { + if (isset($config['proxy_ips'])) { $proxyIps = $config['proxy_ips']; } if (!is_array($proxyIps)) { @@ -383,6 +83,9 @@ class IP // examine proxy headers foreach ($proxyHeaders as $proxyHeader) { if (!empty($_SERVER[$proxyHeader])) { + // this may be buggy if someone has proxy IPs and proxy host headers configured as + // `$_SERVER[$proxyHeader]` could be eg $_SERVER['HTTP_X_FORWARDED_HOST'] and + // include an actual host name, not an IP $proxyIp = self::getLastIpFromList($_SERVER[$proxyHeader], $proxyIps); if (strlen($proxyIp) && stripos($proxyIp, 'unknown') === false) { return $proxyIp; @@ -398,7 +101,7 @@ class IP * * @param string $csv Comma separated list of elements. * @param array $excludedIps Optional list of excluded IP addresses (or IP address ranges). - * @return string Last (non-excluded) IP address in the list. + * @return string Last (non-excluded) IP address in the list or an empty string if all given IPs are excluded. */ public static function getLastIpFromList($csv, $excludedIps = null) { @@ -407,25 +110,14 @@ class IP $elements = explode(',', $csv); for ($i = count($elements); $i--;) { $element = trim(Common::sanitizeInputValue($elements[$i])); - if (empty($excludedIps) || (!in_array($element, $excludedIps) && !self::isIpInRange(self::P2N(self::sanitizeIp($element)), $excludedIps))) { + $ip = \Piwik\Network\IP::fromStringIP(IPUtils::sanitizeIp($element)); + if (empty($excludedIps) || (!in_array($element, $excludedIps) && !$ip->isInRanges($excludedIps))) { return $element; } } + + return ''; } return trim(Common::sanitizeInputValue($csv)); } - - /** - * Retirms the hostname for a given IP address. - * - * @param string $ipStr Human-readable IP address. - * @return string The hostname or unmodified $ipStr on failure. - */ - public static function getHostByAddr($ipStr) - { - // PHP's reverse lookup supports ipv4 and ipv6 - // except on Windows before PHP 5.3 - $host = strtolower(@gethostbyaddr($ipStr)); - return $host === '' ? $ipStr : $host; - } } diff --git a/www/analytics/core/Intl/Data/Provider/CurrencyDataProvider.php b/www/analytics/core/Intl/Data/Provider/CurrencyDataProvider.php new file mode 100644 index 00000000..ae73a5bc --- /dev/null +++ b/www/analytics/core/Intl/Data/Provider/CurrencyDataProvider.php @@ -0,0 +1,33 @@ + array('$', 'US dollar'))`. + * @api + */ + public function getCurrencyList() + { + if ($this->currencyList === null) { + $this->currencyList = require __DIR__ . '/../Resources/currencies.php'; + } + + return $this->currencyList; + } +} diff --git a/www/analytics/core/Intl/Data/Provider/DateTimeFormatProvider.php b/www/analytics/core/Intl/Data/Provider/DateTimeFormatProvider.php new file mode 100644 index 00000000..90ffad60 --- /dev/null +++ b/www/analytics/core/Intl/Data/Provider/DateTimeFormatProvider.php @@ -0,0 +1,83 @@ + language name (in english). + * E.g. `array('en' => 'English', 'ja' => 'Japanese')`. + * @api + */ + public function getLanguageList() + { + if ($this->languageList === null) { + $this->languageList = require __DIR__ . '/../Resources/languages.php'; + } + + return $this->languageList; + } + + /** + * Returns the list of language to country mappings. + * + * @return string[] Array of 2 letter ISO language code => 2 letter ISO country code. + * E.g. `array('fr' => 'fr') // French => France`. + * @api + */ + public function getLanguageToCountryList() + { + if ($this->languageToCountryList === null) { + $this->languageToCountryList = require __DIR__ . '/../Resources/languages-to-countries.php'; + } + + return $this->languageToCountryList; + } +} diff --git a/www/analytics/core/Intl/Data/Provider/RegionDataProvider.php b/www/analytics/core/Intl/Data/Provider/RegionDataProvider.php new file mode 100644 index 00000000..e6661487 --- /dev/null +++ b/www/analytics/core/Intl/Data/Provider/RegionDataProvider.php @@ -0,0 +1,57 @@ +continentList === null) { + $this->continentList = require __DIR__ . '/../Resources/continents.php'; + } + + return $this->continentList; + } + + /** + * Returns the list of valid country codes. + * + * @param bool $includeInternalCodes + * @return string[] Array of 2 letter country ISO codes => 3 letter continent code + * @api + */ + public function getCountryList($includeInternalCodes = false) + { + if ($this->countryList === null) { + $this->countryList = require __DIR__ . '/../Resources/countries.php'; + } + if ($this->countryExtraList === null) { + $this->countryExtraList = require __DIR__ . '/../Resources/countries-extra.php'; + } + + if ($includeInternalCodes) { + return array_merge($this->countryList, $this->countryExtraList); + } + + return $this->countryList; + } +} diff --git a/www/analytics/core/Intl/Data/Resources/continents.php b/www/analytics/core/Intl/Data/Resources/continents.php new file mode 100644 index 00000000..4e346b2c --- /dev/null +++ b/www/analytics/core/Intl/Data/Resources/continents.php @@ -0,0 +1,24 @@ + 'unk', + + // exceptionally reserved + 'ac' => 'afr', // .ac TLD + 'cp' => 'amc', + 'dg' => 'asi', + 'ea' => 'afr', + 'eu' => 'eur', // .eu TLD + 'fx' => 'eur', + 'ic' => 'afr', + 'su' => 'eur', // .su TLD + 'ta' => 'afr', + 'uk' => 'eur', // .uk TLD + + // transitionally reserved + 'an' => 'amc', // former Netherlands Antilles + 'bu' => 'asi', + 'cs' => 'eur', // former Serbia and Montenegro + 'nt' => 'asi', + 'sf' => 'eur', + 'tp' => 'oce', // .tp TLD + 'yu' => 'eur', // .yu TLD + 'zr' => 'afr', + + // MaxMind GeoIP specific + 'a1' => 'unk', + 'a2' => 'unk', + 'ap' => 'asi', + 'o1' => 'unk', + + // Catalonia (Spain) + 'cat' => 'eur', +); diff --git a/www/analytics/core/Intl/Data/Resources/countries.php b/www/analytics/core/Intl/Data/Resources/countries.php new file mode 100644 index 00000000..90485095 --- /dev/null +++ b/www/analytics/core/Intl/Data/Resources/countries.php @@ -0,0 +1,272 @@ + 'eur', + 'ae' => 'asi', + 'af' => 'asi', + 'ag' => 'amc', + 'ai' => 'amc', + 'al' => 'eur', + 'am' => 'asi', + 'ao' => 'afr', + 'aq' => 'ant', + 'ar' => 'ams', + 'as' => 'oce', + 'at' => 'eur', + 'au' => 'oce', + 'aw' => 'amc', + 'ax' => 'eur', + 'az' => 'asi', + 'ba' => 'eur', + 'bb' => 'amc', + 'bd' => 'asi', + 'be' => 'eur', + 'bf' => 'afr', + 'bg' => 'eur', + 'bh' => 'asi', + 'bi' => 'afr', + 'bj' => 'afr', + 'bl' => 'amc', + 'bm' => 'amc', + 'bn' => 'asi', + 'bo' => 'ams', + 'bq' => 'amc', + 'br' => 'ams', + 'bs' => 'amc', + 'bt' => 'asi', + 'bv' => 'ant', + 'bw' => 'afr', + 'by' => 'eur', + 'bz' => 'amc', + 'ca' => 'amn', + 'cc' => 'asi', + 'cd' => 'afr', + 'cf' => 'afr', + 'cg' => 'afr', + 'ch' => 'eur', + 'ci' => 'afr', + 'ck' => 'oce', + 'cl' => 'ams', + 'cm' => 'afr', + 'cn' => 'asi', + 'co' => 'ams', + 'cr' => 'amc', + 'cu' => 'amc', + 'cv' => 'afr', + 'cw' => 'amc', + 'cx' => 'asi', + 'cy' => 'eur', + 'cz' => 'eur', + 'de' => 'eur', + 'dj' => 'afr', + 'dk' => 'eur', + 'dm' => 'amc', + 'do' => 'amc', + 'dz' => 'afr', + 'ec' => 'ams', + 'ee' => 'eur', + 'eg' => 'afr', + 'eh' => 'afr', + 'er' => 'afr', + 'es' => 'eur', + 'et' => 'afr', + 'fi' => 'eur', + 'fj' => 'oce', + 'fk' => 'ams', + 'fm' => 'oce', + 'fo' => 'eur', + 'fr' => 'eur', + 'ga' => 'afr', + 'gb' => 'eur', + 'gd' => 'amc', + 'ge' => 'asi', + 'gf' => 'ams', + 'gg' => 'eur', + 'gh' => 'afr', + 'gi' => 'eur', + 'gl' => 'amn', + 'gm' => 'afr', + 'gn' => 'afr', + 'gp' => 'amc', + 'gq' => 'afr', + 'gr' => 'eur', + 'gs' => 'ant', + 'gt' => 'amc', + 'gu' => 'oce', + 'gw' => 'afr', + 'gy' => 'ams', + 'hk' => 'asi', + 'hm' => 'ant', + 'hn' => 'amc', + 'hr' => 'eur', + 'ht' => 'amc', + 'hu' => 'eur', + 'id' => 'asi', + 'ie' => 'eur', + 'il' => 'asi', + 'im' => 'eur', + 'in' => 'asi', + 'io' => 'asi', + 'iq' => 'asi', + 'ir' => 'asi', + 'is' => 'eur', + 'it' => 'eur', + 'je' => 'eur', + 'jm' => 'amc', + 'jo' => 'asi', + 'jp' => 'asi', + 'ke' => 'afr', + 'kg' => 'asi', + 'kh' => 'asi', + 'ki' => 'oce', + 'km' => 'afr', + 'kn' => 'amc', + 'kp' => 'asi', + 'kr' => 'asi', + 'kw' => 'asi', + 'ky' => 'amc', + 'kz' => 'asi', + 'la' => 'asi', + 'lb' => 'asi', + 'lc' => 'amc', + 'li' => 'eur', + 'lk' => 'asi', + 'lr' => 'afr', + 'ls' => 'afr', + 'lt' => 'eur', + 'lu' => 'eur', + 'lv' => 'eur', + 'ly' => 'afr', + 'ma' => 'afr', + 'mc' => 'eur', + 'md' => 'eur', + 'me' => 'eur', + 'mf' => 'amc', + 'mg' => 'afr', + 'mh' => 'oce', + 'mk' => 'eur', + 'ml' => 'afr', + 'mm' => 'asi', + 'mn' => 'asi', + 'mo' => 'asi', + 'mp' => 'oce', + 'mq' => 'amc', + 'mr' => 'afr', + 'ms' => 'amc', + 'mt' => 'eur', + 'mu' => 'afr', + 'mv' => 'asi', + 'mw' => 'afr', + 'mx' => 'amn', + 'my' => 'asi', + 'mz' => 'afr', + 'na' => 'afr', + 'nc' => 'oce', + 'ne' => 'afr', + 'nf' => 'oce', + 'ng' => 'afr', + 'ni' => 'amc', + 'nl' => 'eur', + 'no' => 'eur', + 'np' => 'asi', + 'nr' => 'oce', + 'nu' => 'oce', + 'nz' => 'oce', + 'om' => 'asi', + 'pa' => 'amc', + 'pe' => 'ams', + 'pf' => 'oce', + 'pg' => 'oce', + 'ph' => 'asi', + 'pk' => 'asi', + 'pl' => 'eur', + 'pm' => 'amn', + 'pn' => 'oce', + 'pr' => 'amc', + 'ps' => 'asi', + 'pt' => 'eur', + 'pw' => 'oce', + 'py' => 'ams', + 'qa' => 'asi', + 're' => 'afr', + 'ro' => 'eur', + 'rs' => 'eur', + 'ru' => 'eur', + 'rw' => 'afr', + 'sa' => 'asi', + 'sb' => 'oce', + 'sc' => 'afr', + 'sd' => 'afr', + 'se' => 'eur', + 'sg' => 'asi', + 'sh' => 'afr', + 'si' => 'eur', + 'sj' => 'eur', + 'sk' => 'eur', + 'sl' => 'afr', + 'sm' => 'eur', + 'sn' => 'afr', + 'so' => 'afr', + 'sr' => 'ams', + 'ss' => 'afr', + 'st' => 'afr', + 'sv' => 'amc', + 'sx' => 'amc', + 'sy' => 'asi', + 'sz' => 'afr', + 'tc' => 'amc', + 'td' => 'afr', + 'tf' => 'ant', + 'tg' => 'afr', + 'th' => 'asi', + 'ti' => 'asi', // Tibet (no iso 3166 code) + 'tj' => 'asi', + 'tk' => 'oce', + 'tl' => 'asi', + 'tm' => 'asi', + 'tn' => 'afr', + 'to' => 'oce', + 'tr' => 'eur', + 'tt' => 'amc', + 'tv' => 'oce', + 'tw' => 'asi', + 'tz' => 'afr', + 'ua' => 'eur', + 'ug' => 'afr', + 'um' => 'oce', + 'us' => 'amn', + 'uy' => 'ams', + 'uz' => 'asi', + 'va' => 'eur', + 'vc' => 'amc', + 've' => 'ams', + 'vg' => 'amc', + 'vi' => 'amc', + 'vn' => 'asi', + 'vu' => 'oce', + 'wf' => 'oce', + 'ws' => 'oce', + 'ye' => 'asi', + 'yt' => 'afr', + 'za' => 'afr', + 'zm' => 'afr', + 'zw' => 'afr', +); diff --git a/www/analytics/core/Intl/Data/Resources/currencies.php b/www/analytics/core/Intl/Data/Resources/currencies.php new file mode 100644 index 00000000..6190c310 --- /dev/null +++ b/www/analytics/core/Intl/Data/Resources/currencies.php @@ -0,0 +1,183 @@ + array('currency symbol', 'description'), + + // Top 5 by global trading volume + 'USD' => array('$', 'US dollar'), + 'EUR' => array('€', 'Euro'), + 'JPY' => array('¥', 'Japanese yen'), + 'GBP' => array('£', 'British pound'), + 'CHF' => array('Fr', 'Swiss franc'), + + 'AFN' => array('؋', 'Afghan afghani'), + 'ALL' => array('L', 'Albanian lek'), + 'DZD' => array('د.ج', 'Algerian dinar'), + 'AOA' => array('Kz', 'Angolan kwanza'), + 'ARS' => array('$', 'Argentine peso'), + 'AMD' => array('դր.', 'Armenian dram'), + 'AWG' => array('ƒ', 'Aruban florin'), + 'AUD' => array('$', 'Australian dollar'), + 'AZN' => array('m', 'Azerbaijani manat'), + 'BSD' => array('$', 'Bahamian dollar'), + 'BHD' => array('.د.ب', 'Bahraini dinar'), + 'BDT' => array('৳', 'Bangladeshi taka'), + 'BBD' => array('$', 'Barbadian dollar'), + 'BYR' => array('Br', 'Belarusian ruble'), + 'BZD' => array('$', 'Belize dollar'), + 'BMD' => array('$', 'Bermudian dollar'), + 'BTC' => array('BTC', 'Bitcoin'), + 'BTN' => array('Nu.', 'Bhutanese ngultrum'), + 'BOB' => array('Bs.', 'Bolivian boliviano'), + 'BAM' => array('KM', 'Bosnia Herzegovina mark'), + 'BWP' => array('P', 'Botswana pula'), + 'BRL' => array('R$', 'Brazilian real'), +// 'GBP' => array('£', 'British pound'), + 'BND' => array('$', 'Brunei dollar'), + 'BGN' => array('лв', 'Bulgarian lev'), + 'BIF' => array('Fr', 'Burundian franc'), + 'KHR' => array('៛', 'Cambodian riel'), + 'CAD' => array('$', 'Canadian dollar'), + 'CVE' => array('$', 'Cape Verdean escudo'), + 'KYD' => array('$', 'Cayman Islands dollar'), + 'XAF' => array('Fr', 'Central African CFA franc'), + 'CLP' => array('$', 'Chilean peso'), + 'CNY' => array('元', 'Chinese yuan'), + 'COP' => array('$', 'Colombian peso'), + 'KMF' => array('Fr', 'Comorian franc'), + 'CDF' => array('Fr', 'Congolese franc'), + 'CRC' => array('₡', 'Costa Rican colón'), + 'HRK' => array('kn', 'Croatian kuna'), + 'XPF' => array('F', 'CFP franc'), + 'CUC' => array('$', 'Cuban convertible peso'), + 'CUP' => array('$', 'Cuban peso'), + 'CMG' => array('ƒ', 'Curaçao and Sint Maarten guilder'), + 'CZK' => array('Kč', 'Czech koruna'), + 'DKK' => array('kr', 'Danish krone'), + 'DJF' => array('Fr', 'Djiboutian franc'), + 'DOP' => array('$', 'Dominican peso'), + 'XCD' => array('$', 'East Caribbean dollar'), + 'EGP' => array('ج.م', 'Egyptian pound'), + 'ERN' => array('Nfk', 'Eritrean nakfa'), + 'ETB' => array('Br', 'Ethiopian birr'), +// 'EUR' => array('€', 'Euro'), + 'FKP' => array('£', 'Falkland Islands pound'), + 'FJD' => array('$', 'Fijian dollar'), + 'GMD' => array('D', 'Gambian dalasi'), + 'GEL' => array('ლ', 'Georgian lari'), + 'GHS' => array('₵', 'Ghanaian cedi'), + 'GIP' => array('£', 'Gibraltar pound'), + 'GTQ' => array('Q', 'Guatemalan quetzal'), + 'GNF' => array('Fr', 'Guinean franc'), + 'GYD' => array('$', 'Guyanese dollar'), + 'HTG' => array('G', 'Haitian gourde'), + 'HNL' => array('L', 'Honduran lempira'), + 'HKD' => array('$', 'Hong Kong dollar'), + 'HUF' => array('Ft', 'Hungarian forint'), + 'ISK' => array('kr', 'Icelandic króna'), + 'INR' => array('‎₹', 'Indian rupee'), + 'IDR' => array('Rp', 'Indonesian rupiah'), + 'IRR' => array('﷼', 'Iranian rial'), + 'IQD' => array('ع.د', 'Iraqi dinar'), + 'ILS' => array('₪', 'Israeli new shekel'), + 'JMD' => array('$', 'Jamaican dollar'), +// 'JPY' => array('¥', 'Japanese yen'), + 'JOD' => array('د.ا', 'Jordanian dinar'), + 'KZT' => array('₸', 'Kazakhstani tenge'), + 'KES' => array('Sh', 'Kenyan shilling'), + 'KWD' => array('د.ك', 'Kuwaiti dinar'), + 'KGS' => array('лв', 'Kyrgyzstani som'), + 'LAK' => array('₭', 'Lao kip'), + 'LBP' => array('ل.ل', 'Lebanese pound'), + 'LSL' => array('L', 'Lesotho loti'), + 'LRD' => array('$', 'Liberian dollar'), + 'LYD' => array('ل.د', 'Libyan dinar'), + 'LTL' => array('Lt', 'Lithuanian litas'), + 'MOP' => array('P', 'Macanese pataca'), + 'MKD' => array('ден', 'Macedonian denar'), + 'MGA' => array('Ar', 'Malagasy ariary'), + 'MWK' => array('MK', 'Malawian kwacha'), + 'MYR' => array('RM', 'Malaysian ringgit'), + 'MVR' => array('ރ.', 'Maldivian rufiyaa'), + 'MRO' => array('UM', 'Mauritanian ouguiya'), + 'MUR' => array('₨', 'Mauritian rupee'), + 'MXN' => array('$', 'Mexican peso'), + 'MDL' => array('L', 'Moldovan leu'), + 'MNT' => array('₮', 'Mongolian tögrög'), + 'MAD' => array('د.م.', 'Moroccan dirham'), + 'MZN' => array('MTn', 'Mozambican metical'), + 'MMK' => array('K', 'Myanma kyat'), + 'NAD' => array('$', 'Namibian dollar'), + 'NPR' => array('₨', 'Nepalese rupee'), + 'ANG' => array('ƒ', 'Netherlands Antillean guilder'), + 'TWD' => array('$', 'New Taiwan dollar'), + 'NZD' => array('$', 'New Zealand dollar'), + 'NIO' => array('C$', 'Nicaraguan córdoba'), + 'NGN' => array('₦', 'Nigerian naira'), + 'KPW' => array('₩', 'North Korean won'), + 'NOK' => array('kr', 'Norwegian krone'), + 'OMR' => array('ر.ع.', 'Omani rial'), + 'PKR' => array('₨', 'Pakistani rupee'), + 'PAB' => array('B/.', 'Panamanian balboa'), + 'PGK' => array('K', 'Papua New Guinean kina'), + 'PYG' => array('₲', 'Paraguayan guaraní'), + 'PEN' => array('S/.', 'Peruvian nuevo sol'), + 'PHP' => array('₱', 'Philippine peso'), + 'PLN' => array('zł', 'Polish złoty'), + 'QAR' => array('ر.ق', 'Qatari riyal'), + 'RON' => array('L', 'Romanian leu'), + 'RUB' => array('руб.', 'Russian ruble'), + 'RWF' => array('Fr', 'Rwandan franc'), + 'SHP' => array('£', 'Saint Helena pound'), + 'SVC' => array('₡', 'Salvadoran colón'), + 'WST' => array('T', 'Samoan tala'), + 'STD' => array('Db', 'São Tomé and Príncipe dobra'), + 'SAR' => array('ر.س', 'Saudi riyal'), + 'RSD' => array('дин. or din.', 'Serbian dinar'), + 'SCR' => array('₨', 'Seychellois rupee'), + 'SLL' => array('Le', 'Sierra Leonean leone'), + 'SGD' => array('$', 'Singapore dollar'), + 'SBD' => array('$', 'Solomon Islands dollar'), + 'SOS' => array('Sh', 'Somali shilling'), + 'ZAR' => array('R', 'South African rand'), + 'KRW' => array('₩', 'South Korean won'), + 'LKR' => array('Rs', 'Sri Lankan rupee'), + 'SDG' => array('جنيه سوداني', 'Sudanese pound'), + 'SRD' => array('$', 'Surinamese dollar'), + 'SZL' => array('L', 'Swazi lilangeni'), + 'SEK' => array('kr', 'Swedish krona'), +// 'CHF' => array('Fr', 'Swiss franc'), + 'SYP' => array('ل.س', 'Syrian pound'), + 'TJS' => array('ЅМ', 'Tajikistani somoni'), + 'TZS' => array('Sh', 'Tanzanian shilling'), + 'THB' => array('฿', 'Thai baht'), + 'TOP' => array('T$', 'Tongan paʻanga'), + 'TTD' => array('$', 'Trinidad and Tobago dollar'), + 'TND' => array('د.ت', 'Tunisian dinar'), + 'TRY' => array('TL', 'Turkish lira'), + 'TMM' => array('m', 'Turkmenistani manat'), + 'UGX' => array('Sh', 'Ugandan shilling'), + 'UAH' => array('₴', 'Ukrainian hryvnia'), + 'AED' => array('د.إ', 'United Arab Emirates dirham'), +// 'USD' => array('$', 'United States dollar'), + 'UYU' => array('$', 'Uruguayan peso'), + 'UZS' => array('лв', 'Uzbekistani som'), + 'VUV' => array('Vt', 'Vanuatu vatu'), + 'VEF' => array('Bs F', 'Venezuelan bolívar'), + 'VND' => array('₫', 'Vietnamese đồng'), + 'XOF' => array('Fr', 'West African CFA franc'), + 'YER' => array('﷼', 'Yemeni rial'), + 'ZMW' => array('ZK', 'Zambian kwacha'), + 'ZWL' => array('$', 'Zimbabwean dollar'), +); diff --git a/www/analytics/core/Intl/Data/Resources/languages-to-countries.php b/www/analytics/core/Intl/Data/Resources/languages-to-countries.php new file mode 100644 index 00000000..91ab0940 --- /dev/null +++ b/www/analytics/core/Intl/Data/Resources/languages-to-countries.php @@ -0,0 +1,60 @@ + 'bg', // Bulgarian => Bulgaria + 'ca' => 'es', // Catalan => Spain + 'cs' => 'cz', // Czech => Czech Republic + 'da' => 'dk', // Danish => Denmark + 'de' => 'de', // German => Germany + 'el' => 'gr', // Greek => Greece + 'es' => 'es', // Spanish => Spain + 'et' => 'ee', // Estonian => Estonia + 'fa' => 'ir', // Farsi => Iran + 'fi' => 'fi', // Finnish => Finland + 'fr' => 'fr', // French => France + 'he' => 'il', // Hebrew => Israel + 'hr' => 'hr', // Croatian => Croatia + 'hu' => 'hu', // Hungarian => Hungary + 'id' => 'id', // Indonesian => Indonesia + 'is' => 'is', // Icelandic => Iceland + 'it' => 'it', // Italian => Italy + 'ja' => 'jp', // Japanese => Japan + 'ko' => 'kr', // Korean => South Korea + 'lt' => 'lt', // Lithuanian => Lithuania + 'lv' => 'lv', // Latvian => Latvia + 'mk' => 'mk', // Macedonian => Macedonia + 'ms' => 'my', // Malay => Malaysia + 'nb' => 'no', // Bokmål => Norway + 'nl' => 'nl', // Dutch => Netherlands + 'nn' => 'no', // Nynorsk => Norway + 'no' => 'no', // Norwegian => Norway + 'pl' => 'pl', // Polish => Poland + 'pt' => 'pt', // Portugese => Portugal + 'ro' => 'ro', // Romanian => Romania + 'ru' => 'ru', // Russian => Russia + 'sk' => 'sk', // Slovak => Slovakia + 'sl' => 'si', // Slovene => Slovenia + 'sq' => 'al', // Albanian => Albania + 'sr' => 'rs', // Serbian => Serbia + 'sv' => 'se', // Swedish => Sweden + 'th' => 'th', // Thai => Thailand + 'bo' => 'ti', // Tibetan => Tibet + 'tr' => 'tr', // Turkish => Turkey + 'uk' => 'ua', // Ukrainian => Ukraine +); diff --git a/www/analytics/core/Intl/Data/Resources/languages.php b/www/analytics/core/Intl/Data/Resources/languages.php new file mode 100644 index 00000000..ca6930f3 --- /dev/null +++ b/www/analytics/core/Intl/Data/Resources/languages.php @@ -0,0 +1,201 @@ + array('Afar'), + 'ab' => array('Abkhazian'), + 'ae' => array('Avestan'), + 'af' => array('Afrikaans'), + 'ak' => array('Akan'), + 'am' => array('Amharic'), + 'an' => array('Aragonese'), + 'ar' => array('Arabic'), + 'as' => array('Assamese'), + 'av' => array('Avaric'), + 'ay' => array('Aymara'), + 'az' => array('Azerbaijani'), + 'ba' => array('Bashkir'), + 'be' => array('Belarusian'), + 'bg' => array('Bulgarian'), + 'bh' => array('Bihari'), // 'Bihari languages' + 'bi' => array('Bislama'), + 'bm' => array('Bambara'), + 'bn' => array('Bengali'), + 'bo' => array('Tibetan'), + 'br' => array('Breton'), + 'bs' => array('Bosnian'), + 'ca' => array('Catalan', 'Valencian'), + 'ce' => array('Chechen'), + 'ch' => array('Chamorro'), + 'co' => array('Corsican'), + 'cr' => array('Cree'), + 'cs' => array('Czech'), + 'cu' => array('Church Slavic', 'Old Slavonic', 'Church Slavonic', 'Old Bulgarian', 'Old Church Slavonic'), + 'cv' => array('Chuvash'), + 'cy' => array('Welsh'), + 'da' => array('Danish'), + 'de' => array('German'), + 'dv' => array('Divehi', 'Dhivehi', 'Maldivian'), + 'dz' => array('Dzongkha'), + 'ee' => array('Ewe'), + 'el' => array('Greek', 'Modern Greek', 'Hellenic'), // Greek, Modern (1453-) + 'en' => array('English'), + 'eo' => array('Esperanto'), + 'es' => array('Spanish', 'Castilian'), + 'et' => array('Estonian'), + 'eu' => array('Basque'), + 'fa' => array('Persian'), + 'ff' => array('Fulah'), + 'fi' => array('Finnish'), + 'fj' => array('Fijian'), + 'fo' => array('Faroese'), + 'fr' => array('French'), + 'fy' => array('Western Frisian'), + 'ga' => array('Irish'), + 'gd' => array('Gaelic', 'Scottish Gaelic'), + 'gl' => array('Galician'), + 'gn' => array('Guarani'), + 'gu' => array('Gujarati'), + 'gv' => array('Manx'), + 'ha' => array('Hausa'), + 'he' => array('Hebrew'), + 'hi' => array('Hindi'), + 'ho' => array('Hiri Motu'), + 'hr' => array('Croatian'), + 'ht' => array('Haitian', 'Haitian Creole'), + 'hu' => array('Hungarian'), + 'hy' => array('Armenian'), + 'hz' => array('Herero'), + 'ia' => array('Interlingua'), // 'Interlingua (International Auxiliary Language Association)' + 'id' => array('Indonesian'), + 'ie' => array('Interlingue', 'Occidental'), + 'ig' => array('Igbo'), + 'ii' => array('Sichuan Yi', 'Nuosu'), + 'ik' => array('Inupiaq'), + 'io' => array('Ido'), + 'is' => array('Icelandic'), + 'it' => array('Italian'), + 'iu' => array('Inuktitut'), + 'ja' => array('Japanese'), + 'jv' => array('Javanese'), + 'ka' => array('Georgian'), + 'kg' => array('Kongo'), + 'ki' => array('Kikuyu', 'Gikuyu'), + 'kj' => array('Kuanyama', 'Kwanyama'), + 'kk' => array('Kazakh'), + 'kl' => array('Kalaallisut', 'Greenlandic'), + 'km' => array('Central Khmer'), + 'kn' => array('Kannada'), + 'ko' => array('Korean'), + 'kr' => array('Kanuri'), + 'ks' => array('Kashmiri'), + 'ku' => array('Kurdish'), + 'kv' => array('Komi'), + 'kw' => array('Cornish'), + 'ky' => array('Kirghiz', 'Kyrgyz'), + 'la' => array('Latin'), + 'lb' => array('Luxembourgish', 'Letzeburgesch'), + 'lg' => array('Ganda'), + 'li' => array('Limburgan', 'Limburger', 'Limburgish'), + 'ln' => array('Lingala'), + 'lo' => array('Lao'), + 'lt' => array('Lithuanian'), + 'lu' => array('Luba-Katanga'), + 'lv' => array('Latvian'), + 'mg' => array('Malagasy'), + 'mh' => array('Marshallese'), + 'mi' => array('Maori'), + 'mk' => array('Macedonian'), + 'ml' => array('Malayalam'), + 'mn' => array('Mongolian'), +// 'mo' => array('Moldavian'), // deprecated + 'mr' => array('Marathi'), + 'ms' => array('Malay'), + 'mt' => array('Maltese'), + 'my' => array('Burmese'), + 'na' => array('Nauru'), + 'nb' => array('Norwegian Bokmål'), + 'nd' => array('North Ndebele'), + 'ne' => array('Nepali'), + 'ng' => array('Ndonga'), + 'nl' => array('Dutch', 'Flemish'), + 'nn' => array('Norwegian Nynorsk'), + 'no' => array('Norwegian'), + 'nr' => array('South Ndebele'), + 'nv' => array('Navajo', 'Navaho'), + 'ny' => array('Chichewa', 'Chewa', 'Nyanja'), + 'oc' => array('Occitan', 'Provençal'), // Occitan (post 1500) + 'oj' => array('Ojibwa'), + 'om' => array('Oromo'), + 'or' => array('Oriya'), + 'os' => array('Ossetian', 'Ossetic'), + 'pa' => array('Panjabi', 'Punjabi'), + 'pi' => array('Pali'), + 'pl' => array('Polish'), + 'ps' => array('Pushto', 'Pashto'), + 'pt' => array('Portuguese'), + 'qu' => array('Quechua'), + 'rm' => array('Romansh'), + 'rn' => array('Rundi'), + 'ro' => array('Romanian', 'Moldavian', 'Moldovan'), + 'ru' => array('Russian'), + 'rw' => array('Kinyarwanda'), + 'sa' => array('Sanskrit'), + 'sc' => array('Sardinian'), + 'sd' => array('Sindhi'), + 'se' => array('Northern Sami'), + 'sg' => array('Sango'), +// 'sh' => array('Serbo-Croatian'), // deprecated + 'si' => array('Sinhala', 'Sinhalese'), + 'sk' => array('Slovak'), + 'sl' => array('Slovenian'), + 'sm' => array('Samoan'), + 'sn' => array('Shona'), + 'so' => array('Somali'), + 'sq' => array('Albanian'), + 'sr' => array('Serbian'), + 'ss' => array('Swati'), + 'st' => array('Southern Soth'), + 'su' => array('Sundanese'), + 'sv' => array('Swedish'), + 'sw' => array('Swahili'), + 'ta' => array('Tamil'), + 'te' => array('Telugu'), + 'tg' => array('Tajik'), + 'th' => array('Thai'), + 'ti' => array('Tigrinya'), + 'tk' => array('Turkmen'), + 'tl' => array('Tagalog'), + 'tn' => array('Tswana'), + 'to' => array('Tonga'), // Tonga (Tonga Islands) + 'tr' => array('Turkish'), + 'ts' => array('Tsonga'), + 'tt' => array('Tatar'), + 'tw' => array('Twi'), + 'ty' => array('Tahitian'), + 'ug' => array('Uighur', 'Uyghur'), + 'uk' => array('Ukrainian'), + 'ur' => array('Urdu'), + 'uz' => array('Uzbek'), + 've' => array('Venda'), + 'vi' => array('Vietnamese'), + 'vo' => array('Volapük'), + 'wa' => array('Walloon'), + 'wo' => array('Wolof'), + 'xh' => array('Xhosa'), + 'yi' => array('Yiddish'), + 'yo' => array('Yoruba'), + 'za' => array('Zhuang', 'Chuang'), + 'zh' => array('Chinese'), + 'zu' => array('Zulu'), +); diff --git a/www/analytics/core/Intl/Locale.php b/www/analytics/core/Intl/Locale.php new file mode 100644 index 00000000..05cf51a8 --- /dev/null +++ b/www/analytics/core/Intl/Locale.php @@ -0,0 +1,42 @@ +doSomething(); - * } catch (Exception $unexpectedError) { - * $debugInfo = new MyDebugInfo($unexpectedError, $myThirdPartyServiceClient); - * Log::debug($debugInfo); - * } - * - * @method static \Piwik\Log getInstance() + * + * + * @deprecated Inject and use Psr\Log\LoggerInterface instead of this class. + * @see \Psr\Log\LoggerInterface */ class Log extends Singleton { @@ -116,86 +69,62 @@ class Log extends Singleton const LOGGER_FILE_PATH_CONFIG_OPTION = 'logger_file_path'; const STRING_MESSAGE_FORMAT_OPTION = 'string_message_format'; - const FORMAT_FILE_MESSAGE_EVENT = 'Log.formatFileMessage'; - - const FORMAT_SCREEN_MESSAGE_EVENT = 'Log.formatScreenMessage'; - - const FORMAT_DATABASE_MESSAGE_EVENT = 'Log.formatDatabaseMessage'; - - const GET_AVAILABLE_WRITERS_EVENT = 'Log.getAvailableWriters'; - /** - * The current logging level. Everything of equal or greater priority will be logged. - * Everything else will be ignored. - * - * @var int - */ - private $currentLogLevel = self::WARN; - - /** - * The array of callbacks executed when logging to a file. Each callback writes a log - * message to a logging backend. - * - * @var array - */ - private $writers = array(); - - /** - * The log message format string that turns a tag name, date-time and message into - * one string to log. + * The backtrace string to use when testing. * * @var string */ - private $logMessageFormat = "%level% %tag%[%datetime%] %message%"; + public static $debugBacktraceForTests; /** - * If we're logging to a file, this is the path to the file to log to. + * Singleton instance. * - * @var string + * @var Log */ - private $logToFilePath; + private static $instance; /** - * True if we're currently setup to log to a screen, false if otherwise. - * - * @var bool + * @var LoggerInterface */ - private $loggingToScreen; + private $logger; - /** - * Constructor. - */ - protected function __construct() + public static function getInstance() { - /** - * access a property that is not overriden by TestingEnvironment before accessing log as the - * log section is used in TestingEnvironment. Otherwise access to magic __get('log') fails in - * TestingEnvironment as it tries to acccess it already here with __get('log'). - * $config->log ==> __get('log') ==> Config.createConfigInstance ==> nested __get('log') ==> returns null - */ - $initConfigToPreventErrorWhenAccessingLog = Config::getInstance()->mail; + if (self::$instance === null) { + self::$instance = StaticContainer::get(__CLASS__); + } + return self::$instance; + } + public static function unsetInstance() + { + self::$instance = null; + } + public static function setSingletonInstance($instance) + { + self::$instance = $instance; + } - $logConfig = Config::getInstance()->log; - $this->setCurrentLogLevelFromConfig($logConfig); - $this->setLogWritersFromConfig($logConfig); - $this->setLogFilePathFromConfig($logConfig); - $this->setStringLogMessageFormat($logConfig); - $this->disableLoggingBasedOnConfig($logConfig); + /** + * @param LoggerInterface $logger + */ + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; } /** * Logs a message using the ERROR log level. * - * _Note: Messages logged with the ERROR level are always logged to the screen in addition - * to configured writers._ - * * @param string $message The log message. This can be a sprintf format string. * @param ... mixed Optional sprintf params. * @api + * + * @deprecated Inject and call Psr\Log\LoggerInterface::error() instead. + * @see \Psr\Log\LoggerInterface::error() */ public static function error($message /* ... */) { - self::logMessage(self::ERROR, $message, array_slice(func_get_args(), 1)); + self::logMessage(Logger::ERROR, $message, array_slice(func_get_args(), 1)); } /** @@ -204,10 +133,13 @@ class Log extends Singleton * @param string $message The log message. This can be a sprintf format string. * @param ... mixed Optional sprintf params. * @api + * + * @deprecated Inject and call Psr\Log\LoggerInterface::warning() instead. + * @see \Psr\Log\LoggerInterface::warning() */ public static function warning($message /* ... */) { - self::logMessage(self::WARN, $message, array_slice(func_get_args(), 1)); + self::logMessage(Logger::WARNING, $message, array_slice(func_get_args(), 1)); } /** @@ -216,10 +148,13 @@ class Log extends Singleton * @param string $message The log message. This can be a sprintf format string. * @param ... mixed Optional sprintf params. * @api + * + * @deprecated Inject and call Psr\Log\LoggerInterface::info() instead. + * @see \Psr\Log\LoggerInterface::info() */ public static function info($message /* ... */) { - self::logMessage(self::INFO, $message, array_slice(func_get_args(), 1)); + self::logMessage(Logger::INFO, $message, array_slice(func_get_args(), 1)); } /** @@ -228,10 +163,13 @@ class Log extends Singleton * @param string $message The log message. This can be a sprintf format string. * @param ... mixed Optional sprintf params. * @api + * + * @deprecated Inject and call Psr\Log\LoggerInterface::debug() instead. + * @see \Psr\Log\LoggerInterface::debug() */ public static function debug($message /* ... */) { - self::logMessage(self::DEBUG, $message, array_slice(func_get_args(), 1)); + self::logMessage(Logger::DEBUG, $message, array_slice(func_get_args(), 1)); } /** @@ -240,396 +178,70 @@ class Log extends Singleton * @param string $message The log message. This can be a sprintf format string. * @param ... mixed Optional sprintf params. * @api + * + * @deprecated Inject and call Psr\Log\LoggerInterface::debug() instead (the verbose level doesn't exist in the PSR standard). + * @see \Psr\Log\LoggerInterface::debug() */ public static function verbose($message /* ... */) { - self::logMessage(self::VERBOSE, $message, array_slice(func_get_args(), 1)); + self::logMessage(Logger::DEBUG, $message, array_slice(func_get_args(), 1)); } /** - * Creates log message combining logging info including a log level, tag name, - * date time, and caller-provided log message. The log message can be set through - * the `[log] string_message_format` INI config option. By default it will - * create log messages like: - * - * **LEVEL [tag:datetime] log message** - * - * @param int $level - * @param string $tag - * @param string $datetime - * @param string $message - * @return string + * @param int $logLevel + * @deprecated Will be removed, log levels are now applied on each Monolog handler. */ - public function formatMessage($level, $tag, $datetime, $message) - { - return str_replace( - array("%tag%", "%message%", "%datetime%", "%level%"), - array($tag, $message, $datetime, $this->getStringLevel($level)), - $this->logMessageFormat - ); - } - - private function setLogWritersFromConfig($logConfig) - { - // set the log writers - $logWriters = $logConfig[self::LOG_WRITERS_CONFIG_OPTION]; - - $logWriters = array_map('trim', $logWriters); - foreach ($logWriters as $writerName) { - $this->addLogWriter($writerName); - } - } - - public function addLogWriter($writerName) - { - if (array_key_exists($writerName, $this->writers)) { - return; - } - - $availableWritersByName = $this->getAvailableWriters(); - - if (empty($availableWritersByName[$writerName])) { - return; - } - - $this->writers[$writerName] = $availableWritersByName[$writerName]; - - if ($writerName == 'screen') { - $this->loggingToScreen = true; - } - } - - private function setCurrentLogLevelFromConfig($logConfig) - { - if (!empty($logConfig[self::LOG_LEVEL_CONFIG_OPTION])) { - $logLevel = $this->getLogLevelFromStringName($logConfig[self::LOG_LEVEL_CONFIG_OPTION]); - - if ($logLevel >= self::NONE // sanity check - && $logLevel <= self::VERBOSE - ) { - $this->setLogLevel($logLevel); - } - } - } - - private function setStringLogMessageFormat($logConfig) - { - if (isset($logConfig['string_message_format'])) { - $this->logMessageFormat = $logConfig['string_message_format']; - } - } - - private function setLogFilePathFromConfig($logConfig) - { - $logPath = $logConfig[self::LOGGER_FILE_PATH_CONFIG_OPTION]; - if (!SettingsServer::isWindows() - && $logPath[0] != '/' - ) { - $logPath = PIWIK_USER_PATH . DIRECTORY_SEPARATOR . $logPath; - } - $logPath = SettingsPiwik::rewriteTmpPathWithHostname($logPath); - if (is_dir($logPath)) { - $logPath .= '/piwik.log'; - } - $this->logToFilePath = $logPath; - } - - private function getAvailableWriters() - { - $writers = array(); - - /** - * This event is called when the Log instance is created. Plugins can use this event to - * make new logging writers available. - * - * A logging writer is a callback with the following signature: - * - * function (int $level, string $tag, string $datetime, string $message) - * - * `$level` is the log level to use, `$tag` is the log tag used, `$datetime` is the date time - * of the logging call and `$message` is the formatted log message. - * - * Logging writers must be associated by name in the array passed to event handlers. The - * name specified can be used in Piwik's INI configuration. - * - * **Example** - * - * public function getAvailableWriters(&$writers) { - * $writers['myloggername'] = function ($level, $tag, $datetime, $message) { - * // ... - * }; - * } - * - * // 'myloggername' can now be used in the log_writers config option. - * - * @param array $writers Array mapping writer names with logging writers. - */ - Piwik::postEvent(self::GET_AVAILABLE_WRITERS_EVENT, array(&$writers)); - - $writers['file'] = array($this, 'logToFile'); - $writers['screen'] = array($this, 'logToScreen'); - $writers['database'] = array($this, 'logToDatabase'); - return $writers; - } - public function setLogLevel($logLevel) { - $this->currentLogLevel = $logLevel; } - private function logToFile($level, $tag, $datetime, $message) + /** + * @deprecated Will be removed, log levels are now applied on each Monolog handler. + */ + public function getLogLevel() { - if (is_string($message)) { - $message = $this->formatMessage($level, $tag, $datetime, $message); - } else { - $logger = $this; + } - /** - * Triggered when trying to log an object to a file. Plugins can use - * this event to convert objects to strings before they are logged. - * - * **Example** - * - * public function formatFileMessage(&$message, $level, $tag, $datetime, $logger) { - * if ($message instanceof MyCustomDebugInfo) { - * $message = $message->formatForFile(); - * } - * } - * - * @param mixed &$message The object that is being logged. Event handlers should - * check if the object is of a certain type and if it is, - * set `$message` to the string that should be logged. - * @param int $level The log level used with this log entry. - * @param string $tag The current plugin that started logging (or if no plugin, - * the current class). - * @param string $datetime Datetime of the logging call. - * @param Log $logger The Log singleton. - */ - Piwik::postEvent(self::FORMAT_FILE_MESSAGE_EVENT, array(&$message, $level, $tag, $datetime, $logger)); + private function doLog($level, $message, $parameters = array()) + { + // To ensure the compatibility with PSR-3, the message must be a string + if ($message instanceof \Exception) { + $parameters['exception'] = $message; + $message = $message->getMessage(); } - if (empty($message)) { + if (is_object($message) || is_array($message) || is_resource($message)) { + $this->logger->warning('Trying to log a message that is not a string', array( + 'exception' => new \InvalidArgumentException('Trying to log a message that is not a string') + )); return; } - if(!file_put_contents($this->logToFilePath, $message . "\n", FILE_APPEND)) { - $message = Filechecks::getErrorMessageMissingPermissions($this->logToFilePath); - throw new \Exception( $message ); - } + $this->logger->log($level, $message, $parameters); } - private function logToScreen($level, $tag, $datetime, $message) + private static function logMessage($level, $message, $parameters) { - static $currentRequestKey; - if (empty($currentRequestKey)) { - $currentRequestKey = substr(Common::generateUniqId(), 0, 5); - } - - if (is_string($message)) { - if (!defined('PIWIK_TEST_MODE')) { - $message = '[' . $currentRequestKey . '] ' . $message; - } - $message = $this->formatMessage($level, $tag, $datetime, $message); - - if (!Common::isPhpCliMode()) { - $message = Common::sanitizeInputValue($message); - $message = '
' . $message . '
'; - } - - } else { - $logger = $this; - - /** - * Triggered when trying to log an object to the screen. Plugins can use - * this event to convert objects to strings before they are logged. - * - * The result of this callback can be HTML so no sanitization is done on the result. - * This means **YOU MUST SANITIZE THE MESSAGE YOURSELF** if you use this event. - * - * **Example** - * - * public function formatScreenMessage(&$message, $level, $tag, $datetime, $logger) { - * if ($message instanceof MyCustomDebugInfo) { - * $message = Common::sanitizeInputValue($message->formatForScreen()); - * } - * } - * - * @param mixed &$message The object that is being logged. Event handlers should - * check if the object is of a certain type and if it is, - * set `$message` to the string that should be logged. - * @param int $level The log level used with this log entry. - * @param string $tag The current plugin that started logging (or if no plugin, - * the current class). - * @param string $datetime Datetime of the logging call. - * @param Log $logger The Log singleton. - */ - Piwik::postEvent(self::FORMAT_SCREEN_MESSAGE_EVENT, array(&$message, $level, $tag, $datetime, $logger)); - } - - if (empty($message)) { - return; - } - - echo $message . "\n"; + self::getInstance()->doLog($level, $message, $parameters); } - private function logToDatabase($level, $tag, $datetime, $message) + public static function getMonologLevel($level) { - if (is_string($message)) { - $message = $this->formatMessage($level, $tag, $datetime, $message); - } else { - $logger = $this; - - /** - * Triggered when trying to log an object to a database table. Plugins can use - * this event to convert objects to strings before they are logged. - * - * **Example** - * - * public function formatDatabaseMessage(&$message, $level, $tag, $datetime, $logger) { - * if ($message instanceof MyCustomDebugInfo) { - * $message = $message->formatForDatabase(); - * } - * } - * - * @param mixed &$message The object that is being logged. Event handlers should - * check if the object is of a certain type and if it is, - * set `$message` to the string that should be logged. - * @param int $level The log level used with this log entry. - * @param string $tag The current plugin that started logging (or if no plugin, - * the current class). - * @param string $datetime Datetime of the logging call. - * @param Log $logger The Log singleton. - */ - Piwik::postEvent(self::FORMAT_DATABASE_MESSAGE_EVENT, array(&$message, $level, $tag, $datetime, $logger)); - } - - if (empty($message)) { - return; - } - - $sql = "INSERT INTO " . Common::prefixTable('logger_message') - . " (tag, timestamp, level, message)" - . " VALUES (?, ?, ?, ?)"; - Db::query($sql, array($tag, $datetime, self::getStringLevel($level), (string)$message)); - } - - private function doLog($level, $message, $sprintfParams = array()) - { - if ($this->shouldLoggerLog($level)) { - $datetime = date("Y-m-d H:i:s"); - if (is_string($message) - && !empty($sprintfParams) - ) { - $message = vsprintf($message, $sprintfParams); - } - - if (version_compare(phpversion(), '5.3.6', '>=')) { - $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT); - } else { - $backtrace = debug_backtrace(); - } - $tag = Plugin::getPluginNameFromBacktrace($backtrace); - - // if we can't determine the plugin, use the name of the calling class - if ($tag == false) { - $tag = $this->getClassNameThatIsLogging($backtrace); - } - - $this->writeMessage($level, $tag, $datetime, $message); - } - } - - private function writeMessage($level, $tag, $datetime, $message) - { - foreach ($this->writers as $writer) { - call_user_func($writer, $level, $tag, $datetime, $message); - } - - // errors are always printed to screen - if ($level == self::ERROR - && !$this->loggingToScreen - ) { - $this->logToScreen($level, $tag, $datetime, $message); - } - } - - private static function logMessage($level, $message, $sprintfParams) - { - self::getInstance()->doLog($level, $message, $sprintfParams); - } - - private function shouldLoggerLog($level) - { - return $level <= $this->currentLogLevel; - } - - private function disableLoggingBasedOnConfig($logConfig) - { - $disableLogging = false; - - if (!empty($logConfig['log_only_when_cli']) - && !Common::isPhpCliMode() - ) { - $disableLogging = true; - } - - if (!empty($logConfig['log_only_when_debug_parameter']) - && !isset($_REQUEST['debug']) - ) { - $disableLogging = true; - } - - if ($disableLogging) { - $this->currentLogLevel = self::NONE; - } - } - - private function getLogLevelFromStringName($name) - { - $name = strtoupper($name); - switch ($name) { - case 'NONE': - return self::NONE; - case 'ERROR': - return self::ERROR; - case 'WARN': - return self::WARN; - case 'INFO': - return self::INFO; - case 'DEBUG': - return self::DEBUG; - case 'VERBOSE': - return self::VERBOSE; + switch ($level) { + case self::ERROR: + return Logger::ERROR; + case self::WARN: + return Logger::WARNING; + case self::INFO: + return Logger::INFO; + case self::DEBUG: + return Logger::DEBUG; + case self::VERBOSE: + return Logger::DEBUG; + case self::NONE: default: - return -1; + // Highest level possible, need to do better in the future... + return Logger::EMERGENCY; } } - - private function getStringLevel($level) - { - static $levelToName = array( - self::NONE => 'NONE', - self::ERROR => 'ERROR', - self::WARN => 'WARN', - self::INFO => 'INFO', - self::DEBUG => 'DEBUG', - self::VERBOSE => 'VERBOSE' - ); - return $levelToName[$level]; - } - - private function getClassNameThatIsLogging($backtrace) - { - foreach ($backtrace as $tracepoint) { - if (isset($tracepoint['class']) - && $tracepoint['class'] != "Piwik\\Log" - && $tracepoint['class'] != "Piwik\\Piwik" - && $tracepoint['class'] != "Piwik\\CronArchive" - ) { - return $tracepoint['class']; - } - } - return false; - } } diff --git a/www/analytics/core/LogDeleter.php b/www/analytics/core/LogDeleter.php new file mode 100644 index 00000000..fc61ec93 --- /dev/null +++ b/www/analytics/core/LogDeleter.php @@ -0,0 +1,110 @@ +rawLogDao = $rawLogDao; + } + + /** + * Deletes visits by ID. This method cascades, so conversions, conversion items and visit actions for + * the visits are also deleted. + * + * @param int[] $visitIds + * @return int The number of deleted visits. + */ + public function deleteVisits($visitIds) + { + $this->deleteConversions($visitIds); + $this->rawLogDao->deleteVisitActionsForVisits($visitIds); + + return $this->rawLogDao->deleteVisits($visitIds); + } + + /** + * Deletes conversions by visit ID. This method cascades, so conversion items are also deleted. + * + * @param int[] $visitIds The list of visits to delete conversions for. + * @return int The number rows deleted. + */ + public function deleteConversions($visitIds) + { + $this->deleteConversionItems($visitIds); + return $this->rawLogDao->deleteConversions($visitIds); + } + + /** + * Deletes conversion items by visit ID. + * + * @param int[] $visitIds The list of visits to delete conversions for. + * @return int The number rows deleted. + */ + public function deleteConversionItems($visitIds) + { + return $this->rawLogDao->deleteConversionItems($visitIds); + } + + /** + * Deletes visits within the specified date range and belonging to the specified site (if any). Visits are + * deleted in chunks, so only `$iterationStep` visits are deleted at a time. + * + * @param string|null $startDatetime A datetime string. Visits that occur at this time or after are deleted. If not supplied, + * visits from the beginning of time are deleted. + * @param string|null $endDatetime A datetime string. Visits that occur before this time are deleted. If not supplied, + * visits from the end of time are deleted. + * @param int|null $idSite The site to delete visits from. + * @param int $iterationStep The number of visits to delete at a single time. + * @param callable $afterChunkDeleted Callback executed after every chunk of visits are deleted. + * @return int The number of visits deleted. + */ + public function deleteVisitsFor($startDatetime, $endDatetime, $idSite = null, $iterationStep = 1000, $afterChunkDeleted = null) + { + $fields = array('idvisit'); + $conditions = array(); + + if (!empty($startDatetime)) { + $conditions[] = array('visit_last_action_time', '>=', $startDatetime); + } + + if (!empty($endDatetime)) { + $conditions[] = array('visit_last_action_time', '<', $endDatetime); + } + + if (!empty($idSite)) { + $conditions[] = array('idsite', '=', $idSite); + } + + $logsDeleted = 0; + $logPurger = $this; + $this->rawLogDao->forAllLogs('log_visit', $fields, $conditions, $iterationStep, function ($logs) use ($logPurger, &$logsDeleted, $afterChunkDeleted) { + $ids = array_map(function ($row) { return reset($row); }, $logs); + $logsDeleted += $logPurger->deleteVisits($ids); + + if (!empty($afterChunkDeleted)) { + $afterChunkDeleted($logsDeleted); + } + }); + + return $logsDeleted; + } +} \ No newline at end of file diff --git a/www/analytics/core/Mail.php b/www/analytics/core/Mail.php index 1fc1faf7..c6c8623c 100644 --- a/www/analytics/core/Mail.php +++ b/www/analytics/core/Mail.php @@ -1,6 +1,6 @@ isEnabled() - ? Piwik::translate('CoreHome_WebAnalyticsReports') - : Piwik::translate('ScheduledReports_PiwikReports'); + + /** @var Translator $translator */ + $translator = StaticContainer::get('Piwik\Translation\Translator'); + + $fromEmailName = Config::getInstance()->General['noreply_email_name']; + + if (empty($fromEmailName) && $customLogo->isEnabled()) { + $fromEmailName = $translator->translate('CoreHome_WebAnalyticsReports'); + } elseif (empty($fromEmailName)) { + $fromEmailName = $translator->translate('ScheduledReports_PiwikReports'); + } + $fromEmailAddress = Config::getInstance()->General['noreply_email_address']; $this->setFrom($fromEmailAddress, $fromEmailName); } @@ -50,20 +61,25 @@ class Mail extends Zend_Mail */ public function setFrom($email, $name = null) { - $hostname = Config::getInstance()->mail['defaultHostnameIfEmpty']; - $piwikHost = Url::getCurrentHost($hostname); + return parent::setFrom( + $this->parseDomainPlaceholderAsPiwikHostName($email), + $name + ); + } - // If known Piwik URL, use it instead of "localhost" - $piwikUrl = SettingsPiwik::getPiwikUrl(); - $url = parse_url($piwikUrl); - if (isset($url['host']) - && $url['host'] != 'localhost' - && $url['host'] != '127.0.0.1' - ) { - $piwikHost = $url['host']; - } - $email = str_replace('{DOMAIN}', $piwikHost, $email); - return parent::setFrom($email, $name); + /** + * Set Reply-To Header + * + * @param string $email + * @param null|string $name + * @return Zend_Mail + */ + public function setReplyTo($email, $name = null) + { + return parent::setReplyTo( + $this->parseDomainPlaceholderAsPiwikHostName($email), + $name + ); } /** @@ -72,27 +88,37 @@ class Mail extends Zend_Mail private function initSmtpTransport() { $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']; - $tr = new \Zend_Mail_Transport_Smtp($mailConfig['host'], $smtpConfig); + $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']; + } + + $host = trim($mailConfig['host']); + $tr = new \Zend_Mail_Transport_Smtp($host, $smtpConfig); Mail::setDefaultTransport($tr); - ini_set("smtp_port", $mailConfig['port']); + @ini_set("smtp_port", $mailConfig['port']); } - public function send($transport = NULL) + public function send($transport = null) { if (defined('PIWIK_TEST_MODE')) { // hack Piwik::postTestEvent("Test.Mail.send", array($this)); @@ -100,4 +126,58 @@ class Mail extends Zend_Mail return parent::send($transport); } } + + public function createAttachment($body, $mimeType = null, $disposition = null, $encoding = null, $filename = null) + { + $filename = $this->sanitiseString($filename); + return parent::createAttachment($body, $mimeType, $disposition, $encoding, $filename); + } + + public function setSubject($subject) + { + $subject = $this->sanitiseString($subject); + return parent::setSubject($subject); + } + + /** + * @param string $email + * @return string + */ + protected function parseDomainPlaceholderAsPiwikHostName($email) + { + $hostname = Config::getInstance()->mail['defaultHostnameIfEmpty']; + $piwikHost = Url::getCurrentHost($hostname); + + // If known Piwik URL, use it instead of "localhost" + $piwikUrl = SettingsPiwik::getPiwikUrl(); + $url = parse_url($piwikUrl); + if ($this->isHostDefinedAndNotLocal($url)) { + $piwikHost = $url['host']; + } + + return str_replace('{DOMAIN}', $piwikHost, $email); + } + + /** + * @param array $url + * @return bool + */ + protected function isHostDefinedAndNotLocal($url) + { + return isset($url['host']) && !Url::isLocalHost($url['host']); + } + + /** + * Replaces characters known to appear incorrectly in some email clients + * + * @param $string + * @return mixed + */ + function sanitiseString($string) + { + $search = array('–', '’'); + $replace = array('-', '\''); + $string = str_replace($search, $replace, $string); + return $string; + } } diff --git a/www/analytics/core/Measurable/Measurable.php b/www/analytics/core/Measurable/Measurable.php new file mode 100644 index 00000000..d80c1f03 --- /dev/null +++ b/www/analytics/core/Measurable/Measurable.php @@ -0,0 +1,32 @@ +id, $this->getType()); + $setting = $settings->getSetting($name); + + if (!empty($setting)) { + return $setting->getValue(); // Calling `getValue` makes sure we respect read permission of this setting + } + + throw new Exception(sprintf('Setting %s does not exist', $name)); + } +} diff --git a/www/analytics/core/Measurable/MeasurableSetting.php b/www/analytics/core/Measurable/MeasurableSetting.php new file mode 100644 index 00000000..91e09704 --- /dev/null +++ b/www/analytics/core/Measurable/MeasurableSetting.php @@ -0,0 +1,70 @@ +writableByCurrentUser = Piwik::isUserHasSomeAdminAccess(); + $this->readableByCurrentUser = Piwik::isUserHasSomeViewAccess(); + } + + /** + * Returns `true` if this setting is writable for the current user, `false` if otherwise. In case it returns + * writable for the current user it will be visible in the Plugin settings UI. + * + * @return bool + */ + public function isWritableByCurrentUser() + { + return $this->writableByCurrentUser; + } + + /** + * Returns `true` if this setting can be displayed for the current user, `false` if otherwise. + * + * @return bool + */ + public function isReadableByCurrentUser() + { + return $this->readableByCurrentUser; + } +} diff --git a/www/analytics/core/Measurable/MeasurableSettings.php b/www/analytics/core/Measurable/MeasurableSettings.php new file mode 100644 index 00000000..7d627e0a --- /dev/null +++ b/www/analytics/core/Measurable/MeasurableSettings.php @@ -0,0 +1,103 @@ +idSite = $idSite; + $this->idType = $idType; + $this->storage = new Storage(Db::get(), $this->idSite); + $this->pluginName = 'MeasurableSettings'; + + $this->init(); + } + + protected function init() + { + $typeManager = new TypeManager(); + $type = $typeManager->getType($this->idType); + $type->configureMeasurableSettings($this); + + /** + * This event is posted when generating settings for a Measurable (website). You can add any Measurable settings + * that you wish to be shown in the Measurable manager (websites manager). If you need to add settings only for + * eg MobileApp measurables you can use eg `$type->getId() === Piwik\Plugins\MobileAppMeasurable\Type::ID` and + * add only settings if the condition is true. + * + * @since Piwik 2.14.0 + * @deprecated will be removed in Piwik 3.0.0 + * + * @param MeasurableSettings $this + * @param \Piwik\Measurable\Type $type + * @param int $idSite + */ + Piwik::postEvent('Measurable.initMeasurableSettings', array($this, $type, $this->idSite)); + } + + public function addSetting(Setting $setting) + { + if ($this->idSite && $setting instanceof MeasurableSetting) { + $setting->writableByCurrentUser = Piwik::isUserHasAdminAccess($this->idSite); + } + + parent::addSetting($setting); + } + + public function save() + { + Piwik::checkUserHasAdminAccess($this->idSite); + + $typeManager = new TypeManager(); + $type = $typeManager->getType($this->idType); + + /** + * Triggered just before Measurable settings are about to be saved. You can use this event for example + * to validate not only one setting but multiple ssetting. For example whether username + * and password matches. + * + * @since Piwik 2.14.0 + * @deprecated will be removed in Piwik 3.0.0 + * + * @param MeasurableSettings $this + * @param \Piwik\Measurable\Type $type + * @param int $idSite + */ + Piwik::postEvent('Measurable.beforeSaveSettings', array($this, $type, $this->idSite)); + + $this->storage->save(); + } + +} + diff --git a/www/analytics/core/Measurable/Settings/Storage.php b/www/analytics/core/Measurable/Settings/Storage.php new file mode 100644 index 00000000..df9748af --- /dev/null +++ b/www/analytics/core/Measurable/Settings/Storage.php @@ -0,0 +1,104 @@ +db = $db; + $this->idSite = $idSite; + } + + protected function deleteSettingsFromStorage() + { + $table = $this->getTableName(); + $sql = "DELETE FROM $table WHERE `idsite` = ?"; + $bind = array($this->idSite); + + $this->db->query($sql, $bind); + } + + public function deleteValue(Setting $setting) + { + $this->toBeDeleted[$setting->getName()] = true; + parent::deleteValue($setting); + } + + public function setValue(Setting $setting, $value) + { + $this->toBeDeleted[$setting->getName()] = false; // prevent from deleting this setting, we will create/update it + parent::setValue($setting, $value); + } + + /** + * Saves (persists) the current setting values in the database. + */ + public function save() + { + $table = $this->getTableName(); + + foreach ($this->toBeDeleted as $name => $delete) { + if ($delete) { + $sql = "DELETE FROM $table WHERE `idsite` = ? and `setting_name` = ?"; + $bind = array($this->idSite, $name); + + $this->db->query($sql, $bind); + } + } + + $this->toBeDeleted = array(); + + foreach ($this->settingsValues as $name => $value) { + $value = serialize($value); + + $sql = "INSERT INTO $table (`idsite`, `setting_name`, `setting_value`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `setting_value` = ?"; + $bind = array($this->idSite, $name, $value, $value); + + $this->db->query($sql, $bind); + } + } + + protected function loadSettings() + { + $sql = "SELECT `setting_name`, `setting_value` FROM " . $this->getTableName() . " WHERE idsite = ?"; + $bind = array($this->idSite); + + $settings =$this->db->fetchAll($sql, $bind); + + $flat = array(); + foreach ($settings as $setting) { + $flat[$setting['setting_name']] = unserialize($setting['setting_value']); + } + + return $flat; + } + + private function getTableName() + { + return Common::prefixTable('site_setting'); + } +} diff --git a/www/analytics/core/Measurable/Type.php b/www/analytics/core/Measurable/Type.php new file mode 100644 index 00000000..e9457a66 --- /dev/null +++ b/www/analytics/core/Measurable/Type.php @@ -0,0 +1,62 @@ +isType('website') to be true (maybe) + return $this->getId() === $typeId; + } + + public function getId() + { + $id = static::ID; + + if (empty($id)) { + $message = 'Type %s does not define an ID. Set the ID constant to fix this issue';; + throw new \Exception(sprintf($message, get_called_class())); + } + + return $id; + } + + public function getDescription() + { + return $this->description; + } + + public function getName() + { + return $this->name; + } + + public function getNamePlural() + { + return $this->namePlural; + } + + public function getHowToSetupUrl() + { + return $this->howToSetupUrl; + } + + public function configureMeasurableSettings(MeasurableSettings $settings) + { + } +} + diff --git a/www/analytics/core/Measurable/Type/TypeManager.php b/www/analytics/core/Measurable/Type/TypeManager.php new file mode 100644 index 00000000..af40d9c6 --- /dev/null +++ b/www/analytics/core/Measurable/Type/TypeManager.php @@ -0,0 +1,39 @@ +findComponents('Type', '\\Piwik\\Measurable\\Type'); + } + + /** + * @param string $typeId + * @return Type|null + */ + public function getType($typeId) + { + foreach ($this->getAllTypes() as $type) { + if ($type->getId() === $typeId) { + return $type; + } + } + + return new Type(); + } +} + diff --git a/www/analytics/core/Menu/Group.php b/www/analytics/core/Menu/Group.php new file mode 100644 index 00000000..a13da035 --- /dev/null +++ b/www/analytics/core/Menu/Group.php @@ -0,0 +1,31 @@ +items[] = array( + 'name' => $subTitleMenu, + 'url' => $url, + 'tooltip' => $tooltip + ); + } + + public function getItems() + { + return $this->items; + } +} diff --git a/www/analytics/core/Menu/MenuAbstract.php b/www/analytics/core/Menu/MenuAbstract.php index 781aed7b..777d97b5 100644 --- a/www/analytics/core/Menu/MenuAbstract.php +++ b/www/analytics/core/Menu/MenuAbstract.php @@ -1,6 +1,6 @@ menu; } + /** + * Let's you register a menu icon for a certain menu category to replace the default arrow icon. + * + * @param string $menuName The translation key of a main menu category, eg 'Dashboard_Dashboard' + * @param string $iconCssClass The css class name of an icon, eg 'icon-user' + */ + public function registerMenuIcon($menuName, $iconCssClass) + { + $this->menuIcons[$menuName] = $iconCssClass; + } + + /** + * Returns a list of available plugin menu instances. + * + * @return \Piwik\Plugin\Menu[] + */ + protected function getAllMenus() + { + if (!empty(self::$menus)) { + return self::$menus; + } + + self::$menus = PluginManager::getInstance()->findComponents('Menu', 'Piwik\\Plugin\\Menu'); + + return self::$menus; + } + + /** + * To use only for tests. + * + * @deprecated The whole $menus cache should be replaced by a real transient cache + */ + public static function clearMenus() + { + self::$menus = array(); + } + /** * Adds a new entry to the menu. * @@ -57,8 +96,9 @@ abstract class MenuAbstract extends Singleton * @param boolean $displayedForCurrentUser Whether this menu entry should be displayed for the * current user. If false, the entry will not be added. * @param int $order The order hint. - * @param false|string $tooltip An optional tooltip to display. - * @api + * @param bool|string $tooltip An optional tooltip to display or false to display the tooltip. + * + * @deprecated since 2.7.0 Use {@link addItem() instead}. Method will be removed in Piwik 3.0 */ public function add($menuName, $subMenuName, $url, $displayedForCurrentUser = true, $order = 50, $tooltip = false) { @@ -66,8 +106,25 @@ abstract class MenuAbstract extends Singleton return; } + $this->addItem($menuName, $subMenuName, $url, $order, $tooltip); + } + + /** + * Adds a new entry to the menu. + * + * @param string $menuName The menu's category name. Can be a translation token. + * @param string $subMenuName The menu item's name. Can be a translation token. + * @param string|array $url The URL the admin menu entry should link to, or an array of query parameters + * that can be used to build the URL. + * @param int $order The order hint. + * @param bool|string $tooltip An optional tooltip to display or false to display the tooltip. + * @since 2.7.0 + * @api + */ + public function addItem($menuName, $subMenuName, $url, $order = 50, $tooltip = false) + { // make sure the idSite value used is numeric (hack-y fix for #3426) - if (!is_numeric(Common::getRequestVar('idSite', false))) { + if (isset($url['idSite']) && !is_numeric($url['idSite'])) { $idSites = API::getInstance()->getSitesIdWithAtLeastViewAccess(); $url['idSite'] = reset($idSites); } @@ -81,6 +138,13 @@ abstract class MenuAbstract extends Singleton ); } + /** + * Removes an existing entry from the menu. + * + * @param string $menuName The menu's category name. Can be a translation token. + * @param bool|string $subMenuName The menu item's name. Can be a translation token. + * @api + */ public function remove($menuName, $subMenuName = false) { $this->menuEntriesToRemove[] = array( @@ -100,21 +164,34 @@ abstract class MenuAbstract extends Singleton */ private function buildMenuItem($menuName, $subMenuName, $url, $order = 50, $tooltip = false) { - if (!isset($this->menu[$menuName]) || empty($subMenuName)) { - $this->menu[$menuName]['_url'] = $url; - if (empty($subMenuName)) { - $this->menu[$menuName]['_order'] = $order; - } - $this->menu[$menuName]['_name'] = $menuName; - $this->menu[$menuName]['_hasSubmenu'] = false; + if (!isset($this->menu[$menuName])) { + $this->menu[$menuName] = array( + '_hasSubmenu' => false, + '_order' => $order + ); + } + + if (empty($subMenuName)) { + $this->menu[$menuName]['_url'] = $url; + $this->menu[$menuName]['_order'] = $order; + $this->menu[$menuName]['_name'] = $menuName; $this->menu[$menuName]['_tooltip'] = $tooltip; + if (!empty($this->menuIcons[$menuName])) { + $this->menu[$menuName]['_icon'] = $this->menuIcons[$menuName]; + } else { + $this->menu[$menuName]['_icon'] = ''; + } } if (!empty($subMenuName)) { $this->menu[$menuName][$subMenuName]['_url'] = $url; $this->menu[$menuName][$subMenuName]['_order'] = $order; $this->menu[$menuName][$subMenuName]['_name'] = $subMenuName; + $this->menu[$menuName][$subMenuName]['_tooltip'] = $tooltip; $this->menu[$menuName]['_hasSubmenu'] = true; - $this->menu[$menuName]['_tooltip'] = $tooltip; + + if (!array_key_exists('_tooltip', $this->menu[$menuName])) { + $this->menu[$menuName]['_tooltip'] = $tooltip; + } } } @@ -135,6 +212,7 @@ abstract class MenuAbstract extends Singleton * @param $subMenuOriginal * @param $mainMenuRenamed * @param $subMenuRenamed + * @api */ public function rename($mainMenuOriginal, $subMenuOriginal, $mainMenuRenamed, $subMenuRenamed) { @@ -148,6 +226,7 @@ abstract class MenuAbstract extends Singleton * @param $mainMenuToEdit * @param $subMenuToEdit * @param $newUrl + * @api */ public function editUrl($mainMenuToEdit, $subMenuToEdit, $newUrl) { @@ -161,13 +240,21 @@ abstract class MenuAbstract extends Singleton { foreach ($this->edits as $edit) { $mainMenuToEdit = $edit[0]; - $subMenuToEdit = $edit[1]; - $newUrl = $edit[2]; + $subMenuToEdit = $edit[1]; + $newUrl = $edit[2]; if ($subMenuToEdit === null) { - $menuDataToEdit = @$this->menu[$mainMenuToEdit]; + if (isset($this->menu[$mainMenuToEdit])) { + $menuDataToEdit = &$this->menu[$mainMenuToEdit]; + } else { + $menuDataToEdit = null; + } } else { - $menuDataToEdit = @$this->menu[$mainMenuToEdit][$subMenuToEdit]; + if (isset($this->menu[$mainMenuToEdit][$subMenuToEdit])) { + $menuDataToEdit = &$this->menu[$mainMenuToEdit][$subMenuToEdit]; + } else { + $menuDataToEdit = null; + } } if (empty($menuDataToEdit)) { @@ -180,16 +267,15 @@ abstract class MenuAbstract extends Singleton private function applyRemoves() { - foreach($this->menuEntriesToRemove as $menuToDelete) { - - if(empty($menuToDelete[1])) { + foreach ($this->menuEntriesToRemove as $menuToDelete) { + if (empty($menuToDelete[1])) { // Delete Main Menu - if(isset($this->menu[$menuToDelete[0]])) { + if (isset($this->menu[$menuToDelete[0]])) { unset($this->menu[$menuToDelete[0]]); } } else { // Delete Sub Menu - if(isset($this->menu[$menuToDelete[0]][$menuToDelete[1]])) { + if (isset($this->menu[$menuToDelete[0]][$menuToDelete[1]])) { unset($this->menu[$menuToDelete[0]][$menuToDelete[1]]); } } @@ -202,9 +288,10 @@ abstract class MenuAbstract extends Singleton { foreach ($this->renames as $rename) { $mainMenuOriginal = $rename[0]; - $subMenuOriginal = $rename[1]; - $mainMenuRenamed = $rename[2]; - $subMenuRenamed = $rename[3]; + $subMenuOriginal = $rename[1]; + $mainMenuRenamed = $rename[2]; + $subMenuRenamed = $rename[3]; + // Are we changing a submenu? if (!empty($subMenuOriginal)) { if (isset($this->menu[$mainMenuOriginal][$subMenuOriginal])) { @@ -214,7 +301,7 @@ abstract class MenuAbstract extends Singleton $this->menu[$mainMenuRenamed][$subMenuRenamed] = $save; } } // Changing a first-level element - else if (isset($this->menu[$mainMenuOriginal])) { + elseif (isset($this->menu[$mainMenuOriginal])) { $save = $this->menu[$mainMenuOriginal]; $save['_name'] = $mainMenuRenamed; unset($this->menu[$mainMenuOriginal]); @@ -238,7 +325,7 @@ abstract class MenuAbstract extends Singleton foreach ($this->menu as $key => &$element) { if (is_null($element)) { unset($this->menu[$key]); - } else if ($element['_hasSubmenu']) { + } elseif ($element['_hasSubmenu']) { uasort($element, array($this, 'menuCompare')); } } @@ -255,15 +342,36 @@ abstract class MenuAbstract extends Singleton */ protected function menuCompare($itemOne, $itemTwo) { - if (!is_array($itemOne) || !is_array($itemTwo) - || !isset($itemOne['_order']) || !isset($itemTwo['_order']) - ) { + if (!is_array($itemOne) && !is_array($itemTwo)) { return 0; } - if ($itemOne['_order'] == $itemTwo['_order']) { - return strcmp($itemOne['_name'], $itemTwo['_name']); + if (!is_array($itemOne) && is_array($itemTwo)) { + return -1; } + + if (is_array($itemOne) && !is_array($itemTwo)) { + return 1; + } + + if (!isset($itemOne['_order']) && !isset($itemTwo['_order'])) { + return 0; + } + + if (!isset($itemOne['_order']) && isset($itemTwo['_order'])) { + return -1; + } + + if (isset($itemOne['_order']) && !isset($itemTwo['_order'])) { + return 1; + } + + if ($itemOne['_order'] == $itemTwo['_order']) { + return strcmp( + @$itemOne['_name'], + @$itemTwo['_name']); + } + return ($itemOne['_order'] < $itemTwo['_order']) ? -1 : 1; } } diff --git a/www/analytics/core/Menu/MenuAdmin.php b/www/analytics/core/Menu/MenuAdmin.php index ee985314..9c05734e 100644 --- a/www/analytics/core/Menu/MenuAdmin.php +++ b/www/analytics/core/Menu/MenuAdmin.php @@ -1,6 +1,6 @@ add( + * $menu->add( * 'MyPlugin_MyTranslatedAdminMenuCategory', * 'MyPlugin_MyTranslatedAdminPageName', * array('module' => 'MyPlugin', 'action' => 'index'), @@ -27,25 +27,79 @@ use Piwik\Piwik; * $order = 2 * ); * } - * + * * @method static \Piwik\Menu\MenuAdmin getInstance() */ class MenuAdmin extends MenuAbstract { /** - * Adds a new AdminMenu entry under the 'Settings' category. - * - * @param string $adminMenuName The name of the admin menu entry. Can be a translation token. - * @param string|array $url The URL the admin menu entry should link to, or an array of query parameters - * that can be used to build the URL. - * @param boolean $displayedForCurrentUser Whether this menu entry should be displayed for the - * current user. If false, the entry will not be added. - * @param int $order The order hint. + * See {@link add()}. Adds a new menu item to the development section of the admin menu. + * @param string $menuName + * @param array $url + * @param int $order + * @param bool|string $tooltip * @api + * @since 2.5.0 */ - public static function addEntry($adminMenuName, $url, $displayedForCurrentUser = true, $order = 20) + public function addDevelopmentItem($menuName, $url, $order = 50, $tooltip = false) { - self::getInstance()->add('General_Settings', $adminMenuName, $url, $displayedForCurrentUser, $order); + $this->addItem('CoreAdminHome_MenuDevelopment', $menuName, $url, $order, $tooltip); + } + + /** + * See {@link add()}. Adds a new menu item to the diagnostic section of the admin menu. + * @param string $menuName + * @param array $url + * @param int $order + * @param bool|string $tooltip + * @api + * @since 2.5.0 + */ + public function addDiagnosticItem($menuName, $url, $order = 50, $tooltip = false) + { + $this->addItem('CoreAdminHome_MenuDiagnostic', $menuName, $url, $order, $tooltip); + } + + /** + * See {@link add()}. Adds a new menu item to the platform section of the admin menu. + * @param string $menuName + * @param array $url + * @param int $order + * @param bool|string $tooltip + * @api + * @since 2.5.0 + */ + public function addPlatformItem($menuName, $url, $order = 50, $tooltip = false) + { + $this->addItem('CorePluginsAdmin_MenuPlatform', $menuName, $url, $order, $tooltip); + } + + /** + * See {@link add()}. Adds a new menu item to the settings section of the admin menu. + * @param string $menuName + * @param array $url + * @param int $order + * @param bool|string $tooltip + * @api + * @since 2.5.0 + */ + public function addSettingsItem($menuName, $url, $order = 50, $tooltip = false) + { + $this->addItem('General_Settings', $menuName, $url, $order, $tooltip); + } + + /** + * See {@link add()}. Adds a new menu item to the manage section of the admin menu. + * @param string $menuName + * @param array $url + * @param int $order + * @param bool|string $tooltip + * @api + * @since 2.5.0 + */ + public function addManageItem($menuName, $url, $order = 50, $tooltip = false) + { + $this->addItem('CoreAdminHome_Administration', $menuName, $url, $order, $tooltip); } /** @@ -58,56 +112,16 @@ class MenuAdmin extends MenuAbstract if (!$this->menu) { /** - * Triggered when collecting all available admin menu items. Subscribe to this event if you want - * to add one or more items to the Piwik admin menu. - * - * Menu items should be added via the {@link add()} method. - * - * **Example** - * - * use Piwik\Menu\MenuAdmin; - * - * public function addMenuItems() - * { - * MenuAdmin::getInstance()->add( - * 'MenuName', - * 'SubmenuName', - * array('module' => 'MyPlugin', 'action' => 'index'), - * $showOnlyIf = Piwik::hasUserSuperUserAccess(), - * $order = 6 - * ); - * } + * @ignore + * @deprecated */ - Piwik::postEvent('Menu.Admin.addItems'); - } - return parent::getMenu(); - } + Piwik::postEvent('Menu.Admin.addItems', array()); - /** - * Returns the current AdminMenu name - * - * @return boolean - */ - public function getCurrentAdminMenuName() - { - $menu = MenuAdmin::getInstance()->getMenu(); - $currentModule = Piwik::getModule(); - $currentAction = Piwik::getAction(); - foreach ($menu as $submenu) { - foreach ($submenu as $subMenuName => $parameters) { - if (strpos($subMenuName, '_') !== 0 && - $parameters['_url']['module'] == $currentModule - && $parameters['_url']['action'] == $currentAction - ) { - return $subMenuName; - } + foreach ($this->getAllMenus() as $menu) { + $menu->configureAdminMenu($this); } } - return false; - } - public static function removeEntry($menuName, $subMenuName = false) - { - MenuAdmin::getInstance()->remove($menuName, $subMenuName); + return parent::getMenu(); } } diff --git a/www/analytics/core/Menu/MenuMain.php b/www/analytics/core/Menu/MenuMain.php index 2cbffe0e..adb6b538 100644 --- a/www/analytics/core/Menu/MenuMain.php +++ b/www/analytics/core/Menu/MenuMain.php @@ -1,91 +1,19 @@ add( - * 'MyPlugin_MyTranslatedMenuCategory', - * 'MyPlugin_MyTranslatedMenuName', - * array('module' => 'MyPlugin', 'action' => 'index'), - * Piwik::isUserHasSomeAdminAccess(), - * $order = 2 - * ); - * } - * - * @api - * @method static \Piwik\Menu\MenuMain getInstance() + * @deprecated since 2.4.0 + * @see MenuReporting + * @method static MenuMain getInstance() + * @ignore */ -class MenuMain extends MenuAbstract +class MenuMain extends MenuReporting { - /** - * Returns if the URL was found in the menu. - * - * @param string $url - * @return boolean - */ - public function isUrlFound($url) - { - $menu = MenuMain::getInstance()->getMenu(); - - foreach ($menu as $subMenus) { - foreach ($subMenus as $subMenuName => $menuUrl) { - if (strpos($subMenuName, '_') !== 0 && $menuUrl['_url'] == $url) { - return true; - } - } - } - return false; - } - - /** - * Triggers the Menu.Reporting.addItems hook and returns the menu. - * - * @return Array - */ - public function getMenu() - { - // We trigger the Event only once! - if (!$this->menu) { - - /** - * Triggered when collecting all available reporting menu items. Subscribe to this event if you - * want to add one or more items to the Piwik reporting menu. - * - * Menu items should be added via the {@link add()} method. - * - * **Example** - * - * use Piwik\Menu\Main; - * - * public function addMenuItems() - * { - * Main::getInstance()->add( - * 'CustomMenuName', - * 'CustomSubmenuName', - * array('module' => 'MyPlugin', 'action' => 'index'), - * $showOnlyIf = Piwik::hasUserSuperUserAccess(), - * $order = 6 - * ); - * } - */ - Piwik::postEvent('Menu.Reporting.addItems'); - } - return parent::getMenu(); - } } diff --git a/www/analytics/core/Menu/MenuReporting.php b/www/analytics/core/Menu/MenuReporting.php new file mode 100644 index 00000000..598a7576 --- /dev/null +++ b/www/analytics/core/Menu/MenuReporting.php @@ -0,0 +1,143 @@ +add( + * 'MyPlugin_MyTranslatedMenuCategory', + * 'MyPlugin_MyTranslatedMenuName', + * array('module' => 'MyPlugin', 'action' => 'index'), + * Piwik::isUserHasSomeAdminAccess(), + * $order = 2 + * ); + * } + * + * @api + * @method static \Piwik\Menu\MenuReporting getInstance() + */ +class MenuReporting extends MenuAbstract +{ + + /** + * See {@link add()}. Adds a new menu item to the visitors section of the reporting menu. + * @param string $menuName + * @param array $url + * @param int $order + * @param bool|string $tooltip + * @api + * @since 2.5.0 + */ + public function addVisitorsItem($menuName, $url, $order = 50, $tooltip = false) + { + $this->addItem('General_Visitors', $menuName, $url, $order, $tooltip); + } + + /** + * See {@link add()}. Adds a new menu item to the actions section of the reporting menu. + * @param string $menuName + * @param array $url + * @param int $order + * @param bool|string $tooltip + * @api + * @since 2.5.0 + */ + public function addActionsItem($menuName, $url, $order = 50, $tooltip = false) + { + $this->addItem('General_Actions', $menuName, $url, $order, $tooltip); + } + + /** + * Should not be a public API yet. We probably have to change the API once we have another use case. + * @ignore + */ + public function addGroup($menuName, $defaultTitle, Group $group, $order = 50, $tooltip = false) + { + $this->menuEntries[] = array( + $menuName, + $defaultTitle, + $group, + $order, + $tooltip + ); + } + + /** + * See {@link add()}. Adds a new menu item to the referrers section of the reporting menu. + * @param string $menuName + * @param array $url + * @param int $order + * @param bool|string $tooltip + * @api + * @since 2.5.0 + */ + public function addReferrersItem($menuName, $url, $order = 50, $tooltip = false) + { + $this->addItem('Referrers_Referrers', $menuName, $url, $order, $tooltip); + } + + /** + * Returns if the URL was found in the menu. + * + * @param string $url + * @return boolean + */ + public function isUrlFound($url) + { + $menu = $this->getMenu(); + + foreach ($menu as $subMenus) { + foreach ($subMenus as $subMenuName => $menuUrl) { + if (strpos($subMenuName, '_') !== 0 && $menuUrl['_url'] == $url) { + return true; + } + } + } + return false; + } + + /** + * Triggers the Menu.Reporting.addItems hook and returns the menu. + * + * @return Array + */ + public function getMenu() + { + if (!$this->menu) { + + /** + * @ignore + * @deprecated + */ + Piwik::postEvent('Menu.Reporting.addItems', array()); + + foreach (Report::getAllReports() as $report) { + if ($report->isEnabled()) { + $report->configureReportingMenu($this); + } + } + + foreach ($this->getAllMenus() as $menu) { + $menu->configureReportingMenu($this); + } + } + + return parent::getMenu(); + } +} diff --git a/www/analytics/core/Menu/MenuTop.php b/www/analytics/core/Menu/MenuTop.php index 98d639b4..687b1b46 100644 --- a/www/analytics/core/Menu/MenuTop.php +++ b/www/analytics/core/Menu/MenuTop.php @@ -1,26 +1,25 @@ add( + * $menu->add( * 'MyPlugin_MyTranslatedMenuCategory', * 'MyPlugin_MyTranslatedMenuName', * array('module' => 'MyPlugin', 'action' => 'index'), @@ -28,40 +27,11 @@ use Piwik\Piwik; * $order = 2 * ); * } - * + * * @method static \Piwik\Menu\MenuTop getInstance() */ class MenuTop extends MenuAbstract { - /** - * Adds a new entry to the TopMenu. - * - * @param string $topMenuName The menu item name. Can be a translation token. - * @param string|array $url The URL the admin menu entry should link to, or an array of query parameters - * that can be used to build the URL. If `$isHTML` is true, this can be a string with - * HTML that is simply embedded. - * @param boolean $displayedForCurrentUser Whether this menu entry should be displayed for the - * current user. If false, the entry will not be added. - * @param int $order The order hint. - * @param bool $isHTML Whether `$url` is an HTML string or a URL that will be rendered as a link. - * @param bool|string $tooltip Optional tooltip to display. - * @api - */ - public static function addEntry($topMenuName, $url, $displayedForCurrentUser = true, $order = 10, $isHTML = false, $tooltip = false) - { - if ($isHTML) { - MenuTop::getInstance()->addHtml($topMenuName, $url, $displayedForCurrentUser, $order, $tooltip); - } else { - MenuTop::getInstance()->add($topMenuName, null, $url, $displayedForCurrentUser, $order, $tooltip); - } - } - - public static function removeEntry($menuName, $subMenuName = false) - { - MenuTop::getInstance()->remove($menuName, $subMenuName); - } - - /** * Directly adds a menu entry containing html. * @@ -70,11 +40,13 @@ class MenuTop extends MenuAbstract * @param boolean $displayedForCurrentUser * @param int $order * @param string $tooltip Tooltip to display. + * @api */ public function addHtml($menuName, $data, $displayedForCurrentUser, $order, $tooltip) { if ($displayedForCurrentUser) { if (!isset($this->menu[$menuName])) { + $this->menu[$menuName]['_name'] = $menuName; $this->menu[$menuName]['_html'] = $data; $this->menu[$menuName]['_order'] = $order; $this->menu[$menuName]['_hasSubmenu'] = false; @@ -93,29 +65,16 @@ class MenuTop extends MenuAbstract if (!$this->menu) { /** - * Triggered when collecting all available menu items that are be displayed on the very top of every - * page, next to the login/logout links. - * - * Subscribe to this event if you want to add one or more items to the top menu. - * - * Menu items should be added via the {@link addEntry()} method. - * - * **Example** - * - * use Piwik\Menu\MenuTop; - * - * public function addMenuItems() - * { - * MenuTop::addEntry( - * 'TopMenuName', - * array('module' => 'MyPlugin', 'action' => 'index'), - * $showOnlyIf = Piwik::hasUserSuperUserAccess(), - * $order = 6 - * ); - * } + * @ignore + * @deprecated */ - Piwik::postEvent('Menu.Top.addItems'); + Piwik::postEvent('Menu.Top.addItems', array()); + + foreach ($this->getAllMenus() as $menu) { + $menu->configureTopMenu($this); + } } + return parent::getMenu(); } } diff --git a/www/analytics/core/Menu/MenuUser.php b/www/analytics/core/Menu/MenuUser.php new file mode 100755 index 00000000..ac3bc295 --- /dev/null +++ b/www/analytics/core/Menu/MenuUser.php @@ -0,0 +1,91 @@ +add( + * 'MyPlugin_MyTranslatedMenuCategory', + * 'MyPlugin_MyTranslatedMenuName', + * array('module' => 'MyPlugin', 'action' => 'index'), + * Piwik::isUserHasSomeAdminAccess(), + * $order = 2 + * ); + * } + * + * @method static MenuUser getInstance() + */ +class MenuUser extends MenuAbstract +{ + + /** + * See {@link add()}. Adds a new menu item to the manage section of the user menu. + * @param string $menuName + * @param array $url + * @param int $order + * @param bool|string $tooltip + * @api + * @since 2.5.0 + */ + public function addPersonalItem($menuName, $url, $order = 50, $tooltip = false) + { + $this->addItem('UsersManager_MenuPersonal', $menuName, $url, $order, $tooltip); + } + + /** + * See {@link add()}. Adds a new menu item to the manage section of the user menu. + * @param string $menuName + * @param array $url + * @param int $order + * @param bool|string $tooltip + * @api + * @since 2.5.0 + */ + public function addManageItem($menuName, $url, $order = 50, $tooltip = false) + { + $this->addItem('CoreAdminHome_MenuManage', $menuName, $url, $order, $tooltip); + } + + /** + * See {@link add()}. Adds a new menu item to the platform section of the user menu. + * @param string $menuName + * @param array $url + * @param int $order + * @param bool|string $tooltip + * @api + * @since 2.5.0 + */ + public function addPlatformItem($menuName, $url, $order = 50, $tooltip = false) + { + $this->addItem('CorePluginsAdmin_MenuPlatform', $menuName, $url, $order, $tooltip); + } + + /** + * Triggers the Menu.User.addItems hook and returns the menu. + * + * @return Array + */ + public function getMenu() + { + if (!$this->menu) { + foreach ($this->getAllMenus() as $menu) { + $menu->configureUserMenu($this); + } + } + + return parent::getMenu(); + } +} diff --git a/www/analytics/core/Metrics.php b/www/analytics/core/Metrics.php index b2ae4c73..50cd33c0 100644 --- a/www/analytics/core/Metrics.php +++ b/www/analytics/core/Metrics.php @@ -1,6 +1,6 @@ 'nb_uniq_visitors', + Metrics::INDEX_NB_UNIQ_FINGERPRINTS => 'nb_uniq_fingerprints', Metrics::INDEX_NB_VISITS => 'nb_visits', Metrics::INDEX_NB_ACTIONS => 'nb_actions', + Metrics::INDEX_NB_USERS => 'nb_users', Metrics::INDEX_MAX_ACTIONS => 'max_actions', Metrics::INDEX_SUM_VISIT_LENGTH => 'sum_visit_length', Metrics::INDEX_BOUNCE_COUNT => 'bounce_count', @@ -98,6 +113,7 @@ class Metrics Metrics::INDEX_REVENUE => 'revenue', Metrics::INDEX_GOALS => 'goals', Metrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_nb_uniq_visitors', + Metrics::INDEX_SUM_DAILY_NB_USERS => 'sum_daily_nb_users', // Actions metrics Metrics::INDEX_PAGE_NB_HITS => 'nb_hits', @@ -131,11 +147,14 @@ class Metrics Metrics::INDEX_EVENT_SUM_EVENT_VALUE => 'sum_event_value', Metrics::INDEX_EVENT_MIN_EVENT_VALUE => 'min_event_value', Metrics::INDEX_EVENT_MAX_EVENT_VALUE => 'max_event_value', - Metrics::INDEX_EVENT_NB_HITS_WITH_VALUE => 'nb_events_with_value' + Metrics::INDEX_EVENT_NB_HITS_WITH_VALUE => 'nb_events_with_value', + // Contents + Metrics::INDEX_CONTENT_NB_IMPRESSIONS => 'nb_impressions', + Metrics::INDEX_CONTENT_NB_INTERACTIONS => 'nb_interactions' ); - static public $mappingFromIdToNameGoal = array( + public static $mappingFromIdToNameGoal = array( Metrics::INDEX_GOAL_NB_CONVERSIONS => 'nb_conversions', Metrics::INDEX_GOAL_NB_VISITS_CONVERTED => 'nb_visits_converted', Metrics::INDEX_GOAL_REVENUE => 'revenue', @@ -146,29 +165,44 @@ class Metrics Metrics::INDEX_GOAL_ECOMMERCE_ITEMS => 'items', ); - static protected $metricsAggregatedFromLogs = array( + protected static $metricsAggregatedFromLogs = array( Metrics::INDEX_NB_UNIQ_VISITORS, Metrics::INDEX_NB_VISITS, Metrics::INDEX_NB_ACTIONS, + Metrics::INDEX_NB_USERS, Metrics::INDEX_MAX_ACTIONS, Metrics::INDEX_SUM_VISIT_LENGTH, Metrics::INDEX_BOUNCE_COUNT, Metrics::INDEX_NB_VISITS_CONVERTED, ); - static public function getVisitsMetricNames() + public static function getVisitsMetricNames() { $names = array(); + foreach (self::$metricsAggregatedFromLogs as $metricId) { $names[$metricId] = self::$mappingFromIdToName[$metricId]; } + return $names; } - static public function getMappingFromIdToName() + public static function getMappingFromNameToId() { - $idToName = array_flip(self::$mappingFromIdToName); - return $idToName; + static $nameToId = null; + if ($nameToId === null) { + $nameToId = array_flip(self::$mappingFromIdToName); + } + return $nameToId; + } + + public static function getMappingFromNameToIdGoal() + { + static $nameToId = null; + if ($nameToId === null) { + $nameToId = array_flip(self::$mappingFromIdToNameGoal); + } + return $nameToId; } /** @@ -178,7 +212,7 @@ class Metrics * * @ignore */ - static public function isLowerValueBetter($column) + public static function isLowerValueBetter($column) { $lowerIsBetterPatterns = array( 'bounce', 'exit' @@ -200,11 +234,11 @@ class Metrics * @return string * @ignore */ - static public function getUnit($column, $idSite) + public static function getUnit($column, $idSite) { $nameToUnit = array( '_rate' => '%', - 'revenue' => MetricsFormatter::getCurrencySymbol($idSite), + 'revenue' => Site::getCurrencySymbolFor($idSite), '_time_' => 's' ); @@ -217,8 +251,15 @@ class Metrics return ''; } - static public function getDefaultMetricTranslations() + public static function getDefaultMetricTranslations() { + $cacheId = CacheId::pluginAware('DefaultMetricTranslations'); + $cache = PiwikCache::getTransientCache(); + + if ($cache->contains($cacheId)) { + return $cache->fetch($cacheId); + } + $translations = array( 'label' => 'General_ColumnLabel', 'date' => 'General_Date', @@ -248,6 +289,7 @@ class Metrics $afterEntry = ' ' . Piwik::translate('General_AfterEntry'); $translations['sum_daily_nb_uniq_visitors'] = Piwik::translate('General_ColumnNbUniqVisitors') . $dailySum; + $translations['sum_daily_nb_users'] = Piwik::translate('General_ColumnNbUsers') . $dailySum; $translations['sum_daily_entry_nb_uniq_visitors'] = Piwik::translate('General_ColumnUniqueEntrances') . $dailySum; $translations['sum_daily_exit_nb_uniq_visitors'] = Piwik::translate('General_ColumnUniqueExits') . $dailySum; $translations['entry_nb_actions'] = Piwik::translate('General_ColumnNbActions') . $afterEntry; @@ -262,24 +304,44 @@ class Metrics */ Piwik::postEvent('Metrics.getDefaultMetricTranslations', array(&$translations)); - $translations = array_map(array('\\Piwik\\Piwik','translate'), $translations); + $translations = array_map(array('\\Piwik\\Piwik', 'translate'), $translations); + + $cache->save($cacheId, $translations); return $translations; } - static public function getDefaultMetrics() + public static function getDefaultMetrics() { + $cacheId = CacheId::languageAware('DefaultMetrics'); + $cache = PiwikCache::getTransientCache(); + + if ($cache->contains($cacheId)) { + return $cache->fetch($cacheId); + } + $translations = array( 'nb_visits' => 'General_ColumnNbVisits', 'nb_uniq_visitors' => 'General_ColumnNbUniqVisitors', 'nb_actions' => 'General_ColumnNbActions', + 'nb_users' => 'General_ColumnNbUsers', ); - $translations = array_map(array('\\Piwik\\Piwik','translate'), $translations); + $translations = array_map(array('\\Piwik\\Piwik', 'translate'), $translations); + + $cache->save($cacheId, $translations); + return $translations; } - static public function getDefaultProcessedMetrics() + public static function getDefaultProcessedMetrics() { + $cacheId = CacheId::languageAware('DefaultProcessedMetrics'); + $cache = PiwikCache::getTransientCache(); + + if ($cache->contains($cacheId)) { + return $cache->fetch($cacheId); + } + $translations = array( // Processed in AddColumnsProcessedMetrics 'nb_actions_per_visit' => 'General_ColumnActionsPerVisit', @@ -287,22 +349,25 @@ class Metrics 'bounce_rate' => 'General_ColumnBounceRate', 'conversion_rate' => 'General_ColumnConversionRate', ); - return array_map(array('\\Piwik\\Piwik','translate'), $translations); + $translations = array_map(array('\\Piwik\\Piwik', 'translate'), $translations); + + $cache->save($cacheId, $translations); + + return $translations; } - static public function getReadableColumnName($columnIdRaw) + public static function getReadableColumnName($columnIdRaw) { $mappingIdToName = self::$mappingFromIdToName; if (array_key_exists($columnIdRaw, $mappingIdToName)) { - return $mappingIdToName[$columnIdRaw]; } return $columnIdRaw; } - static public function getMetricIdsToProcessReportTotal() + public static function getMetricIdsToProcessReportTotal() { return array( self::INDEX_NB_VISITS, @@ -321,12 +386,20 @@ class Metrics ); } - static public function getDefaultMetricsDocumentation() + public static function getDefaultMetricsDocumentation() { - $documentation = array( + $cacheId = CacheId::pluginAware('DefaultMetricsDocumentation'); + $cache = PiwikCache::getTransientCache(); + + if ($cache->contains($cacheId)) { + return $cache->fetch($cacheId); + } + + $translations = array( 'nb_visits' => 'General_ColumnNbVisitsDocumentation', 'nb_uniq_visitors' => 'General_ColumnNbUniqVisitorsDocumentation', 'nb_actions' => 'General_ColumnNbActionsDocumentation', + 'nb_users' => 'General_ColumnNbUsersDocumentation', 'nb_actions_per_visit' => 'General_ColumnActionsPerVisitDocumentation', 'avg_time_on_site' => 'General_ColumnAvgTimeOnSiteDocumentation', 'bounce_rate' => 'General_ColumnBounceRateDocumentation', @@ -335,7 +408,19 @@ class Metrics 'nb_hits' => 'General_ColumnPageviewsDocumentation', 'exit_rate' => 'General_ColumnExitRateDocumentation' ); - return array_map(array('\\Piwik\\Piwik','translate'), $documentation); + + /** + * Use this event to register translations for metrics documentation processed by your plugin. + * + * @param string[] $translations The array mapping of column_name => Plugin_TranslationForColumnDocumentation + */ + Piwik::postEvent('Metrics.getDefaultMetricDocumentationTranslations', array(&$translations)); + + $translations = array_map(array('\\Piwik\\Piwik', 'translate'), $translations); + + $cache->save($cacheId, $translations); + + return $translations; } public static function getPercentVisitColumn() diff --git a/www/analytics/core/Metrics/Formatter.php b/www/analytics/core/Metrics/Formatter.php new file mode 100644 index 00000000..078edb23 --- /dev/null +++ b/www/analytics/core/Metrics/Formatter.php @@ -0,0 +1,286 @@ +decimalPoint === null) { + $locale = localeconv(); + + $this->decimalPoint = $locale['decimal_point']; + $this->thousandsSeparator = $locale['thousands_sep']; + } + + return number_format($value, $precision, $this->decimalPoint, $this->thousandsSeparator); + } + + /** + * Returns a prettified time value (in seconds). + * + * @param int $numberOfSeconds The number of seconds. + * @param bool $displayTimeAsSentence If set to true, will output `"5min 17s"`, if false `"00:05:17"`. + * @param bool $round Whether to round to the nearest second or not. + * @return string + * @api + */ + public function getPrettyTimeFromSeconds($numberOfSeconds, $displayTimeAsSentence = false, $round = false) + { + $numberOfSeconds = $round ? (int)$numberOfSeconds : (float)$numberOfSeconds; + + $isNegative = false; + if ($numberOfSeconds < 0) { + $numberOfSeconds = -1 * $numberOfSeconds; + $isNegative = true; + } + + // Display 01:45:17 time format + if ($displayTimeAsSentence === false) { + $days = floor($numberOfSeconds / 86400); + $hours = floor(($reminder = ($numberOfSeconds - $days * 86400)) / 3600); + $minutes = floor(($reminder = ($reminder - $hours * 3600)) / 60); + $seconds = floor($reminder - $minutes * 60); + if ($days == 0) { + $time = sprintf("%02s", $hours) . ':' . sprintf("%02s", $minutes) . ':' . sprintf("%02s", $seconds); + } else { + $time = sprintf(Piwik::translate('Intl_NDays'), $days) . " " . sprintf("%02s", $hours) . ':' . sprintf("%02s", $minutes) . ':' . sprintf("%02s", $seconds); + } + $centiSeconds = ($numberOfSeconds * 100) % 100; + if ($centiSeconds) { + $time .= '.' . sprintf("%02s", $centiSeconds); + } + if ($isNegative) { + $time = '-' . $time; + } + return $time; + } + $secondsInYear = 86400 * 365.25; + + $years = floor($numberOfSeconds / $secondsInYear); + $minusYears = $numberOfSeconds - $years * $secondsInYear; + $days = floor($minusYears / 86400); + + $minusDays = $numberOfSeconds - $days * 86400; + $hours = floor($minusDays / 3600); + + $minusDaysAndHours = $minusDays - $hours * 3600; + $minutes = floor($minusDaysAndHours / 60); + + $seconds = $minusDaysAndHours - $minutes * 60; + $precision = ($seconds > 0 && $seconds < 0.01 ? 3 : 2); + $seconds = NumberFormatter::getInstance()->formatNumber(round($seconds, $precision), $precision); + + if ($years > 0) { + $return = sprintf(Piwik::translate('General_YearsDays'), $years, $days); + } elseif ($days > 0) { + $return = sprintf(Piwik::translate('General_DaysHours'), $days, $hours); + } elseif ($hours > 0) { + $return = sprintf(Piwik::translate('General_HoursMinutes'), $hours, $minutes); + } elseif ($minutes > 0) { + $return = sprintf(Piwik::translate('General_MinutesSeconds'), $minutes, $seconds); + } else { + $return = sprintf(Piwik::translate('Intl_NSecondsShort'), $seconds); + } + + if ($isNegative) { + $return = '-' . $return; + } + + return $return; + } + + /** + * Returns a prettified memory size value. + * + * @param number $size The size in bytes. + * @param string $unit The specific unit to use, if any. If null, the unit is determined by $size. + * @param int $precision The precision to use when rounding. + * @return string eg, `'128 M'` or `'256 K'`. + * @api + */ + public function getPrettySizeFromBytes($size, $unit = null, $precision = 1) + { + if ($size == 0) { + return '0 M'; + } + + list($size, $sizeUnit) = $this->getPrettySizeFromBytesWithUnit($size, $unit, $precision); + return $size . " " . $sizeUnit; + } + + /** + * Returns a pretty formated monetary value using the currency associated with a site. + * + * @param int|string $value The monetary value to format. + * @param int $idSite The ID of the site whose currency will be used. + * @return string + * @api + */ + public function getPrettyMoney($value, $idSite) + { + $space = ' '; + $currencySymbol = Site::getCurrencySymbolFor($idSite); + $currencyBefore = $currencySymbol . $space; + $currencyAfter = ''; + // (maybe more currencies prefer this notation?) + $currencySymbolToAppend = array('€', 'kr', 'zł'); + // manually put the currency symbol after the amount + if (in_array($currencySymbol, $currencySymbolToAppend)) { + $currencyAfter = $space . $currencySymbol; + $currencyBefore = ''; + } + // if the input is a number (it could be a string or INPUT form), + // and if this number is not an int, we round to precision 2 + if (is_numeric($value)) { + if ($value == round($value)) { + // 0.0 => 0 + $value = round($value); + } else { + $precision = GoalManager::REVENUE_PRECISION; + $value = sprintf("%01." . $precision . "f", $value); + } + } + $prettyMoney = $currencyBefore . $value . $currencyAfter; + return $prettyMoney; + } + + /** + * Returns a percent string from a quotient value. Forces the use of a '.' + * decimal place. + * + * @param float $value + * @return string + * @api + */ + public function getPrettyPercentFromQuotient($value) + { + $result = ($value * 100) . '%'; + return Common::forceDotAsSeparatorForDecimalPoint($result); + } + + /** + * Formats all metrics, including processed metrics, for a DataTable. Metrics to format + * are found through report metadata and DataTable metadata. + * + * @param DataTable $dataTable The table to format metrics for. + * @param Report|null $report The report the table belongs to. + * @param string[]|null $metricsToFormat Whitelist of names of metrics to format. + * @api + */ + public function formatMetrics(DataTable $dataTable, Report $report = null, $metricsToFormat = null) + { + $metrics = $this->getMetricsToFormat($dataTable, $report); + if (empty($metrics) + || $dataTable->getMetadata(self::PROCESSED_METRICS_FORMATTED_FLAG) + ) { + return; + } + + $dataTable->setMetadata(self::PROCESSED_METRICS_FORMATTED_FLAG, true); + + if ($metricsToFormat !== null) { + $metricMatchRegex = $this->makeRegexToMatchMetrics($metricsToFormat); + $metrics = array_filter($metrics, function (ProcessedMetric $metric) use ($metricMatchRegex) { + return preg_match($metricMatchRegex, $metric->getName()); + }); + } + + foreach ($metrics as $name => $metric) { + if (!$metric->beforeFormat($report, $dataTable)) { + continue; + } + + foreach ($dataTable->getRows() as $row) { + $columnValue = $row->getColumn($name); + if ($columnValue !== false) { + $row->setColumn($name, $metric->format($columnValue, $this)); + } + + $subtable = $row->getSubtable(); + if (!empty($subtable)) { + $this->formatMetrics($subtable, $report, $metricsToFormat); + } + } + } + } + + protected function getPrettySizeFromBytesWithUnit($size, $unit = null, $precision = 1) + { + $units = array('B', 'K', 'M', 'G', 'T'); + $numUnits = count($units) - 1; + + $currentUnit = null; + foreach ($units as $idx => $currentUnit) { + if ($unit && $unit !== $currentUnit) { + $size = $size / 1024; + } elseif ($unit && $unit === $currentUnit) { + break; + } elseif ($size >= 1024 && $idx != $numUnits) { + $size = $size / 1024; + } else { + break; + } + } + + $size = round($size, $precision); + + return array($size, $currentUnit); + } + + private function makeRegexToMatchMetrics($metricsToFormat) + { + $metricsRegexParts = array(); + foreach ($metricsToFormat as $metricFilter) { + if ($metricFilter[0] == '/') { + $metricsRegexParts[] = '(?:' . substr($metricFilter, 1, strlen($metricFilter) - 2) . ')'; + } else { + $metricsRegexParts[] = preg_quote($metricFilter); + } + } + return '/^' . implode('|', $metricsRegexParts) . '$/'; + } + + /** + * @param DataTable $dataTable + * @param Report $report + * @return Metric[] + */ + private function getMetricsToFormat(DataTable $dataTable, Report $report = null) + { + return Report::getMetricsForTable($dataTable, $report, $baseType = 'Piwik\\Plugin\\Metric'); + } +} diff --git a/www/analytics/core/Metrics/Formatter/Html.php b/www/analytics/core/Metrics/Formatter/Html.php new file mode 100644 index 00000000..60c2f43c --- /dev/null +++ b/www/analytics/core/Metrics/Formatter/Html.php @@ -0,0 +1,43 @@ +replaceSpaceWithNonBreakingSpace($result); + return $result; + } + + public function getPrettySizeFromBytes($size, $unit = null, $precision = 1) + { + $result = parent::getPrettySizeFromBytes($size, $unit, $precision); + $result = $this->replaceSpaceWithNonBreakingSpace($result); + return $result; + } + + public function getPrettyMoney($value, $idSite) + { + $result = parent::getPrettyMoney($value, $idSite); + $result = $this->replaceSpaceWithNonBreakingSpace($result); + return $result; + } + + private function replaceSpaceWithNonBreakingSpace($value) + { + return str_replace(' ', ' ', $value); + } +} diff --git a/www/analytics/core/MetricsFormatter.php b/www/analytics/core/MetricsFormatter.php index 10efb19c..0bf5a1a7 100644 --- a/www/analytics/core/MetricsFormatter.php +++ b/www/analytics/core/MetricsFormatter.php @@ -1,242 +1,72 @@ getPrettyNumber($value); } - /** - * Returns a prettified time value (in seconds). - * - * @param int $numberOfSeconds The number of seconds. - * @param bool $displayTimeAsSentence If set to true, will output `"5min 17s"`, if false `"00:05:17"`. - * @param bool $isHtml If true, replaces all spaces with `' '`. - * @param bool $round Whether to round to the nearest second or not. - * @return string - */ public static function getPrettyTimeFromSeconds($numberOfSeconds, $displayTimeAsSentence = true, $isHtml = true, $round = false) { - $numberOfSeconds = $round ? (int)$numberOfSeconds : (float)$numberOfSeconds; - - $isNegative = false; - if ($numberOfSeconds < 0) { - $numberOfSeconds = -1 * $numberOfSeconds; - $isNegative = true; - } - - // Display 01:45:17 time format - if ($displayTimeAsSentence === false) { - $hours = floor($numberOfSeconds / 3600); - $minutes = floor(($reminder = ($numberOfSeconds - $hours * 3600)) / 60); - $seconds = floor($reminder - $minutes * 60); - $time = sprintf("%02s", $hours) . ':' . sprintf("%02s", $minutes) . ':' . sprintf("%02s", $seconds); - $centiSeconds = ($numberOfSeconds * 100) % 100; - if ($centiSeconds) { - $time .= '.' . sprintf("%02s", $centiSeconds); - } - if ($isNegative) { - $time = '-' . $time; - } - return $time; - } - $secondsInYear = 86400 * 365.25; - - $years = floor($numberOfSeconds / $secondsInYear); - $minusYears = $numberOfSeconds - $years * $secondsInYear; - $days = floor($minusYears / 86400); - - $minusDays = $numberOfSeconds - $days * 86400; - $hours = floor($minusDays / 3600); - - $minusDaysAndHours = $minusDays - $hours * 3600; - $minutes = floor($minusDaysAndHours / 60); - - $seconds = $minusDaysAndHours - $minutes * 60; - $precision = ($seconds > 0 && $seconds < 0.01 ? 3 : 2); - $seconds = round($seconds, $precision); - - if ($years > 0) { - $return = sprintf(Piwik::translate('General_YearsDays'), $years, $days); - } elseif ($days > 0) { - $return = sprintf(Piwik::translate('General_DaysHours'), $days, $hours); - } elseif ($hours > 0) { - $return = sprintf(Piwik::translate('General_HoursMinutes'), $hours, $minutes); - } elseif ($minutes > 0) { - $return = sprintf(Piwik::translate('General_MinutesSeconds'), $minutes, $seconds); - } else { - $return = sprintf(Piwik::translate('General_Seconds'), $seconds); - } - - if ($isNegative) { - $return = '-' . $return; - } - - if ($isHtml) { - return str_replace(' ', ' ', $return); - } - return $return; + return self::getFormatter($isHtml)->getPrettyTimeFromSeconds($numberOfSeconds, $displayTimeAsSentence, $round); } - /** - * Returns a prettified memory size value. - * - * @param number $size The size in bytes. - * @param string $unit The specific unit to use, if any. If null, the unit is determined by $size. - * @param int $precision The precision to use when rounding. - * @return string eg, `'128 M'` or `'256 K'`. - */ public static function getPrettySizeFromBytes($size, $unit = null, $precision = 1) { - if ($size == 0) { - return '0 M'; - } - - $units = array('B', 'K', 'M', 'G', 'T'); - foreach ($units as $currentUnit) { - if ($size >= 1024 && $unit != $currentUnit) { - $size = $size / 1024; - } else { - break; - } - } - return round($size, $precision) . " " . $currentUnit; + return self::getFormatter()->getPrettySizeFromBytes($size, $unit, $precision); } - /** - * Returns a pretty formated monetary value using the currency associated with a site. - * - * @param int|string $value The monetary value to format. - * @param int $idSite The ID of the site whose currency will be used. - * @param bool $isHtml If true, replaces all spaces with `' '`. - * @return string - */ public static function getPrettyMoney($value, $idSite, $isHtml = true) { - $currencyBefore = MetricsFormatter::getCurrencySymbol($idSite); - - $space = ' '; - if ($isHtml) { - $space = ' '; - } - - $currencyAfter = ''; - // manually put the currency symbol after the amount for euro - // (maybe more currencies prefer this notation?) - if (in_array($currencyBefore, array('€', 'kr'))) { - $currencyAfter = $space . $currencyBefore; - $currencyBefore = ''; - } - - // if the input is a number (it could be a string or INPUT form), - // and if this number is not an int, we round to precision 2 - if (is_numeric($value)) { - if ($value == round($value)) { - // 0.0 => 0 - $value = round($value); - } else { - $precision = GoalManager::REVENUE_PRECISION; - $value = sprintf("%01." . $precision . "f", $value); - } - } - $prettyMoney = $currencyBefore . $space . $value . $currencyAfter; - return $prettyMoney; + return self::getFormatter($isHtml)->getPrettyMoney($value, $idSite); } - /** - * Prettifies a metric value based on the column name. - * - * @param int $idSite The ID of the site the metric is for (used if the column value is an amount of money). - * @param string $columnName The metric name. - * @param mixed $value The metric value. - * @param bool $isHtml If true, replaces all spaces with `' '`. - * @return string - */ public static function getPrettyValue($idSite, $columnName, $value, $isHtml) { - // Display time in human readable - if (strpos($columnName, 'time') !== false) { - // Little hack: Display 15s rather than 00:00:15, only for "(avg|min|max)_generation_time" - $timeAsSentence = (substr($columnName, -16) == '_time_generation'); - return self::getPrettyTimeFromSeconds($value, $timeAsSentence); - } - // Add revenue symbol to revenues - if (strpos($columnName, 'revenue') !== false && strpos($columnName, 'evolution') === false) { - return self::getPrettyMoney($value, $idSite, $isHtml); - } - // Add % symbol to rates - if (strpos($columnName, '_rate') !== false) { - if (strpos($value, "%") === false) { - return $value . "%"; - } - } - return $value; + return ProcessedReport::getPrettyValue(self::getFormatter($isHtml), $idSite, $columnName, $value); } - /** - * Returns the currency symbol for a site. - * - * @param int $idSite The ID of the site to return the currency symbol for. - * @return string eg, `'$'`. - */ public static function getCurrencySymbol($idSite) { - $symbols = MetricsFormatter::getCurrencyList(); - $site = new Site($idSite); - $currency = $site->getCurrency(); - if (isset($symbols[$currency])) { - return $symbols[$currency][0]; - } - return ''; + return Site::getCurrencySymbolFor($idSite); } - /** - * Returns the list of all known currency symbols. - * - * @return array An array mapping currency codes to their respective currency symbols - * and a description, eg, `array('USD' => array('$', 'US dollar'))`. - */ public static function getCurrencyList() { - static $currenciesList = null; - if (is_null($currenciesList)) { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Currencies.php'; - $currenciesList = $GLOBALS['Piwik_CurrencyList']; - } - return $currenciesList; + return Site::getCurrencyList(); } } diff --git a/www/analytics/core/Nonce.php b/www/analytics/core/Nonce.php index 1538e312..1b64b879 100644 --- a/www/analytics/core/Nonce.php +++ b/www/analytics/core/Nonce.php @@ -1,6 +1,6 @@ nonce; @@ -100,7 +100,7 @@ class Nonce * * @param string $id The unique nonce ID. */ - static public function discardNonce($id) + public static function discardNonce($id) { $ns = new SessionNamespace($id); $ns->unsetAll(); @@ -108,10 +108,10 @@ class Nonce /** * Returns the **Origin** HTTP header or `false` if not found. - * + * * @return string|bool */ - static public function getOrigin() + public static function getOrigin() { if (!empty($_SERVER['HTTP_ORIGIN'])) { return $_SERVER['HTTP_ORIGIN']; @@ -124,7 +124,7 @@ class Nonce * * @return array */ - static public function getAcceptableOrigins() + public static function getAcceptableOrigins() { $host = Url::getCurrentHost(null); $port = ''; @@ -140,8 +140,10 @@ class Nonce } // standard ports - $origins[] = 'http://' . $host; - $origins[] = 'https://' . $host; + $origins = array( + 'http://' . $host, + 'https://' . $host, + ); // non-standard ports if (!empty($port) && $port != 80 && $port != 443) { @@ -154,13 +156,13 @@ class Nonce /** * Verifies and discards a nonce. - * + * * @param string $nonceName The nonce's unique ID. See {@link getNonce()}. * @param string|null $nonce The nonce from the client. If `null`, the value from the * **nonce** query parameter is used. - * @throws Exception if the nonce is invalid. See {@link verifyNonce()}. + * @throws \Exception if the nonce is invalid. See {@link verifyNonce()}. */ - static public function checkNonce($nonceName, $nonce = null) + public static function checkNonce($nonceName, $nonce = null) { if ($nonce === null) { $nonce = Common::getRequestVar('nonce', null, 'string'); diff --git a/www/analytics/core/Notification.php b/www/analytics/core/Notification.php index b7465fd6..b7c576eb 100644 --- a/www/analytics/core/Notification.php +++ b/www/analytics/core/Notification.php @@ -1,6 +1,6 @@ context = Notification::CONTEXT_ERROR; * Notification\Manager::notify('myUniqueNotificationId', $notification); - * + * * **Display a temporary success message** - * + * * $notification = new Notificiation('Success'); * $notification->context = Notification::CONTEXT_SUCCESS; * $notification->type = Notification::TYPE_TOAST; * Notification\Manager::notify('myUniqueNotificationId', $notification); - * + * * **Display a message near the top of the screen** - * + * * $notification = new Notification('Urgent: Your password has expired!'); * $notification->context = Notification::CONTEXT_INFO; * $notification->type = Notification::TYPE_PERSISTENT; * $notification->priority = Notification::PRIORITY_MAX; - * + * * @api */ class Notification @@ -75,7 +75,7 @@ class Notification /** * If this flag is applied, no close icon will be displayed. _Note: persistent notifications always have a close * icon._ - * + * * See {@link $flags}. */ const FLAG_NO_CLEAR = 1; @@ -99,39 +99,39 @@ class Notification /** * The notification title. The title is optional and is displayed directly before the message content. - * + * * @var string */ public $title; /** * The notification message. Must be set. - * + * * @var string */ public $message; /** * Contains extra display options. - * + * * Usage: `$notification->flags = Notification::FLAG_BAR | Notification::FLAG_FOO`. - * + * * @var int */ public $flags = self::FLAG_NO_CLEAR; /** * The notification's display type. See `TYPE_*` constants in {@link Notification}. - * + * * @var string */ public $type = self::TYPE_TRANSIENT; /** * The notification's context (message type). See `CONTEXT_*` constants in {@link Notification}. - * + * * A notification's context determines how it will be styled. - * + * * @var string */ public $context = self::CONTEXT_INFO; @@ -139,7 +139,7 @@ class Notification /** * The notification's priority. The higher the priority, the higher the order. See `PRIORITY_*` * constants in {@link Notification} to see possible priority values. - * + * * @var int */ public $priority; @@ -147,14 +147,14 @@ class Notification /** * If true, the message will not be escaped before being outputted as HTML. If you set this to * `true`, make sure you escape text yourself in order to avoid XSS vulnerabilities. - * + * * @var bool */ public $raw = false; /** * Constructor. - * + * * @param string $message The notification message. * @throws \Exception If the message is empty. */ @@ -169,7 +169,7 @@ class Notification /** * Returns `1` if the notification will be displayed without a close button, `0` if otherwise. - * + * * @return int `1` or `0`. */ public function hasNoClear() @@ -184,7 +184,7 @@ class Notification /** * Returns the notification's priority. If no priority has been set, a priority will be set based * on the notification's context. - * + * * @return int */ public function getPriority() diff --git a/www/analytics/core/Notification/Manager.php b/www/analytics/core/Notification/Manager.php index 57471e1a..e7eafec4 100644 --- a/www/analytics/core/Notification/Manager.php +++ b/www/analytics/core/Notification/Manager.php @@ -1,6 +1,6 @@ notifications[$id] = $notification; } + private static function removeOldestNotificationsIfThereAreTooMany() + { + $maxNotificationsInSession = 30; + + $session = static::getSession(); + + while (count($session->notifications) >= $maxNotificationsInSession) { + array_shift($session->notifications); + } + } + private static function getAllNotifications() { + if (!self::isEnabled()) { + return array(); + } + $session = static::getSession(); return $session->notifications; @@ -116,12 +137,21 @@ class Manager private static function removeNotification($id) { + if (!self::isEnabled()) { + return; + } + $session = static::getSession(); if (array_key_exists($id, $session->notifications)) { unset($session->notifications[$id]); } } + private static function isEnabled() + { + return Session::isWritable() && Session::isReadable(); + } + /** * @return SessionNamespace */ @@ -131,7 +161,7 @@ class Manager static::$session = new SessionNamespace('notification'); } - if (empty(static::$session->notifications)) { + if (empty(static::$session->notifications) && self::isEnabled()) { static::$session->notifications = array(); } diff --git a/www/analytics/core/NumberFormatter.php b/www/analytics/core/NumberFormatter.php new file mode 100644 index 00000000..2b8656b8 --- /dev/null +++ b/www/analytics/core/NumberFormatter.php @@ -0,0 +1,325 @@ +patternNumber = $translator->translate('Intl_NumberFormatNumber'); + $this->patternCurrency = $translator->translate('Intl_NumberFormatCurrency'); + $this->patternPercent = $translator->translate('Intl_NumberFormatPercent'); + $this->symbolPlus = $translator->translate('Intl_NumberSymbolPlus'); + $this->symbolMinus = $translator->translate('Intl_NumberSymbolMinus'); + $this->symbolPercent = $translator->translate('Intl_NumberSymbolPercent'); + $this->symbolGroup = $translator->translate('Intl_NumberSymbolGroup'); + $this->symbolDecimal = $translator->translate('Intl_NumberSymbolDecimal'); + } + + /** + * Parses the given pattern and returns patterns for positive and negative numbers + * + * @param string $pattern + * @return array + */ + protected function parsePattern($pattern) + { + $patterns = explode(';', $pattern); + if (!isset($patterns[1])) { + // No explicit negative pattern was provided, construct it. + $patterns[1] = '-' . $patterns[0]; + } + return $patterns; + } + + /** + * Formats a given number or percent value (if $value starts or ends with a %) + * + * @param string|int|float $value + * @param int $maximumFractionDigits + * @param int $minimumFractionDigits + * @return mixed|string + */ + public function format($value, $maximumFractionDigits=0, $minimumFractionDigits=0) + { + if (is_string($value) + && trim($value, '%') != $value + ) { + return $this->formatPercent($value, $maximumFractionDigits, $minimumFractionDigits); + } + + return $this->formatNumber($value, $maximumFractionDigits, $minimumFractionDigits); + } + + /** + * Formats a given number + * + * @see \Piwik\NumberFormatter::format() + * + * @param string|int|float $value + * @param int $maximumFractionDigits + * @param int $minimumFractionDigits + * @return mixed|string + */ + public function formatNumber($value, $maximumFractionDigits=0, $minimumFractionDigits=0) + { + + static $positivePattern, $negativePattern; + + if (empty($positivePatter) || empty($negativePattern)) { + list($positivePattern, $negativePattern) = $this->parsePattern($this->patternNumber); + } + $negative = $this->isNegative($value); + $pattern = $negative ? $negativePattern : $positivePattern; + + return $this->formatNumberWithPattern($pattern, $value, $maximumFractionDigits, $minimumFractionDigits); + } + + /** + * Formats given number as percent value + * @param string|int|float $value + * @param int $maximumFractionDigits + * @param int $minimumFractionDigits + * @return mixed|string + */ + public function formatPercent($value, $maximumFractionDigits=0, $minimumFractionDigits=0) + { + static $positivePattern, $negativePattern; + + if (empty($positivePatter) || empty($negativePattern)) { + list($positivePattern, $negativePattern) = $this->parsePattern($this->patternPercent); + } + + $newValue = trim($value, " \0\x0B%"); + if (!is_numeric($newValue)) { + return $value; + } + + $negative = $this->isNegative($value); + $pattern = $negative ? $negativePattern : $positivePattern; + + return $this->formatNumberWithPattern($pattern, $newValue, $maximumFractionDigits, $minimumFractionDigits); + } + + + /** + * Formats given number as percent value, but keep the leading + sign if found + * + * @param $value + * @return string + */ + public function formatPercentEvolution($value) + { + $isPositiveEvolution = !empty($value) && ($value > 0 || $value[0] == '+'); + + $formatted = self::formatPercent($value); + + if($isPositiveEvolution) { + return $this->symbolPlus . $formatted; + } + return $formatted; + } + + /** + * Formats given number as percent value + * @param string|int|float $value + * @param string $currency + * @param int $precision + * @return mixed|string + */ + public function formatCurrency($value, $currency, $precision=2) + { + static $positivePattern, $negativePattern; + + if (empty($positivePatter) || empty($negativePattern)) { + list($positivePattern, $negativePattern) = $this->parsePattern($this->patternCurrency); + } + + $newValue = trim($value, " \0\x0B$currency"); + if (!is_numeric($newValue)) { + return $value; + } + + $negative = $this->isNegative($value); + $pattern = $negative ? $negativePattern : $positivePattern; + + if ($newValue == round($newValue)) { + // if no fraction digits available, don't show any + $value = $this->formatNumberWithPattern($pattern, $newValue, 0, 0); + } else { + // show given count of fraction digits otherwise + $value = $this->formatNumberWithPattern($pattern, $newValue, $precision, $precision); + } + + return str_replace('¤', $currency, $value); + } + + /** + * Formats the given number with the given pattern + * + * @param string $pattern + * @param string|int|float $value + * @param int $maximumFractionDigits + * @param int $minimumFractionDigits + * @return mixed|string + */ + protected function formatNumberWithPattern($pattern, $value, $maximumFractionDigits=0, $minimumFractionDigits=0) + { + if (!is_numeric($value)) { + return $value; + } + + $this->usesGrouping = (strpos($pattern, ',') !== false); + // if pattern has number groups, parse them. + if ($this->usesGrouping) { + preg_match('/#+0/', $pattern, $primaryGroupMatches); + $this->primaryGroupSize = $this->secondaryGroupSize = strlen($primaryGroupMatches[0]); + $numberGroups = explode(',', $pattern); + // check for distinct secondary group size. + if (count($numberGroups) > 2) { + $this->secondaryGroupSize = strlen($numberGroups[1]); + } + } + + // Ensure that the value is positive and has the right number of digits. + $negative = $this->isNegative($value); + $signMultiplier = $negative ? '-1' : '1'; + $value = $value / $signMultiplier; + $value = round($value, $maximumFractionDigits); + // Split the number into major and minor digits. + $valueParts = explode('.', $value); + $majorDigits = $valueParts[0]; + // Account for maximumFractionDigits = 0, where the number won't + // have a decimal point, and $valueParts[1] won't be set. + $minorDigits = isset($valueParts[1]) ? $valueParts[1] : ''; + if ($this->usesGrouping) { + // Reverse the major digits, since they are grouped from the right. + $majorDigits = array_reverse(str_split($majorDigits)); + // Group the major digits. + $groups = array(); + $groups[] = array_splice($majorDigits, 0, $this->primaryGroupSize); + while (!empty($majorDigits)) { + $groups[] = array_splice($majorDigits, 0, $this->secondaryGroupSize); + } + // Reverse the groups and the digits inside of them. + $groups = array_reverse($groups); + foreach ($groups as &$group) { + $group = implode(array_reverse($group)); + } + // Reconstruct the major digits. + $majorDigits = implode(',', $groups); + } + if ($minimumFractionDigits < $maximumFractionDigits) { + // Strip any trailing zeroes. + $minorDigits = rtrim($minorDigits, '0'); + if (strlen($minorDigits) < $minimumFractionDigits) { + // Now there are too few digits, re-add trailing zeroes + // until the desired length is reached. + $neededZeroes = $minimumFractionDigits - strlen($minorDigits); + $minorDigits .= str_repeat('0', $neededZeroes); + } + } + // Assemble the final number and insert it into the pattern. + $value = $minorDigits ? $majorDigits . '.' . $minorDigits : $majorDigits; + $value = preg_replace('/#(?:[\.,]#+)*0(?:[,\.][0#]+)*/', $value, $pattern); + // Localize the number. + $value = $this->replaceSymbols($value); + return $value; + } + + + /** + * Replaces number symbols with their localized equivalents. + * + * @param string $value The value being formatted. + * + * @return string + * + * @see http://cldr.unicode.org/translation/number-symbols + */ + protected function replaceSymbols($value) + { + $replacements = array( + '.' => $this->symbolDecimal, + ',' => $this->symbolGroup, + '+' => $this->symbolPlus, + '-' => $this->symbolMinus, + '%' => $this->symbolPercent, + ); + return strtr($value, $replacements); + } + + /** + * @param $value + * @return bool + */ + protected function isNegative($value) + { + return $value < 0; + } + + /** + * @deprecated + * @return self + */ + public static function getInstance() + { + return StaticContainer::get('Piwik\NumberFormatter'); + } +} \ No newline at end of file diff --git a/www/analytics/core/Option.php b/www/analytics/core/Option.php index 4a98138d..d79df967 100644 --- a/www/analytics/core/Option.php +++ b/www/analytics/core/Option.php @@ -1,6 +1,6 @@ setValue($name, $value, $autoload); + self::getInstance()->setValue($name, $value, $autoload); } /** @@ -79,7 +79,7 @@ class Option */ public static function delete($name, $value = null) { - return self::getInstance()->deleteValue($name, $value); + self::getInstance()->deleteValue($name, $value); } /** @@ -91,7 +91,7 @@ class Option */ public static function deleteLike($namePattern, $value = null) { - return self::getInstance()->deleteNameLike($namePattern, $value); + self::getInstance()->deleteNameLike($namePattern, $value); } public static function clearCachedOption($name) @@ -127,21 +127,33 @@ class Option * Singleton instance * @var \Piwik\Option */ - static private $instance = null; + private static $instance = null; /** * Returns Singleton instance * * @return \Piwik\Option */ - static private function getInstance() + private static function getInstance() { if (self::$instance == null) { self::$instance = new self; } + return self::$instance; } + /** + * Sets the singleton instance. For testing purposes. + * + * @param mixed + * @ignore + */ + public static function setSingletonInstance($instance) + { + self::$instance = $instance; + } + /** * Private Constructor */ @@ -162,12 +174,14 @@ class Option if (isset($this->all[$name])) { return $this->all[$name]; } - $value = Db::fetchOne('SELECT option_value ' . - 'FROM `' . Common::prefixTable('option') . '`' . - 'WHERE option_name = ?', $name); + + $value = Db::fetchOne('SELECT option_value FROM `' . Common::prefixTable('option') . '` ' . + 'WHERE option_name = ?', $name); + if ($value === false) { return false; } + $this->all[$name] = $value; return $value; } @@ -175,20 +189,24 @@ class Option protected function setValue($name, $value, $autoLoad = 0) { $autoLoad = (int)$autoLoad; - Db::query('INSERT INTO `' . Common::prefixTable('option') . '` (option_name, option_value, autoload) ' . - ' VALUES (?, ?, ?) ' . - ' ON DUPLICATE KEY UPDATE option_value = ?', - array($name, $value, $autoLoad, $value)); + + $sql = 'INSERT INTO `' . Common::prefixTable('option') . '` (option_name, option_value, autoload) ' . + ' VALUES (?, ?, ?) ' . + ' ON DUPLICATE KEY UPDATE option_value = ?'; + $bind = array($name, $value, $autoLoad, $value); + + Db::query($sql, $bind); + $this->all[$name] = $value; } protected function deleteValue($name, $value) { - $sql = 'DELETE FROM `' . Common::prefixTable('option') . '` WHERE option_name = ?'; + $sql = 'DELETE FROM `' . Common::prefixTable('option') . '` WHERE option_name = ?'; $bind[] = $name; if (isset($value)) { - $sql .= ' AND option_value = ?'; + $sql .= ' AND option_value = ?'; $bind[] = $value; } @@ -199,11 +217,11 @@ class Option protected function deleteNameLike($name, $value = null) { - $sql = 'DELETE FROM `' . Common::prefixTable('option') . '` WHERE option_name LIKE ?'; + $sql = 'DELETE FROM `' . Common::prefixTable('option') . '` WHERE option_name LIKE ?'; $bind[] = $name; if (isset($value)) { - $sql .= ' AND option_value = ?'; + $sql .= ' AND option_value = ?'; $bind[] = $value; } @@ -214,13 +232,15 @@ class Option protected function getNameLike($name) { - $sql = 'SELECT option_name, option_value FROM ' . Common::prefixTable('option') . ' WHERE option_name LIKE ?'; + $sql = 'SELECT option_name, option_value FROM `' . Common::prefixTable('option') . '` WHERE option_name LIKE ?'; $bind = array($name); + $rows = Db::fetchAll($sql, $bind); $result = array(); - foreach (Db::fetchAll($sql, $bind) as $row) { + foreach ($rows as $row) { $result[$row['option_name']] = $row['option_value']; } + return $result; } @@ -235,9 +255,10 @@ class Option return; } - $all = Db::fetchAll('SELECT option_value, option_name - FROM `' . Common::prefixTable('option') . '` - WHERE autoload = 1'); + $table = Common::prefixTable('option'); + $sql = 'SELECT option_value, option_name FROM `' . $table . '` WHERE autoload = 1'; + $all = Db::fetchAll($sql); + foreach ($all as $option) { $this->all[$option['option_name']] = $option['option_value']; } diff --git a/www/analytics/core/Period.php b/www/analytics/core/Period.php index f97fbcaa..0abd1f4b 100644 --- a/www/analytics/core/Period.php +++ b/www/analytics/core/Period.php @@ -1,6 +1,6 @@ date = clone $date; - } - /** - * Creates a new Period instance with a period ID and {@link Date} instance. - * - * _Note: This method cannot create {@link Period\Range} periods._ - * - * @param string $strPeriod `"day"`, `"week"`, `"month"`, `"year"`, `"range"`. - * @param Date|string $date A date within the period or the range of dates. - * @throws Exception If `$strPeriod` is invalid. - * @return \Piwik\Period - */ - static public function factory($strPeriod, $date) - { - if (is_string($date)) { - if (Period::isMultiplePeriod($date, $strPeriod) || $strPeriod == 'range') { - return new Range($strPeriod, $date); - } - - $date = Date::factory($date); - } - - switch ($strPeriod) { - case 'day': - return new Day($date); - break; - - case 'week': - return new Week($date); - break; - - case 'month': - return new Month($date); - break; - - case 'year': - return new Year($date); - break; - - default: - $message = Piwik::translate( - 'General_ExceptionInvalidPeriod', array($strPeriod, 'day, week, month, year, range')); - throw new Exception($message); - break; - } + $this->translator = StaticContainer::get('Piwik\Translation\Translator'); } /** * Returns true if `$dateString` and `$period` represent multiple periods. - * + * * Will return true for date/period combinations where date references multiple * dates and period is not `'range'`. For example, will return true for: - * + * * - **date** = `2012-01-01,2012-02-01` and **period** = `'day'` * - **date** = `2012-01-01,2012-02-01` and **period** = `'week'` * - **date** = `last7` and **period** = `'month'` - * + * * etc. - * + * * @static - * @param $dateString The **date** query parameter value. - * @param $period The **period** query parameter value. + * @param $dateString string The **date** query parameter value. + * @param $period string The **period** query parameter value. * @return boolean */ public static function isMultiplePeriod($dateString, $period) @@ -139,35 +91,22 @@ abstract class Period } /** - * Creates a Period instance using a period, date and timezone. + * Checks the given date format whether it is a correct date format and if not, throw an exception. * - * @param string $timezone The timezone of the date. Only used if `$date` is `'now'`, `'today'`, - * `'yesterday'` or `'yesterdaySameTime'`. - * @param string $period The period string: `"day"`, `"week"`, `"month"`, `"year"`, `"range"`. - * @param string $date The date or date range string. Can be a special value including - * `'now'`, `'today'`, `'yesterday'`, `'yesterdaySameTime'`. - * @return \Piwik\Period + * For valid date formats have a look at the {@link \Piwik\Date::factory()} method and + * {@link isMultiplePeriod()} method. + * + * @param string $dateString + * @throws \Exception If `$dateString` is in an invalid format or if the time is before + * Tue, 06 Aug 1991. */ - public static function makePeriodFromQueryParams($timezone, $period, $date) + public static function checkDateFormat($dateString) { - if (empty($timezone)) { - $timezone = 'UTC'; + if (self::isMultiplePeriod($dateString, 'day')) { + return; } - if ($period == 'range') { - $oPeriod = new Period\Range('range', $date, $timezone, Date::factory('today', $timezone)); - } else { - if (!($date instanceof Date)) { - if ($date == 'now' || $date == 'today') { - $date = date('Y-m-d', Date::factory('now', $timezone)->getTimestamp()); - } elseif ($date == 'yesterday' || $date == 'yesterdaySameTime') { - $date = date('Y-m-d', Date::factory('now', $timezone)->subDay(1)->getTimestamp()); - } - $date = Date::factory($date); - } - $oPeriod = Period::factory($period, $date); - } - return $oPeriod; + Date::factory($dateString); } /** @@ -178,16 +117,20 @@ abstract class Period public function getDateStart() { $this->generate(); + if (count($this->subperiods) == 0) { return $this->getDate(); } + $periods = $this->getSubperiods(); + /** @var $currentPeriod Period */ $currentPeriod = $periods[0]; while ($currentPeriod->getNumberOfSubperiods() > 0) { - $periods = $currentPeriod->getSubperiods(); + $periods = $currentPeriod->getSubperiods(); $currentPeriod = $periods[0]; } + return $currentPeriod->getDate(); } @@ -199,22 +142,26 @@ abstract class Period public function getDateEnd() { $this->generate(); + if (count($this->subperiods) == 0) { return $this->getDate(); } + $periods = $this->getSubperiods(); + /** @var $currentPeriod Period */ $currentPeriod = $periods[count($periods) - 1]; while ($currentPeriod->getNumberOfSubperiods() > 0) { - $periods = $currentPeriod->getSubperiods(); + $periods = $currentPeriod->getSubperiods(); $currentPeriod = $periods[count($periods) - 1]; } + return $currentPeriod->getDate(); } /** * Returns the period ID. - * + * * @return int A unique integer for this type of period. */ public function getId() @@ -224,7 +171,7 @@ abstract class Period /** * Returns the label for the current period. - * + * * @return string `"day"`, `"week"`, `"month"`, `"year"`, `"range"` */ public function getLabel() @@ -247,7 +194,7 @@ abstract class Period /** * Returns the number of available subperiods. - * + * * @return int */ public function getNumberOfSubperiods() @@ -259,7 +206,7 @@ abstract class Period /** * Returns the set of Period instances that together make up this period. For a year, * this would be 12 months. For a month this would be 28-31 days. Etc. - * + * * @return Period[] */ public function getSubperiods() @@ -290,16 +237,18 @@ abstract class Period public function toString($format = "Y-m-d") { $this->generate(); + $dateString = array(); foreach ($this->subperiods as $period) { $dateString[] = $period->toString($format); } + return $dateString; } /** * See {@link toString()}. - * + * * @return string */ public function __toString() @@ -309,7 +258,7 @@ abstract class Period /** * Returns a pretty string describing this period. - * + * * @return string */ abstract public function getPrettyString(); @@ -317,7 +266,7 @@ abstract class Period /** * Returns a short string description of this period that is localized with the currently used * language. - * + * * @return string */ abstract public function getLocalizedShortString(); @@ -325,18 +274,141 @@ abstract class Period /** * Returns a long string description of this period that is localized with the currently used * language. - * + * * @return string */ abstract public function getLocalizedLongString(); /** - * Returns a succinct string describing this period. - * + * Returns the label of the period type that is one size smaller than this one, or null if + * it's the smallest. + * + * Range periods and other such 'period collections' are not considered as separate from + * the value type of the collection. So a range period will return the result of the + * subperiod's `getImmediateChildPeriodLabel()` method. + * + * @ignore + * @return string|null + */ + abstract public function getImmediateChildPeriodLabel(); + + /** + * Returns the label of the period type that is one size bigger than this one, or null + * if it's the biggest. + * + * Range periods and other such 'period collections' are not considered as separate from + * the value type of the collection. So a range period will return the result of the + * subperiod's `getParentPeriodLabel()` method. + * + * @ignore + */ + abstract public function getParentPeriodLabel(); + + /** + * Returns the date range string comprising two dates + * * @return string eg, `'2012-01-01,2012-01-31'`. */ public function getRangeString() { - return $this->getDateStart()->toString("Y-m-d") . "," . $this->getDateEnd()->toString("Y-m-d"); + $dateStart = $this->getDateStart(); + $dateEnd = $this->getDateEnd(); + + return $dateStart->toString("Y-m-d") . "," . $dateEnd->toString("Y-m-d"); + } + + /** + * @param string $format + * + * @return mixed + */ + protected function getTranslatedRange($format) + { + $dateStart = $this->getDateStart(); + $dateEnd = $this->getDateEnd(); + list($formatStart, $formatEnd) = $this->explodeFormat($format); + + $string = $dateStart->getLocalized($formatStart); + $string .= $dateEnd->getLocalized($formatEnd); + + return $string; + } + + /** + * Explodes the given format into two pieces. One that can be user for start date and the other for end date + * + * @param $format + * @return array + */ + protected function explodeFormat($format) + { + $intervalTokens = array( + array('d', 'E', 'C'), + array('M', 'L'), + array('y') + ); + + $offset = strlen($format); + // replace string literals encapsulated by ' with same country of * + $cleanedFormat = preg_replace_callback('/(\'[^\']+\')/', array($this, 'replaceWithStars'), $format); + + // search for first duplicate date field + foreach ($intervalTokens AS $tokens) { + if (preg_match_all('/[' . implode('|', $tokens) . ']+/', $cleanedFormat, $matches, PREG_OFFSET_CAPTURE) && + count($matches[0]) > 1 && $offset > $matches[0][1][1] + ) { + $offset = $matches[0][1][1]; + } + } + + return array(substr($format, 0, $offset), substr($format, $offset)); + } + + private function replaceWithStars($matches) + { + return str_repeat("*", strlen($matches[0])); + } + + protected function getRangeFormat($short = false) + { + $maxDifference = 'D'; + if ($this->getDateStart()->toString('y') != $this->getDateEnd()->toString('y')) { + $maxDifference = 'Y'; + } elseif ($this->getDateStart()->toString('m') != $this->getDateEnd()->toString('m')) { + $maxDifference = 'M'; + } + + $dateTimeFormatProvider = StaticContainer::get('Piwik\Intl\Data\Provider\DateTimeFormatProvider'); + + return $dateTimeFormatProvider->getRangeFormatPattern($short, $maxDifference); + } + + /** + * Returns all child periods that exist within this periods entire date range. Cascades + * downwards over all period types that are smaller than this one. For example, month periods + * will cascade to week and day periods and year periods will cascade to month, week and day + * periods. + * + * The method will not return periods that are outside the range of this period. + * + * @return Period[] + * @ignore + */ + public function getAllOverlappingChildPeriods() + { + return $this->getAllOverlappingChildPeriodsInRange($this->getDateStart(), $this->getDateEnd()); + } + + private function getAllOverlappingChildPeriodsInRange(Date $dateStart, Date $dateEnd) + { + $result = array(); + + $childPeriodType = $this->getImmediateChildPeriodLabel(); + if (empty($childPeriodType)) { + return $result; + } + + $childPeriods = Factory::build($childPeriodType, $dateStart->toString() . ',' . $dateEnd->toString()); + return array_merge($childPeriods->getSubperiods(), $childPeriods->getAllOverlappingChildPeriodsInRange($dateStart, $dateEnd)); } } diff --git a/www/analytics/core/Period/Day.php b/www/analytics/core/Period/Day.php index 422843e1..108d42e2 100644 --- a/www/analytics/core/Period/Day.php +++ b/www/analytics/core/Period/Day.php @@ -1,6 +1,6 @@ getDateStart(); - $out = $date->getLocalized(Piwik::translate('CoreHome_ShortDateFormat')); + $date = $this->getDateStart(); + $out = $date->getLocalized(Date::DATE_FORMAT_DAY_MONTH); return $out; } @@ -50,9 +52,8 @@ class Day extends Period public function getLocalizedLongString() { //"Mon 15 Aug" - $date = $this->getDateStart(); - $template = Piwik::translate('CoreHome_DateFormat'); - $out = $date->getLocalized($template); + $date = $this->getDateStart(); + $out = $date->getLocalized(Date::DATE_FORMAT_LONG); return $out; } @@ -99,4 +100,14 @@ class Day extends Period { return $this->toString(); } + + public function getImmediateChildPeriodLabel() + { + return null; + } + + public function getParentPeriodLabel() + { + return 'week'; + } } diff --git a/www/analytics/core/Period/Factory.php b/www/analytics/core/Period/Factory.php new file mode 100644 index 00000000..88c29a92 --- /dev/null +++ b/www/analytics/core/Period/Factory.php @@ -0,0 +1,130 @@ +getTimestamp()); + } elseif ($date == 'yesterday' || $date == 'yesterdaySameTime') { + $date = date('Y-m-d', Date::factory('now', $timezone)->subDay(1)->getTimestamp()); + } + $date = Date::factory($date); + } + $oPeriod = Factory::build($period, $date); + } + return $oPeriod; + } + + /** + * @param $period + * @return bool + */ + public static function isPeriodEnabledForAPI($period) + { + $periodValidator = new PeriodValidator(); + return $periodValidator->isPeriodAllowedForAPI($period); + } + + /** + * @return array + */ + public static function getPeriodsEnabledForAPI() + { + $periodValidator = new PeriodValidator(); + return $periodValidator->getPeriodsAllowedForAPI(); + } +} diff --git a/www/analytics/core/Period/Month.php b/www/analytics/core/Period/Month.php index 4132c9c8..7a52bd06 100644 --- a/www/analytics/core/Period/Month.php +++ b/www/analytics/core/Period/Month.php @@ -1,6 +1,6 @@ getDateStart()->getLocalized(Piwik::translate('CoreHome_ShortMonthFormat')); + $out = $this->getDateStart()->getLocalized(Date::DATE_FORMAT_MONTH_SHORT); return $out; } @@ -37,7 +39,7 @@ class Month extends Period public function getLocalizedLongString() { //"August 2009" - $out = $this->getDateStart()->getLocalized(Piwik::translate('CoreHome_LongMonthFormat')); + $out = $this->getDateStart()->getLocalized(Date::DATE_FORMAT_MONTH_LONG); return $out; } @@ -60,15 +62,65 @@ class Month extends Period if ($this->subperiodsProcessed) { return; } + parent::generate(); $date = $this->date; - $startMonth = $date->setDay(1); - $currentDay = clone $startMonth; - while ($currentDay->compareMonth($startMonth) == 0) { - $this->addSubperiod(new Day($currentDay)); - $currentDay = $currentDay->addDay(1); + $startMonth = $date->setDay(1)->setTime('00:00:00'); + $endMonth = $startMonth->addPeriod(1, 'month')->setDay(1)->subDay(1); + + $this->processOptimalSubperiods($startMonth, $endMonth); + } + + /** + * Determine which kind of period is best to use. + * See Range.test.php + * + * @param Date $startDate + * @param Date $endDate + */ + protected function processOptimalSubperiods($startDate, $endDate) + { + while ($startDate->isEarlier($endDate) + || $startDate == $endDate) { + $week = new Week($startDate); + $startOfWeek = $week->getDateStart(); + $endOfWeek = $week->getDateEnd(); + + if ($endOfWeek->isLater($endDate)) { + $this->fillDayPeriods($startDate, $endDate); + } elseif ($startOfWeek == $startDate) { + $this->addSubperiod($week); + } else { + $this->fillDayPeriods($startDate, $endOfWeek); + } + + $startDate = $endOfWeek->addDay(1); } } + + /** + * Fills the periods from startDate to endDate with days + * + * @param Date $startDate + * @param Date $endDate + */ + private function fillDayPeriods($startDate, $endDate) + { + while (($startDate->isEarlier($endDate) || $startDate == $endDate)) { + $this->addSubperiod(new Day($startDate)); + $startDate = $startDate->addDay(1); + } + } + + public function getImmediateChildPeriodLabel() + { + return 'week'; + } + + public function getParentPeriodLabel() + { + return 'year'; + } } diff --git a/www/analytics/core/Period/PeriodValidator.php b/www/analytics/core/Period/PeriodValidator.php new file mode 100644 index 00000000..b29077b3 --- /dev/null +++ b/www/analytics/core/Period/PeriodValidator.php @@ -0,0 +1,52 @@ +getPeriodsAllowedForUI()); + } + + /** + * @param string $period + * @return bool + */ + public function isPeriodAllowedForAPI($period) + { + return in_array($period, $this->getPeriodsAllowedForAPI()); + } + + /** + * @return string[] + */ + public function getPeriodsAllowedForUI() + { + $periodsAllowed = Config::getInstance()->General['enabled_periods_UI']; + + return array_map('trim', explode(',', $periodsAllowed)); + } + + /** + * @return string[] + */ + public function getPeriodsAllowedForAPI() + { + $periodsAllowed = Config::getInstance()->General['enabled_periods_API']; + + return array_map('trim', explode(',', $periodsAllowed)); + } +} diff --git a/www/analytics/core/Period/Range.php b/www/analytics/core/Period/Range.php index ab542fe8..7073ad2c 100644 --- a/www/analytics/core/Period/Range.php +++ b/www/analytics/core/Period/Range.php @@ -1,6 +1,6 @@ strPeriod = $strPeriod; - $this->strDate = $strDate; + $this->strDate = $strDate; + $this->timezone = $timezone; $this->defaultEndDate = null; - $this->timezone = $timezone; + if ($today === false) { $today = Date::factory('now', $this->timezone); } + $this->today = $today; + + $this->translator = StaticContainer::get('Piwik\Translation\Translator'); + } + + private function getCache() + { + return Cache::getTransientCache(); + } + + private function getCacheId() + { + $end = ''; + if ($this->defaultEndDate) { + $end = $this->defaultEndDate->getTimestamp(); + } + + $today = $this->today->getTimestamp(); + + return 'range' . $this->strPeriod . $this->strDate . $this->timezone . $end . $today; + } + + private function loadAllFromCache() + { + $range = $this->getCache()->fetch($this->getCacheId()); + + if (!empty($range)) { + foreach ($range as $key => $val) { + $this->$key = $val; + } + } + } + + private function cacheAll() + { + $props = get_object_vars($this); + + $this->getCache()->save($this->getCacheId(), $props); } /** @@ -61,14 +108,7 @@ class Range extends Period */ public function getLocalizedShortString() { - //"30 Dec 08 - 26 Feb 09" - $dateStart = $this->getDateStart(); - $dateEnd = $this->getDateEnd(); - $template = Piwik::translate('CoreHome_ShortDateFormatWithYear'); - $shortDateStart = $dateStart->getLocalized($template); - $shortDateEnd = $dateEnd->getLocalized($template); - $out = "$shortDateStart - $shortDateEnd"; - return $out; + return $this->getTranslatedRange($this->getRangeFormat(true)); } /** @@ -78,7 +118,7 @@ class Range extends Period */ public function getLocalizedLongString() { - return $this->getLocalizedShortString(); + return $this->getTranslatedRange($this->getRangeFormat()); } /** @@ -90,9 +130,11 @@ class Range extends Period public function getDateStart() { $dateStart = parent::getDateStart(); + if (empty($dateStart)) { throw new Exception("Specified date range is invalid."); } + return $dateStart; } @@ -103,7 +145,7 @@ class Range extends Period */ public function getPrettyString() { - $out = Piwik::translate('General_DateRangeFromTo', array($this->getDateStart()->toString(), $this->getDateEnd()->toString())); + $out = $this->translator->translate('General_DateRangeFromTo', array($this->getDateStart()->toString(), $this->getDateEnd()->toString())); return $out; } @@ -150,6 +192,12 @@ class Range extends Period return; } + $this->loadAllFromCache(); + + if ($this->subperiodsProcessed) { + return; + } + parent::generate(); if (preg_match('/(last|previous)([0-9]*)/', $this->strDate, $regs)) { @@ -180,20 +228,16 @@ class Range extends Period // last1 means only one result ; last2 means 2 results so we remove only 1 to the days/weeks/etc $lastN--; - $lastN = abs($lastN); + if ($lastN < 0) { + $lastN = 0; + } $startDate = $endDate->addPeriod(-1 * $lastN, $period); - } elseif ($dateRange = Range::parseDateRange($this->strDate)) { $strDateStart = $dateRange[1]; $strDateEnd = $dateRange[2]; $startDate = Date::factory($strDateStart); - if ($strDateEnd == 'today') { - $strDateEnd = 'now'; - } elseif ($strDateEnd == 'yesterday') { - $strDateEnd = 'yesterdaySameTime'; - } // we set the timezone in the Date object only if the date is relative eg. 'today', 'yesterday', 'now' $timezone = null; if (strpos($strDateEnd, '-') === false) { @@ -201,16 +245,20 @@ class Range extends Period } $endDate = Date::factory($strDateEnd, $timezone); } else { - throw new Exception(Piwik::translate('General_ExceptionInvalidDateRange', array($this->strDate, ' \'lastN\', \'previousN\', \'YYYY-MM-DD,YYYY-MM-DD\''))); + throw new Exception($this->translator->translate('General_ExceptionInvalidDateRange', array($this->strDate, ' \'lastN\', \'previousN\', \'YYYY-MM-DD,YYYY-MM-DD\''))); } + if ($this->strPeriod != 'range') { $this->fillArraySubPeriods($startDate, $endDate, $this->strPeriod); + $this->cacheAll(); return; } + $this->processOptimalSubperiods($startDate, $endDate); // When period=range, we want End Date to be the actual specified end date, // rather than the end of the month / week / whatever is used for processing this range $this->endDate = $endDate; + $this->cacheAll(); } /** @@ -220,12 +268,14 @@ class Range extends Period * @param string $dateString * @return mixed array(1 => dateStartString, 2 => dateEndString) or `false` if the input was not a date range. */ - static public function parseDateRange($dateString) + public static function parseDateRange($dateString) { $matched = preg_match('/^([0-9]{4}-[0-9]{1,2}-[0-9]{1,2}),(([0-9]{4}-[0-9]{1,2}-[0-9]{1,2})|today|now|yesterday)$/D', trim($dateString), $regs); + if (empty($matched)) { return false; } + return $regs; } @@ -241,6 +291,7 @@ class Range extends Period if (!is_null($this->endDate)) { return $this->endDate; } + return parent::getDateEnd(); } @@ -278,7 +329,7 @@ class Range extends Period ) { $this->addSubperiod($year); $endOfPeriod = $endOfYear; - } else if ($startDate == $startOfMonth + } elseif ($startDate == $startOfMonth && ($endOfMonth->isEarlier($endDate) || $endOfMonth == $endDate || $endOfMonth->isLater($this->today) @@ -338,14 +389,14 @@ class Range extends Period protected function fillArraySubPeriods($startDate, $endDate, $period) { $arrayPeriods = array(); - $endSubperiod = Period::factory($period, $endDate); + $endSubperiod = Period\Factory::build($period, $endDate); $arrayPeriods[] = $endSubperiod; // set end date to start of end period since we're comparing against start date. $endDate = $endSubperiod->getDateStart(); while ($endDate->isLater($startDate)) { $endDate = $endDate->addPeriod(-1, $period); - $subPeriod = Period::factory($period, $endDate); + $subPeriod = Period\Factory::build($period, $endDate); $arrayPeriods[] = $subPeriod; } $arrayPeriods = array_reverse($arrayPeriods); @@ -400,8 +451,9 @@ class Range extends Period $strLastDate = false; $lastPeriod = false; if ($period != 'range' && !preg_match('/(last|previous)([0-9]*)/', $date, $regs)) { - if (strpos($date, ',')) // date in the form of 2011-01-01,2011-02-02 - { + if (strpos($date, ',')) { + // date in the form of 2011-01-01,2011-02-02 + $rangePeriod = new Range($period, $date); $lastStartDate = $rangePeriod->getDateStart()->subPeriod($subXPeriods, $period); @@ -425,7 +477,7 @@ class Range extends Period * @param int $lastN The number of periods of type `$period` that the result range should * span. * @param string $endDate The desired end date of the range. - * @param Site $site The site whose timezone should be used. + * @param \Piwik\Site $site The site whose timezone should be used. * @return string The date range string, eg, `'2012-01-02,2013-01-02'`. * @api */ @@ -434,6 +486,7 @@ class Range extends Period $last30Relative = new Range($period, $lastN, $site->getTimezone()); $last30Relative->setDefaultEndDate(Date::factory($endDate)); $date = $last30Relative->getDateStart()->toString() . "," . $last30Relative->getDateEnd()->toString(); + return $date; } @@ -448,4 +501,28 @@ class Range extends Period return $isEndOfWeekLaterThanEndDate; } + + /** + * Returns the date range string comprising two dates + * + * @return string eg, `'2012-01-01,2012-01-31'`. + */ + public function getRangeString() + { + $dateStart = $this->getDateStart(); + $dateEnd = $this->getDateEnd(); + + return $dateStart->toString("Y-m-d") . "," . $dateEnd->toString("Y-m-d"); + } + + public function getImmediateChildPeriodLabel() + { + $subperiods = $this->getSubperiods(); + return reset($subperiods)->getImmediateChildPeriodLabel(); + } + + public function getParentPeriodLabel() + { + return null; + } } diff --git a/www/analytics/core/Period/Week.php b/www/analytics/core/Period/Week.php index 0cf18c06..0029855a 100644 --- a/www/analytics/core/Period/Week.php +++ b/www/analytics/core/Period/Week.php @@ -1,6 +1,6 @@ getDateStart(); - $dateEnd = $this->getDateEnd(); - - $string = Piwik::translate('CoreHome_ShortWeekFormat'); - $string = self::getTranslatedRange($string, $dateStart, $dateEnd); - return $string; + return $this->getTranslatedRange($this->getRangeFormat(true)); } /** @@ -41,25 +35,8 @@ class Week extends Period */ public function getLocalizedLongString() { - $format = Piwik::translate('CoreHome_LongWeekFormat'); - $string = self::getTranslatedRange($format, $this->getDateStart(), $this->getDateEnd()); - return Piwik::translate('CoreHome_PeriodWeek') . " " . $string; - } - - /** - * @param string $format - * @param \Piwik\Date $dateStart - * @param \Piwik\Date $dateEnd - * - * @return mixed - */ - static protected function getTranslatedRange($format, $dateStart, $dateEnd) - { - $string = str_replace('From%', '%', $format); - $string = $dateStart->getLocalized($string); - $string = str_replace('To%', '%', $string); - $string = $dateEnd->getLocalized($string); - return $string; + $string = $this->getTranslatedRange($this->getRangeFormat()); + return $this->translator->translate('Intl_PeriodWeek') . " " . $string; } /** @@ -69,10 +46,11 @@ class Week extends Period */ public function getPrettyString() { - $out = Piwik::translate('General_DateRangeFromTo', - array($this->getDateStart()->toString(), - $this->getDateEnd()->toString()) - ); + $dateStart = $this->getDateStart(); + $dateEnd = $this->getDateEnd(); + + $out = $this->translator->translate('General_DateRangeFromTo', array($dateStart->toString(), $dateEnd->toString())); + return $out; } @@ -84,6 +62,7 @@ class Week extends Period if ($this->subperiodsProcessed) { return; } + parent::generate(); $date = $this->date; @@ -99,4 +78,14 @@ class Week extends Period $currentDay = $currentDay->addDay(1); } } + + public function getImmediateChildPeriodLabel() + { + return 'day'; + } + + public function getParentPeriodLabel() + { + return 'month'; + } } diff --git a/www/analytics/core/Period/Year.php b/www/analytics/core/Period/Year.php index 97fb8fa0..208c6bbf 100644 --- a/www/analytics/core/Period/Year.php +++ b/www/analytics/core/Period/Year.php @@ -1,6 +1,6 @@ getDateStart()->getLocalized("%longYear%"); + $out = $this->getDateStart()->getLocalized(Date::DATE_FORMAT_YEAR); return $out; } @@ -58,6 +60,7 @@ class Year extends Period if ($this->subperiodsProcessed) { return; } + parent::generate(); $year = $this->date->toString("Y"); @@ -75,13 +78,25 @@ class Year extends Period * @param string $format * @return array */ - function toString($format = 'ignored') + public function toString($format = 'ignored') { $this->generate(); + $stringMonth = array(); foreach ($this->subperiods as $month) { $stringMonth[] = $month->getDateStart()->toString("Y") . "-" . $month->getDateStart()->toString("m") . "-01"; } + return $stringMonth; } + + public function getImmediateChildPeriodLabel() + { + return 'month'; + } + + public function getParentPeriodLabel() + { + return null; + } } diff --git a/www/analytics/core/Piwik.php b/www/analytics/core/Piwik.php index 95abfe78..89b1d6ce 100644 --- a/www/analytics/core/Piwik.php +++ b/www/analytics/core/Piwik.php @@ -1,6 +1,6 @@ 1, - 'week' => 2, - 'month' => 3, - 'year' => 4, - 'range' => 5, + 'day' => Day::PERIOD_ID, + 'week' => Week::PERIOD_ID, + 'month' => Month::PERIOD_ID, + 'year' => Year::PERIOD_ID, + 'range' => Range::PERIOD_ID, ); /** * The idGoal query parameter value for the special 'abandoned carts' goal. - * + * * @api */ const LABEL_ID_GOAL_IS_ECOMMERCE_CART = 'ecommerceAbandonedCart'; /** * The idGoal query parameter value for the special 'ecommerce' goal. - * + * * @api */ const LABEL_ID_GOAL_IS_ECOMMERCE_ORDER = 'ecommerceOrder'; @@ -64,7 +62,7 @@ class Piwik * * @param string $message */ - static public function error($message = '') + public static function error($message = '') { trigger_error($message, E_USER_ERROR); } @@ -75,15 +73,13 @@ class Piwik * * @param string $message */ - static public function exitWithErrorMessage($message) + public static function exitWithErrorMessage($message) { - if (!Common::isPhpCliMode()) { - @header('Content-Type: text/html; charset=utf-8'); - } + Common::sendHeader('Content-Type: text/html; charset=utf-8'); $output = "\n" . - "
" . - "

" . + "

" . + "

" . $message . "

"; print($output); @@ -98,7 +94,7 @@ class Piwik * @param number $i2 * @return number The result of the division or zero */ - static public function secureDiv($i1, $i2) + public static function secureDiv($i1, $i2) { if (is_numeric($i1) && is_numeric($i2) && floatval($i2) != 0) { return $i1 / $i2; @@ -114,110 +110,25 @@ class Piwik * @param int $precision * @return number */ - static public function getPercentageSafe($dividend, $divisor, $precision = 0) + public static function getPercentageSafe($dividend, $divisor, $precision = 0) + { + return self::getQuotientSafe(100 * $dividend, $divisor, $precision); + } + + /** + * Safely compute a ratio. Returns 0 if divisor is 0 (to avoid division by 0 error). + * + * @param number $dividend + * @param number $divisor + * @param int $precision + * @return number + */ + public static function getQuotientSafe($dividend, $divisor, $precision = 0) { if ($divisor == 0) { return 0; } - return round(100 * $dividend / $divisor, $precision); - } - - /** - * Returns the Javascript code to be inserted on every page to track - * - * @param int $idSite - * @param string $piwikUrl http://path/to/piwik/directory/ - * @return string - */ - static public function getJavascriptCode($idSite, $piwikUrl, $mergeSubdomains = false, $groupPageTitlesByDomain = false, - $mergeAliasUrls = false, $visitorCustomVariables = false, $pageCustomVariables = false, - $customCampaignNameQueryParam = false, $customCampaignKeywordParam = false, - $doNotTrack = false) - { - // changes made to this code should be mirrored in plugins/CoreAdminHome/javascripts/jsTrackingGenerator.js var generateJsCode - $jsCode = file_get_contents(PIWIK_INCLUDE_PATH . "/plugins/Zeitgeist/templates/javascriptCode.tpl"); - $jsCode = htmlentities($jsCode); - if(substr($piwikUrl, 0, 4) !== 'http') { - $piwikUrl = 'http://' . $piwikUrl; - } - preg_match('~^(http|https)://(.*)$~D', $piwikUrl, $matches); - $piwikUrl = rtrim(@$matches[2], "/"); - - // Build optional parameters to be added to text - $options = ''; - if ($groupPageTitlesByDomain) { - $options .= ' _paq.push(["setDocumentTitle", document.domain + "/" + document.title]);' . PHP_EOL; - } - if ($mergeSubdomains || $mergeAliasUrls) { - $options .= self::getJavascriptTagOptions($idSite, $mergeSubdomains, $mergeAliasUrls); - } - $maxCustomVars = Plugins\CustomVariables\CustomVariables::getMaxCustomVariables(); - if ($visitorCustomVariables) { - $options .= ' // you can set up to ' . $maxCustomVars . ' custom variables for each visitor' . PHP_EOL; - $index = 0; - foreach ($visitorCustomVariables as $visitorCustomVariable) { - $options .= ' _paq.push(["setCustomVariable", '.$index++.', "'.$visitorCustomVariable[0].'", "'.$visitorCustomVariable[1].'", "visit"]);' . PHP_EOL; - } - } - if ($pageCustomVariables) { - $options .= ' // you can set up to ' . $maxCustomVars . ' custom variables for each action (page view, download, click, site search)' . PHP_EOL; - $index = 0; - foreach ($pageCustomVariables as $pageCustomVariable) { - $options .= ' _paq.push(["setCustomVariable", '.$index++.', "'.$pageCustomVariable[0].'", "'.$pageCustomVariable[1].'", "page"]);' . PHP_EOL; - } - } - if ($customCampaignNameQueryParam) { - $options .= ' _paq.push(["setCampaignNameKey", "'.$customCampaignNameQueryParam.'"]);' . PHP_EOL; - } - if ($customCampaignKeywordParam) { - $options .= ' _paq.push(["setCampaignKeywordKey", "'.$customCampaignKeywordParam.'"]);' . PHP_EOL; - } - if ($doNotTrack) { - $options .= ' _paq.push(["setDoNotTrack", true]);' . PHP_EOL; - } - - $codeImpl = array( - 'idSite' => $idSite, - 'piwikUrl' => Common::sanitizeInputValue($piwikUrl), - 'options' => $options - ); - $parameters = compact('mergeSubdomains', 'groupPageTitlesByDomain', 'mergeAliasUrls', 'visitorCustomVariables', - 'pageCustomVariables', 'customCampaignNameQueryParam', 'customCampaignKeywordParam', - 'doNotTrack'); - - /** - * Triggered when generating JavaScript tracking code server side. Plugins can use - * this event to customise the JavaScript tracking code that is displayed to the - * user. - * - * @param array &$codeImpl An array containing snippets of code that the event handler - * can modify. Will contain the following elements: - * - * - **idSite**: The ID of the site being tracked. - * - **piwikUrl**: The tracker URL to use. - * - **options**: A string of JavaScript code that customises - * the JavaScript tracker. - * - * The **httpsPiwikUrl** element can be set if the HTTPS - * domain is different from the normal domain. - * @param array $parameters The parameters supplied to the `Piwik::getJavascriptCode()`. - */ - self::postEvent('Piwik.getJavascriptCode', array(&$codeImpl, $parameters)); - - if (!empty($codeImpl['httpsPiwikUrl'])) { - $setTrackerUrl = 'var u=(("https:" == document.location.protocol) ? "https://{$httpsPiwikUrl}/" : ' - . '"http://{$piwikUrl}/");'; - - $codeImpl['httpsPiwikUrl'] = rtrim($codeImpl['httpsPiwikUrl'], "/"); - } else { - $setTrackerUrl = 'var u=(("https:" == document.location.protocol) ? "https" : "http") + "://{$piwikUrl}/";'; - } - $codeImpl = array('setTrackerUrl' => htmlentities($setTrackerUrl)) + $codeImpl; - - foreach ($codeImpl as $keyToReplace => $replaceWith) { - $jsCode = str_replace('{$' . $keyToReplace . '}', $replaceWith, $jsCode); - } - return $jsCode; + return round($dividend / $divisor, $precision); } /** @@ -225,7 +136,7 @@ class Piwik * * @return string */ - static public function getRandomTitle() + public static function getRandomTitle() { static $titles = array( 'Web analytics', @@ -234,11 +145,8 @@ class Piwik 'Analytics', 'Real Time Analytics', 'Analytics in Real time', - 'Open Source Analytics', - 'Open Source Web Analytics', - 'Free Website Analytics', - 'Free Web Analytics', 'Analytics Platform', + 'Data Platform', ); $id = abs(intval(md5(Url::getCurrentHost()))); $title = $titles[$id % count($titles)]; @@ -255,7 +163,7 @@ class Piwik * @return string * @api */ - static public function getCurrentUserEmail() + public static function getCurrentUserEmail() { $user = APIUsersManager::getInstance()->getUser(Piwik::getCurrentUserLogin()); return $user['email']; @@ -266,7 +174,7 @@ class Piwik * * @return array */ - static public function getAllSuperUserAccessEmailAddresses() + public static function getAllSuperUserAccessEmailAddresses() { $emails = array(); @@ -289,9 +197,14 @@ class Piwik * @return string * @api */ - static public function getCurrentUserLogin() + public static function getCurrentUserLogin() { - return Access::getInstance()->getLogin(); + $login = Access::getInstance()->getLogin(); + + if (empty($login)) { + return 'anonymous'; + } + return $login; } /** @@ -300,7 +213,7 @@ class Piwik * @return string * @api */ - static public function getCurrentUserTokenAuth() + public static function getCurrentUserTokenAuth() { return Access::getInstance()->getTokenAuth(); } @@ -313,7 +226,7 @@ class Piwik * @return bool * @api */ - static public function hasUserSuperUserAccessOrIsTheUser($theUser) + public static function hasUserSuperUserAccessOrIsTheUser($theUser) { try { self::checkUserHasSuperUserAccessOrIsTheUser($theUser); @@ -330,7 +243,7 @@ class Piwik * @throws NoAccessException If the user is neither the Super User nor the user `$theUser`. * @api */ - static public function checkUserHasSuperUserAccessOrIsTheUser($theUser) + public static function checkUserHasSuperUserAccessOrIsTheUser($theUser) { try { if (Piwik::getCurrentUserLogin() !== $theUser) { @@ -349,7 +262,7 @@ class Piwik * @return bool * @api */ - static public function hasTheUserSuperUserAccess($theUser) + public static function hasTheUserSuperUserAccess($theUser) { if (empty($theUser)) { return false; @@ -374,18 +287,18 @@ class Piwik return false; } - /** * Returns true if the current user has Super User access. * * @return bool * @api */ - static public function hasUserSuperUserAccess() + public static function hasUserSuperUserAccess() { try { - self::checkUserHasSuperUserAccess(); - return true; + $hasAccess = Access::getInstance()->hasSuperUserAccess(); + + return $hasAccess; } catch (Exception $e) { return false; } @@ -397,9 +310,10 @@ class Piwik * @return bool * @api */ - static public function isUserIsAnonymous() + public static function isUserIsAnonymous() { - return Piwik::getCurrentUserLogin() == 'anonymous'; + $currentUserLogin = Piwik::getCurrentUserLogin(); + return $currentUserLogin == 'anonymous'; } /** @@ -408,7 +322,7 @@ class Piwik * @throws NoAccessException if the current user is the anonymous user. * @api */ - static public function checkUserIsNotAnonymous() + public static function checkUserIsNotAnonymous() { if (Access::getInstance()->hasSuperUserAccess()) { return; @@ -422,9 +336,12 @@ class Piwik * Helper method user to set the current as superuser. * This should be used with great care as this gives the user all permissions. * + * This method is deprecated, use {@link Access::doAsSuperUser()} instead. + * * @param bool $bool true to set current user as Super User + * @deprecated */ - static public function setUserHasSuperUserAccess($bool = true) + public static function setUserHasSuperUserAccess($bool = true) { Access::getInstance()->setSuperUserAccess($bool); } @@ -435,7 +352,7 @@ class Piwik * @throws Exception if the current user is not the superuser. * @api */ - static public function checkUserHasSuperUserAccess() + public static function checkUserHasSuperUserAccess() { Access::getInstance()->checkUserHasSuperUserAccess(); } @@ -447,7 +364,7 @@ class Piwik * @return bool * @api */ - static public function isUserHasAdminAccess($idSites) + public static function isUserHasAdminAccess($idSites) { try { self::checkUserHasAdminAccess($idSites); @@ -464,7 +381,7 @@ class Piwik * @throws Exception If user doesn't have admin access. * @api */ - static public function checkUserHasAdminAccess($idSites) + public static function checkUserHasAdminAccess($idSites) { Access::getInstance()->checkUserHasAdminAccess($idSites); } @@ -475,14 +392,9 @@ class Piwik * @return bool * @api */ - static public function isUserHasSomeAdminAccess() + public static function isUserHasSomeAdminAccess() { - try { - self::checkUserHasSomeAdminAccess(); - return true; - } catch (Exception $e) { - return false; - } + return Access::getInstance()->isUserHasSomeAdminAccess(); } /** @@ -491,7 +403,7 @@ class Piwik * @throws Exception if user doesn't have admin access to any site. * @api */ - static public function checkUserHasSomeAdminAccess() + public static function checkUserHasSomeAdminAccess() { Access::getInstance()->checkUserHasSomeAdminAccess(); } @@ -503,7 +415,7 @@ class Piwik * @return bool * @api */ - static public function isUserHasViewAccess($idSites) + public static function isUserHasViewAccess($idSites) { try { self::checkUserHasViewAccess($idSites); @@ -520,7 +432,7 @@ class Piwik * @throws Exception if the current user does not have view access to every site in the list. * @api */ - static public function checkUserHasViewAccess($idSites) + public static function checkUserHasViewAccess($idSites) { Access::getInstance()->checkUserHasViewAccess($idSites); } @@ -531,7 +443,7 @@ class Piwik * @return bool * @api */ - static public function isUserHasSomeViewAccess() + public static function isUserHasSomeViewAccess() { try { self::checkUserHasSomeViewAccess(); @@ -547,7 +459,7 @@ class Piwik * @throws Exception if user doesn't have view access to any site. * @api */ - static public function checkUserHasSomeViewAccess() + public static function checkUserHasSomeViewAccess() { Access::getInstance()->checkUserHasSomeViewAccess(); } @@ -562,10 +474,11 @@ class Piwik * in case another Login plugin is being used. * * @return string + * @api */ - static public function getLoginPluginName() + public static function getLoginPluginName() { - return Registry::get('auth')->getName(); + return StaticContainer::get('Piwik\Auth')->getName(); } /** @@ -573,17 +486,17 @@ class Piwik * * @return Plugin */ - static public function getCurrentPlugin() + public static function getCurrentPlugin() { return \Piwik\Plugin\Manager::getInstance()->getLoadedPlugin(Piwik::getModule()); } /** - * Returns the current module read from the URL (eg. 'API', 'UserSettings', etc.) + * Returns the current module read from the URL (eg. 'API', 'DevicesDetection', etc.) * * @return string */ - static public function getModule() + public static function getModule() { return Common::getRequestVar('module', '', 'string'); } @@ -593,7 +506,7 @@ class Piwik * * @return string */ - static public function getAction() + public static function getAction() { return Common::getRequestVar('action', '', 'string'); } @@ -607,7 +520,7 @@ class Piwik * @param array|string $columns * @return array */ - static public function getArrayFromApiParameter($columns) + public static function getArrayFromApiParameter($columns) { if (empty($columns)) { return array(); @@ -628,7 +541,7 @@ class Piwik * @param array $parameters The query parameter values to modify before redirecting. * @api */ - static public function redirectToModule($newModule, $newAction = '', $parameters = array()) + public static function redirectToModule($newModule, $newAction = '', $parameters = array()) { $newUrl = 'index.php' . Url::getCurrentQueryStringWithParametersModified( array('module' => $newModule, 'action' => $newAction) @@ -648,28 +561,30 @@ class Piwik * @return bool * @api */ - static public function isValidEmailString($emailAddress) + public static function isValidEmailString($emailAddress) { - return (preg_match('/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9_.-]+\.[a-zA-Z]{2,7}$/D', $emailAddress) > 0); + /** @var \Zend_Validate_EmailAddress $zendEmailValidator */ + $zendEmailValidator = StaticContainer::get('Zend_Validate_EmailAddress'); + return $zendEmailValidator->isValid($emailAddress); } /** * Returns `true` if the login is valid. - * + * * _Warning: does not check if the login already exists! You must use UsersManager_API->userExists as well._ * * @param string $userLogin * @throws Exception * @return bool */ - static public function checkValidLoginString($userLogin) + public static function checkValidLoginString($userLogin) { if (!SettingsPiwik::isUserCredentialsSanityCheckEnabled() && !empty($userLogin) ) { return; } - $loginMinimumLength = 3; + $loginMinimumLength = 2; $loginMaximumLength = 100; $l = strlen($userLogin); if (!($l >= $loginMinimumLength @@ -687,7 +602,7 @@ class Piwik * @param array $types List of class names that $o is expected to be one of. * @throws Exception if $o is not an instance of the types contained in $types. */ - static public function checkObjectTypeIs($o, $types) + public static function checkObjectTypeIs($o, $types) { foreach ($types as $type) { if ($o instanceof $type) { @@ -709,13 +624,14 @@ class Piwik * @param array $array * @return bool */ - static public function isAssociativeArray($array) + public static function isAssociativeArray($array) { reset($array); if (!is_numeric(key($array)) || key($array) != 0 - ) // first key must be 0 - { + ) { + // first key must be 0 + return true; } @@ -728,7 +644,7 @@ class Piwik if ($next === null) { break; - } else if ($current + 1 != $next) { + } elseif ($current + 1 != $next) { return true; } } @@ -736,6 +652,18 @@ class Piwik return false; } + public static function isMultiDimensionalArray($array) + { + $first = reset($array); + foreach ($array as $first) { + if (is_array($first)) { + // Yes, this is a multi dim array + return true; + } + } + + return false; + } /** * Returns the class name of an object without its namespace. @@ -750,7 +678,6 @@ class Piwik return end($parts); } - /** * Post an event to Piwik's event dispatcher which will execute the event's observers. * @@ -769,7 +696,7 @@ class Piwik /** * Register an observer to an event. - * + * * **_Note: Observers should normally be defined in plugin objects. It is unlikely that you will * need to use this function._** * @@ -800,48 +727,43 @@ class Piwik * @param string $translationId Translation ID, eg, `'General_Date'`. * @param array|string|int $args `sprintf` arguments to be applied to the internationalized * string. + * @param string|null $language Optionally force the language. * @return string The translated string or `$translationId`. * @api */ - public static function translate($translationId, $args = array()) + public static function translate($translationId, $args = array(), $language = null) { - if (!is_array($args)) { - $args = array($args); - } + /** @var Translator $translator */ + $translator = StaticContainer::get('Piwik\Translation\Translator'); - if (strpos($translationId, "_") !== false) { - list($plugin, $key) = explode("_", $translationId, 2); - if (isset($GLOBALS['Piwik_translations'][$plugin]) && isset($GLOBALS['Piwik_translations'][$plugin][$key])) { - $translationId = $GLOBALS['Piwik_translations'][$plugin][$key]; - } - } - if (count($args) == 0) { - return $translationId; - } - return vsprintf($translationId, $args); + return $translator->translate($translationId, $args, $language); } - protected static function getJavascriptTagOptions($idSite, $mergeSubdomains, $mergeAliasUrls) + /** + * 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::hasUserSuperUserAccess(); + + self::setUserHasSuperUserAccess(); + try { - $websiteUrls = APISitesManager::getInstance()->getSiteUrlsFromId($idSite); - } catch (\Exception $e) { - return ''; + $result = $function(); + } catch (Exception $ex) { + self::setUserHasSuperUserAccess($isSuperUser); + + throw $ex; } - // We need to parse_url to isolate hosts - $websiteHosts = array(); - foreach ($websiteUrls as $site_url) { - $referrerParsed = parse_url($site_url); - $websiteHosts[] = $referrerParsed['host']; - } - $options = ''; - if ($mergeSubdomains && !empty($websiteHosts)) { - $options .= ' _paq.push(["setCookieDomain", "*.' . $websiteHosts[0] . '"]);' . PHP_EOL; - } - if ($mergeAliasUrls && !empty($websiteHosts)) { - $urls = '["*.' . implode('","*.', $websiteHosts) . '"]'; - $options .= ' _paq.push(["setDomains", ' . $urls . ']);' . PHP_EOL; - } - return $options; + + self::setUserHasSuperUserAccess($isSuperUser); + + return $result; } } diff --git a/www/analytics/core/PiwikPro/Advertising.php b/www/analytics/core/PiwikPro/Advertising.php new file mode 100644 index 00000000..deea4fa9 --- /dev/null +++ b/www/analytics/core/PiwikPro/Advertising.php @@ -0,0 +1,141 @@ +pluginManager = $pluginManager; + $this->config = $config; + } + + /** + * Returns true if it is ok to show some Piwik PRO advertising in the Piwik UI. + * @return bool + */ + public function arePiwikProAdsEnabled() + { + if ($this->pluginManager->isPluginActivated('EnterpriseAdmin') + || $this->pluginManager->isPluginActivated('LoginAdmin') + || $this->pluginManager->isPluginActivated('CloudAdmin') + || $this->pluginManager->isPluginActivated('WhiteLabel')) { + return false; + } + + $showAds = $this->config->General['piwik_pro_ads_enabled']; + + return !empty($showAds); + } + + /** + * Get URL for promoting the Piwik Cloud. + * + * @param string $campaignMedium + * @param string $campaignContent + * @return string + */ + public function getPromoUrlForCloud($campaignMedium, $campaignContent = '') + { + $url = 'https://piwik.pro/cloud/?'; + + $campaign = $this->getCampaignParametersForPromoUrl( + $name = self::CAMPAIGN_NAME_UPGRADE_TO_CLOUD, + $campaignMedium, + $campaignContent + ); + + return $url . $campaign; + } + + /** + * Get URL for promoting Piwik On Premises. + * @param string $campaignMedium + * @param string $campaignContent + * @return string + */ + public function getPromoUrlForOnPremises($campaignMedium, $campaignContent = '') + { + $url = 'https://piwik.pro/c/upgrade/?'; + + $campaign = $this->getCampaignParametersForPromoUrl( + $name = self::CAMPAIGN_NAME_UPGRADE_TO_PRO, + $campaignMedium, + $campaignContent + ); + + return $url . $campaign; + } + + /** + * Appends campaign parameters to the given URL for promoting any Piwik PRO service. + * @param string $url + * @param string $campaignName + * @param string $campaignMedium + * @param string $campaignContent + * @return string + */ + public function addPromoCampaignParametersToUrl($url, $campaignName, $campaignMedium, $campaignContent = '') + { + if (empty($url)) { + return ''; + } + + if (strpos($url, '?') === false) { + $url .= '?'; + } else { + $url .= '&'; + } + + $url .= $this->getCampaignParametersForPromoUrl($campaignName, $campaignMedium, $campaignContent); + + return $url; + } + + /** + * Generates campaign URL parameters that can be used with any promotion link for Piwik PRO. + * + * @param string $campaignName + * @param string $campaignMedium + * @param string $campaignContent Optional + * @return string URL parameters without a leading ? or & + */ + private function getCampaignParametersForPromoUrl($campaignName, $campaignMedium, $campaignContent = '') + { + $campaignName = sprintf('pk_campaign=%s&pk_medium=%s&pk_source=Piwik_App', $campaignName, $campaignMedium); + + if (!empty($campaignContent)) { + $campaignName .= '&pk_content=' . $campaignContent; + } + + return $campaignName; + } +} diff --git a/www/analytics/core/Plugin.php b/www/analytics/core/Plugin.php index 1392fc3b..ff938a65 100644 --- a/www/analytics/core/Plugin.php +++ b/www/analytics/core/Plugin.php @@ -1,6 +1,6 @@ 'getReportMetadata', @@ -65,17 +66,17 @@ require_once PIWIK_INCLUDE_PATH . '/core/Plugin/MetadataLoader.php'; * ) * ); * } - * + * * public function install() * { * Db::exec("CREATE TABLE " . Common::prefixTable('mytable') . "..."); * } - * + * * public function uninstall() * { * Db::exec("DROP TABLE IF EXISTS " . Common::prefixTable('mytable')); * } - * + * * public function getReportMetadata(&$metadata) * { * // ... @@ -86,7 +87,7 @@ require_once PIWIK_INCLUDE_PATH . '/core/Plugin/MetadataLoader.php'; * // ... * } * } - * + * * @api */ class Plugin @@ -105,6 +106,15 @@ class Plugin */ private $pluginInformation; + /** + * As the cache is used quite often we avoid having to create instances all the time. We reuse it which is not + * perfect but efficient. If the cache is used we need to make sure to call setId() before usage as there + * is maybe a different key set since last usage. + * + * @var \Piwik\Cache\Eager + */ + private $cache; + /** * Constructor. * @@ -121,11 +131,27 @@ class Plugin } $this->pluginName = $pluginName; - $metadataLoader = new MetadataLoader($pluginName); - $this->pluginInformation = $metadataLoader->load(); + $cacheId = 'Plugin' . $pluginName . 'Metadata'; + $cache = Cache::getEagerCache(); - if ($this->hasDefinedPluginInformationInPluginClass() && $metadataLoader->hasPluginJson()) { - throw new \Exception('Plugin ' . $pluginName . ' has defined the method getInformation() and as well as having a plugin.json file. Please delete the getInformation() method from the plugin class. Alternatively, you may delete the plugin directory from plugins/' . $pluginName); + if ($cache->contains($cacheId)) { + $this->pluginInformation = $cache->fetch($cacheId); + } else { + $metadataLoader = new MetadataLoader($pluginName); + $this->pluginInformation = $metadataLoader->load(); + + if ($this->hasDefinedPluginInformationInPluginClass() && $metadataLoader->hasPluginJson()) { + throw new \Exception('Plugin ' . $pluginName . ' has defined the method getInformation() and as well as having a plugin.json file. Please delete the getInformation() method from the plugin class. Alternatively, you may delete the plugin directory from plugins/' . $pluginName); + } + + $cache->save($cacheId, $this->pluginInformation); + } + } + + private function createCacheIfNeeded() + { + if (is_null($this->cache)) { + $this->cache = Cache::getEagerCache(); } } @@ -147,7 +173,7 @@ class Plugin /** * Returns plugin information, including: - * + * * - 'description' => string // 1-2 sentence description of the plugin * - 'author' => string // plugin author * - 'author_homepage' => string // author homepage URL (or email "mailto:youremail@example.org") @@ -166,12 +192,12 @@ class Plugin } /** - * Returns a list of hooks with associated event observers. - * + * Returns a list of events with associated event observers. + * * Derived classes should use this method to associate callbacks with events. * * @return array eg, - * + * * array( * 'API.getReportMetadata' => 'myPluginFunction', * 'Another.event' => array( @@ -183,10 +209,20 @@ class Plugin * 'before' => true // execute before callbacks w/o ordering * ) * ) + * @since 2.15.0 + */ + public function registerEvents() + { + return array(); + } + + /** + * @deprecated since 2.15.0 use {@link registerEvents()} instead. + * @return array */ public function getListHooksRegistered() { - return array(); + return $this->registerEvents(); } /** @@ -201,12 +237,12 @@ class Plugin /** * Installs the plugin. Derived classes should implement this class if the plugin * needs to: - * + * * - create tables * - update existing tables * - etc. - * - * @throws Exception if installation of fails for some reason. + * + * @throws \Exception if installation of fails for some reason. */ public function install() { @@ -216,10 +252,10 @@ class Plugin /** * Uninstalls the plugins. Derived classes should implement this method if the changes * made in {@link install()} need to be undone during uninstallation. - * + * * In most cases, if you have an {@link install()} method, you should provide * an {@link uninstall()} method. - * + * * @throws \Exception if uninstallation of fails for some reason. */ public function uninstall() @@ -276,6 +312,89 @@ class Plugin return $this->pluginName; } + /** + * Tries to find a component such as a Menu or Tasks within this plugin. + * + * @param string $componentName The name of the component you want to look for. In case you request a + * component named 'Menu' it'll look for a file named 'Menu.php' within the + * root of the plugin folder that implements a class named + * Piwik\Plugin\$PluginName\Menu . If such a file exists but does not implement + * this class it'll silently ignored. + * @param string $expectedSubclass If not empty, a check will be performed whether a found file extends the + * given subclass. If the requested file exists but does not extend this class + * a warning will be shown to advice a developer to extend this certain class. + * + * @return \stdClass|null Null if the requested component does not exist or an instance of the found + * component. + */ + public function findComponent($componentName, $expectedSubclass) + { + $this->createCacheIfNeeded(); + + $cacheId = 'Plugin' . $this->pluginName . $componentName . $expectedSubclass; + + $componentFile = sprintf('%s/plugins/%s/%s.php', PIWIK_INCLUDE_PATH, $this->pluginName, $componentName); + + if ($this->cache->contains($cacheId)) { + $classname = $this->cache->fetch($cacheId); + + if (empty($classname)) { + return null; // might by "false" in case has no menu, widget, ... + } + + if (file_exists($componentFile)) { + include_once $componentFile; + } + } else { + $this->cache->save($cacheId, false); // prevent from trying to load over and over again for instance if there is no Menu for a plugin + + if (!file_exists($componentFile)) { + return null; + } + + require_once $componentFile; + + $classname = sprintf('Piwik\\Plugins\\%s\\%s', $this->pluginName, $componentName); + + if (!class_exists($classname)) { + return null; + } + + if (!empty($expectedSubclass) && !is_subclass_of($classname, $expectedSubclass)) { + Log::warning(sprintf('Cannot use component %s for plugin %s, class %s does not extend %s', + $componentName, $this->pluginName, $classname, $expectedSubclass)); + return null; + } + + $this->cache->save($cacheId, $classname); + } + + return StaticContainer::get($classname); + } + + public function findMultipleComponents($directoryWithinPlugin, $expectedSubclass) + { + $this->createCacheIfNeeded(); + + $cacheId = 'Plugin' . $this->pluginName . $directoryWithinPlugin . $expectedSubclass; + + if ($this->cache->contains($cacheId)) { + $components = $this->cache->fetch($cacheId); + + if ($this->includeComponents($components)) { + return $components; + } else { + // problem including one cached file, refresh cache + } + } + + $components = $this->doFindMultipleComponents($directoryWithinPlugin, $expectedSubclass); + + $this->cache->save($cacheId, $components); + + return $components; + } + /** * Detect whether there are any missing dependencies. * @@ -315,12 +434,97 @@ class Plugin { foreach ($backtrace as $tracepoint) { // try and discern the plugin name - if (isset($tracepoint['class']) - && preg_match("/Piwik\\\\Plugins\\\\([a-zA-Z_0-9]+)\\\\/", $tracepoint['class'], $matches) - ) { - return $matches[1]; + if (isset($tracepoint['class'])) { + $className = self::getPluginNameFromNamespace($tracepoint['class']); + if ($className) { + return $className; + } } } return false; } + + /** + * Extracts the plugin name from a namespace name or a fully qualified class name. Returns `false` + * if we can't find one. + * + * @param string $namespaceOrClassName The namespace or class string. + * @return string|false + */ + public static function getPluginNameFromNamespace($namespaceOrClassName) + { + if (preg_match("/Piwik\\\\Plugins\\\\([a-zA-Z_0-9]+)\\\\/", $namespaceOrClassName, $matches)) { + return $matches[1]; + } else { + return false; + } + } + + /** + * Override this method in your plugin class if you want your plugin to be loaded during tracking. + * + * Note: If you define your own dimension or handle a tracker event, your plugin will automatically + * be detected as a tracker plugin. + * + * @return bool + * @internal + */ + public function isTrackerPlugin() + { + return false; + } + + /** + * @param $directoryWithinPlugin + * @param $expectedSubclass + * @return array + */ + private function doFindMultipleComponents($directoryWithinPlugin, $expectedSubclass) + { + $components = array(); + + $baseDir = PIWIK_INCLUDE_PATH . '/plugins/' . $this->pluginName . '/' . $directoryWithinPlugin; + $files = Filesystem::globr($baseDir, '*.php'); + + foreach ($files as $file) { + require_once $file; + + $fileName = str_replace(array($baseDir . '/', '.php'), '', $file); + $klassName = sprintf('Piwik\\Plugins\\%s\\%s\\%s', $this->pluginName, $directoryWithinPlugin, str_replace('/', '\\', $fileName)); + + if (!class_exists($klassName)) { + continue; + } + + if (!empty($expectedSubclass) && !is_subclass_of($klassName, $expectedSubclass)) { + continue; + } + + $klass = new \ReflectionClass($klassName); + + if ($klass->isAbstract()) { + continue; + } + + $components[$file] = $klassName; + } + return $components; + } + + /** + * @param $components + * @return bool true if all files were included, false if any file cannot be read + */ + private function includeComponents($components) + { + foreach ($components as $file => $klass) { + if (!is_readable($file)) { + return false; + } + } + foreach ($components as $file => $klass) { + include_once $file; + } + return true; + } } diff --git a/www/analytics/core/Plugin/API.php b/www/analytics/core/Plugin/API.php index 3a4e26a5..c54e10a8 100644 --- a/www/analytics/core/Plugin/API.php +++ b/www/analytics/core/Plugin/API.php @@ -1,6 +1,6 @@ Link - * + * * @api */ -abstract class API extends Singleton +abstract class API { + private static $instances; + /** + * Returns the singleton instance for the derived class. If the singleton instance + * has not been created, this method will create it. + * + * @return static + */ + public static function getInstance() + { + $class = get_called_class(); + + if (!isset(self::$instances[$class])) { + $container = StaticContainer::getContainer(); + + $refl = new \ReflectionClass($class); + + if (!$refl->getConstructor() || $refl->getConstructor()->isPublic()) { + self::$instances[$class] = $container->get($class); + } else { + /** @var LoggerInterface $logger */ + $logger = $container->get('Psr\Log\LoggerInterface'); + + // BC with API defining a protected constructor + $logger->notice('The API class {class} defines a protected constructor which is deprecated, make the constructor public instead', array('class' => $class)); + self::$instances[$class] = new $class; + } + } + + return self::$instances[$class]; + } + + /** + * Used in tests only + * @ignore + * @deprecated + */ + public static function unsetInstance() + { + $class = get_called_class(); + unset(self::$instances[$class]); + } + + /** + * Sets the singleton instance. For testing purposes. + * @ignore + * @deprecated + */ + public static function setSingletonInstance($instance) + { + $class = get_called_class(); + self::$instances[$class] = $instance; + } } diff --git a/www/analytics/core/Plugin/AggregatedMetric.php b/www/analytics/core/Plugin/AggregatedMetric.php new file mode 100644 index 00000000..6324390e --- /dev/null +++ b/www/analytics/core/Plugin/AggregatedMetric.php @@ -0,0 +1,21 @@ +getLogAggregator(); - * + * * $data = $logAggregator->queryVisitsByDimension(...); - * + * * $dataTable = new DataTable(); * $dataTable->addRowsFromSimpleArray($data); - * + * * $archiveProcessor = $this->getProcessor(); * $archiveProcessor->insertBlobRecords('MyPlugin_myReport', $dataTable->getSerialized(500)); * } - * + * * public function aggregateMultipleReports() * { * $archiveProcessor = $this->getProcessor(); * $archiveProcessor->aggregateDataTableRecords('MyPlugin_myReport', 500); * } * } - * + * * @api */ abstract class Archiver @@ -61,7 +61,7 @@ abstract class Archiver /** * Constructor. - * + * * @param ArchiveProcessor $processor The ArchiveProcessor instance to use when persisting archive * data. */ @@ -73,12 +73,12 @@ abstract class Archiver /** * Archives data for a day period. - * + * * Implementations of this method should do more computation intensive activities such * as aggregating data across log tables. Since this method only deals w/ data logged for a day, * aggregating individual log table rows isn't a problem. Doing this for any larger period, * however, would cause performance degradation. - * + * * Aggregate log table rows using a {@link Piwik\DataAccess\LogAggregator} instance. Get a * {@link Piwik\DataAccess\LogAggregator} instance using the {@link getLogAggregator()} method. */ @@ -86,11 +86,11 @@ abstract class Archiver /** * Archives data for a non-day period. - * + * * Implementations of this method should only aggregate existing reports of subperiods of the * current period. For example, it is more efficient to aggregate reports for each day of a * week than to aggregate each log entry of the week. - * + * * Use {@link Piwik\ArchiveProcessor::aggregateNumericMetrics()} and {@link Piwik\ArchiveProcessor::aggregateDataTableRecords()} * to aggregate archived reports. Get the {@link Piwik\ArchiveProcessor} instance using the {@link getProcessor()} * method. @@ -100,7 +100,7 @@ abstract class Archiver /** * Returns a {@link Piwik\ArchiveProcessor} instance that can be used to insert archive data for * the period, segment and site we are archiving data for. - * + * * @return \Piwik\ArchiveProcessor * @api */ @@ -112,7 +112,7 @@ abstract class Archiver /** * Returns a {@link Piwik\DataAccess\LogAggregator} instance that can be used to aggregate log table rows * for this period, segment and site. - * + * * @return \Piwik\DataAccess\LogAggregator * @api */ diff --git a/www/analytics/core/Plugin/ComponentFactory.php b/www/analytics/core/Plugin/ComponentFactory.php new file mode 100644 index 00000000..c0b3c7bf --- /dev/null +++ b/www/analytics/core/Plugin/ComponentFactory.php @@ -0,0 +1,131 @@ +findMultipleComponents($subnamespace, $componentTypeClass); + foreach ($components as $class) { + if ($class == $desiredComponentClass) { + return new $class(); + } + } + + Log::debug("ComponentFactory::%s: Could not find requested component (args = ['%s', '%s', '%s']).", + __FUNCTION__, $pluginName, $componentClassSimpleName, $componentTypeClass); + + return null; + } + + /** + * Finds a component instance that satisfies a given predicate. + * + * @param string $componentTypeClass The fully qualified class name of the component type, eg, + * `"Piwik\Plugin\Report"`. + * @param string $pluginName|false The name of the plugin the component is expected to belong to, + * eg, `'DevicesDetection'`. + * @param callback $predicate + * @return mixed The component that satisfies $predicate or null if not found. + */ + public static function getComponentIf($componentTypeClass, $pluginName, $predicate) + { + $pluginManager = PluginManager::getInstance(); + + // get components to search through + $subnamespace = $componentTypeClass::COMPONENT_SUBNAMESPACE; + if (empty($pluginName)) { + $components = $pluginManager->findMultipleComponents($subnamespace, $componentTypeClass); + } else { + $plugin = self::getActivatedPlugin(__FUNCTION__, $pluginName); + if (empty($plugin)) { + return null; + } + + $components = $plugin->findMultipleComponents($subnamespace, $componentTypeClass); + } + + // find component that satisfieds predicate + foreach ($components as $class) { + $component = new $class(); + if ($predicate($component)) { + return $component; + } + } + + Log::debug("ComponentFactory::%s: Could not find component that satisfies predicate (args = ['%s', '%s', '%s']).", + __FUNCTION__, $componentTypeClass, $pluginName, get_class($predicate)); + + return null; + } + + /** + * @param string $function + * @param string $pluginName + * @return null|\Piwik\Plugin + */ + private static function getActivatedPlugin($function, $pluginName) + { + $pluginManager = PluginManager::getInstance(); + try { + if (!$pluginManager->isPluginActivated($pluginName)) { + Log::debug("ComponentFactory::%s: component for deactivated plugin ('%s') requested.", + $function, $pluginName); + + return null; + } + + return $pluginManager->getLoadedPlugin($pluginName); + } catch (Exception $e) { + Log::debug($e); + + return null; + } + } +} diff --git a/www/analytics/core/Plugin/ConsoleCommand.php b/www/analytics/core/Plugin/ConsoleCommand.php index ede6952f..29e2fc65 100644 --- a/www/analytics/core/Plugin/ConsoleCommand.php +++ b/www/analytics/core/Plugin/ConsoleCommand.php @@ -1,6 +1,6 @@ render(); * } * } - * + * * **Linking to a controller action** * * Link - * + * */ abstract class Controller { /** * The plugin name, eg. `'Referrers'`. - * + * * @var string * @api */ @@ -93,7 +97,7 @@ abstract class Controller /** * The value of the **idSite** query parameter. - * + * * @var int * @api */ @@ -101,7 +105,7 @@ abstract class Controller /** * The Site object created with {@link $idSite}. - * + * * @var Site * @api */ @@ -109,7 +113,7 @@ abstract class Controller /** * Constructor. - * + * * @api */ public function __construct() @@ -177,6 +181,50 @@ abstract class Controller $this->strDate = $date->toString(); } + /** + * Returns values that are enabled for the parameter &period= + * @return array eg. array('day', 'week', 'month', 'year', 'range') + */ + protected static function getEnabledPeriodsInUI() + { + $periodValidator = new PeriodValidator(); + return $periodValidator->getPeriodsAllowedForUI(); + } + + /** + * @return array + */ + private static function getEnabledPeriodsNames() + { + $availablePeriods = self::getEnabledPeriodsInUI(); + $periodNames = array( + 'day' => array( + 'singular' => Piwik::translate('Intl_PeriodDay'), + 'plural' => Piwik::translate('Intl_PeriodDays') + ), + 'week' => array( + 'singular' => Piwik::translate('Intl_PeriodWeek'), + 'plural' => Piwik::translate('Intl_PeriodWeeks') + ), + 'month' => array( + 'singular' => Piwik::translate('Intl_PeriodMonth'), + 'plural' => Piwik::translate('Intl_PeriodMonths') + ), + 'year' => array( + 'singular' => Piwik::translate('Intl_PeriodYear'), + 'plural' => Piwik::translate('Intl_PeriodYears') + ), + // Note: plural is not used for date range + 'range' => array( + 'singular' => Piwik::translate('General_DateRangeInPeriodList'), + 'plural' => Piwik::translate('General_DateRangeInPeriodList') + ), + ); + + $periodNames = array_intersect_key($periodNames, array_fill_keys($availablePeriods, true)); + return $periodNames; + } + /** * Returns the name of the default method that will be called * when visiting: index.php?module=PluginName without the action parameter. @@ -200,10 +248,60 @@ abstract class Controller return $view->render(); } + /** + * Assigns the given variables to the template and renders it. + * + * Example: + * + * public function myControllerAction () { + * return $this->renderTemplate('index', array( + * 'answerToLife' => '42' + * )); + * } + * + * This will render the 'index.twig' file within the plugin templates folder and assign the view variable + * `answerToLife` to `42`. + * + * @param string $template The name of the template file. If only a name is given it will automatically use + * the template within the plugin folder. For instance 'myTemplate' will result in + * '@$pluginName/myTemplate.twig'. Alternatively you can include the full path: + * '@anyOtherFolder/otherTemplate'. The trailing '.twig' is not needed. + * @param array $variables For instance array('myViewVar' => 'myValue'). In template you can use {{ myViewVar }} + * @return string + * @since 2.5.0 + * @api + */ + protected function renderTemplate($template, array $variables = array()) + { + if (false === strpos($template, '@') || false === strpos($template, '/')) { + $template = '@' . $this->pluginName . '/' . $template; + } + + $view = new View($template); + + // alternatively we could check whether the templates extends either admin.twig or dashboard.twig and based on + // that call the correct method. This will be needed once we unify Controller and ControllerAdmin see + // https://github.com/piwik/piwik/issues/6151 + if ($this instanceof ControllerAdmin) { + $this->setBasicVariablesView($view); + } elseif (empty($this->site) || empty($this->idSite)) { + $this->setBasicVariablesView($view); + } else { + $this->setGeneralVariablesView($view); + } + + foreach ($variables as $key => $value) { + $view->$key = $value; + } + + return $view->render(); + } + /** * Convenience method that creates and renders a ViewDataTable for a API method. * - * @param string $apiAction The name of the API action (eg, `'getResolution'`). + * @param string|\Piwik\Plugin\Report $apiAction The name of the API action (eg, `'getResolution'`) or + * an instance of an report. * @param bool $controllerAction The name of the Controller action name that is rendering the report. Defaults * to the `$apiAction`. * @param bool $fetch If `true`, the rendered string is returned, if `false` it is `echo`'d. @@ -214,6 +312,21 @@ abstract class Controller */ protected function renderReport($apiAction, $controllerAction = false) { + if (empty($controllerAction) && is_string($apiAction)) { + $report = Report::factory($this->pluginName, $apiAction); + + if (!empty($report)) { + $apiAction = $report; + } + } + + if ($apiAction instanceof Report) { + $this->checkSitePermission(); + $apiAction->checkIsEnabled(); + + return $apiAction->render(); + } + $pluginName = $this->pluginName; /** @var Proxy $apiProxy */ @@ -250,7 +363,7 @@ abstract class Controller protected function getLastUnitGraph($currentModuleName, $currentControllerAction, $apiMethod) { $view = ViewDataTableFactory::build( - 'graphEvolution', $apiMethod, $currentModuleName . '.' . $currentControllerAction, $forceDefault = true); + Evolution::ID, $apiMethod, $currentModuleName . '.' . $currentControllerAction, $forceDefault = true); $view->config->show_goals = false; return $view; } @@ -282,7 +395,7 @@ abstract class Controller $date = Common::getRequestVar('date'); $meta = \Piwik\Plugins\API\API::getInstance()->getReportMetadata($idSite, $period, $date); - $columns = array_merge($columnsToDisplay, $selectableColumns); + $columns = array_merge($columnsToDisplay ? $columnsToDisplay : array(), $selectableColumns); $translations = array_combine($columns, $columns); foreach ($meta as $reportMeta) { if ($reportMeta['action'] == 'get' && !isset($reportMeta['parameters'])) { @@ -296,6 +409,7 @@ abstract class Controller // initialize the graph and load the data $view = $this->getLastUnitGraph($currentModuleName, $currentControllerAction, $apiMethod); + if ($columnsToDisplay !== false) { $view->config->columns_to_display = $columnsToDisplay; } @@ -379,11 +493,11 @@ abstract class Controller /** * Returns a URL to a sparkline image for a report served by the current plugin. - * + * * The result of this URL should be used with the [sparkline()](/api-reference/Piwik/View#twig) twig function. - * + * * The current site ID and period will be used. - * + * * @param string $action Method name of the controller that serves the report. * @param array $customParameters The array of query parameter name/value pairs that * should be set in result URL. @@ -439,9 +553,9 @@ abstract class Controller /** * Assigns variables to {@link Piwik\View} instances that display an entire page. - * + * * The following variables assigned: - * + * * **date** - The value of the **date** query parameter. * **idSite** - The value of the **idSite** query parameter. * **rawDate** - The value of the **date** query parameter. @@ -454,81 +568,105 @@ abstract class Controller * **config_action_url_category_delimiter** - The value of the `[General] action_url_category_delimiter` * INI config option. * **topMenu** - The result of `MenuTop::getInstance()->getMenu()`. - * + * * As well as the variables set by {@link setPeriodVariablesView()}. - * + * * Will exit on error. - * + * * @param View $view * @return void * @api */ protected function setGeneralVariablesView($view) { + $view->idSite = $this->idSite; + $this->checkSitePermission(); + $this->setPeriodVariablesView($view); + + $view->siteName = $this->site->getName(); + $view->siteMainUrl = $this->site->getMainUrl(); + + $siteTimezone = $this->site->getTimezone(); + + $datetimeMinDate = $this->site->getCreationDate()->getDatetime(); + $minDate = Date::factory($datetimeMinDate, $siteTimezone); + $this->setMinDateView($minDate, $view); + + $maxDate = Date::factory('now', $siteTimezone); + $this->setMaxDateView($maxDate, $view); + + $rawDate = Common::getRequestVar('date'); + Period::checkDateFormat($rawDate); + + $periodStr = Common::getRequestVar('period'); + + if ($periodStr != 'range') { + $date = Date::factory($this->strDate); + $validDate = $this->getValidDate($date, $minDate, $maxDate); + $period = Period\Factory::build($periodStr, $validDate); + + if ($date->toString() !== $validDate->toString()) { + // we to not always change date since it could convert a strDate "today" to "YYYY-MM-DD" + // only change $this->strDate if it was not valid before + $this->setDate($validDate); + } + } else { + $period = new Range($periodStr, $rawDate, $siteTimezone); + } + + // Setting current period start & end dates, for pre-setting the calendar when "Date Range" is selected + $dateStart = $period->getDateStart(); + $dateStart = $this->getValidDate($dateStart, $minDate, $maxDate); + + $dateEnd = $period->getDateEnd(); + $dateEnd = $this->getValidDate($dateEnd, $minDate, $maxDate); + + if ($periodStr == 'range') { + // make sure we actually display the correct calendar pretty date + $newRawDate = $dateStart->toString() . ',' . $dateEnd->toString(); + $period = new Range($periodStr, $newRawDate, $siteTimezone); + } + $view->date = $this->strDate; + $view->prettyDate = self::getCalendarPrettyDate($period); + $view->prettyDateLong = $period->getLocalizedLongString(); + $view->rawDate = $rawDate; + $view->startDate = $dateStart; + $view->endDate = $dateEnd; - try { - $view->idSite = $this->idSite; - if (empty($this->site) || empty($this->idSite)) { - throw new Exception("The requested website idSite is not found in the request, or is invalid. - Please check that you are logged in Piwik and have permission to access the specified website."); - } - $this->setPeriodVariablesView($view); + $language = LanguagesManager::getLanguageForSession(); + $view->language = !empty($language) ? $language : LanguagesManager::getLanguageCodeForCurrentUser(); - $rawDate = Common::getRequestVar('date'); - $periodStr = Common::getRequestVar('period'); - if ($periodStr != 'range') { - $date = Date::factory($this->strDate); - $period = Period::factory($periodStr, $date); - } else { - $period = new Range($periodStr, $rawDate, $this->site->getTimezone()); - } - $view->rawDate = $rawDate; - $view->prettyDate = self::getCalendarPrettyDate($period); + $this->setBasicVariablesView($view); - $view->siteName = $this->site->getName(); - $view->siteMainUrl = $this->site->getMainUrl(); + $view->topMenu = MenuTop::getInstance()->getMenu(); + $view->userMenu = MenuUser::getInstance()->getMenu(); - $datetimeMinDate = $this->site->getCreationDate()->getDatetime(); - $minDate = Date::factory($datetimeMinDate, $this->site->getTimezone()); - $this->setMinDateView($minDate, $view); - - $maxDate = Date::factory('now', $this->site->getTimezone()); - $this->setMaxDateView($maxDate, $view); - - // Setting current period start & end dates, for pre-setting the calendar when "Date Range" is selected - $dateStart = $period->getDateStart(); - if ($dateStart->isEarlier($minDate)) { - $dateStart = $minDate; - } - $dateEnd = $period->getDateEnd(); - if ($dateEnd->isLater($maxDate)) { - $dateEnd = $maxDate; - } - - $view->startDate = $dateStart; - $view->endDate = $dateEnd; - - $language = LanguagesManager::getLanguageForSession(); - $view->language = !empty($language) ? $language : LanguagesManager::getLanguageCodeForCurrentUser(); - - $this->setBasicVariablesView($view); - - $view->topMenu = MenuTop::getInstance()->getMenu(); + $notifications = $view->notifications; + if (empty($notifications)) { $view->notifications = NotificationManager::getAllNotificationsToDisplay(); NotificationManager::cancelAllNonPersistent(); - } catch (Exception $e) { - Piwik_ExitWithMessage($e->getMessage(), $e->getTraceAsString()); } } + private function getValidDate(Date $date, Date $minDate, Date $maxDate) + { + if ($date->isEarlier($minDate)) { + $date = $minDate; + } + + if ($date->isLater($maxDate)) { + $date = $maxDate; + } + + return $date; + } + /** * Assigns a set of generally useful variables to a {@link Piwik\View} instance. - * + * * The following variables assigned: - * - * **debugTrackVisitsInsidePiwikUI** - The value of the `[Debug] track_visits_inside_piwik_ui` - * INI config option. + * * **isSuperUser** - True if the current user is the Super User, false if otherwise. * **hasSomeAdminAccess** - True if the current user has admin access to at least one site, * false if otherwise. @@ -539,7 +677,7 @@ abstract class Controller * **hasSVGLogo** - True if there is a SVG logo, false if otherwise. * **enableFrames** - The value of the `[General] enable_framed_pages` INI config option. If * true, {@link Piwik\View::setXFrameOptions()} is called on the view. - * + * * Also calls {@link setHostValidationVariablesView()}. * * @param View $view @@ -548,15 +686,17 @@ abstract class Controller protected function setBasicVariablesView($view) { $view->clientSideConfig = PiwikConfig::getInstance()->getClientSideOptions(); - $view->debugTrackVisitsInsidePiwikUI = PiwikConfig::getInstance()->Debug['track_visits_inside_piwik_ui']; $view->isSuperUser = Access::getInstance()->hasSuperUserAccess(); $view->hasSomeAdminAccess = Piwik::isUserHasSomeAdminAccess(); $view->hasSomeViewAccess = Piwik::isUserHasSomeViewAccess(); $view->isUserIsAnonymous = Piwik::isUserIsAnonymous(); $view->hasSuperUserAccess = Piwik::hasUserSuperUserAccess(); - $customLogo = new CustomLogo(); - $view->isCustomLogo = $customLogo->isEnabled(); + if (!Piwik::isUserIsAnonymous()) { + $view->emailSuperUser = implode(',', Piwik::getAllSuperUserAccessEmailAddresses()); + } + + $this->addCustomLogoInfo($view); $view->logoHeader = \Piwik\Plugins\API\API::getInstance()->getHeaderLogoUrl(); $view->logoLarge = \Piwik\Plugins\API\API::getInstance()->getLogoUrl(); @@ -574,6 +714,13 @@ abstract class Controller self::setHostValidationVariablesView($view); } + protected function addCustomLogoInfo($view) + { + $customLogo = new CustomLogo(); + $view->isCustomLogo = $customLogo->isEnabled(); + $view->customFavicon = $customLogo->getPathUserFavicon(); + } + /** * Checks if the current host is valid and sets variables on the given view, including: * @@ -631,7 +778,7 @@ abstract class Controller $validHost, '' )); - } else if (Piwik::isUserIsAnonymous()) { + } elseif (Piwik::isUserIsAnonymous()) { $view->invalidHostMessage = $warningStart . ' ' . Piwik::translate('CoreHome_InjectedHostNonSuperUserWarning', array( "
", @@ -660,7 +807,7 @@ abstract class Controller /** * Sets general period variables on a view, including: - * + * * - **displayUniqueVisitors** - Whether unique visitors should be displayed for the current * period. * - **period** - The value of the **period** query parameter. @@ -677,33 +824,27 @@ abstract class Controller return; } + $periodValidator = new PeriodValidator(); + $currentPeriod = Common::getRequestVar('period'); $view->displayUniqueVisitors = SettingsPiwik::isUniqueVisitorsEnabled($currentPeriod); - $availablePeriods = array('day', 'week', 'month', 'year', 'range'); - if (!in_array($currentPeriod, $availablePeriods)) { - throw new Exception("Period must be one of: " . implode(",", $availablePeriods)); + $availablePeriods = $periodValidator->getPeriodsAllowedForUI(); + + if (! $periodValidator->isPeriodAllowedForUI($currentPeriod)) { + throw new Exception("Period must be one of: " . implode(", ", $availablePeriods)); } - $periodNames = array( - 'day' => array('singular' => Piwik::translate('CoreHome_PeriodDay'), 'plural' => Piwik::translate('CoreHome_PeriodDays')), - 'week' => array('singular' => Piwik::translate('CoreHome_PeriodWeek'), 'plural' => Piwik::translate('CoreHome_PeriodWeeks')), - 'month' => array('singular' => Piwik::translate('CoreHome_PeriodMonth'), 'plural' => Piwik::translate('CoreHome_PeriodMonths')), - 'year' => array('singular' => Piwik::translate('CoreHome_PeriodYear'), 'plural' => Piwik::translate('CoreHome_PeriodYears')), - // Note: plural is not used for date range - 'range' => array('singular' => Piwik::translate('General_DateRangeInPeriodList'), 'plural' => Piwik::translate('General_DateRangeInPeriodList')), - ); $found = array_search($currentPeriod, $availablePeriods); - if ($found !== false) { - unset($availablePeriods[$found]); - } + unset($availablePeriods[$found]); + $view->period = $currentPeriod; $view->otherPeriods = $availablePeriods; - $view->periodsNames = $periodNames; + $view->periodsNames = self::getEnabledPeriodsNames(); } /** * Helper method used to redirect the current HTTP request to another module/action. - * + * * This function will exit immediately after executing. * * @param string $moduleToRedirect The plugin to redirect to, eg. `"MultiSites"`. @@ -717,132 +858,46 @@ abstract class Controller public function redirectToIndex($moduleToRedirect, $actionToRedirect, $websiteId = null, $defaultPeriod = null, $defaultDate = null, $parameters = array()) { - if (empty($websiteId)) { - $websiteId = $this->getDefaultWebsiteId(); - } - if (empty($defaultDate)) { - $defaultDate = $this->getDefaultDate(); - } - if (empty($defaultPeriod)) { - $defaultPeriod = $this->getDefaultPeriod(); - } - $parametersString = ''; - if (!empty($parameters)) { - $parametersString = '&' . Url::getQueryStringFromParameters($parameters); - } - - if ($websiteId) { - $url = "Location: index.php?module=" . $moduleToRedirect - . "&action=" . $actionToRedirect - . "&idSite=" . $websiteId - . "&period=" . $defaultPeriod - . "&date=" . $defaultDate - . $parametersString; - header($url); - exit; + try { + $this->doRedirectToUrl($moduleToRedirect, $actionToRedirect, $websiteId, $defaultPeriod, $defaultDate, $parameters); + } catch (Exception $e) { + // no website ID to default to, so could not redirect } if (Piwik::hasUserSuperUserAccess()) { - Piwik_ExitWithMessage("Error: no website was found in this Piwik installation. -
Check the table '" . Common::prefixTable('site') . "' in your database, it should contain your Piwik websites.", false, true); + $siteTableName = Common::prefixTable('site'); + $message = "Error: no website was found in this Piwik installation. +
Check the table '$siteTableName' in your database, it should contain your Piwik websites."; + + $ex = new NoWebsiteFoundException($message); + $ex->setIsHtmlMessage(); + + throw $ex; } - $currentLogin = Piwik::getCurrentUserLogin(); - if (!empty($currentLogin) - && $currentLogin != 'anonymous' - ) { + if (!Piwik::isUserIsAnonymous()) { + $currentLogin = Piwik::getCurrentUserLogin(); $emails = implode(',', Piwik::getAllSuperUserAccessEmailAddresses()); - $errorMessage = sprintf(Piwik::translate('CoreHome_NoPrivilegesAskPiwikAdmin'), $currentLogin, "
", ""); - $errorMessage .= "

   getName() . "&action=logout'>› " . Piwik::translate('General_Logout') . "
"; - Piwik_ExitWithMessage($errorMessage, false, true); + $errorMessage = sprintf(Piwik::translate('CoreHome_NoPrivilegesAskPiwikAdmin'), $currentLogin, "
", ""); + $errorMessage .= "

   › " . Piwik::translate('General_Logout') . "
"; + + $ex = new NoPrivilegesException($errorMessage); + $ex->setIsHtmlMessage(); + + throw $ex; } echo FrontController::getInstance()->dispatch(Piwik::getLoginPluginName(), false); exit; } - /** - * Returns default site ID that Piwik should load. - * - * _Note: This value is a Piwik setting set by each user._ - * - * @return bool|int - * @api - */ - protected function getDefaultWebsiteId() - { - $defaultWebsiteId = false; - - // User preference: default website ID to load - $defaultReport = APIUsersManager::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), APIUsersManager::PREFERENCE_DEFAULT_REPORT); - if (is_numeric($defaultReport)) { - $defaultWebsiteId = $defaultReport; - } - - if ($defaultWebsiteId && Piwik::isUserHasViewAccess($defaultWebsiteId)) { - return $defaultWebsiteId; - } - - $sitesId = APISitesManager::getInstance()->getSitesIdWithAtLeastViewAccess(); - if (!empty($sitesId)) { - return $sitesId[0]; - } - return false; - } - - /** - * Returns default date for Piwik reports. - * - * _Note: This value is a Piwik setting set by each user._ - * - * @return string `'today'`, `'2010-01-01'`, etc. - * @api - */ - protected function getDefaultDate() - { - // NOTE: a change in this function might mean a change in plugins/UsersManager/javascripts/usersSettings.js as well - $userSettingsDate = APIUsersManager::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), APIUsersManager::PREFERENCE_DEFAULT_REPORT_DATE); - if ($userSettingsDate == 'yesterday') { - return $userSettingsDate; - } - // if last7, last30, etc. - if (strpos($userSettingsDate, 'last') === 0 - || strpos($userSettingsDate, 'previous') === 0 - ) { - return $userSettingsDate; - } - return 'today'; - } - - /** - * Returns default period type for Piwik reports. - * - * @return string `'day'`, `'week'`, `'month'`, `'year'` or `'range'` - * @api - */ - protected function getDefaultPeriod() - { - $userSettingsDate = APIUsersManager::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), APIUsersManager::PREFERENCE_DEFAULT_REPORT_DATE); - if ($userSettingsDate === false) { - return PiwikConfig::getInstance()->General['default_period']; - } - if (in_array($userSettingsDate, array('today', 'yesterday'))) { - return 'day'; - } - if (strpos($userSettingsDate, 'last') === 0 - || strpos($userSettingsDate, 'previous') === 0 - ) { - return 'range'; - } - return $userSettingsDate; - } /** * Checks that the token_auth in the URL matches the currently logged-in user's token_auth. - * + * * This is a protection against CSRF and should be used in all controller * methods that modify Piwik or any user settings. - * + * * **The token_auth should never appear in the browser's address bar.** * * @throws \Piwik\NoAccessException If the token doesn't match. @@ -850,7 +905,14 @@ abstract class Controller */ protected function checkTokenInUrl() { - if (Common::getRequestVar('token_auth', false) != Piwik::getCurrentUserTokenAuth()) { + $tokenRequest = Common::getRequestVar('token_auth', false); + $tokenUser = Piwik::getCurrentUserTokenAuth(); + + if (empty($tokenRequest) && empty($tokenUser)) { + return; // UI tests + } + + if ($tokenRequest !== $tokenUser) { throw new NoAccessException(Piwik::translate('General_ExceptionInvalidToken')); } } @@ -864,8 +926,9 @@ abstract class Controller */ public static function getCalendarPrettyDate($period) { - if ($period instanceof Month) // show month name when period is for a month - { + if ($period instanceof Month) { + // show month name when period is for a month + return $period->getLocalizedLongString(); } else { return $period->getPrettyString(); @@ -881,7 +944,7 @@ abstract class Controller */ public static function getPrettyDate($date, $period) { - return self::getCalendarPrettyDate(Period::factory($period, Date::factory($date))); + return self::getCalendarPrettyDate(Period\Factory::build($period, Date::factory($date))); } /** @@ -914,7 +977,7 @@ abstract class Controller if ($evolutionPercent < 0) { $class = "negative-evolution"; $img = "arrow_down.png"; - } else if ($evolutionPercent == 0) { + } elseif ($evolutionPercent == 0) { $class = "neutral-evolution"; $img = "stop.png"; } else { @@ -923,6 +986,9 @@ abstract class Controller $titleEvolutionPercent = '+' . $titleEvolutionPercent; } + $currentValue = NumberFormatter::getInstance()->format($currentValue); + $pastValue = NumberFormatter::getInstance()->format($pastValue); + $title = Piwik::translate('General_EvolutionSummaryGeneric', array( Piwik::translate('General_NVisits', $currentValue), $date, @@ -941,4 +1007,38 @@ abstract class Controller return $result; } + + protected function checkSitePermission() + { + if (!empty($this->idSite) && empty($this->site)) { + throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'view'", $this->idSite))); + } elseif (empty($this->site) || empty($this->idSite)) { + throw new Exception("The requested website idSite is not found in the request, or is invalid. + Please check that you are logged in Piwik and have permission to access the specified website."); + } + } + + /** + * @param $moduleToRedirect + * @param $actionToRedirect + * @param $websiteId + * @param $defaultPeriod + * @param $defaultDate + * @param $parameters + * @throws Exception + */ + private function doRedirectToUrl($moduleToRedirect, $actionToRedirect, $websiteId, $defaultPeriod, $defaultDate, $parameters) + { + $menu = new Menu(); + + $parameters = array_merge( + $menu->urlForDefaultUserParams($websiteId, $defaultPeriod, $defaultDate), + $parameters + ); + $queryParams = !empty($parameters) ? '&' . Url::getQueryStringFromParameters($parameters) : ''; + $url = "index.php?module=%s&action=%s"; + $url = sprintf($url, $moduleToRedirect, $actionToRedirect); + $url = $url . $queryParams; + Url::redirectToUrl($url); + } } diff --git a/www/analytics/core/Plugin/ControllerAdmin.php b/www/analytics/core/Plugin/ControllerAdmin.php index 56051341..e503a615 100644 --- a/www/analytics/core/Plugin/ControllerAdmin.php +++ b/www/analytics/core/Plugin/ControllerAdmin.php @@ -1,6 +1,6 @@ Tracker['record_statistics']; @@ -42,6 +44,7 @@ abstract class ControllerAdmin extends Controller private static function notifyAnyInvalidPlugin() { $missingPlugins = \Piwik\Plugin\Manager::getInstance()->getMissingPlugins(); + if (empty($missingPlugins)) { return; } @@ -49,12 +52,15 @@ abstract class ControllerAdmin extends Controller if (!Piwik::hasUserSuperUserAccess()) { return; } + $pluginsLink = Url::getCurrentQueryStringWithParametersModified(array( 'module' => 'CorePluginsAdmin', 'action' => 'plugins' )); + $invalidPluginsWarning = Piwik::translate('CoreAdminHome_InvalidPluginsWarning', array( self::getPiwikVersion(), '' . implode('', $missingPlugins) . '')) + . "
" . Piwik::translate('CoreAdminHome_InvalidPluginsYouCanUninstall', array( '', '' @@ -63,7 +69,7 @@ abstract class ControllerAdmin extends Controller $notification = new Notification($invalidPluginsWarning); $notification->raw = true; $notification->context = Notification::CONTEXT_WARNING; - $notification->title = Piwik::translate('General_Warning') . ':'; + $notification->title = Piwik::translate('General_Warning'); Notification\Manager::notify('ControllerAdmin_InvalidPluginsWarning', $notification); } @@ -81,10 +87,40 @@ abstract class ControllerAdmin extends Controller self::setBasicVariablesAdminView($view); } + private static function notifyIfURLIsNotSecure() + { + $isURLSecure = ProxyHttp::isHttps(); + if ($isURLSecure) { + return; + } + + if (!Piwik::hasUserSuperUserAccess()) { + return; + } + + if(Url::isLocalHost(Url::getCurrentHost())) { + return; + } + + + $message = Piwik::translate('General_CurrentlyUsingUnsecureHttp'); + + $message .= " "; + + $message .= Piwik::translate('General_ReadThisToLearnMore', + array('', '') + ); + + $notification = new Notification($message); + $notification->context = Notification::CONTEXT_WARNING; + $notification->raw = true; + Notification\Manager::notify('ControllerAdmin_HttpIsUsed', $notification); + } + /** * @ignore */ - static public function displayWarningIfConfigFileNotWritable() + public static function displayWarningIfConfigFileNotWritable() { $isConfigFileWritable = PiwikConfig::getInstance()->isFileWritable(); @@ -99,36 +135,69 @@ abstract class ControllerAdmin extends Controller } } - /** - * See http://dev.piwik.org/trac/ticket/4439#comment:8 and https://github.com/eaccelerator/eaccelerator/issues/12 - * - * Eaccelerator does not support closures and is known to be not comptabile with Piwik. Therefore we are disabling - * it automatically. At this point it looks like Eaccelerator is no longer under development and the bug has not - * been fixed within a year. - */ - public static function disableEacceleratorIfEnabled() - { - $isEacceleratorUsed = ini_get('eaccelerator.enable'); - - if (!empty($isEacceleratorUsed)) { - self::$isEacceleratorUsed = true; - - @ini_set('eaccelerator.enable', 0); - } - } private static function notifyIfEAcceleratorIsUsed() { - if (self::$isEacceleratorUsed) { - $message = sprintf("You are using the PHP accelerator & optimizer eAccelerator which is known to be not compatible with Piwik. - We have disabled eAccelerator, which might affect the performance of Piwik. - Read the %srelated ticket%s for more information and how to fix this problem.", - '', ''); + $isEacceleratorUsed = ini_get('eaccelerator.enable'); + if (empty($isEacceleratorUsed)) { + return; + } + $message = sprintf("You are using the PHP accelerator & optimizer eAccelerator which is known to be not compatible with Piwik. + We have disabled eAccelerator, which might affect the performance of Piwik. + Read the %srelated ticket%s for more information and how to fix this problem.", + '', ''); + $notification = new Notification($message); + $notification->context = Notification::CONTEXT_WARNING; + $notification->raw = true; + Notification\Manager::notify('ControllerAdmin_EacceleratorIsUsed', $notification); + } + + private static function notifyWhenPhpVersionIsEOL() + { + $deprecatedMajorPhpVersion = null; + if(self::isPhpVersion53()) { + $deprecatedMajorPhpVersion = '5.3'; + } elseif(self::isPhpVersion54()) { + $deprecatedMajorPhpVersion = '5.4'; + } + + $notifyPhpIsEOL = Piwik::hasUserSuperUserAccess() && $deprecatedMajorPhpVersion; + if (!$notifyPhpIsEOL) { + return; + } + + $nextRequiredMinimumPHP = '5.5'; + + $message = Piwik::translate('General_WarningPiwikWillStopSupportingPHPVersion', array($deprecatedMajorPhpVersion, $nextRequiredMinimumPHP)) + . "\n " + . Piwik::translate('General_WarningPhpVersionXIsTooOld', $deprecatedMajorPhpVersion); + + $notification = new Notification($message); + $notification->title = Piwik::translate('General_Warning'); + $notification->priority = Notification::PRIORITY_LOW; + $notification->context = Notification::CONTEXT_WARNING; + $notification->type = Notification::TYPE_TRANSIENT; + $notification->flags = Notification::FLAG_NO_CLEAR; + NotificationManager::notify('DeprecatedPHPVersionCheck', $notification); + } + + private static function notifyWhenDebugOnDemandIsEnabled($trackerSetting) + { + if (!Development::isEnabled() + && Piwik::hasUserSuperUserAccess() && + TrackerConfig::getConfigValue($trackerSetting)) { + + $message = Piwik::translate('General_WarningDebugOnDemandEnabled'); + $message = sprintf($message, '"' . $trackerSetting . '"', '"[Tracker] ' . $trackerSetting . '"', '"0"', + '"config/config.ini.php"'); $notification = new Notification($message); + $notification->title = Piwik::translate('General_Warning'); + $notification->priority = Notification::PRIORITY_LOW; $notification->context = Notification::CONTEXT_WARNING; - $notification->raw = true; - Notification\Manager::notify('ControllerAdmin_EacceleratorIsUsed', $notification); + $notification->type = Notification::TYPE_TRANSIENT; + $notification->flags = Notification::FLAG_NO_CLEAR; + NotificationManager::notify('Tracker' . $trackerSetting, $notification); } } @@ -140,7 +209,6 @@ abstract class ControllerAdmin extends Controller * - **statisticsNotRecorded** - Set to true if the `[Tracker] record_statistics` INI * config is `0`. If not `0`, this variable will not be defined. * - **topMenu** - The result of `MenuTop::getInstance()->getMenu()`. - * - **currentAdminMenuName** - The currently selected admin menu name. * - **enableFrames** - The value of the `[General] enable_framed_pages` INI config option. If * true, {@link Piwik\View::setXFrameOptions()} is called on the view. * - **isSuperUser** - Whether the current user is a superuser or not. @@ -155,17 +223,20 @@ abstract class ControllerAdmin extends Controller * @param View $view * @api */ - static public function setBasicVariablesAdminView(View $view) + public static function setBasicVariablesAdminView(View $view) { self::notifyWhenTrackingStatisticsDisabled(); self::notifyIfEAcceleratorIsUsed(); + self::notifyIfURLIsNotSecure(); - $view->topMenu = MenuTop::getInstance()->getMenu(); - $view->currentAdminMenuName = MenuAdmin::getInstance()->getCurrentAdminMenuName(); + $view->topMenu = MenuTop::getInstance()->getMenu(); + $view->userMenu = MenuUser::getInstance()->getMenu(); $view->isDataPurgeSettingsEnabled = self::isDataPurgeSettingsEnabled(); - $view->enableFrames = PiwikConfig::getInstance()->General['enable_framed_settings']; - if (!$view->enableFrames) { + $enableFrames = PiwikConfig::getInstance()->General['enable_framed_settings']; + $view->enableFrames = $enableFrames; + + if (!$enableFrames) { $view->setXFrameOptions('sameorigin'); } @@ -175,19 +246,27 @@ abstract class ControllerAdmin extends Controller self::checkPhpVersion($view); + self::notifyWhenPhpVersionIsEOL(); + self::notifyWhenDebugOnDemandIsEnabled('debug'); + self::notifyWhenDebugOnDemandIsEnabled('debug_on_demand'); + $adminMenu = MenuAdmin::getInstance()->getMenu(); $view->adminMenu = $adminMenu; - $view->notifications = NotificationManager::getAllNotificationsToDisplay(); - NotificationManager::cancelAllNonPersistent(); + $notifications = $view->notifications; + + if (empty($notifications)) { + $view->notifications = NotificationManager::getAllNotificationsToDisplay(); + NotificationManager::cancelAllNonPersistent(); + } } - static public function isDataPurgeSettingsEnabled() + public static function isDataPurgeSettingsEnabled() { return (bool) Config::getInstance()->General['enable_delete_old_data_settings_admin']; } - static protected function getPiwikVersion() + protected static function getPiwikVersion() { return "Piwik " . Version::VERSION; } @@ -202,12 +281,13 @@ abstract class ControllerAdmin extends Controller $view->phpIsNewEnough = version_compare($view->phpVersion, '5.3.0', '>='); } - protected function getDefaultWebsiteId() + private static function isPhpVersion53() { - $sitesId = \Piwik\Plugins\SitesManager\API::getInstance()->getSitesIdWithAdminAccess(); - if (!empty($sitesId)) { - return $sitesId[0]; - } - return parent::getDefaultWebsiteId(); + return strpos(PHP_VERSION, '5.3') === 0; + } + + private static function isPhpVersion54() + { + return strpos(PHP_VERSION, '5.4') === 0; } } diff --git a/www/analytics/core/Plugin/Dependency.php b/www/analytics/core/Plugin/Dependency.php index 0448f8a7..bcade2b3 100644 --- a/www/analytics/core/Plugin/Dependency.php +++ b/www/analytics/core/Plugin/Dependency.php @@ -1,6 +1,6 @@ ='; $required = trim($required); @@ -97,7 +97,8 @@ class Dependency if (!empty($plugin)) { return $plugin->getVersion(); } - } catch (\Exception $e) {} + } catch (\Exception $e) { + } } return ''; diff --git a/www/analytics/core/Plugin/Dimension/ActionDimension.php b/www/analytics/core/Plugin/Dimension/ActionDimension.php new file mode 100644 index 00000000..ce8a1eed --- /dev/null +++ b/www/analytics/core/Plugin/Dimension/ActionDimension.php @@ -0,0 +1,254 @@ + array("ADD COLUMN `$this->columnName` $this->columnType", "ADD INDEX ...") + ); + ``` + * @api + */ + public function install() + { + if (empty($this->columnName) || empty($this->columnType)) { + return array(); + } + + return array( + $this->tableName => 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)) { + return array(); + } + + return array( + $this->tableName => 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)) { + return; + } + + try { + $sql = "ALTER TABLE `" . Common::prefixTable($this->tableName) . "` DROP COLUMN `$this->columnName`"; + Db::exec($sql); + } catch (Exception $e) { + if (!Db::get()->isErrNo($e, '1091')) { + throw $e; + } + } + } + + /** + * Get the version of the dimension which is used for update checks. + * @return string + * @ignore + */ + public function getVersion() + { + return $this->columnType; + } + + /** + * If the value you want to save for your dimension is something like a page title or page url, you usually do not + * want to save the raw value over and over again to save bytes in the database. Instead you want to save each value + * once in the log_action table and refer to this value by its ID in the log_link_visit_action table. You can do + * this by returning an action id in "getActionId()" and by returning a value here. If a value should be ignored + * or not persisted just return boolean false. Please note if you return a value here and you implement the event + * "onNewAction" the value will be probably overwritten by the other event. So make sure to implement only one of + * those. + * + * @param Request $request + * @param Action $action + * + * @return false|mixed + * @api + */ + public function onLookupAction(Request $request, Action $action) + { + return false; + } + + /** + * An action id. The value returned by the lookup action will be associated with this id in the log_action table. + * @return int + * @throws Exception in case not implemented + */ + public function getActionId() + { + throw new Exception('You need to overwrite the getActionId method in case you implement the onLookupAction method in class: ' . get_class($this)); + } + + /** + * This event is triggered before a new action is logged to the `log_link_visit_action` table. It overwrites any + * looked up action so it makes usually no sense to implement both methods but it sometimes does. You can assign + * any value to the column or return boolan false in case you do not want to save any value. + * + * @param Request $request + * @param Visitor $visitor + * @param Action $action + * + * @return mixed|false + * @api + */ + public function onNewAction(Request $request, Visitor $visitor, Action $action) + { + return false; + } + + /** + * 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) + { + $sqlSegment = $segment->getSqlSegment(); + if (!empty($this->columnName) && empty($sqlSegment)) { + $segment->setSqlSegment($this->tableName . '.' . $this->columnName); + } + + parent::addSegment($segment); + } + + /** + * Get all action dimensions that are defined by all activated plugins. + * @return ActionDimension[] + * @ignore + */ + public static function getAllDimensions() + { + $cacheId = CacheId::pluginAware('ActionDimensions'); + $cache = PiwikCache::getTransientCache(); + + if (!$cache->contains($cacheId)) { + $plugins = PluginManager::getInstance()->getPluginsLoadedAndActivated(); + $instances = array(); + + foreach ($plugins as $plugin) { + foreach (self::getDimensions($plugin) as $instance) { + $instances[] = $instance; + } + } + + $cache->save($cacheId, $instances); + } + + return $cache->fetch($cacheId); + } + + /** + * Get all action dimensions that are defined by the given plugin. + * @param Plugin $plugin + * @return ActionDimension[] + * @ignore + */ + public static function getDimensions(Plugin $plugin) + { + $dimensions = $plugin->findMultipleComponents('Columns', '\\Piwik\\Plugin\\Dimension\\ActionDimension'); + $instances = array(); + + foreach ($dimensions as $dimension) { + $instances[] = new $dimension(); + } + + return $instances; + } +} diff --git a/www/analytics/core/Plugin/Dimension/ConversionDimension.php b/www/analytics/core/Plugin/Dimension/ConversionDimension.php new file mode 100644 index 00000000..ff1bff55 --- /dev/null +++ b/www/analytics/core/Plugin/Dimension/ConversionDimension.php @@ -0,0 +1,247 @@ + array("ADD COLUMN `$this->columnName` $this->columnType", "ADD INDEX ...") + ); + ``` + * @api + */ + public function install() + { + if (empty($this->columnName) || empty($this->columnType)) { + return array(); + } + + return array( + $this->tableName => array("ADD COLUMN `$this->columnName` $this->columnType") + ); + } + + /** + * @see ActionDimension::update() + * @return array + * @ignore + */ + public function update() + { + if (empty($this->columnName) || empty($this->columnType)) { + return array(); + } + + return array( + $this->tableName => 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)) { + return; + } + + try { + $sql = "ALTER TABLE `" . Common::prefixTable($this->tableName) . "` DROP COLUMN `$this->columnName`"; + Db::exec($sql); + } catch (Exception $e) { + if (!Db::get()->isErrNo($e, '1091')) { + throw $e; + } + } + } + + /** + * @see ActionDimension::getVersion() + * @return string + * @ignore + */ + public function getVersion() + { + return $this->columnType; + } + + /** + * 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) + { + $sqlSegment = $segment->getSqlSegment(); + if (!empty($this->columnName) && empty($sqlSegment)) { + $segment->setSqlSegment($this->tableName . '.' . $this->columnName); + } + + parent::addSegment($segment); + } + + /** + * Get all conversion dimensions that are defined by all activated plugins. + * @ignore + */ + public static function getAllDimensions() + { + $cacheId = CacheId::pluginAware('ConversionDimensions'); + $cache = PiwikCache::getTransientCache(); + + if (!$cache->contains($cacheId)) { + $plugins = PluginManager::getInstance()->getPluginsLoadedAndActivated(); + $instances = array(); + + foreach ($plugins as $plugin) { + foreach (self::getDimensions($plugin) as $instance) { + $instances[] = $instance; + } + } + + $cache->save($cacheId, $instances); + } + + return $cache->fetch($cacheId); + } + + /** + * Get all conversion dimensions that are defined by the given plugin. + * @param Plugin $plugin + * @return ConversionDimension[] + * @ignore + */ + public static function getDimensions(Plugin $plugin) + { + $dimensions = $plugin->findMultipleComponents('Columns', '\\Piwik\\Plugin\\Dimension\\ConversionDimension'); + $instances = array(); + + foreach ($dimensions as $dimension) { + $instances[] = new $dimension(); + } + + return $instances; + } + + /** + * This event is triggered when an ecommerce order is converted. Any returned value will be persist in the database. + * Return boolean `false` if you do not want to change the value in some cases. + * + * @param Request $request + * @param Visitor $visitor + * @param Action|null $action + * @param GoalManager $goalManager + * + * @return mixed|false + * @api + */ + public function onEcommerceOrderConversion(Request $request, Visitor $visitor, $action, GoalManager $goalManager) + { + return false; + } + + /** + * This event is triggered when an ecommerce cart update is converted. Any returned value will be persist in the + * database. Return boolean `false` if you do not want to change the value in some cases. + * + * @param Request $request + * @param Visitor $visitor + * @param Action|null $action + * @param GoalManager $goalManager + * + * @return mixed|false + * @api + */ + public function onEcommerceCartUpdateConversion(Request $request, Visitor $visitor, $action, GoalManager $goalManager) + { + return false; + } + + /** + * This event is triggered when an any custom goal is converted. Any returned value will be persist in the + * database. Return boolean `false` if you do not want to change the value in some cases. + * + * @param Request $request + * @param Visitor $visitor + * @param Action|null $action + * @param GoalManager $goalManager + * + * @return mixed|false + * @api + */ + public function onGoalConversion(Request $request, Visitor $visitor, $action, GoalManager $goalManager) + { + return false; + } +} diff --git a/www/analytics/core/Plugin/Dimension/DimensionMetadataProvider.php b/www/analytics/core/Plugin/Dimension/DimensionMetadataProvider.php new file mode 100644 index 00000000..089c60c0 --- /dev/null +++ b/www/analytics/core/Plugin/Dimension/DimensionMetadataProvider.php @@ -0,0 +1,107 @@ +actionReferenceColumnsOverride = $actionReferenceColumnsOverride; + } + + /** + * Returns a list of idaction column names organized by table name. Uses dimension metadata + * to find idaction columns dynamically. + * + * Note: It is not currently possible to use the Piwik platform to add idaction columns to tables + * other than log_link_visit_action (w/o doing something unsupported), so idaction columns in + * other tables are hard coded. + * + * @return array[] + */ + public function getActionReferenceColumnsByTable() + { + $result = array( + 'log_link_visit_action' => array('idaction_url', + 'idaction_url_ref', + 'idaction_name_ref' + ), + + 'log_conversion' => array('idaction_url'), + + 'log_visit' => array('visit_exit_idaction_url', + 'visit_exit_idaction_name', + 'visit_entry_idaction_url', + 'visit_entry_idaction_name'), + + 'log_conversion_item' => array('idaction_sku', + 'idaction_name', + 'idaction_category', + 'idaction_category2', + 'idaction_category3', + 'idaction_category4', + 'idaction_category5') + ); + + $dimensionIdActionColumns = $this->getVisitActionTableActionReferences(); + $result['log_link_visit_action'] = array_unique( + array_merge($result['log_link_visit_action'], $dimensionIdActionColumns)); + + foreach ($this->actionReferenceColumnsOverride as $table => $columns) { + if (empty($result[$table])) { + $result[$table] = $columns; + } else { + $result[$table] = array_unique(array_merge($result[$table], $columns)); + } + } + + return $result; + } + + private function getVisitActionTableActionReferences() + { + $idactionColumns = array(); + foreach (ActionDimension::getAllDimensions() as $actionDimension) { + if ($this->isActionReference($actionDimension)) { + $idactionColumns[] = $actionDimension->getColumnName(); + } + } + return $idactionColumns; + } + + + /** + * Returns `true` if the column for this dimension is a reference to the `log_action` table (ie, an "idaction column"), + * `false` if otherwise. + * + * @return bool + */ + private function isActionReference(ActionDimension $dimension) + { + try { + $dimension->getActionId(); + + return true; + } catch (\Exception $ex) { + return false; + } + } +} diff --git a/www/analytics/core/Plugin/Dimension/VisitDimension.php b/www/analytics/core/Plugin/Dimension/VisitDimension.php new file mode 100644 index 00000000..3f258da4 --- /dev/null +++ b/www/analytics/core/Plugin/Dimension/VisitDimension.php @@ -0,0 +1,396 @@ + array("ADD COLUMN `$this->columnName` $this->columnType", "ADD INDEX ...") + ); + ``` + * @api + */ + public function install() + { + if (empty($this->columnType) || empty($this->columnName)) { + return array(); + } + + $changes = array( + $this->tableName => array("ADD COLUMN `$this->columnName` $this->columnType") + ); + + if ($this->isHandlingLogConversion()) { + $changes['log_conversion'] = array("ADD COLUMN `$this->columnName` $this->columnType"); + } + + return $changes; + } + + /** + * @see ActionDimension::update() + * @param array $conversionColumns An array of currently installed columns in the conversion table. + * @return array + * @ignore + */ + public function update($conversionColumns) + { + if (!$this->columnType) { + return array(); + } + + $changes = array(); + + $changes[$this->tableName] = array("MODIFY COLUMN `$this->columnName` $this->columnType"); + + $handlingConversion = $this->isHandlingLogConversion(); + $hasConversionColumn = array_key_exists($this->columnName, $conversionColumns); + + if ($hasConversionColumn && $handlingConversion) { + $changes['log_conversion'] = array("MODIFY COLUMN `$this->columnName` $this->columnType"); + } elseif (!$hasConversionColumn && $handlingConversion) { + $changes['log_conversion'] = array("ADD COLUMN `$this->columnName` $this->columnType"); + } elseif ($hasConversionColumn && !$handlingConversion) { + $changes['log_conversion'] = array("DROP COLUMN `$this->columnName`"); + } + + return $changes; + } + + /** + * @see ActionDimension::getVersion() + * @return string + * @ignore + */ + public function getVersion() + { + return $this->columnType . $this->isHandlingLogConversion(); + } + + private function isHandlingLogConversion() + { + if (empty($this->columnName) || empty($this->columnType)) { + return false; + } + + return $this->hasImplementedEvent('onAnyGoalConversion'); + } + + /** + * 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)) { + return; + } + + try { + $sql = "ALTER TABLE `" . Common::prefixTable($this->tableName) . "` DROP COLUMN `$this->columnName`"; + Db::exec($sql); + } catch (Exception $e) { + if (!Db::get()->isErrNo($e, '1091')) { + throw $e; + } + } + + try { + if (!$this->isHandlingLogConversion()) { + return; + } + + $sql = "ALTER TABLE `" . Common::prefixTable('log_conversion') . "` DROP COLUMN `$this->columnName`"; + Db::exec($sql); + } catch (Exception $e) { + if (!Db::get()->isErrNo($e, '1091')) { + throw $e; + } + } + } + + /** + * 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) + { + $sqlSegment = $segment->getSqlSegment(); + if (!empty($this->columnName) && empty($sqlSegment)) { + $segment->setSqlSegment('log_visit.' . $this->columnName); + } + + parent::addSegment($segment); + } + + /** + * Sometimes you may want to make sure another dimension is executed before your dimension so you can persist + * this dimensions' value depending on the value of other dimensions. You can do this by defining an array of + * dimension names. If you access any value of any other column within your events, you should require them here. + * Otherwise those values may not be available. + * @return array + * @api + */ + public function getRequiredVisitFields() + { + return array(); + } + + /** + * The `onNewVisit` method is triggered when a new visitor is detected. This means you can define an initial + * value for this user here. By returning boolean `false` no value will be saved. Once the user makes another action + * the event "onExistingVisit" is executed. Meaning for each visitor this method is executed once. + * + * @param Request $request + * @param Visitor $visitor + * @param Action|null $action + * @return mixed|false + * @api + */ + public function onNewVisit(Request $request, Visitor $visitor, $action) + { + return false; + } + + /** + * The `onExistingVisit` method is triggered when a visitor was recognized meaning it is not a new visitor. + * You can overwrite any previous value set by the event `onNewVisit` by implemting this event. By returning boolean + * `false` no value will be updated. + * + * @param Request $request + * @param Visitor $visitor + * @param Action|null $action + * @return mixed|false + * @api + */ + public function onExistingVisit(Request $request, Visitor $visitor, $action) + { + return false; + } + + /** + * This event is executed shortly after `onNewVisit` or `onExistingVisit` in case the visitor converted a goal. + * Usually this event is not needed and you can simply remove this method therefore. An example would be for + * instance to persist the last converted action url. Return boolean `false` if you do not want to change the + * current value. + * + * @param Request $request + * @param Visitor $visitor + * @param Action|null $action + * @return mixed|false + * @api + */ + public function onConvertedVisit(Request $request, Visitor $visitor, $action) + { + return false; + } + + /** + * By implementing this event you can persist a value to the `log_conversion` table in case a conversion happens. + * The persisted value will be logged along the conversion and will not be changed afterwards. This allows you to + * generate reports that shows for instance which url was called how often for a specific conversion. Once you + * implement this event and a $columnType is defined a column in the `log_conversion` MySQL table will be + * created automatically. + * + * @param Request $request + * @param Visitor $visitor + * @param Action|null $action + * @return mixed|false + * @api + */ + public function onAnyGoalConversion(Request $request, Visitor $visitor, $action) + { + return false; + } + + /** + * This hook is executed by the tracker when determining if an action is the start of a new visit + * or part of an existing one. Derived classes can use it to force new visits based on dimension + * data. + * + * For example, the Campaign dimension in the Referrers plugin will force a new visit if the + * campaign information for the current action is different from the last. + * + * @param Request $request The current tracker request information. + * @param Visitor $visitor The information for the currently recognized visitor. + * @param Action|null $action The current action information (if any). + * @return bool Return true to force a visit, false if otherwise. + * @api + */ + public function shouldForceNewVisit(Request $request, Visitor $visitor, Action $action = null) + { + return false; + } + + /** + * Get all visit dimensions that are defined by all activated plugins. + * @return VisitDimension[] + */ + public static function getAllDimensions() + { + $cacheId = CacheId::pluginAware('VisitDimensions'); + $cache = PiwikCache::getTransientCache(); + + if (!$cache->contains($cacheId)) { + $plugins = PluginManager::getInstance()->getPluginsLoadedAndActivated(); + $instances = array(); + + foreach ($plugins as $plugin) { + foreach (self::getDimensions($plugin) as $instance) { + $instances[] = $instance; + } + } + + $instances = self::sortDimensions($instances); + + $cache->save($cacheId, $instances); + } + + return $cache->fetch($cacheId); + } + + /** + * @ignore + * @param VisitDimension[] $dimensions + */ + public static function sortDimensions($dimensions) + { + $sorted = array(); + $exists = array(); + + // we first handle all the once without dependency + foreach ($dimensions as $index => $dimension) { + $fields = $dimension->getRequiredVisitFields(); + if (empty($fields)) { + $sorted[] = $dimension; + $exists[] = $dimension->getColumnName(); + unset($dimensions[$index]); + } + } + + // find circular references + // and remove dependencies whose column cannot be resolved because it is not installed / does not exist / is defined by core + $depenencies = array(); + foreach ($dimensions as $dimension) { + $depenencies[$dimension->getColumnName()] = $dimension->getRequiredVisitFields(); + } + + foreach ($depenencies as $column => $fields) { + foreach ($fields as $key => $field) { + if (empty($depenencies[$field]) && !in_array($field, $exists)) { + // we cannot resolve that dependency as it does not exist + unset($depenencies[$column][$key]); + } elseif (!empty($depenencies[$field]) && in_array($column, $depenencies[$field])) { + throw new Exception("Circular reference detected for required field $field in dimension $column"); + } + } + } + + $count = 0; + while (count($dimensions) > 0) { + $count++; + if ($count > 1000) { + foreach ($dimensions as $dimension) { + $sorted[] = $dimension; + } + break; // to prevent an endless loop + } + foreach ($dimensions as $key => $dimension) { + $fields = $depenencies[$dimension->getColumnName()]; + if (count(array_intersect($fields, $exists)) === count($fields)) { + $sorted[] = $dimension; + $exists[] = $dimension->getColumnName(); + unset($dimensions[$key]); + } + } + } + + return $sorted; + } + + /** + * Get all visit dimensions that are defined by the given plugin. + * @param Plugin $plugin + * @return VisitDimension[] + * @ignore + */ + public static function getDimensions(Plugin $plugin) + { + $dimensions = $plugin->findMultipleComponents('Columns', '\\Piwik\\Plugin\\Dimension\\VisitDimension'); + $instances = array(); + + foreach ($dimensions as $dimension) { + $instances[] = new $dimension(); + } + + return $instances; + } +} diff --git a/www/analytics/core/Plugin/Manager.php b/www/analytics/core/Plugin/Manager.php index b8f135dc..63a3e0aa 100644 --- a/www/analytics/core/Plugin/Manager.php +++ b/www/analytics/core/Plugin/Manager.php @@ -1,6 +1,6 @@ pluginList = $pluginList; + } /** * Loads plugin that are enabled */ public function loadActivatedPlugins() { - $pluginsToLoad = Config::getInstance()->Plugins['Plugins']; + $pluginsToLoad = $this->getActivatedPluginsFromConfig(); $this->loadPlugins($pluginsToLoad); } @@ -97,11 +109,8 @@ class Manager extends Singleton */ public function loadCorePluginsDuringTracker() { - $pluginsToLoad = Config::getInstance()->Plugins['Plugins']; - $pluginsToLoad = array_diff($pluginsToLoad, Tracker::getPluginsNotToLoad()); - if(defined('PIWIK_TEST_MODE')) { - $pluginsToLoad = array_intersect($pluginsToLoad, $this->getPluginsToLoadDuringTests()); - } + $pluginsToLoad = $this->pluginList->getActivatedPlugins(); + $pluginsToLoad = array_diff($pluginsToLoad, $this->getTrackerPluginsNotToLoad()); $this->loadPlugins($pluginsToLoad); } @@ -110,72 +119,62 @@ class Manager extends Singleton */ public function loadTrackerPlugins() { - $this->unloadPlugins(); - $pluginsTracker = PiwikConfig::getInstance()->Plugins_Tracker['Plugins_Tracker']; + $cacheId = 'PluginsTracker'; + $cache = Cache::getEagerCache(); + + if ($cache->contains($cacheId)) { + $pluginsTracker = $cache->fetch($cacheId); + } else { + $this->unloadPlugins(); + $this->loadActivatedPlugins(); + + $pluginsTracker = array(); + + foreach ($this->loadedPlugins as $pluginName => $plugin) { + if ($this->isTrackerPlugin($plugin)) { + $pluginsTracker[] = $pluginName; + } + } + + if (!empty($pluginsTracker)) { + $cache->save($cacheId, $pluginsTracker); + } + } + if (empty($pluginsTracker)) { + $this->unloadPlugins(); return array(); } - $pluginsTracker = array_diff($pluginsTracker, Tracker::getPluginsNotToLoad()); - if(defined('PIWIK_TEST_MODE')) { - $pluginsTracker = array_intersect($pluginsTracker, $this->getPluginsToLoadDuringTests()); - } + $pluginsTracker = array_diff($pluginsTracker, $this->getTrackerPluginsNotToLoad()); $this->doNotLoadAlwaysActivatedPlugins(); $this->loadPlugins($pluginsTracker); + + // we could simply unload all plugins first before loading plugins but this way it is much faster + // since we won't have to create each plugin again and we won't have to parse each plugin metadata file + // again etc + $this->makeSureOnlyActivatedPluginsAreLoaded(); + return $pluginsTracker; } - public function getPluginsToLoadDuringTests() + /** + * Do not load the specified plugins (used during testing, to disable Provider plugin) + * @param array $plugins + */ + public function setTrackerPluginsNotToLoad($plugins) { - $toLoad = array(); - - $loadStandalonePluginsDuringTests = @Config::getInstance()->DebugTests['enable_load_standalone_plugins_during_tests']; - - foreach($this->readPluginsDirectory() as $plugin) { - $forceDisable = array( - 'ExampleVisualization', // adds an icon - 'LoginHttpAuth', // other Login plugins would conflict - ); - if(in_array($plugin, $forceDisable)) { - continue; - } - - // Load all default plugins - $isPluginBundledWithCore = $this->isPluginBundledWithCore($plugin); - - // Load plugins from submodules - $isPluginOfficiallySupported = $this->isPluginOfficialAndNotBundledWithCore($plugin); - - // Also load plugins which are Git repositories (eg. being developed) - $isPluginHasGitRepository = file_exists( PIWIK_INCLUDE_PATH . '/plugins/' . $plugin . '/.git/config'); - - $loadPlugin = $isPluginBundledWithCore || $isPluginOfficiallySupported; - - if($loadStandalonePluginsDuringTests) { - $loadPlugin = $loadPlugin || $isPluginHasGitRepository; - } else { - $loadPlugin = $loadPlugin && !$isPluginHasGitRepository; - } - - // Do not enable other Themes - $disabledThemes = $this->coreThemesDisabledByDefault; - - // PleineLune is officially supported, yet we don't want to enable another theme in tests (we test for Morpheus) - $disabledThemes[] = "PleineLune"; - - $isThemeDisabled = in_array($plugin, $disabledThemes); - - $loadPlugin = $loadPlugin && !$isThemeDisabled; - if($loadPlugin) { - $toLoad[] = $plugin; - } - } - return $toLoad; + $this->trackerPluginsNotToLoad = $plugins; } - public function getCorePluginsDisabledByDefault() + /** + * Get list of plugins to not load + * + * @return array + */ + public function getTrackerPluginsNotToLoad() { - return array_merge( $this->corePluginsDisabledByDefault, $this->coreThemesDisabledByDefault); + return $this->trackerPluginsNotToLoad; } // If a plugin hooks onto at least an event starting with "Tracker.", we load the plugin during tracker @@ -188,7 +187,7 @@ class Manager extends Singleton public function isPluginOfficialAndNotBundledWithCore($pluginName) { static $gitModules; - if(empty($gitModules)) { + if (empty($gitModules)) { $gitModules = file_get_contents(PIWIK_INCLUDE_PATH . '/.gitmodules'); } // All submodules are officially maintained plugins @@ -199,27 +198,16 @@ class Manager extends Singleton /** * Update Plugins config * - * @param array $plugins Plugins + * @param array $pluginsToLoad Plugins */ private function updatePluginsConfig($pluginsToLoad) { + $pluginsToLoad = $this->pluginList->sortPlugins($pluginsToLoad); $section = PiwikConfig::getInstance()->Plugins; $section['Plugins'] = $pluginsToLoad; PiwikConfig::getInstance()->Plugins = $section; } - /** - * Update Plugins_Tracker config - * - * @param array $plugins Plugins - */ - private function updatePluginsTrackerConfig($plugins) - { - $section = PiwikConfig::getInstance()->Plugins_Tracker; - $section['Plugins_Tracker'] = $plugins; - PiwikConfig::getInstance()->Plugins_Tracker = $section; - } - /** * Update PluginsInstalled config * @@ -232,6 +220,12 @@ class Manager extends Singleton PiwikConfig::getInstance()->PluginsInstalled = $section; } + public function clearPluginsInstalledConfig() + { + $this->updatePluginsInstalledConfig(array()); + PiwikConfig::getInstance()->forceSave(); + } + /** * Returns true if plugin is always activated * @@ -264,7 +258,20 @@ class Manager extends Singleton public function isPluginActivated($name) { return in_array($name, $this->pluginsToLoad) - || $this->isPluginAlwaysActivated($name); + || ($this->doLoadAlwaysActivatedPlugins && $this->isPluginAlwaysActivated($name)); + } + + /** + * Checks whether the given plugin is activated, if not triggers an exception. + * + * @param string $pluginName + * @throws PluginDeactivatedException + */ + public function checkIsPluginActivated($pluginName) + { + if (!$this->isPluginActivated($pluginName)) { + throw new PluginDeactivatedException($pluginName); + } } /** @@ -310,6 +317,8 @@ class Manager extends Singleton */ public function deactivatePlugin($pluginName) { + $this->clearCache($pluginName); + // execute deactivate() to let the plugin do cleanups $this->executePluginDeactivate($pluginName); @@ -317,7 +326,59 @@ class Manager extends Singleton $this->removePluginFromConfig($pluginName); - $this->clearCache($pluginName); + /** + * Event triggered after a plugin has been deactivated. + * + * @param string $pluginName The plugin that has been deactivated. + */ + Piwik::postEvent('PluginManager.pluginDeactivated', array($pluginName)); + } + + /** + * Tries to find the given components such as a Menu or Tasks implemented by plugins. + * This method won't cache the found components. If you need to find the same component multiple times you might + * want to cache the result to save a tiny bit of time. + * + * @param string $componentName The name of the component you want to look for. In case you request a + * component named 'Menu' it'll look for a file named 'Menu.php' within the + * root of all plugin folders that implement a class named + * Piwik\Plugin\$PluginName\Menu. + * @param string $expectedSubclass If not empty, a check will be performed whether a found file extends the + * given subclass. If the requested file exists but does not extend this class + * a warning will be shown to advice a developer to extend this certain class. + * + * @return \stdClass[] + */ + public function findComponents($componentName, $expectedSubclass) + { + $plugins = $this->getPluginsLoadedAndActivated(); + $components = array(); + + foreach ($plugins as $plugin) { + $component = $plugin->findComponent($componentName, $expectedSubclass); + + if (!empty($component)) { + $components[] = $component; + } + } + + return $components; + } + + public function findMultipleComponents($directoryWithinPlugin, $expectedSubclass) + { + $plugins = $this->getPluginsLoadedAndActivated(); + $found = array(); + + foreach ($plugins as $plugin) { + $components = $plugin->findMultipleComponents($directoryWithinPlugin, $expectedSubclass); + + if (!empty($components)) { + $found = array_merge($found, $components); + } + } + + return $found; } /** @@ -333,7 +394,9 @@ class Manager extends Singleton if ($this->isPluginLoaded($pluginName)) { throw new \Exception("To uninstall the plugin $pluginName, first disable it in Piwik > Settings > Plugins"); } - $this->returnLoadedPluginsInfo(); + $this->loadAllPluginsAndGetTheirInfo(); + + \Piwik\Settings\Manager::cleanupPluginSettings($pluginName); $this->executePluginDeactivate($pluginName); $this->executePluginUninstall($pluginName); @@ -343,9 +406,7 @@ class Manager extends Singleton $this->unloadPluginFromMemory($pluginName); $this->removePluginFromConfig($pluginName); - - Option::delete('version_' . $pluginName); - \Piwik\Settings\Manager::cleanupPluginSettings($pluginName); + $this->removeInstalledVersionFromOptionTable($pluginName); $this->clearCache($pluginName); self::deletePluginFromFilesystem($pluginName); @@ -360,6 +421,7 @@ class Manager extends Singleton */ private function clearCache($pluginName) { + $this->resetTransientCache(); Filesystem::deleteAllCacheOnUpdate($pluginName); } @@ -371,19 +433,16 @@ class Manager extends Singleton /** * Install loaded plugins * + * @throws * @return array Error messages of plugin install fails */ public function installLoadedPlugins() { - $messages = array(); + Log::debug("Loaded plugins: " . implode(", ", array_keys($this->getLoadedPlugins()))); + foreach ($this->getLoadedPlugins() as $plugin) { - try { - $this->installPluginIfNecessary($plugin); - } catch (\Exception $e) { - $messages[] = $e->getMessage(); - } + $this->installPluginIfNecessary($plugin); } - return $messages; } /** @@ -394,7 +453,7 @@ class Manager extends Singleton */ public function activatePlugin($pluginName) { - $plugins = PiwikConfig::getInstance()->Plugins['Plugins']; + $plugins = $this->pluginList->getActivatedPlugins(); if (in_array($pluginName, $plugins)) { throw new \Exception("Plugin '$pluginName' already activated."); } @@ -421,20 +480,26 @@ class Manager extends Singleton $this->clearCache($pluginName); + /** + * Event triggered after a plugin has been activated. + * + * @param string $pluginName The plugin that has been activated. + */ + Piwik::postEvent('PluginManager.pluginActivated', array($pluginName)); } protected function isPluginInFilesystem($pluginName) { $existingPlugins = $this->readPluginsDirectory(); $isPluginInFilesystem = array_search($pluginName, $existingPlugins) !== false; - return Filesystem::isValidFilename($pluginName) + return $this->isValidPluginName($pluginName) && $isPluginInFilesystem; } /** * Returns the currently enabled theme. - * - * If no theme is enabled, the **Zeitgeist** plugin is returned (this is the base and default theme). + * + * If no theme is enabled, the **Morpheus** plugin is returned (this is the base and default theme). * * @return Plugin * @api @@ -494,7 +559,7 @@ class Manager extends Singleton * * @return array An array that maps plugin names with arrays of plugin information. Plugin * information consists of the following entries: - * + * * - **activated**: Whether the plugin is activated. * - **alwaysActivated**: Whether the plugin should always be activated, * or not. @@ -505,20 +570,21 @@ class Manager extends Singleton * See {@link Piwik\Plugin::getInformation()}. * @api */ - public function returnLoadedPluginsInfo() + public function loadAllPluginsAndGetTheirInfo() { - $language = Translate::getLanguageToLoad(); + /** @var Translator $translator */ + $translator = StaticContainer::get('Piwik\Translation\Translator'); $plugins = array(); $listPlugins = array_merge( $this->readPluginsDirectory(), - PiwikConfig::getInstance()->Plugins['Plugins'] + $this->pluginList->getActivatedPlugins() ); $listPlugins = array_unique($listPlugins); foreach ($listPlugins as $pluginName) { // Hide plugins that are never going to be used - if($this->isPluginBogus($pluginName)) { + if ($this->isPluginBogus($pluginName)) { continue; } @@ -531,7 +597,7 @@ class Manager extends Singleton 'uninstallable' => true, ); } else { - $this->loadTranslation($pluginName, $language); + $translator->addDirectory(self::getPluginsDirectory() . $pluginName . '/lang'); $this->loadPlugin($pluginName); $info = array( 'activated' => $this->isPluginActivated($pluginName), @@ -542,7 +608,6 @@ class Manager extends Singleton $plugins[$pluginName] = $info; } - $this->loadPluginTranslations(); $loadedPlugins = $this->getLoadedPlugins(); foreach ($loadedPlugins as $oPlugin) { @@ -557,10 +622,10 @@ class Manager extends Singleton ); $plugins[$pluginName] = $info; } + return $plugins; } - protected static function isManifestFileFound($path) { return file_exists($path . "/" . MetadataLoader::PLUGIN_JSON_FILENAME); @@ -574,43 +639,38 @@ class Manager extends Singleton */ public function isPluginBundledWithCore($name) { - // Reading the plugins from the global.ini.php config file - $pluginsBundledWithPiwik = PiwikConfig::getInstance()->getFromGlobalConfig('Plugins'); - $pluginsBundledWithPiwik = $pluginsBundledWithPiwik['Plugins']; - - return (!empty($pluginsBundledWithPiwik) - && in_array($name, $pluginsBundledWithPiwik)) - || in_array($name, $this->getCorePluginsDisabledByDefault()) - || $name == self::DEFAULT_THEME; + return $this->isPluginEnabledByDefault($name) + || in_array($name, $this->pluginList->getCorePluginsDisabledByDefault()) + || $name == self::DEFAULT_THEME; } protected function isPluginThirdPartyAndBogus($pluginName) { - if($this->isPluginBundledWithCore($pluginName)) { + if ($this->isPluginBundledWithCore($pluginName)) { return false; } - if($this->isPluginBogus($pluginName)) { + if ($this->isPluginBogus($pluginName)) { return true; } $path = $this->getPluginsDirectory() . $pluginName; - if(!$this->isManifestFileFound($path)) { + if (!$this->isManifestFileFound($path)) { return true; } return false; } - /** - * Load the specified plugins. + * Load AND activates the specified plugins. It will also overwrite all previously loaded plugins, so it acts + * as a setter. * * @param array $pluginsToLoad Array of plugins to load. */ public function loadPlugins(array $pluginsToLoad) { - $pluginsToLoad = array_unique($pluginsToLoad); - $this->pluginsToLoad = $pluginsToLoad; - $this->reloadPlugins(); + $this->resetTransientCache(); + $this->pluginsToLoad = $this->makePluginsToLoad($pluginsToLoad); + $this->reloadActivatedPlugins(); } /** @@ -629,23 +689,6 @@ class Manager extends Singleton $this->doLoadAlwaysActivatedPlugins = false; } - /** - * Load translations for loaded plugins - * - * @param bool|string $language Optional language code - */ - public function loadPluginTranslations($language = false) - { - if (empty($language)) { - $language = Translate::getLanguageToLoad(); - } - $plugins = $this->getLoadedPlugins(); - - foreach ($plugins as $plugin) { - $this->loadTranslation($plugin, $language); - } - } - /** * Execute postLoad() hook for loaded plugins */ @@ -672,7 +715,7 @@ class Manager extends Singleton * * array( * 'UserCountry' => Plugin $pluginObject, - * 'UserSettings' => Plugin $pluginObject, + * 'UserLanguage' => Plugin $pluginObject, * ); * * @return Plugin[] @@ -706,22 +749,28 @@ class Manager extends Singleton * * array( * 'UserCountry' => Plugin $pluginObject, - * 'UserSettings' => Plugin $pluginObject, + * 'UserLanguage' => Plugin $pluginObject, * ); * * @return Plugin[] */ public function getPluginsLoadedAndActivated() { - $plugins = $this->getLoadedPlugins(); - $enabled = $this->getActivatedPlugins(); + if (is_null($this->pluginsLoadedAndActivated)) { + $enabled = $this->getActivatedPlugins(); - if(empty($enabled)) { - return array(); + if (empty($enabled)) { + return array(); + } + + $plugins = $this->getLoadedPlugins(); + $enabled = array_combine($enabled, $enabled); + $plugins = array_intersect_key($plugins, $enabled); + + $this->pluginsLoadedAndActivated = $plugins; } - $enabled = array_combine($enabled, $enabled); - $plugins = array_intersect_key($plugins, $enabled); - return $plugins; + + return $this->pluginsLoadedAndActivated; } /** @@ -729,7 +778,7 @@ class Manager extends Singleton * * array( * 'UserCountry' - * 'UserSettings' + * 'UserLanguage' * ); * * @return string[] @@ -739,6 +788,13 @@ class Manager extends Singleton return $this->pluginsToLoad; } + public function getActivatedPluginsFromConfig() + { + $plugins = $this->pluginList->getActivatedPlugins(); + + return $this->makePluginsToLoad($plugins); + } + /** * Returns a Plugin object by name. * @@ -758,14 +814,8 @@ class Manager extends Singleton * Load the plugins classes installed. * Register the observers for every plugin. */ - private function reloadPlugins() + private function reloadActivatedPlugins() { - if ($this->doLoadAlwaysActivatedPlugins) { - $this->pluginsToLoad = array_merge($this->pluginsToLoad, $this->pluginToAlwaysActivate); - } - - $this->pluginsToLoad = array_unique($this->pluginsToLoad); - $pluginsToPostPendingEventsTo = array(); foreach ($this->pluginsToLoad as $pluginName) { if (!$this->isPluginLoaded($pluginName) @@ -802,7 +852,6 @@ class Manager extends Singleton return $ignored; } - /** * Returns the name of all plugins found in this Piwik instance * (including those not enabled and themes) @@ -811,18 +860,19 @@ class Manager extends Singleton */ public static function getAllPluginsNames() { + $pluginList = StaticContainer::get('Piwik\Application\Kernel\PluginList'); + $pluginsToLoad = array_merge( - PiwikConfig::getInstance()->Plugins['Plugins'], self::getInstance()->readPluginsDirectory(), - self::getInstance()->getCorePluginsDisabledByDefault() + $pluginList->getCorePluginsDisabledByDefault() ); $pluginsToLoad = array_values(array_unique($pluginsToLoad)); return $pluginsToLoad; } - /** - * Loads the plugin filename and instantiates the plugin with the given name, eg. UserCountry + * Loads the plugin filename and instantiates the plugin with the given name, eg. UserCountry. + * Contrary to loadPlugins() it does not activate the plugin, it only loads it. * * @param string $pluginName * @throws \Exception @@ -839,6 +889,11 @@ class Manager extends Singleton return $newPlugin; } + public function isValidPluginName($pluginName) + { + return (bool) preg_match('/^[a-zA-Z]([a-zA-Z0-9]*)$/D', $pluginName); + } + /** * @param $pluginName * @return Plugin @@ -849,16 +904,15 @@ class Manager extends Singleton $pluginFileName = sprintf("%s/%s.php", $pluginName, $pluginName); $pluginClassName = $pluginName; - if (!Filesystem::isValidFilename($pluginName)) { - throw new \Exception(sprintf("The plugin filename '%s' is not a valid filename", $pluginFileName)); + if (!$this->isValidPluginName($pluginName)) { + throw new \Exception(sprintf("The plugin filename '%s' is not a valid plugin name", $pluginFileName)); } $path = self::getPluginsDirectory() . $pluginFileName; - if (!file_exists($path)) { // Create the smallest minimal Piwik Plugin - // Eg. Used for Zeitgeist default theme which does not have a Zeitgeist.php file + // Eg. Used for Morpheus default theme which does not have a Morpheus.php file return new Plugin($pluginName); } @@ -885,6 +939,11 @@ class Manager extends Singleton return "\\Piwik\\Plugins\\$pluginName\\$className"; } + private function resetTransientCache() + { + $this->pluginsLoadedAndActivated = null; + } + /** * Unload plugin * @@ -893,6 +952,8 @@ class Manager extends Singleton */ public function unloadPlugin($plugin) { + $this->resetTransientCache(); + if (!($plugin instanceof Plugin)) { $oPlugin = $this->loadPlugin($plugin); if ($oPlugin === null) { @@ -911,6 +972,8 @@ class Manager extends Singleton */ public function unloadPlugins() { + $this->resetTransientCache(); + $pluginsLoaded = $this->getLoadedPlugins(); foreach ($pluginsLoaded as $plugin) { $this->unloadPlugin($plugin); @@ -937,63 +1000,15 @@ class Manager extends Singleton * * @param string $pluginName plugin name without prefix (eg. 'UserCountry') * @param Plugin $newPlugin + * @internal */ - private function addLoadedPlugin($pluginName, Plugin $newPlugin) + public function addLoadedPlugin($pluginName, Plugin $newPlugin) { + $this->resetTransientCache(); + $this->loadedPlugins[$pluginName] = $newPlugin; } - /** - * Load translation - * - * @param Plugin $plugin - * @param string $langCode - * @throws \Exception - * @return bool whether the translation was found and loaded - */ - private function loadTranslation($plugin, $langCode) - { - // we are in Tracker mode if Loader is not (yet) loaded - if (!class_exists('Piwik\\Loader', false)) { - return false; - } - - if (is_string($plugin)) { - $pluginName = $plugin; - } else { - $pluginName = $plugin->getPluginName(); - } - - $path = self::getPluginsDirectory() . $pluginName . '/lang/%s.json'; - - $defaultLangPath = sprintf($path, $langCode); - $defaultEnglishLangPath = sprintf($path, 'en'); - - $translationsLoaded = false; - - // merge in english translations as default first - if (file_exists($defaultEnglishLangPath)) { - $translations = $this->getTranslationsFromFile($defaultEnglishLangPath); - $translationsLoaded = true; - if (isset($translations[$pluginName])) { - // only merge translations of plugin - prevents overwritten strings - Translate::mergeTranslationArray(array($pluginName => $translations[$pluginName])); - } - } - - // merge in specific language translations (to overwrite english defaults) - if (file_exists($defaultLangPath)) { - $translations = $this->getTranslationsFromFile($defaultLangPath); - $translationsLoaded = true; - if (isset($translations[$pluginName])) { - // only merge translations of plugin - prevents overwritten strings - Translate::mergeTranslationArray(array($pluginName => $translations[$pluginName])); - } - } - - return $translationsLoaded; - } - /** * Return names of all installed plugins. * @@ -1002,7 +1017,7 @@ class Manager extends Singleton */ public function getInstalledPluginsName() { - $pluginNames = PiwikConfig::getInstance()->PluginsInstalled['PluginsInstalled']; + $pluginNames = Config::getInstance()->PluginsInstalled['PluginsInstalled']; return $pluginNames; } @@ -1016,17 +1031,17 @@ class Manager extends Singleton public function getMissingPlugins() { $missingPlugins = array(); - if (isset(PiwikConfig::getInstance()->Plugins['Plugins'])) { - $plugins = PiwikConfig::getInstance()->Plugins['Plugins']; - foreach ($plugins as $pluginName) { - // if a plugin is listed in the config, but is not loaded, it does not exist in the folder - if (!self::getInstance()->isPluginLoaded($pluginName) - && !$this->isPluginBogus($pluginName) - ) { - $missingPlugins[] = $pluginName; - } + + $plugins = $this->pluginList->getActivatedPlugins(); + foreach ($plugins as $pluginName) { + // if a plugin is listed in the config, but is not loaded, it does not exist in the folder + if (!self::getInstance()->isPluginLoaded($pluginName) + && !$this->isPluginBogus($pluginName) + ) { + $missingPlugins[] = $pluginName; } } + return $missingPlugins; } @@ -1047,29 +1062,37 @@ class Manager extends Singleton $this->executePluginInstall($plugin); $pluginsInstalled[] = $pluginName; $this->updatePluginsInstalledConfig($pluginsInstalled); - Updater::recordComponentSuccessfullyUpdated($plugin->getPluginName(), $plugin->getVersion()); + $updater = new Updater(); + $updater->markComponentSuccessfullyUpdated($plugin->getPluginName(), $plugin->getVersion()); $saveConfig = true; } - if ($this->isTrackerPlugin($plugin)) { - $pluginsTracker = PiwikConfig::getInstance()->Plugins_Tracker['Plugins_Tracker']; - if (is_null($pluginsTracker)) { - $pluginsTracker = array(); - } - if (!in_array($pluginName, $pluginsTracker)) { - $pluginsTracker[] = $pluginName; - $this->updatePluginsTrackerConfig($pluginsTracker); - $saveConfig = true; - } - } - if ($saveConfig) { PiwikConfig::getInstance()->forceSave(); + $this->clearCache($pluginName); } } public function isTrackerPlugin(Plugin $plugin) { + if (!$this->isPluginInstalled($plugin->getPluginName())) { + return false; + } + + if ($plugin->isTrackerPlugin()) { + return true; + } + + $dimensions = VisitDimension::getDimensions($plugin); + if (!empty($dimensions)) { + return true; + } + + $dimensions = ActionDimension::getDimensions($plugin); + if (!empty($dimensions)) { + return true; + } + $hooks = $plugin->getListHooksRegistered(); $hookNames = array_keys($hooks); foreach ($hookNames as $name) { @@ -1080,6 +1103,12 @@ class Manager extends Singleton return true; } } + + $dimensions = ConversionDimension::getDimensions($plugin); + if (!empty($dimensions)) { + return true; + } + return false; } @@ -1095,7 +1124,7 @@ class Manager extends Singleton */ private function removePluginFromPluginsInstalledConfig($pluginName) { - $pluginsInstalled = PiwikConfig::getInstance()->PluginsInstalled['PluginsInstalled']; + $pluginsInstalled = Config::getInstance()->PluginsInstalled['PluginsInstalled']; $key = array_search($pluginName, $pluginsInstalled); if ($key !== false) { unset($pluginsInstalled[$key]); @@ -1109,7 +1138,7 @@ class Manager extends Singleton */ private function removePluginFromPluginsConfig($pluginName) { - $pluginsEnabled = PiwikConfig::getInstance()->Plugins['Plugins']; + $pluginsEnabled = $this->pluginList->getActivatedPlugins(); $key = array_search($pluginName, $pluginsEnabled); if ($key !== false) { unset($pluginsEnabled[$key]); @@ -1117,39 +1146,6 @@ class Manager extends Singleton $this->updatePluginsConfig($pluginsEnabled); } - private function removePluginFromTrackerConfig($pluginName) - { - $pluginsTracker = PiwikConfig::getInstance()->Plugins_Tracker['Plugins_Tracker']; - if (!is_null($pluginsTracker)) { - $key = array_search($pluginName, $pluginsTracker); - if ($key !== false) { - unset($pluginsTracker[$key]); - $this->updatePluginsTrackerConfig($pluginsTracker); - } - } - } - - /** - * @param string $pathToTranslationFile - * @throws \Exception - * @return mixed - */ - private function getTranslationsFromFile($pathToTranslationFile) - { - $data = file_get_contents($pathToTranslationFile); - $translations = json_decode($data, true); - - if (is_null($translations) && Common::hasJsonErrorOccurred()) { - $jsonError = Common::getLastJsonError(); - - $message = sprintf('Not able to load translation file %s: %s', $pathToTranslationFile, $jsonError); - - throw new \Exception($message); - } - - return $translations; - } - /** * @param $pluginName * @return bool @@ -1197,6 +1193,8 @@ class Manager extends Singleton */ private function unloadPluginFromMemory($pluginName) { + $this->unloadPlugin($pluginName); + $key = array_search($pluginName, $this->pluginsToLoad); if ($key !== false) { unset($this->pluginsToLoad[$key]); @@ -1209,7 +1207,6 @@ class Manager extends Singleton private function removePluginFromConfig($pluginName) { $this->removePluginFromPluginsConfig($pluginName); - $this->removePluginFromTrackerConfig($pluginName); PiwikConfig::getInstance()->forceSave(); } @@ -1223,6 +1220,72 @@ class Manager extends Singleton $plugin->uninstall(); } catch (\Exception $e) { } + + if (empty($plugin)) { + return; + } + + try { + $visitDimensions = VisitDimension::getAllDimensions(); + + foreach (VisitDimension::getDimensions($plugin) as $dimension) { + $this->uninstallDimension(VisitDimension::INSTALLER_PREFIX, $dimension, $visitDimensions); + } + } catch (\Exception $e) { + } + + try { + $actionDimensions = ActionDimension::getAllDimensions(); + + foreach (ActionDimension::getDimensions($plugin) as $dimension) { + $this->uninstallDimension(ActionDimension::INSTALLER_PREFIX, $dimension, $actionDimensions); + } + } catch (\Exception $e) { + } + + try { + $conversionDimensions = ConversionDimension::getAllDimensions(); + + foreach (ConversionDimension::getDimensions($plugin) as $dimension) { + $this->uninstallDimension(ConversionDimension::INSTALLER_PREFIX, $dimension, $conversionDimensions); + } + } catch (\Exception $e) { + } + } + + /** + * @param VisitDimension|ActionDimension|ConversionDimension $dimension + * @param VisitDimension[]|ActionDimension[]|ConversionDimension[] $allDimensions + * @return bool + */ + private function doesAnotherPluginDefineSameColumnWithDbEntry($dimension, $allDimensions) + { + $module = $dimension->getModule(); + $columnName = $dimension->getColumnName(); + + foreach ($allDimensions as $dim) { + if ($dim->getColumnName() === $columnName && + $dim->hasColumnType() && + $dim->getModule() !== $module) { + return true; + } + } + + return false; + } + + /** + * @param string $prefix column installer prefix + * @param ConversionDimension|VisitDimension|ActionDimension $dimension + * @param VisitDimension[]|ActionDimension[]|ConversionDimension[] $allDimensions + */ + private function uninstallDimension($prefix, Dimension $dimension, $allDimensions) + { + if (!$this->doesAnotherPluginDefineSameColumnWithDbEntry($dimension, $allDimensions)) { + $dimension->uninstall(); + + $this->removeInstalledVersionFromOptionTable($prefix . $dimension->getColumnName()); + } } /** @@ -1234,18 +1297,66 @@ class Manager extends Singleton $pluginsInstalled = $this->getInstalledPluginsName(); return in_array($pluginName, $pluginsInstalled); } -} -/** - */ -class PluginException extends \Exception -{ - function __construct($pluginName, $message) + private function removeInstalledVersionFromOptionTable($name) { - parent::__construct("There was a problem installing the plugin " . $pluginName . ": " . $message . " - If this plugin has already been installed, and if you want to hide this message, you must add the following line under the - [PluginsInstalled] - entry in your config/config.ini.php file: - PluginsInstalled[] = $pluginName"); + $updater = new Updater(); + $updater->markComponentSuccessfullyUninstalled($name); + } + + private function makeSureOnlyActivatedPluginsAreLoaded() + { + foreach ($this->getLoadedPlugins() as $pluginName => $plugin) { + if (!in_array($pluginName, $this->pluginsToLoad)) { + $this->unloadPlugin($plugin); + } + } + } + + /** + * Reading the plugins from the global.ini.php config file + * + * @return array + */ + protected function getPluginsFromGlobalIniConfigFile() + { + return $this->pluginList->getPluginsBundledWithPiwik(); + } + + /** + * @param $name + * @return bool + */ + protected function isPluginEnabledByDefault($name) + { + $pluginsBundledWithPiwik = $this->getPluginsFromGlobalIniConfigFile(); + if (empty($pluginsBundledWithPiwik)) { + return false; + } + return in_array($name, $pluginsBundledWithPiwik); + } + + /** + * @param array $pluginsToLoad + * @return array + */ + private function makePluginsToLoad(array $pluginsToLoad) + { + $pluginsToLoad = array_unique($pluginsToLoad); + if ($this->doLoadAlwaysActivatedPlugins) { + $pluginsToLoad = array_merge($pluginsToLoad, $this->pluginToAlwaysActivate); + } + $pluginsToLoad = array_unique($pluginsToLoad); + $pluginsToLoad = $this->pluginList->sortPlugins($pluginsToLoad); + return $pluginsToLoad; + } + + public function loadPluginTranslations() + { + /** @var Translator $translator */ + $translator = StaticContainer::get('Piwik\Translation\Translator'); + foreach ($this->getAllPluginsNames() as $pluginName) { + $translator->addDirectory(self::getPluginsDirectory() . $pluginName . '/lang'); + } } } diff --git a/www/analytics/core/Plugin/Menu.php b/www/analytics/core/Plugin/Menu.php new file mode 100644 index 00000000..dd1dcaf7 --- /dev/null +++ b/www/analytics/core/Plugin/Menu.php @@ -0,0 +1,271 @@ +addItem('UI Framework', '', $this->urlForDefaultAction(), $orderId = 30); + * // will add a menu item that leads to the default action of the plugin controller when a user clicks on it. + * // The default action is usually the `index` action - meaning the `index()` method the controller - + * // but the default action can be customized within a controller + * ``` + * + * @param array $additionalParams Optional URL parameters that will be appended to the URL + * @return array + * + * @since 2.7.0 + * @api + */ + protected function urlForDefaultAction($additionalParams = array()) + { + $params = (array) $additionalParams; + $params['action'] = ''; + $params['module'] = $this->getModule(); + + return $params; + } + + /** + * Generates a URL for the given action. In your plugin controller you have to create a method with the same name + * as this method will be executed when a user clicks on the menu item. If you want to generate a URL for the + * action of another module, meaning not your plugin, you should use the method {@link urlForModuleAction()}. + * + * @param string $controllerAction The name of the action that should be executed within your controller + * @param array $additionalParams Optional URL parameters that will be appended to the URL + * @return array + * + * @since 2.7.0 + * @api + */ + protected function urlForAction($controllerAction, $additionalParams = array()) + { + $module = $this->getModule(); + $this->checkisValidCallable($module, $controllerAction); + + $params = (array) $additionalParams; + $params['action'] = $controllerAction; + $params['module'] = $module; + + return $params; + } + + /** + * Generates a URL for the given action of the given module. We usually do not recommend to use this method as you + * should make sure the method of that module actually exists. If the plugin owner of that module changes the method + * in a future version your link might no longer work. If you want to link to an action of your controller use the + * method {@link urlForAction()}. Note: We will generate a link only if the given module is installed and activated. + * + * @param string $module The name of the module/plugin the action belongs to. The module name is case sensitive. + * @param string $controllerAction The name of the action that should be executed within your controller + * @param array $additionalParams Optional URL parameters that will be appended to the URL + * @return array|null Returns null if the given module is either not installed or not activated. Returns the array + * of query parameter names and values to the given module action otherwise. + * + * @since 2.7.0 + * // not API for now + */ + protected function urlForModuleAction($module, $controllerAction, $additionalParams = array()) + { + $this->checkisValidCallable($module, $controllerAction); + + $pluginManager = PluginManager::getInstance(); + + if (!$pluginManager->isPluginLoaded($module) || + !$pluginManager->isPluginActivated($module)) { + return null; + } + + $params = (array) $additionalParams; + $params['action'] = $controllerAction; + $params['module'] = $module; + + return $params; + } + + /** + * Generates a URL to the given action of the current module, and it will also append some URL query parameters from the + * User preferences: idSite, period, date. If you do not need the parameters idSite, period and date to be generated + * use {@link urlForAction()} instead. + * + * @param string $controllerAction The name of the action that should be executed within your controller + * @param array $additionalParams Optional URL parameters that will be appended to the URL + * @return array Returns the array of query parameter names and values to the given module action and idSite date and period. + * + */ + protected function urlForActionWithDefaultUserParams($controllerAction, $additionalParams = array()) + { + $module = $this->getModule(); + + return $this->urlForModuleActionWithDefaultUserParams($module, $controllerAction, $additionalParams); + } + + /** + * Generates a URL to the given action of the given module, and it will also append some URL query parameters from the + * User preferences: idSite, period, date. If you do not need the parameters idSite, period and date to be generated + * use {@link urlForModuleAction()} instead. + * + * @param string $module The name of the module/plugin the action belongs to. The module name is case sensitive. + * @param string $controllerAction The name of the action that should be executed within your controller + * @param array $additionalParams Optional URL parameters that will be appended to the URL + * @return array|null Returns the array of query parameter names and values to the given module action and idSite date and period. + * Returns null if the module or action is invalid. + * + */ + protected function urlForModuleActionWithDefaultUserParams($module, $controllerAction, $additionalParams = array()) + { + $urlModuleAction = $this->urlForModuleAction($module, $controllerAction); + + $date = Common::getRequestVar('date', false); + if ($date) { + $urlModuleAction['date'] = $date; + } + $period = Common::getRequestVar('period', false); + if ($period) { + $urlModuleAction['period'] = $period; + } + + // We want the current query parameters to override the user's defaults + return array_merge( + $this->urlForDefaultUserParams(), + $urlModuleAction, + $additionalParams + ); + } + + /** + * Returns the &idSite=X&period=Y&date=Z query string fragment, + * fetched from current logged-in user's preferences. + * + * @param bool $websiteId + * @param bool $defaultPeriod + * @param bool $defaultDate + * @return string eg '&idSite=1&period=week&date=today' + * @throws \Exception in case a website was not specified and a default website id could not be found + */ + public function urlForDefaultUserParams($websiteId = false, $defaultPeriod = false, $defaultDate = false) + { + $userPreferences = new UserPreferences(); + if (empty($websiteId)) { + $websiteId = $userPreferences->getDefaultWebsiteId(); + } + if (empty($websiteId)) { + throw new \Exception("A website ID was not specified and a website to default to could not be found."); + } + if (empty($defaultDate)) { + $defaultDate = $userPreferences->getDefaultDate(); + } + if (empty($defaultPeriod)) { + $defaultPeriod = $userPreferences->getDefaultPeriod(false); + } + return array( + 'idSite' => $websiteId, + 'period' => $defaultPeriod, + 'date' => $defaultDate, + ); + } + + /** + * Configures the reporting menu which should only contain links to reports of a specific site such as + * "Search Engines", "Page Titles" or "Locations & Provider". + */ + public function configureReportingMenu(MenuReporting $menu) + { + } + + /** + * Configures the top menu which is supposed to contain analytics related items such as the + * "All Websites Dashboard". + */ + public function configureTopMenu(MenuTop $menu) + { + } + + /** + * Configures the user menu which is supposed to contain user and help related items such as + * "User settings", "Alerts" or "Email Reports". + */ + public function configureUserMenu(MenuUser $menu) + { + } + + /** + * Configures the admin menu which is supposed to contain only administration related items such as + * "Websites", "Users" or "Plugin settings". + */ + public function configureAdminMenu(MenuAdmin $menu) + { + } + + private function checkisValidCallable($module, $action) + { + if (!Development::isEnabled()) { + return; + } + + $prefix = 'Menu item added in ' . get_class($this) . ' will fail when being selected. '; + + if (!is_string($action)) { + Development::error($prefix . 'No valid action is specified. Make sure the defined action that should be executed is a string.'); + } + + $reportAction = lcfirst(substr($action, 4)); + if (Report::factory($module, $reportAction)) { + return; + } + + $controllerClass = '\\Piwik\\Plugins\\' . $module . '\\Controller'; + + if (!Development::methodExists($controllerClass, $action)) { + Development::error($prefix . 'The defined action "' . $action . '" does not exist in ' . $controllerClass . '". Make sure to define such a method.'); + } + + if (!Development::isCallableMethod($controllerClass, $action)) { + Development::error($prefix . 'The defined action "' . $action . '" is not callable on "' . $controllerClass . '". Make sure the method is public.'); + } + } +} diff --git a/www/analytics/core/Plugin/MetadataLoader.php b/www/analytics/core/Plugin/MetadataLoader.php index 088c6b29..5a6b2617 100644 --- a/www/analytics/core/Plugin/MetadataLoader.php +++ b/www/analytics/core/Plugin/MetadataLoader.php @@ -1,6 +1,6 @@ getDefaultPluginInformation(); + $plugin = $this->loadPluginInfoJson(); + + // use translated plugin description if available + if ($defaults['description'] != Piwik::translate($defaults['description'])) { + unset($plugin['description']); + } + return array_merge( - $this->getDefaultPluginInformation(), - $this->loadPluginInfoJson() + $defaults, + $plugin ); } @@ -67,7 +74,7 @@ class MetadataLoader { $descriptionKey = $this->pluginName . '_PluginDescription'; return array( - 'description' => Piwik::translate($descriptionKey), + 'description' => $descriptionKey, 'homepage' => 'http://piwik.org/', 'authors' => array(array('name' => 'Piwik', 'homepage' => 'http://piwik.org/')), 'license' => 'GPL v3+', @@ -95,12 +102,13 @@ class MetadataLoader return array(); } - $info = Common::json_decode($json, $assoc = true); + $info = json_decode($json, $assoc = true); if (!is_array($info) || empty($info) ) { throw new Exception("Invalid JSON file: $path"); } + return $info; } } diff --git a/www/analytics/core/Plugin/Metric.php b/www/analytics/core/Plugin/Metric.php new file mode 100644 index 00000000..6349ca69 --- /dev/null +++ b/www/analytics/core/Plugin/Metric.php @@ -0,0 +1,189 @@ +getColumn($columnName); + + if ($value === false) { + if (empty($mappingNameToId)) { + $mappingNameToId = Metrics::getMappingFromNameToId(); + } + + if (isset($mappingNameToId[$columnName])) { + return $row->getColumn($mappingNameToId[$columnName]); + } + } + + return $value; + } elseif (!empty($row)) { + if (array_key_exists($columnName, $row)) { + return $row[$columnName]; + } else { + if (empty($mappingNameToId)) { + $mappingNameToId = Metrics::getMappingFromNameToId(); + } + + if (isset($mappingNameToId[$columnName])) { + $columnName = $mappingNameToId[$columnName]; + + if (array_key_exists($columnName, $row)) { + return $row[$columnName]; + } + } + } + } + + return null; + } + + /** + * Helper method that will determine the actual column name for a metric in a + * {@link Piwik\DataTable} and return every column value for this name. + * + * @param DataTable $table + * @param string $columnName + * @param int[]|null $mappingNameToId A custom mapping of metric names to special index values. By + * default {@link Metrics::getMappingFromNameToId()} is used. + * @return array + */ + public static function getMetricValues(DataTable $table, $columnName, $mappingNameToId = null) + { + if (empty($mappingIdToName)) { + $mappingNameToId = Metrics::getMappingFromNameToId(); + } + + $columnName = self::getActualMetricColumn($table, $columnName, $mappingNameToId); + return $table->getColumn($columnName); + } + + /** + * Helper method that determines the actual column for a metric in a {@link Piwik\DataTable}. + * + * @param DataTable $table + * @param string $columnName + * @param int[]|null $mappingNameToId A custom mapping of metric names to special index values. By + * default {@link Metrics::getMappingFromNameToId()} is used. + * @return string + */ + public static function getActualMetricColumn(DataTable $table, $columnName, $mappingNameToId = null) + { + if (empty($mappingIdToName)) { + $mappingNameToId = Metrics::getMappingFromNameToId(); + } + + $firstRow = $table->getFirstRow(); + if (!empty($firstRow) + && $firstRow->getColumn($columnName) === false + ) { + $columnName = $mappingNameToId[$columnName]; + } + return $columnName; + } +} diff --git a/www/analytics/core/Plugin/PluginException.php b/www/analytics/core/Plugin/PluginException.php new file mode 100644 index 00000000..83816ca3 --- /dev/null +++ b/www/analytics/core/Plugin/PluginException.php @@ -0,0 +1,35 @@ +
+ $message +

+ If you want to hide this message you must remove the following line under the [Plugins] entry in your + 'config/config.ini.php' file to disable this plugin.
+ Plugins[] = $pluginName +

If this plugin has already been installed, you must add the following line under the + [PluginsInstalled] entry in your 'config/config.ini.php' file:
+ PluginsInstalled[] = $pluginName"); + } + + public function isHtmlMessage() + { + return true; + } +} diff --git a/www/analytics/core/Plugin/ProcessedMetric.php b/www/analytics/core/Plugin/ProcessedMetric.php new file mode 100644 index 00000000..108612c8 --- /dev/null +++ b/www/analytics/core/Plugin/ProcessedMetric.php @@ -0,0 +1,70 @@ +pluginManager = $pluginManager; + } + + /** + * @return ReleaseChannel[] + */ + public function getAllReleaseChannels() + { + $classNames = $this->pluginManager->findMultipleComponents('ReleaseChannel', 'Piwik\\UpdateCheck\\ReleaseChannel'); + $channels = array(); + + foreach ($classNames as $className) { + $channels[] = StaticContainer::get($className); + } + + usort($channels, function (ReleaseChannel $a, ReleaseChannel $b) { + if ($a->getOrder() === $b->getOrder()) { + return 0; + } + + return ($a->getOrder() < $b->getOrder()) ? -1 : 1; + }); + + return $channels; + } + + /** + * @return ReleaseChannel + */ + public function getActiveReleaseChannel() + { + $channel = Config::getInstance()->General['release_channel']; + $channel = $this->factory($channel); + + if (!empty($channel)) { + return $channel; + } + + $channels = $this->getAllReleaseChannels(); + + // we default to the one with lowest id + return reset($channels); + } + + /** + * Sets the given release channel in config but does not save id. $config->forceSave() still needs to be called + * @param string $channel + */ + public function setActiveReleaseChannelId($channel) + { + $general = Config::getInstance()->General; + $general['release_channel'] = $channel; + Config::getInstance()->General = $general; + } + + public function isValidReleaseChannelId($releaseChannelId) + { + $channel = $this->factory($releaseChannelId); + + return !empty($channel); + } + + /** + * @param string $releaseChannelId + * @return ReleaseChannel + */ + private function factory($releaseChannelId) + { + $releaseChannelId = strtolower($releaseChannelId); + + foreach ($this->getAllReleaseChannels() as $releaseChannel) { + if ($releaseChannelId === strtolower($releaseChannel->getId())) { + return $releaseChannel; + } + } + } +} \ No newline at end of file diff --git a/www/analytics/core/Plugin/Report.php b/www/analytics/core/Plugin/Report.php new file mode 100644 index 00000000..5ab8583a --- /dev/null +++ b/www/analytics/core/Plugin/Report.php @@ -0,0 +1,1001 @@ + 5)`. + * @var null|array + * @api + */ + protected $parameters = null; + + /** + * An instance of a dimension if the report has one. You can create a new dimension using the Piwik console CLI tool + * if needed. + * @var \Piwik\Columns\Dimension + */ + protected $dimension; + + /** + * The name of the API action to load a subtable if supported. The action has to be of the same module. For instance + * a report "getKeywords" might support a subtable "getSearchEngines" which shows how often a keyword was searched + * via a specific search engine. + * @var string + * @api + */ + protected $actionToLoadSubTables = ''; + + /** + * The order of the report. Depending on the order the report gets a different position in the list of widgets, + * the menu and the mobile app. + * @var int + * @api + */ + protected $order = 1; + + /** + * Separator for building recursive labels (or paths) + * @var string + * @api + */ + protected $recursiveLabelSeparator = ' - '; + + /** + * Default sort column. Either a column name or a column id. + * + * @var string|int + */ + protected $defaultSortColumn = 'nb_visits'; + + /** + * Default sort desc. If true will sort by default desc, if false will sort by default asc + * + * @var bool + */ + protected $defaultSortOrderDesc = true; + + /** + * @var array + * @ignore + */ + public static $orderOfReports = array( + 'General_MultiSitesSummary', + 'VisitsSummary_VisitsSummary', + 'Goals_Ecommerce', + 'General_Actions', + 'Events_Events', + 'Actions_SubmenuSitesearch', + 'Referrers_Referrers', + 'Goals_Goals', + 'General_Visitors', + 'DevicesDetection_DevicesDetection', + 'General_VisitorSettings', + 'API' + ); + + /** + * The constructur initializes the module, action and the default metrics. If you want to overwrite any of those + * values or if you want to do any work during initializing overwrite the method {@link init()}. + * @ignore + */ + final public function __construct() + { + $classname = get_class($this); + $parts = explode('\\', $classname); + + if (5 === count($parts)) { + $this->module = $parts[2]; + $this->action = lcfirst($parts[4]); + } + + $this->init(); + } + + /** + * Here you can do any instance initialization and overwrite any default values. You should avoid doing time + * consuming initialization here and if possible delay as long as possible. An instance of this report will be + * created in most page requests. + * @api + */ + protected function init() + { + } + + /** + * Defines whether a report is enabled or not. For instance some reports might not be available to every user or + * might depend on a setting (such as Ecommerce) of a site. In such a case you can perform any checks and then + * return `true` or `false`. If your report is only available to users having super user access you can do the + * following: `return Piwik::hasUserSuperUserAccess();` + * @return bool + * @api + */ + public function isEnabled() + { + return true; + } + + /** + * This method checks whether the report is available, see {@isEnabled()}. If not, it triggers an exception + * containing a message that will be displayed to the user. You can overwrite this message in case you want to + * customize the error message. Eg. + * ``` + if (!$this->isEnabled()) { + throw new Exception('Setting XYZ is not enabled or the user has not enough permission'); + } + * ``` + * @throws \Exception + * @api + */ + public function checkIsEnabled() + { + if (!$this->isEnabled()) { + throw new Exception(Piwik::translate('General_ExceptionReportNotEnabled')); + } + } + + /** + * Returns the id of the default visualization for this report. Eg 'table' or 'pie'. Defaults to the HTML table. + * @return string + * @api + */ + public function getDefaultTypeViewDataTable() + { + return HtmlTable::ID; + } + + /** + * Returns if the default viewDataTable type should always be used. e.g. the type won't be changeable through config or url params. + * Defaults to false + * @return bool + */ + public function alwaysUseDefaultViewDataTable() + { + return false; + } + + /** + * Here you can configure how your report should be displayed and which capabilities your report has. For instance + * whether your report supports a "search" or not. EG `$view->config->show_search = false`. You can also change the + * default request config. For instance you can change how many rows are displayed by default: + * `$view->requestConfig->filter_limit = 10;`. See {@link ViewDataTable} for more information. + * @param ViewDataTable $view + * @api + */ + public function configureView(ViewDataTable $view) + { + } + + /** + * Renders a report depending on the configured ViewDataTable see {@link configureView()} and + * {@link getDefaultTypeViewDataTable()}. If you want to customize the render process or just render any custom view + * you can overwrite this method. + * + * @return string + * @throws \Exception In case the given API action does not exist yet. + * @api + */ + public function render() + { + $apiProxy = Proxy::getInstance(); + + if (!$apiProxy->isExistingApiAction($this->module, $this->action)) { + throw new Exception("Invalid action name '$this->action' for '$this->module' plugin."); + } + + $apiAction = $apiProxy->buildApiActionName($this->module, $this->action); + + $view = ViewDataTableFactory::build(null, $apiAction, $this->module . '.' . $this->action); + + $rendered = $view->render(); + + return $rendered; + } + + /** + * By default a widget will be configured for this report if a {@link $widgetTitle} is set. If you want to customize + * the way the widget is added or modify any other behavior you can overwrite this method. + * @param WidgetsList $widget + * @api + */ + public function configureWidget(WidgetsList $widget) + { + if ($this->widgetTitle) { + $params = array(); + if (!empty($this->widgetParams) && is_array($this->widgetParams)) { + $params = $this->widgetParams; + } + $widget->add($this->category, $this->widgetTitle, $this->module, $this->action, $params); + } + } + + /** + * By default a menu item will be added to the reporting menu if a {@link $menuTitle} is set. If you want to + * customize the way the item is added or modify any other behavior you can overwrite this method. For instance + * in case you need to add additional url properties beside module and action which are added by default. + * @param \Piwik\Menu\MenuReporting $menu + * @api + */ + public function configureReportingMenu(MenuReporting $menu) + { + if ($this->menuTitle) { + $action = $this->getMenuControllerAction(); + if ($this->isEnabled()) { + $menu->addItem($this->category, + $this->menuTitle, + array('module' => $this->module, 'action' => $action), + $this->order); + } + } + } + + /** + * @ignore + * @see $recursiveLabelSeparator + */ + public function getRecursiveLabelSeparator() + { + return $this->recursiveLabelSeparator; + } + + /** + * Returns an array of supported metrics and their corresponding translations. Eg `array('nb_visits' => 'Visits')`. + * By default the given {@link $metrics} are used and their corresponding translations are looked up automatically. + * If a metric is not translated, you should add the default metric translation for this metric using + * the {@hook Metrics.getDefaultMetricTranslations} event. If you want to overwrite any default metric translation + * you should overwrite this method, call this parent method to get all default translations and overwrite any + * custom metric translations. + * @return array + * @api + */ + public function getMetrics() + { + return $this->getMetricTranslations($this->metrics); + } + + /** + * Returns the list of metrics required at minimum for a report factoring in the columns requested by + * the report requester. + * + * This will return all the metrics requested (or all the metrics in the report if nothing is requested) + * **plus** the metrics required to calculate the requested processed metrics. + * + * This method should be used in **Plugin.get** API methods. + * + * @param string[]|null $allMetrics The list of all available unprocessed metrics. Defaults to this report's + * metrics. + * @param string[]|null $restrictToColumns The requested columns. + * @return string[] + */ + public function getMetricsRequiredForReport($allMetrics = null, $restrictToColumns = null) + { + if (empty($allMetrics)) { + $allMetrics = $this->metrics; + } + + if (empty($restrictToColumns)) { + $restrictToColumns = array_merge($allMetrics, array_keys($this->getProcessedMetrics())); + } + + $processedMetricsById = $this->getProcessedMetricsById(); + $metricsSet = array_flip($allMetrics); + + $metrics = array(); + foreach ($restrictToColumns as $column) { + if (isset($processedMetricsById[$column])) { + $metrics = array_merge($metrics, $processedMetricsById[$column]->getDependentMetrics()); + } elseif (isset($metricsSet[$column])) { + $metrics[] = $column; + } + } + return array_unique($metrics); + } + + /** + * Returns an array of supported processed metrics and their corresponding translations. Eg + * `array('nb_visits' => 'Visits')`. By default the given {@link $processedMetrics} are used and their + * corresponding translations are looked up automatically. If a metric is not translated, you should add the + * default metric translation for this metric using the {@hook Metrics.getDefaultMetricTranslations} event. If you + * want to overwrite any default metric translation you should overwrite this method, call this parent method to + * get all default translations and overwrite any custom metric translations. + * @return array|mixed + * @api + */ + public function getProcessedMetrics() + { + if (!is_array($this->processedMetrics)) { + return $this->processedMetrics; + } + + return $this->getMetricTranslations($this->processedMetrics); + } + + /** + * Returns the array of all metrics displayed by this report. + * + * @return array + * @api + */ + public function getAllMetrics() + { + $processedMetrics = $this->getProcessedMetrics() ?: array(); + return array_keys(array_merge($this->getMetrics(), $processedMetrics)); + } + + /** + * Returns an array of metric documentations and their corresponding translations. Eg + * `array('nb_visits' => 'If a visitor comes to your website for the first time or if he visits a page more than 30 minutes after...')`. + * By default the given {@link $metrics} are used and their corresponding translations are looked up automatically. + * If there is a metric documentation not found, you should add the default metric documentation translation for + * this metric using the {@hook Metrics.getDefaultMetricDocumentationTranslations} event. If you want to overwrite + * any default metric translation you should overwrite this method, call this parent method to get all default + * translations and overwrite any custom metric translations. + * @return array + * @api + */ + protected function getMetricsDocumentation() + { + $translations = Metrics::getDefaultMetricsDocumentation(); + $documentation = array(); + + foreach ($this->metrics as $metric) { + if (!empty($translations[$metric])) { + $documentation[$metric] = $translations[$metric]; + } + } + + $processedMetrics = $this->processedMetrics ?: array(); + foreach ($processedMetrics as $processedMetric) { + if (is_string($processedMetric) && !empty($translations[$processedMetric])) { + $documentation[$processedMetric] = $translations[$processedMetric]; + } elseif ($processedMetric instanceof ProcessedMetric) { + $name = $processedMetric->getName(); + $metricDocs = $processedMetric->getDocumentation(); + if (empty($metricDocs)) { + $metricDocs = @$translations[$name]; + } + + if (!empty($metricDocs)) { + $documentation[$processedMetric->getName()] = $metricDocs; + } + } + } + + return $documentation; + } + + /** + * @return bool + * @ignore + */ + public function hasGoalMetrics() + { + return $this->hasGoalMetrics; + } + + /** + * If the report is enabled the report metadata for this report will be built and added to the list of available + * reports. Overwrite this method and leave it empty in case you do not want your report to be added to the report + * metadata. In this case your report won't be visible for instance in the mobile app and scheduled reports + * generator. We recommend to change this behavior only if you are familiar with the Piwik core. `$infos` contains + * the current requested date, period and site. + * @param $availableReports + * @param $infos + * @api + */ + public function configureReportMetadata(&$availableReports, $infos) + { + if (!$this->isEnabled()) { + return; + } + + $report = $this->buildReportMetadata(); + + if (!empty($report)) { + $availableReports[] = $report; + } + } + + /** + * Builts the report metadata for this report. Can be useful in case you want to change the behavior of + * {@link configureReportMetadata()}. + * @return array + * @ignore + */ + protected function buildReportMetadata() + { + $report = array( + 'category' => $this->getCategory(), + 'name' => $this->getName(), + 'module' => $this->getModule(), + 'action' => $this->getAction() + ); + + if (null !== $this->parameters) { + $report['parameters'] = $this->parameters; + } + + if (!empty($this->dimension)) { + $report['dimension'] = $this->dimension->getName(); + } + + if (!empty($this->documentation)) { + $report['documentation'] = $this->documentation; + } + + if (true === $this->isSubtableReport) { + $report['isSubtableReport'] = $this->isSubtableReport; + } + + $report['metrics'] = $this->getMetrics(); + $report['metricsDocumentation'] = $this->getMetricsDocumentation(); + $report['processedMetrics'] = $this->getProcessedMetrics(); + + if (!empty($this->actionToLoadSubTables)) { + $report['actionToLoadSubTables'] = $this->actionToLoadSubTables; + } + + if (true === $this->constantRowsCount) { + $report['constantRowsCount'] = $this->constantRowsCount; + } + + $report['order'] = $this->order; + + return $report; + } + + /** + * @ignore + */ + public function getDefaultSortColumn() + { + return $this->defaultSortColumn; + } + + /** + * @ignore + */ + public function getDefaultSortOrder() + { + if ($this->defaultSortOrderDesc) { + return Sort::ORDER_DESC; + } + + return Sort::ORDER_ASC; + } + + /** + * Get the list of related reports if there are any. They will be displayed for instance below a report as a + * recommended related report. + * + * @return Report[] + * @api + */ + public function getRelatedReports() + { + return array(); + } + + /** + * Gets the translated widget title if one is defined. + * @return string + * @ignore + */ + public function getWidgetTitle() + { + if ($this->widgetTitle) { + return Piwik::translate($this->widgetTitle); + } + } + + /** + * Get the name of the report + * @return string + * @ignore + */ + public function getName() + { + return $this->name; + } + + /** + * Get the name of the module. + * @return string + * @ignore + */ + public function getModule() + { + return $this->module; + } + + /** + * Get the name of the action. + * @return string + * @ignore + */ + public function getAction() + { + return $this->action; + } + + /** + * Get the translated name of the category the report belongs to. + * @return string + * @ignore + */ + public function getCategory() + { + return Piwik::translate($this->category); + } + + /** + * Get the translation key of the category the report belongs to. + * @return string + * @ignore + */ + public function getCategoryKey() + { + return $this->category; + } + + /** + * @return \Piwik\Columns\Dimension + * @ignore + */ + public function getDimension() + { + return $this->dimension; + } + + /** + * Returns the order of the report + * @return int + * @ignore + */ + public function getOrder() + { + return $this->order; + } + + /** + * Get the menu title if one is defined. + * @return string + * @ignore + */ + public function getMenuTitle() + { + return $this->menuTitle; + } + + /** + * Get the action to load sub tables if one is defined. + * @return string + * @ignore + */ + public function getActionToLoadSubTables() + { + return $this->actionToLoadSubTables; + } + + /** + * Returns the Dimension instance of this report's subtable report. + * + * @return Dimension|null The subtable report's dimension or null if there is subtable report or + * no dimension for the subtable report. + * @api + */ + public function getSubtableDimension() + { + if (empty($this->actionToLoadSubTables)) { + return null; + } + + list($subtableReportModule, $subtableReportAction) = $this->getSubtableApiMethod(); + + $subtableReport = self::factory($subtableReportModule, $subtableReportAction); + if (empty($subtableReport)) { + return null; + } + + return $subtableReport->getDimension(); + } + + /** + * Returns true if the report is for another report's subtable, false if otherwise. + * + * @return bool + */ + public function isSubtableReport() + { + return $this->isSubtableReport; + } + + /** + * Fetches the report represented by this instance. + * + * @param array $paramOverride Query parameter overrides. + * @return DataTable + * @api + */ + public function fetch($paramOverride = array()) + { + return Request::processRequest($this->module . '.' . $this->action, $paramOverride); + } + + /** + * Fetches a subtable for the report represented by this instance. + * + * @param int $idSubtable The subtable ID. + * @param array $paramOverride Query parameter overrides. + * @return DataTable + * @api + */ + public function fetchSubtable($idSubtable, $paramOverride = array()) + { + $paramOverride = array('idSubtable' => $idSubtable) + $paramOverride; + + list($module, $action) = $this->getSubtableApiMethod(); + return Request::processRequest($module . '.' . $action, $paramOverride); + } + + /** + * Get an instance of a specific report belonging to the given module and having the given action. + * @param string $module + * @param string $action + * @return null|\Piwik\Plugin\Report + * @api + */ + public static function factory($module, $action) + { + $listApiToReport = self::getMapOfModuleActionsToReport(); + $api = $module . '.' . ucfirst($action); + + if (!array_key_exists($api, $listApiToReport)) { + return null; + } + + $klassName = $listApiToReport[$api]; + + return new $klassName; + } + + private static function getMapOfModuleActionsToReport() + { + $cacheId = CacheId::pluginAware('ReportFactoryMap'); + + $cache = Cache::getEagerCache(); + if ($cache->contains($cacheId)) { + $mapApiToReport = $cache->fetch($cacheId); + } else { + $reports = self::getAllReports(); + + $mapApiToReport = array(); + foreach ($reports as $report) { + $key = $report->getModule() . '.' . ucfirst($report->getAction()); + $mapApiToReport[$key] = get_class($report); + } + + $cache->save($cacheId, $mapApiToReport); + } + + return $mapApiToReport; + } + + /** + * Returns a list of all available reports. Even not enabled reports will be returned. They will be already sorted + * depending on the order and category of the report. + * @return \Piwik\Plugin\Report[] + * @api + */ + public static function getAllReports() + { + $reports = self::getAllReportClasses(); + $cacheId = CacheId::languageAware('Reports' . md5(implode('', $reports))); + $cache = PiwikCache::getTransientCache(); + + + if (!$cache->contains($cacheId)) { + $instances = array(); + + foreach ($reports as $report) { + $instances[] = new $report(); + } + + usort($instances, array('self', 'sort')); + + $cache->save($cacheId, $instances); + } + + return $cache->fetch($cacheId); + } + + /** + * Returns class names of all Report metadata classes. + * + * @return string[] + * @api + */ + public static function getAllReportClasses() + { + return PluginManager::getInstance()->findMultipleComponents('Reports', '\\Piwik\\Plugin\\Report'); + } + + /** + * API metadata are sorted by category/name, + * with a little tweak to replicate the standard Piwik category ordering + * + * @param Report $a + * @param Report $b + * @return int + */ + private static function sort($a, $b) + { + return ($category = strcmp(array_search($a->category, self::$orderOfReports), array_search($b->category, self::$orderOfReports))) == 0 + ? ($a->order < $b->order ? -1 : 1) + : $category; + } + + private function getMetricTranslations($metricsToTranslate) + { + $translations = Metrics::getDefaultMetricTranslations(); + $metrics = array(); + + foreach ($metricsToTranslate as $metric) { + if ($metric instanceof Metric) { + $metricName = $metric->getName(); + $translation = $metric->getTranslatedName(); + } else { + $metricName = $metric; + $translation = @$translations[$metric]; + } + + $metrics[$metricName] = $translation ?: $metricName; + } + + return $metrics; + } + + private function getMenuControllerAction() + { + return self::PREFIX_ACTION_IN_MENU . ucfirst($this->action); + } + + private function getSubtableApiMethod() + { + if (strpos($this->actionToLoadSubTables, '.') !== false) { + return explode('.', $this->actionToLoadSubTables); + } else { + return array($this->module, $this->actionToLoadSubTables); + } + } + + /** + * Finds a top level report that provides stats for a specific Dimension. + * + * @param Dimension $dimension The dimension whose report we're looking for. + * @return Report|null The + * @api + */ + public static function getForDimension(Dimension $dimension) + { + return ComponentFactory::getComponentIf(__CLASS__, $dimension->getModule(), function (Report $report) use ($dimension) { + return !$report->isSubtableReport() + && $report->getDimension() + && $report->getDimension()->getId() == $dimension->getId(); + }); + } + + /** + * Returns an array mapping the ProcessedMetrics served by this report by their string names. + * + * @return ProcessedMetric[] + */ + public function getProcessedMetricsById() + { + $processedMetrics = $this->processedMetrics ?: array(); + + $result = array(); + foreach ($processedMetrics as $processedMetric) { + if ($processedMetric instanceof ProcessedMetric) { // instanceof check for backwards compatibility + $result[$processedMetric->getName()] = $processedMetric; + } + } + return $result; + } + + /** + * Returns the Metrics that are displayed by a DataTable of a certain Report type. + * + * Includes ProcessedMetrics and Metrics. + * + * @param DataTable $dataTable + * @param Report|null $report + * @param string $baseType The base type each metric class needs to be of. + * @return Metric[] + * @api + */ + public static function getMetricsForTable(DataTable $dataTable, Report $report = null, $baseType = 'Piwik\\Plugin\\Metric') + { + $metrics = $dataTable->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME) ?: array(); + + if (!empty($report)) { + $metrics = array_merge($metrics, $report->getProcessedMetricsById()); + } + + $result = array(); + + /** @var Metric $metric */ + foreach ($metrics as $metric) { + if (!($metric instanceof $baseType)) { + continue; + } + + $result[$metric->getName()] = $metric; + } + + return $result; + } + + /** + * Returns the ProcessedMetrics that should be computed and formatted for a DataTable of a + * certain report. The ProcessedMetrics returned are those specified by the Report metadata + * as well as the DataTable metadata. + * + * @param DataTable $dataTable + * @param Report|null $report + * @return ProcessedMetric[] + * @api + */ + public static function getProcessedMetricsForTable(DataTable $dataTable, Report $report = null) + { + return self::getMetricsForTable($dataTable, $report, 'Piwik\\Plugin\\ProcessedMetric'); + } +} diff --git a/www/analytics/core/Plugin/RequestProcessors.php b/www/analytics/core/Plugin/RequestProcessors.php new file mode 100644 index 00000000..82727448 --- /dev/null +++ b/www/analytics/core/Plugin/RequestProcessors.php @@ -0,0 +1,27 @@ +findMultipleComponents('Tracker', 'Piwik\\Tracker\\RequestProcessor'); + + $instances = array(); + foreach ($processors as $processor) { + $instances[] = StaticContainer::get($processor); + } + + return $instances; + } +} diff --git a/www/analytics/core/Plugin/Segment.php b/www/analytics/core/Plugin/Segment.php new file mode 100644 index 00000000..2a20208e --- /dev/null +++ b/www/analytics/core/Plugin/Segment.php @@ -0,0 +1,341 @@ +setType(\Piwik\Plugin\Segment::TYPE_DIMENSION); + $segment->setName('General_EntryKeyword'); + $segment->setCategory('General_Visit'); + $segment->setSegment('entryKeyword'); + $segment->setSqlSegment('log_visit.entry_keyword'); + $segment->setAcceptedValues('Any keywords people search for on your website such as "help" or "imprint"'); + ``` + * @api + * @since 2.5.0 + */ +class Segment +{ + /** + * Segment type 'dimension'. Can be used along with {@link setType()}. + * @api + */ + const TYPE_DIMENSION = 'dimension'; + + /** + * Segment type 'metric'. Can be used along with {@link setType()}. + * @api + */ + const TYPE_METRIC = 'metric'; + + private $type; + private $category; + private $name; + private $segment; + private $sqlSegment; + private $sqlFilter; + private $sqlFilterValue; + private $acceptValues; + private $permission; + private $suggestedValuesCallback; + private $unionOfSegments; + + /** + * If true, this segment will only be visible to the user if the user has view access + * to one of the requested sites (see API.getSegmentsMetadata). + * + * @var bool + */ + private $requiresAtLeastViewAccess = false; + + /** + * @ignore + */ + final public function __construct() + { + $this->init(); + } + + /** + * Here you can initialize this segment and set any default values. It is called directly after the object is + * created. + * @api + */ + protected function init() + { + } + + /** + * 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." + * + * @param string $acceptedValues + * @api + */ + public function setAcceptedValues($acceptedValues) + { + $this->acceptValues = $acceptedValues; + } + + /** + * Set (overwrite) the category this segment belongs to. It should be a translation key such as 'General_Actions' + * or 'General_Visit'. + * @param string $category + * @api + */ + public function setCategory($category) + { + $this->category = $category; + } + + /** + * Set (overwrite) the segment display name. This name will be visible in the API and the UI. It should be a + * translation key such as 'Actions_ColumnEntryPageTitle' or 'Resolution_ColumnResolution'. + * @param string $name + * @api + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Set (overwrite) the name of the segment. The name should be lower case first and has to be unique. The segment + * name defined here needs to be set in the URL to actually apply this segment. Eg if the segment is 'searches' + * you need to set "&segment=searches>0" in the UI. + * @param string $segment + * @api + */ + public function setSegment($segment) + { + $this->segment = $segment; + $this->check(); + } + + /** + * Sometimes you want users to set 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. + * + * @param string|\Closure $sqlFilter + * @api + */ + public function setSqlFilter($sqlFilter) + { + $this->sqlFilter = $sqlFilter; + } + + /** + * Similar to {@link setSqlFilter()} 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. This callback is called shortly before {@link setSqlFilter()}. + * @param string|array $sqlFilterValue + * @api + */ + public function setSqlFilterValue($sqlFilterValue) + { + $this->sqlFilterValue = $sqlFilterValue; + } + + /** + * Defines to which column in the MySQL database the segment belongs: 'mytablename.mycolumnname'. Eg + * 'log_visit.idsite'. When a segment is applied the given or filtered value will be compared with this column. + * + * @param string $sqlSegment + * @api + */ + public function setSqlSegment($sqlSegment) + { + $this->sqlSegment = $sqlSegment; + $this->check(); + } + + /** + * Set a list of segments that should be used instead of fetching the values from a single column. + * All set segments will be applied via an OR operator. + * + * @param array $segments + * @api + */ + public function setUnionOfSegments($segments) + { + $this->unionOfSegments = $segments; + $this->check(); + } + + /** + * @return array + * @ignore + */ + public function getUnionOfSegments() + { + return $this->unionOfSegments; + } + + /** + * @return string + * @ignore + */ + public function getSqlSegment() + { + return $this->sqlSegment; + } + + /** + * Set (overwrite) the type of this segment which is usually either a 'dimension' or a 'metric'. + * @param string $type See constansts TYPE_* + * @api + */ + public function setType($type) + { + $this->type = $type; + } + + /** + * @return string + * @ignore + */ + public function getType() + { + return $this->type; + } + + /** + * @return string + * @ignore + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the name of this segment as it should appear in segment expressions. + * + * @return string + */ + public function getSegment() + { + return $this->segment; + } + + /** + * Set callback which will be executed when user will call for suggested values for segment. + * + * @param callable $suggestedValuesCallback + */ + public function setSuggestedValuesCallback($suggestedValuesCallback) + { + $this->suggestedValuesCallback = $suggestedValuesCallback; + } + + /** + * You can restrict the access to this segment by passing a boolean `false`. For instance if you want to make + * a certain segment only available to users having super user access you could do the following: + * `$segment->setPermission(Piwik::hasUserSuperUserAccess());` + * @param bool $permission + * @api + */ + public function setPermission($permission) + { + $this->permission = $permission; + } + + /** + * @return array + * @ignore + */ + public function toArray() + { + $segment = array( + 'type' => $this->type, + 'category' => $this->category, + 'name' => $this->name, + 'segment' => $this->segment, + 'sqlSegment' => $this->sqlSegment, + ); + + if (!empty($this->unionOfSegments)) { + $segment['unionOfSegments'] = $this->unionOfSegments; + } + + if (!empty($this->sqlFilter)) { + $segment['sqlFilter'] = $this->sqlFilter; + } + + if (!empty($this->sqlFilterValue)) { + $segment['sqlFilterValue'] = $this->sqlFilterValue; + } + + if (!empty($this->acceptValues)) { + $segment['acceptedValues'] = $this->acceptValues; + } + + if (isset($this->permission)) { + $segment['permission'] = $this->permission; + } + + if (is_callable($this->suggestedValuesCallback)) { + $segment['suggestedValuesCallback'] = $this->suggestedValuesCallback; + } + + return $segment; + } + + /** + * Returns true if this segment should only be visible to the user if the user has view access + * to one of the requested sites (see API.getSegmentsMetadata), false if it should always be + * visible to the user (even the anonymous user). + * + * @return boolean + * @ignore + */ + public function isRequiresAtLeastViewAccess() + { + return $this->requiresAtLeastViewAccess; + } + + /** + * Sets whether the segment should only be visible if the user requesting it has view access + * to one of the requested sites and if the user is not the anonymous user. + * + * @param boolean $requiresAtLeastViewAccess + * @ignore + */ + public function setRequiresAtLeastViewAccess($requiresAtLeastViewAccess) + { + $this->requiresAtLeastViewAccess = $requiresAtLeastViewAccess; + } + + private function check() + { + if ($this->sqlSegment && $this->unionOfSegments) { + throw new Exception(sprintf('Union of segments and SQL segment is set for segment "%s", use only one of them', $this->name)); + } + + if ($this->segment && $this->unionOfSegments && in_array($this->segment, $this->unionOfSegments, true)) { + throw new Exception(sprintf('The segment %s contains a union segment to itself', $this->name)); + } + } +} diff --git a/www/analytics/core/Plugin/Settings.php b/www/analytics/core/Plugin/Settings.php index 20a5a305..c26581e4 100644 --- a/www/analytics/core/Plugin/Settings.php +++ b/www/analytics/core/Plugin/Settings.php @@ -1,6 +1,6 @@ [setting-value] ). - * - * @var array - */ - private $settingsValues = array(); - private $introduction; - private $pluginName; + protected $pluginName; + + /** + * @var StorageInterface + */ + protected $storage; /** * Constructor. - * - * @param string $pluginName The name of the plugin these settings are for. */ - public function __construct($pluginName) + public function __construct($pluginName = null) { - $this->pluginName = $pluginName; + if (!empty($pluginName)) { + $this->pluginName = $pluginName; + } else { + $classname = get_class($this); + $parts = explode('\\', $classname); + + if (3 <= count($parts)) { + $this->pluginName = $parts[2]; + } + } + + $this->storage = Storage\Factory::make($this->pluginName); $this->init(); - $this->loadSettings(); + } + + /** + * @ignore + */ + public function getPluginName() + { + return $this->pluginName; + } + + /** + * @ignore + * @return Setting + */ + public function getSetting($name) + { + if (array_key_exists($name, $this->settings)) { + return $this->settings[$name]; + } } /** @@ -90,7 +115,7 @@ abstract class Settings implements StorageInterface /** * Returns the introduction text for this plugin's settings. - * + * * @return string */ public function getIntroduction() @@ -106,14 +131,17 @@ abstract class Settings implements StorageInterface public function getSettingsForCurrentUser() { $settings = array_filter($this->getSettings(), function (Setting $setting) { - return $setting->canBeDisplayedForCurrentUser(); + return $setting->isWritableByCurrentUser(); }); - uasort($settings, function ($setting1, $setting2) use ($settings) { + $settings2 = $settings; + + uasort($settings, function ($setting1, $setting2) use ($settings2) { + /** @var Setting $setting1 */ /** @var Setting $setting2 */ if ($setting1->getOrder() == $setting2->getOrder()) { // preserve order for settings having same order - foreach ($settings as $setting) { + foreach ($settings2 as $setting) { if ($setting1 === $setting) { return -1; } @@ -142,12 +170,57 @@ abstract class Settings implements StorageInterface return $this->settings; } + /** + * Makes a new plugin setting available. + * + * @param Setting $setting + * @throws \Exception If there is a setting with the same name that already exists. + * If the name contains non-alphanumeric characters. + */ + protected function addSetting(Setting $setting) + { + $name = $setting->getName(); + + if (!ctype_alnum(str_replace('_', '', $name))) { + $msg = sprintf('The setting name "%s" in plugin "%s" is not valid. Only underscores, alpha and numerical characters are allowed', $setting->getName(), $this->pluginName); + throw new \Exception($msg); + } + + if (array_key_exists($name, $this->settings)) { + throw new \Exception(sprintf('A setting with name "%s" does already exist for plugin "%s"', $setting->getName(), $this->pluginName)); + } + + $this->setDefaultTypeAndFieldIfNeeded($setting); + $this->addValidatorIfNeeded($setting); + + $setting->setStorage($this->storage); + $setting->setPluginName($this->pluginName); + + $this->settings[$name] = $setting; + } + /** * Saves (persists) the current setting values in the database. */ public function save() { - Option::set($this->getOptionKey(), serialize($this->settingsValues)); + $this->storage->save(); + + SettingsStorage::clearCache(); + + /** + * Triggered after a plugin settings have been updated. + * + * **Example** + * + * Piwik::addAction('Settings.MyPlugin.settingsUpdated', function (Settings $settings) { + * $value = $settings->someSetting->getValue(); + * // Do something with the new setting value + * }); + * + * @param Settings $settings The plugin settings object. + */ + Piwik::postEvent(sprintf('Settings.%s.settingsUpdated', $this->pluginName), array($this)); } /** @@ -158,138 +231,9 @@ abstract class Settings implements StorageInterface { Piwik::checkUserHasSuperUserAccess(); - Option::delete($this->getOptionKey()); - $this->settingsValues = array(); - } + $this->storage->deleteAllValues(); - /** - * Returns the current value for a setting. If no value is stored, the default value - * is be returned. - * - * @param Setting $setting - * @return mixed - * @throws \Exception If the setting does not exist or if the current user is not allowed to change the value - * of this setting. - */ - public function getSettingValue(Setting $setting) - { - $this->checkIsValidSetting($setting->getName()); - - if (array_key_exists($setting->getKey(), $this->settingsValues)) { - - return $this->settingsValues[$setting->getKey()]; - } - - return $setting->defaultValue; - } - - /** - * Sets (overwrites) the value of a setting in memory. To persist the change, {@link save()} must be - * called afterwards, otherwise the change has no effect. - * - * Before the setting is changed, the {@link Piwik\Settings\Setting::$validate} and - * {@link Piwik\Settings\Setting::$transform} closures will be invoked (if defined). If there is no validation - * filter, the setting value will be casted to the appropriate data type. - * - * @param Setting $setting - * @param string $value - * @throws \Exception If the setting does not exist or if the current user is not allowed to change the value - * of this setting. - */ - public function setSettingValue(Setting $setting, $value) - { - $this->checkIsValidSetting($setting->getName()); - - if ($setting->validate && $setting->validate instanceof \Closure) { - call_user_func($setting->validate, $value, $setting); - } - - if ($setting->transform && $setting->transform instanceof \Closure) { - $value = call_user_func($setting->transform, $value, $setting); - } elseif (isset($setting->type)) { - settype($value, $setting->type); - } - - $this->settingsValues[$setting->getKey()] = $value; - } - - /** - * Unsets a setting value in memory. To persist the change, {@link save()} must be - * called afterwards, otherwise the change has no effect. - * - * @param Setting $setting - */ - public function removeSettingValue(Setting $setting) - { - $this->checkHasEnoughPermission($setting); - - $key = $setting->getKey(); - - if (array_key_exists($key, $this->settingsValues)) { - unset($this->settingsValues[$key]); - } - } - - /** - * Makes a new plugin setting available. - * - * @param Setting $setting - * @throws \Exception If there is a setting with the same name that already exists. - * If the name contains non-alphanumeric characters. - */ - protected function addSetting(Setting $setting) - { - if (!ctype_alnum($setting->getName())) { - $msg = sprintf('The setting name "%s" in plugin "%s" is not valid. Only alpha and numerical characters are allowed', $setting->getName(), $this->pluginName); - throw new \Exception($msg); - } - - if (array_key_exists($setting->getName(), $this->settings)) { - throw new \Exception(sprintf('A setting with name "%s" does already exist for plugin "%s"', $setting->getName(), $this->pluginName)); - } - - $this->setDefaultTypeAndFieldIfNeeded($setting); - $this->addValidatorIfNeeded($setting); - - $setting->setStorage($this); - - $this->settings[$setting->getName()] = $setting; - } - - private function getOptionKey() - { - return 'Plugin_' . $this->pluginName . '_Settings'; - } - - private function loadSettings() - { - $values = Option::get($this->getOptionKey()); - - if (!empty($values)) { - $this->settingsValues = unserialize($values); - } - } - - private function checkIsValidSetting($name) - { - $setting = $this->getSetting($name); - - if (empty($setting)) { - throw new \Exception(sprintf('The setting %s does not exist', $name)); - } - - $this->checkHasEnoughPermission($setting); - } - - /** - * @param $name - * @return Setting|null - */ - private function getSetting($name) - { - if (array_key_exists($name, $this->settings)) { - return $this->settings[$name]; - } + SettingsStorage::clearCache(); } private function getDefaultType($controlType) @@ -320,30 +264,16 @@ abstract class Settings implements StorageInterface return $defaultControlTypes[$type]; } - /** - * @param $setting - * @throws \Exception - */ - private function checkHasEnoughPermission(Setting $setting) - { - // When the request is a Tracker request, allow plugins to read/write settings - if(SettingsServer::isTrackerApiRequest()) { - return; - } - - if (!$setting->canBeDisplayedForCurrentUser()) { - $errorMsg = Piwik::translate('CoreAdminHome_PluginSettingChangeNotAllowed', array($setting->getName(), $this->pluginName)); - throw new \Exception($errorMsg); - } - } - private function setDefaultTypeAndFieldIfNeeded(Setting $setting) { - if (!is_null($setting->uiControlType) && is_null($setting->type)) { + $hasControl = !is_null($setting->uiControlType); + $hasType = !is_null($setting->type); + + if ($hasControl && !$hasType) { $setting->type = $this->getDefaultType($setting->uiControlType); - } elseif (!is_null($setting->type) && is_null($setting->uiControlType)) { + } elseif ($hasType && !$hasControl) { $setting->uiControlType = $this->getDefaultCONTROL($setting->type); - } elseif (is_null($setting->uiControlType) && is_null($setting->type)) { + } elseif (!$hasControl && !$hasType) { $setting->type = static::TYPE_STRING; $setting->uiControlType = static::CONTROL_TEXT; } @@ -360,7 +290,7 @@ abstract class Settings implements StorageInterface $setting->validate = function ($value) use ($setting, $pluginName) { $errorMsg = Piwik::translate('CoreAdminHome_PluginSettingsValueNotAllowed', - array($setting->title, $pluginName)); + array($setting->title, $pluginName)); if (is_array($value) && $setting->type == Settings::TYPE_ARRAY) { foreach ($value as $val) { diff --git a/www/analytics/core/Plugin/Tasks.php b/www/analytics/core/Plugin/Tasks.php new file mode 100644 index 00000000..19f9e5bb --- /dev/null +++ b/www/analytics/core/Plugin/Tasks.php @@ -0,0 +1,154 @@ +daily('myMethodName') + } + + /** + * @return Task[] $tasks + */ + public function getScheduledTasks() + { + return $this->tasks; + } + + /** + * Schedule the given tasks/method to run once every hour. + * + * @param string $methodName The name of the method that will be called when the task is being + * exectuted. To make it work you need to create a public method having the + * given method name in your Tasks class. + * @param null|string $methodParameter Can be null if the task does not need any parameter or a string. It is not + * possible to specify multiple parameters as an array etc. If you need to + * pass multiple parameters separate them via any characters such as '###'. + * For instance '$param1###$param2###$param3' + * @param int $priority Can be any constant such as self::LOW_PRIORITY + * + * @return Schedule + * @api + */ + protected function hourly($methodName, $methodParameter = null, $priority = self::NORMAL_PRIORITY) + { + return $this->custom($this, $methodName, $methodParameter, 'hourly', $priority); + } + + /** + * Schedule the given tasks/method to run once every day. + * + * See {@link hourly()} + * @api + */ + protected function daily($methodName, $methodParameter = null, $priority = self::NORMAL_PRIORITY) + { + return $this->custom($this, $methodName, $methodParameter, 'daily', $priority); + } + + /** + * Schedule the given tasks/method to run once every week. + * + * See {@link hourly()} + * @api + */ + protected function weekly($methodName, $methodParameter = null, $priority = self::NORMAL_PRIORITY) + { + return $this->custom($this, $methodName, $methodParameter, 'weekly', $priority); + } + + /** + * Schedule the given tasks/method to run once every month. + * + * See {@link hourly()} + * @api + */ + protected function monthly($methodName, $methodParameter = null, $priority = self::NORMAL_PRIORITY) + { + return $this->custom($this, $methodName, $methodParameter, 'monthly', $priority); + } + + /** + * Schedules the given tasks/method to run depending at the given scheduled time. Unlike the convenient methods + * such as {@link hourly()} you need to specify the object on which the given method should be called. This can be + * either an instance of a class or a class name. For more information about these parameters see {@link hourly()} + * + * @param string|object $objectOrClassName + * @param string $methodName + * @param null|string $methodParameter + * @param string|Schedule $time + * @param int $priority + * + * @return \Piwik\Scheduler\Schedule\Schedule + * + * @throws \Exception If a wrong time format is given. Needs to be either a string such as 'daily', 'weekly', ... + * or an instance of {@link Piwik\Scheduler\Schedule\Schedule} + * + * @api + */ + protected function custom($objectOrClassName, $methodName, $methodParameter, $time, $priority = self::NORMAL_PRIORITY) + { + $this->checkIsValidTask($objectOrClassName, $methodName); + + if (is_string($time)) { + $time = Schedule::factory($time); + } + + if (!($time instanceof Schedule)) { + throw new \Exception('$time should be an instance of Schedule'); + } + + $this->scheduleTask(new Task($objectOrClassName, $methodName, $methodParameter, $time, $priority)); + + return $time; + } + + /** + * In case you need very high flexibility and none of the other convenient methods such as {@link hourly()} or + * {@link custom()} suit you, you can use this method to add a custom scheduled task. + * + * @param Task $task + */ + protected function scheduleTask(Task $task) + { + $this->tasks[] = $task; + } + + private function checkIsValidTask($objectOrClassName, $methodName) + { + Development::checkMethodIsCallable($objectOrClassName, $methodName, 'The registered task is not valid as the method'); + } +} diff --git a/www/analytics/core/Plugin/ViewDataTable.php b/www/analytics/core/Plugin/ViewDataTable.php index 069e08de..6e190388 100644 --- a/www/analytics/core/Plugin/ViewDataTable.php +++ b/www/analytics/core/Plugin/ViewDataTable.php @@ -1,6 +1,6 @@ render(); * } - * + * * **Using {@link Piwik\Plugin\Controller::renderReport}** - * + * * First, a controller method that displays a single report: - * + * * public function myReport() * { * return $this->renderReport(__FUNCTION__);` * } - * + * * Then the event handler for the {@hook ViewDataTable.configure} event: - * + * * public function configureViewDataTable(ViewDataTable $view) * { * switch ($view->requestConfig->apiMethodToRequestDataTable) { @@ -111,32 +111,32 @@ use Piwik\ViewDataTable\RequestConfig as VizRequest; * break; * } * } - * + * * **Using custom configuration objects in a new visualization** - * + * * class MyVisualizationConfig extends Piwik\ViewDataTable\Config * { * public $my_new_property = true; * } - * + * * class MyVisualizationRequestConfig extends Piwik\ViewDataTable\RequestConfig * { * public $my_new_property = false; * } - * + * * class MyVisualization extends Piwik\Plugin\ViewDataTable * { * public static function getDefaultConfig() * { * return new MyVisualizationConfig(); * } - * + * * public static function getDefaultRequestConfig() * { * return new MyVisualizationRequestConfig(); * } * } - * + * * * @api */ @@ -153,14 +153,14 @@ abstract class ViewDataTable implements ViewInterface /** * Contains display properties for this visualization. - * + * * @var \Piwik\ViewDataTable\Config */ public $config; /** * Contains request properties for this visualization. - * + * * @var \Piwik\ViewDataTable\RequestConfig */ public $requestConfig; @@ -175,7 +175,7 @@ abstract class ViewDataTable implements ViewInterface * Posts the {@hook ViewDataTable.configure} event which plugins can use to configure the * way reports are displayed. */ - public function __construct($controllerAction, $apiMethodToRequestDataTable) + public function __construct($controllerAction, $apiMethodToRequestDataTable, $overrideParams = array()) { list($controllerName, $controllerAction) = explode('.', $controllerAction); @@ -191,13 +191,53 @@ abstract class ViewDataTable implements ViewInterface $this->requestConfig->apiMethodToRequestDataTable = $apiMethodToRequestDataTable; + $report = Report::factory($this->requestConfig->getApiModuleToRequest(), $this->requestConfig->getApiMethodToRequest()); + + if (!empty($report)) { + /** @var Report $report */ + $subtable = $report->getActionToLoadSubTables(); + if (!empty($subtable)) { + $this->config->subtable_controller_action = $subtable; + } + + $this->config->show_goals = $report->hasGoalMetrics(); + + $relatedReports = $report->getRelatedReports(); + if (!empty($relatedReports)) { + foreach ($relatedReports as $relatedReport) { + $widgetTitle = $relatedReport->getWidgetTitle(); + + if ($widgetTitle && Common::getRequestVar('widget', 0, 'int')) { + $relatedReportName = $widgetTitle; + } else { + $relatedReportName = $relatedReport->getName(); + } + + $this->config->addRelatedReport($relatedReport->getModule() . '.' . $relatedReport->getAction(), + $relatedReportName); + } + } + + $metrics = $report->getMetrics(); + if (!empty($metrics)) { + $this->config->addTranslations($metrics); + } + + $processedMetrics = $report->getProcessedMetrics(); + if (!empty($processedMetrics)) { + $this->config->addTranslations($processedMetrics); + } + + $report->configureView($this); + } + /** * Triggered during {@link ViewDataTable} construction. Subscribers should customize * the view based on the report that is being displayed. - * + * * Plugins that define their own reports must subscribe to this event in order to * specify how the Piwik UI should display the report. - * + * * **Example** * * // event handler @@ -210,7 +250,7 @@ abstract class ViewDataTable implements ViewInterface * break; * } * } - * + * * @param ViewDataTable $view The instance to configure. */ Piwik::postEvent('ViewDataTable.configure', array($this)); @@ -229,16 +269,17 @@ abstract class ViewDataTable implements ViewInterface $this->requestConfig->filter_excludelowpop_value = $function(); } + $this->overrideViewPropertiesWithParams($overrideParams); $this->overrideViewPropertiesWithQueryParams(); } protected function assignRelatedReportsTitle() { - if(!empty($this->config->related_reports_title)) { + if (!empty($this->config->related_reports_title)) { // title already assigned by a plugin return; } - if(count($this->config->related_reports) == 1) { + if (count($this->config->related_reports) == 1) { $this->config->related_reports_title = Piwik::translate('General_RelatedReport') . ':'; } else { $this->config->related_reports_title = Piwik::translate('General_RelatedReports') . ':'; @@ -247,12 +288,12 @@ abstract class ViewDataTable implements ViewInterface /** * Returns the default config instance. - * + * * Visualizations that define their own display properties should override this method and * return an instance of their new {@link Piwik\ViewDataTable\Config} descendant. * * See the last example {@link ViewDataTable here} for more information. - * + * * @return \Piwik\ViewDataTable\Config */ public static function getDefaultConfig() @@ -262,12 +303,12 @@ abstract class ViewDataTable implements ViewInterface /** * Returns the default request config instance. - * + * * Visualizations that define their own request properties should override this method and * return an instance of their new {@link Piwik\ViewDataTable\RequestConfig} descendant. * * See the last example {@link ViewDataTable here} for more information. - * + * * @return \Piwik\ViewDataTable\RequestConfig */ public static function getDefaultRequestConfig() @@ -275,7 +316,7 @@ abstract class ViewDataTable implements ViewInterface return new VizRequest(); } - protected function loadDataTableFromAPI($fixedRequestParams = array()) + protected function loadDataTableFromAPI() { if (!is_null($this->dataTable)) { // data table is already there @@ -283,14 +324,14 @@ abstract class ViewDataTable implements ViewInterface return $this->dataTable; } - $this->dataTable = $this->request->loadDataTableFromAPI($fixedRequestParams); + $this->dataTable = $this->request->loadDataTableFromAPI(); return $this->dataTable; } /** * Returns the viewDataTable ID for this DataTable visualization. - * + * * Derived classes should not override this method. They should instead declare a const ID field * with the viewDataTable ID. * @@ -306,13 +347,13 @@ abstract class ViewDataTable implements ViewInterface throw new \Exception($message); } - return $id; + return $id; } /** * Returns `true` if this instance's or any of its ancestors' viewDataTable IDs equals the supplied ID, * `false` if otherwise. - * + * * Can be used to test whether a ViewDataTable object is an instance of a certain visualization or not, * without having to know where that visualization is. * @@ -399,7 +440,7 @@ abstract class ViewDataTable implements ViewInterface if (property_exists($this->requestConfig, $name)) { $this->requestConfig->$name = $this->getPropertyFromQueryParam($name, $this->requestConfig->$name); } elseif (property_exists($this->config, $name)) { - $this->config->$name = $this->getPropertyFromQueryParam($name, $this->config->$name); + $this->config->$name = $this->getPropertyFromQueryParam($name, $this->config->$name); } } @@ -443,7 +484,7 @@ abstract class ViewDataTable implements ViewInterface /** * Returns `true` if this visualization can display some type of data or not. - * + * * New visualization classes should override this method if they can only visualize certain * types of data. The evolution graph visualization, for example, can only visualize * sets of DataTables. If the API method used results in a single DataTable, the evolution @@ -456,4 +497,63 @@ abstract class ViewDataTable implements ViewInterface { return $view->config->show_all_views_icons; } + + private function overrideViewPropertiesWithParams($overrideParams) + { + if (empty($overrideParams)) { + return; + } + + foreach ($overrideParams as $key => $value) { + if (property_exists($this->requestConfig, $key)) { + $this->requestConfig->$key = $value; + } elseif (property_exists($this->config, $key)) { + $this->config->$key = $value; + } elseif ($key != 'enable_filter_excludelowpop') { + $this->config->custom_parameters[$key] = $value; + } + } + } + + /** + * Display a meaningful error message when any invalid parameter is being set. + * + * @param $overrideParams + * @throws + */ + public function throwWhenSettingNonOverridableParameter($overrideParams) + { + $nonOverridableParams = $this->getNonOverridableParams($overrideParams); + if(count($nonOverridableParams) > 0) { + throw new \Exception(sprintf( + "Setting parameters %s is not allowed. Please report this bug to the Piwik team.", + implode(" and ", $nonOverridableParams) + )); + } + } + + /** + * @param $overrideParams + * @return array + */ + public function getNonOverridableParams($overrideParams) + { + $paramsCannotBeOverridden = array(); + foreach ($overrideParams as $paramName => $paramValue) { + if (property_exists($this->requestConfig, $paramName)) { + $allowedParams = $this->requestConfig->overridableProperties; + } elseif (property_exists($this->config, $paramName)) { + $allowedParams = $this->config->overridableProperties; + } else { + // setting Config.custom_parameters is always allowed + continue; + } + + if (!in_array($paramName, $allowedParams)) { + $paramsCannotBeOverridden[] = $paramName; + } + } + return $paramsCannotBeOverridden; + } + } diff --git a/www/analytics/core/Plugin/Visualization.php b/www/analytics/core/Plugin/Visualization.php index a8ab8292..fcd88912 100644 --- a/www/analytics/core/Plugin/Visualization.php +++ b/www/analytics/core/Plugin/Visualization.php @@ -1,6 +1,6 @@ requestConfig->request_parameters_to_modify['date'] = $previousDate . ',' . $date; * } - * + * * // since we load the previous period's data too, we need to override the logic to * // check if there is data or not. * public function isThereDataToDisplay() * { * $tables = $this->dataTable->getDataTables() * $requestedDataTable = end($tables); - * + * * return $requestedDataTable->getRowsCount() != 0; * } * } - * + * * **Force properties to be set to certain values** - * + * * class MyVisualization extends Visualization * { * // ensure that some properties are set to certain values before rendering. @@ -133,9 +139,9 @@ class Visualization extends ViewDataTable { /** * The Twig template file to use when rendering, eg, `"@MyPlugin/_myVisualization.twig"`. - * + * * Must be defined by classes that extend Visualization. - * + * * @api */ const TEMPLATE_FILE = ''; @@ -143,8 +149,14 @@ class Visualization extends ViewDataTable private $templateVars = array(); private $reportLastUpdatedMessage = null; private $metadata = null; + protected $metricsFormatter = null; - final public function __construct($controllerAction, $apiMethodToRequestDataTable) + /** + * @var Report + */ + protected $report; + + final public function __construct($controllerAction, $apiMethodToRequestDataTable, $params = array()) { $templateFile = static::TEMPLATE_FILE; @@ -152,7 +164,11 @@ class Visualization extends ViewDataTable throw new \Exception('You have not defined a constant named TEMPLATE_FILE in your visualization class.'); } - parent::__construct($controllerAction, $apiMethodToRequestDataTable); + $this->metricsFormatter = new HtmlFormatter(); + + parent::__construct($controllerAction, $apiMethodToRequestDataTable, $params); + + $this->report = Report::factory($this->requestConfig->getApiModuleToRequest(), $this->requestConfig->getApiMethodToRequest()); } protected function buildView() @@ -160,26 +176,29 @@ class Visualization extends ViewDataTable $this->overrideSomeConfigPropertiesIfNeeded(); try { - $this->beforeLoadDataTable(); - - $this->loadDataTableFromAPI(array('disable_generic_filters' => 1)); + $this->loadDataTableFromAPI(); $this->postDataTableLoadedFromAPI(); $requestPropertiesAfterLoadDataTable = $this->requestConfig->getProperties(); $this->applyFilters(); + $this->addVisualizationInfoFromMetricMetadata(); $this->afterAllFiltersAreApplied(); $this->beforeRender(); $this->logMessageIfRequestPropertiesHaveChanged($requestPropertiesAfterLoadDataTable); - } catch (NoAccessException $e) { throw $e; } catch (\Exception $e) { - Log::warning("Failed to get data from API: " . $e->getMessage() . "\n" . $e->getTraceAsString()); + Log::error("Failed to get data from API: " . $e->getMessage() . "\n" . $e->getTraceAsString()); - $loadingError = array('message' => $e->getMessage()); + $message = $e->getMessage(); + if (\Piwik_ShouldPrintBackTraceWithMessage()) { + $message .= "\n" . $e->getTraceAsString(); + } + + $loadingError = array('message' => $message); } $view = new View("@CoreHome/_dataTable"); @@ -192,6 +211,7 @@ class Visualization extends ViewDataTable $view->visualization = $this; $view->visualizationTemplate = static::TEMPLATE_FILE; $view->visualizationCssClass = $this->getDefaultDataTableCssClass(); + $view->reportMetdadata = $this->getReportMetadata(); if (null === $this->dataTable) { $view->dataTable = null; @@ -216,17 +236,78 @@ class Visualization extends ViewDataTable return $view; } + /** + * @internal + */ + protected function loadDataTableFromAPI() + { + if (!is_null($this->dataTable)) { + // data table is already there + // this happens when setDataTable has been used + return $this->dataTable; + } + + // we build the request (URL) to call the API + $request = $this->buildApiRequestArray(); + + $module = $this->requestConfig->getApiModuleToRequest(); + $method = $this->requestConfig->getApiMethodToRequest(); + + PluginManager::getInstance()->checkIsPluginActivated($module); + + $class = ApiRequest::getClassNameAPI($module); + $dataTable = Proxy::getInstance()->call($class, $method, $request); + + $response = new ResponseBuilder($format = 'original', $request); + $response->disableSendHeader(); + $response->disableDataTablePostProcessor(); + + $this->dataTable = $response->getResponse($dataTable, $module, $method); + } + + private function getReportMetadata() + { + $request = $this->request->getRequestArray() + $_GET + $_POST; + + $idSite = Common::getRequestVar('idSite', null, 'string', $request); + $module = $this->requestConfig->getApiModuleToRequest(); + $action = $this->requestConfig->getApiMethodToRequest(); + + $apiParameters = array(); + $idDimension = Common::getRequestVar('idDimension', 0, 'int'); + $idGoal = Common::getRequestVar('idGoal', 0, 'int'); + if ($idDimension > 0) { + $apiParameters['idDimension'] = $idDimension; + } + if ($idGoal > 0) { + $apiParameters['idGoal'] = $idGoal; + } + + $metadata = ApiApi::getInstance()->getMetadata($idSite, $module, $action, $apiParameters); + + if (!empty($metadata)) { + return array_shift($metadata); + } + + return false; + } + private function overrideSomeConfigPropertiesIfNeeded() { if (empty($this->config->footer_icons)) { $this->config->footer_icons = ViewDataTableManager::configureFooterIcons($this); } - if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated('Goals')) { + if (!$this->isPluginActivated('Goals')) { $this->config->show_goals = false; } } + private function isPluginActivated($pluginName) + { + return PluginManager::getInstance()->isPluginActivated($pluginName); + } + /** * Assigns a template variable making it available in the Twig template specified by * {@link TEMPLATE_FILE}. @@ -248,9 +329,9 @@ class Visualization extends ViewDataTable /** * Returns `true` if there is data to display, `false` if otherwise. - * + * * Derived classes should override this method if they change the amount of data that is loaded. - * + * * @api */ protected function isThereDataToDisplay() @@ -281,7 +362,7 @@ class Visualization extends ViewDataTable } if (empty($this->requestConfig->filter_sort_column)) { - $this->requestConfig->setDefaultSort($this->config->columns_to_display, $hasNbUniqVisitors); + $this->requestConfig->setDefaultSort($this->config->columns_to_display, $hasNbUniqVisitors, $columns); } // deal w/ table metadata @@ -289,38 +370,78 @@ class Visualization extends ViewDataTable $this->metadata = $this->dataTable->getAllTableMetadata(); if (isset($this->metadata[DataTable::ARCHIVED_DATE_METADATA_NAME])) { - $this->config->report_last_updated_message = $this->makePrettyArchivedOnText(); + $this->reportLastUpdatedMessage = $this->makePrettyArchivedOnText(); + } + } + + $pivotBy = Common::getRequestVar('pivotBy', false) ?: $this->requestConfig->pivotBy; + if (empty($pivotBy) + && $this->dataTable instanceof DataTable + ) { + $this->config->disablePivotBySubtableIfTableHasNoSubtables($this->dataTable); + } + } + + private function addVisualizationInfoFromMetricMetadata() + { + $dataTable = $this->dataTable instanceof DataTable\Map ? $this->dataTable->getFirstRow() : $this->dataTable; + + $metrics = Report::getMetricsForTable($dataTable, $this->report); + + // TODO: instead of iterating & calling translate everywhere, maybe we can get all translated names in one place. + // may be difficult, though, since translated metrics are specific to the report. + foreach ($metrics as $metric) { + $name = $metric->getName(); + + if (empty($this->config->translations[$name])) { + $this->config->translations[$name] = $metric->getTranslatedName(); + } + + if (empty($this->config->metrics_documentation[$name])) { + $this->config->metrics_documentation[$name] = $metric->getDocumentation(); } } } private function applyFilters() { - list($priorityFilters, $otherFilters) = $this->config->getFiltersToRun(); + $postProcessor = $this->makeDataTablePostProcessor(); // must be created after requestConfig is final + $self = $this; - // First, filters that delete rows - foreach ($priorityFilters as $filter) { - $this->dataTable->filter($filter[0], $filter[1]); - } + $postProcessor->setCallbackBeforeGenericFilters(function (DataTable\DataTableInterface $dataTable) use ($self, $postProcessor) { - $this->beforeGenericFiltersAreAppliedToLoadedDataTable(); + $self->setDataTable($dataTable); - if (!$this->requestConfig->areGenericFiltersDisabled()) { - $this->applyGenericFilters(); - } + // First, filters that delete rows + foreach ($self->config->getPriorityFilters() as $filter) { + $dataTable->filter($filter[0], $filter[1]); + } - $this->afterGenericFiltersAreAppliedToLoadedDataTable(); + $self->beforeGenericFiltersAreAppliedToLoadedDataTable(); - // queue other filters so they can be applied later if queued filters are disabled - foreach ($otherFilters as $filter) { - $this->dataTable->queueFilter($filter[0], $filter[1]); - } + if (!in_array($self->requestConfig->filter_sort_column, $self->config->columns_to_display)) { + $hasNbUniqVisitors = in_array('nb_uniq_visitors', $self->config->columns_to_display); + $columns = $dataTable->getColumns(); + $self->requestConfig->setDefaultSort($self->config->columns_to_display, $hasNbUniqVisitors, $columns); + } - // Finally, apply datatable filters that were queued (should be 'presentation' filters that - // do not affect the number of rows) - if (!$this->requestConfig->areQueuedFiltersDisabled()) { - $this->dataTable->applyQueuedFilters(); - } + $postProcessor->setRequest($self->buildApiRequestArray()); + }); + + $postProcessor->setCallbackAfterGenericFilters(function (DataTable\DataTableInterface $dataTable) use ($self) { + + $self->setDataTable($dataTable); + + $self->afterGenericFiltersAreAppliedToLoadedDataTable(); + + // queue other filters so they can be applied later if queued filters are disabled + foreach ($self->config->getPresentationFilters() as $filter) { + $dataTable->queueFilter($filter[0], $filter[1]); + } + + }); + + $this->dataTable = $postProcessor->process($this->dataTable); } private function removeEmptyColumnsFromDisplay() @@ -355,16 +476,16 @@ class Visualization extends ViewDataTable $today = mktime(0, 0, 0); if ($date->getTimestamp() > $today) { - $elapsedSeconds = time() - $date->getTimestamp(); - $timeAgo = MetricsFormatter::getPrettyTimeFromSeconds($elapsedSeconds); + $timeAgo = $this->metricsFormatter->getPrettyTimeFromSeconds($elapsedSeconds); return Piwik::translate('CoreHome_ReportGeneratedXAgo', $timeAgo); } - $prettyDate = $date->getLocalized("%longYear%, %longMonth% %day%") . $date->toString('S'); + $prettyDate = $date->getLocalized(Date::DATE_FORMAT_SHORT); - return Piwik::translate('CoreHome_ReportGeneratedOn', $prettyDate); + $timezoneAppend = ' (UTC)'; + return Piwik::translate('CoreHome_ReportGeneratedOn', $prettyDate) . $timezoneAppend; } /** @@ -379,7 +500,7 @@ class Visualization extends ViewDataTable */ private function hasReportBeenPurged() { - if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated('PrivacyManager')) { + if (!$this->isPluginActivated('PrivacyManager')) { return false; } @@ -399,7 +520,7 @@ class Visualization extends ViewDataTable foreach ($this->config->clientSideProperties as $name) { if (property_exists($this->requestConfig, $name)) { $result[$name] = $this->getIntIfValueIsBool($this->requestConfig->$name); - } else if (property_exists($this->config, $name)) { + } elseif (property_exists($this->config, $name)) { $result[$name] = $this->getIntIfValueIsBool($this->config->$name); } } @@ -453,7 +574,7 @@ class Visualization extends ViewDataTable if (property_exists($this->requestConfig, $name)) { $valueToConvert = $this->requestConfig->$name; - } else if (property_exists($this->config, $name)) { + } elseif (property_exists($this->config, $name)) { $valueToConvert = $this->config->$name; } @@ -481,6 +602,7 @@ class Visualization extends ViewDataTable 'filter_excludelowpop', 'filter_excludelowpop_value', ); + foreach ($deleteFromJavascriptVariables as $name) { if (isset($javascriptVariablesToSet[$name])) { unset($javascriptVariablesToSet[$name]); @@ -497,9 +619,11 @@ class Visualization extends ViewDataTable /** * Hook that is called before loading report data from the API. - * + * * Use this method to change the request parameters that is sent to the API when requesting * data. + * + * @api */ public function beforeLoadDataTable() { @@ -507,9 +631,11 @@ class Visualization extends ViewDataTable /** * Hook that is executed before generic filters are applied. - * + * * Use this method if you need access to the entire dataset (since generic filters will * limit and truncate reports). + * + * @api */ public function beforeGenericFiltersAreAppliedToLoadedDataTable() { @@ -517,6 +643,8 @@ class Visualization extends ViewDataTable /** * Hook that is executed after generic filters are applied. + * + * @api */ public function afterGenericFiltersAreAppliedToLoadedDataTable() { @@ -525,6 +653,8 @@ class Visualization extends ViewDataTable /** * Hook that is executed after the report data is loaded and after all filters have been applied. * Use this method to format the report data before the view is rendered. + * + * @api */ public function afterAllFiltersAreApplied() { @@ -533,27 +663,24 @@ class Visualization extends ViewDataTable /** * Hook that is executed directly before rendering. Use this hook to force display properties to * be a certain value, despite changes from plugins and query parameters. + * + * @api */ public function beforeRender() { // eg $this->config->showFooterColumns = true; } - /** - * Second, generic filters (Sort, Limit, Replace Column Names, etc.) - */ - private function applyGenericFilters() + private function makeDataTablePostProcessor() { - $requestArray = $this->request->getRequestArray(); - $request = \Piwik\API\Request::getRequestArrayFromString($requestArray); + $request = $this->buildApiRequestArray(); + $module = $this->requestConfig->getApiModuleToRequest(); + $method = $this->requestConfig->getApiMethodToRequest(); - if (false === $this->config->enable_sort) { - $request['filter_sort_column'] = ''; - $request['filter_sort_order'] = ''; - } + $processor = new DataTablePostProcessor($module, $method, $request); + $processor->setFormatter($this->metricsFormatter); - $genericFilter = new \Piwik\API\DataTableGenericFilter($request); - $genericFilter->filter($this->dataTable); + return $processor; } private function logMessageIfRequestPropertiesHaveChanged(array $requestPropertiesBefore) @@ -563,6 +690,15 @@ class Visualization extends ViewDataTable $diff = array_diff_assoc($this->makeSureArrayContainsOnlyStrings($requestProperties), $this->makeSureArrayContainsOnlyStrings($requestPropertiesBefore)); + if (!empty($diff['filter_sort_column'])) { + // this here might be ok as it can be changed after data loaded but before filters applied + unset($diff['filter_sort_column']); + } + if (!empty($diff['filter_sort_order'])) { + // this here might be ok as it can be changed after data loaded but before filters applied + unset($diff['filter_sort_order']); + } + if (empty($diff)) { return; } @@ -592,4 +728,30 @@ class Visualization extends ViewDataTable return $result; } + + /** + * @internal + * + * @return array + */ + public function buildApiRequestArray() + { + $requestArray = $this->request->getRequestArray(); + $request = APIRequest::getRequestArrayFromString($requestArray); + + if (false === $this->config->enable_sort) { + $request['filter_sort_column'] = ''; + $request['filter_sort_order'] = ''; + } + + if (!array_key_exists('format_metrics', $request) || $request['format_metrics'] === 'bc') { + $request['format_metrics'] = '1'; + } + + if (!$this->requestConfig->disable_queued_filters && array_key_exists('disable_queued_filters', $request)) { + unset($request['disable_queued_filters']); + } + + return $request; + } } diff --git a/www/analytics/core/Plugin/Widgets.php b/www/analytics/core/Plugin/Widgets.php new file mode 100644 index 00000000..10566a9a --- /dev/null +++ b/www/analytics/core/Plugin/Widgets.php @@ -0,0 +1,198 @@ +category; + } + + private function getModule() + { + $className = get_class($this); + $className = explode('\\', $className); + + return $className[2]; + } + + /** + * Adds a widget. You can add a widget by calling this method and passing the name of the widget as well as a method + * name that will be executed to render the widget. The method can be defined either directly here in this widget + * class or in the controller in case you want to reuse the same action for instance in the menu etc. + * @api + */ + protected function addWidget($name, $method, $parameters = array()) + { + $this->addWidgetWithCustomCategory($this->category, $name, $method, $parameters); + } + + /** + * Adds a widget with a custom category. By default all widgets that you define in your class will be added under + * the same category which is defined in the {@link $category} property. Sometimes you may have a widget that + * belongs to a different category where this method comes handy. It does the same as {@link addWidget()} but + * allows you to define the category name as well. + * @api + */ + protected function addWidgetWithCustomCategory($category, $name, $method, $parameters = array()) + { + $this->checkIsValidWidget($name, $method); + + $this->widgets[] = array('category' => $category, + 'name' => $name, + 'params' => $parameters, + 'method' => $method, + 'module' => $this->getModule()); + } + + /** + * Here you can add one or multiple widgets. To do so call the method {@link addWidget()} or + * {@link addWidgetWithCustomCategory()}. + * @api + */ + protected function init() + { + } + + /** + * @ignore + */ + public function getWidgets() + { + $this->widgets = array(); + + $this->init(); + + return $this->widgets; + } + + /** + * Allows you to configure previously added widgets. + * For instance you can remove any widgets defined by any plugin by calling the + * {@link \Piwik\WidgetsList::remove()} method. + * + * @param WidgetsList $widgetsList + * @api + */ + public function configureWidgetsList(WidgetsList $widgetsList) + { + } + + /** + * @return \Piwik\Plugin\Widgets[] + * @ignore + */ + public static function getAllWidgets() + { + return PluginManager::getInstance()->findComponents('Widgets', 'Piwik\\Plugin\\Widgets'); + } + + /** + * @ignore + * @return Widgets|null + */ + public static function factory($module, $action) + { + if (empty($module) || empty($action)) { + return; + } + + $pluginManager = PluginManager::getInstance(); + + try { + if (!$pluginManager->isPluginActivated($module)) { + return; + } + + $plugin = $pluginManager->getLoadedPlugin($module); + } catch (\Exception $e) { + // we are not allowed to use possible widgets, plugin is not active + return; + } + + /** @var Widgets $widgetContainer */ + $widgetContainer = $plugin->findComponent('Widgets', 'Piwik\\Plugin\\Widgets'); + + if (empty($widgetContainer)) { + // plugin does not define any widgets, we cannot do anything + return; + } + + if (!is_callable(array($widgetContainer, $action))) { + // widget does not implement such a method, we cannot do anything + return; + } + + // the widget class implements such an action, but we have to check whether it is actually exposed and whether + // it was maybe disabled by another plugin, this is only possible by checking the widgetslist, unfortunately + if (!WidgetsList::isDefined($module, $action)) { + return; + } + + return $widgetContainer; + } + + private function checkIsValidWidget($name, $method) + { + if (!Development::isEnabled()) { + return; + } + + if (empty($name)) { + Development::error('No name is defined for added widget having method "' . $method . '" in ' . get_class($this)); + } + + if (Development::isCallableMethod($this, $method)) { + return; + } + + $controllerClass = 'Piwik\\Plugins\\' . $this->getModule() . '\\Controller'; + + if (!Development::methodExists($this, $method) && + !Development::methodExists($controllerClass, $method)) { + Development::error('The added method "' . $method . '" neither exists in "' . get_class($this) . '" nor "' . $controllerClass . '". Make sure to define such a method.'); + } + + $definedInClass = get_class($this); + + if (Development::methodExists($controllerClass, $method)) { + if (Development::isCallableMethod($controllerClass, $method)) { + return; + } + + $definedInClass = $controllerClass; + } + + Development::error('The method "' . $method . '" is not callable on "' . $definedInClass . '". Make sure the method is public.'); + } +} diff --git a/www/analytics/core/PluginDeactivatedException.php b/www/analytics/core/PluginDeactivatedException.php new file mode 100644 index 00000000..fe426676 --- /dev/null +++ b/www/analytics/core/PluginDeactivatedException.php @@ -0,0 +1,20 @@ + Plugins page in Piwik."); + } +} diff --git a/www/analytics/core/Profiler.php b/www/analytics/core/Profiler.php index ae9c5e8b..12c1e46f 100644 --- a/www/analytics/core/Profiler.php +++ b/www/analytics/core/Profiler.php @@ -1,6 +1,6 @@ getProfiler(); if (!$profiler->getEnabled()) { - throw new \Exception("To display the profiler you should enable enable_sql_profiler on your config/config.ini.php file"); + // To display the profiler you should enable enable_sql_profiler on your config/config.ini.php file + return; } $infoIndexedByQuery = array(); @@ -93,7 +103,7 @@ class Profiler return $a['sum_time_ms'] < $b['sum_time_ms']; } - static private function sortTimeDesc($a, $b) + private static function sortTimeDesc($a, $b) { return $a['sumTimeMs'] < $b['sumTimeMs']; } @@ -133,7 +143,9 @@ class Profiler { $totalTime = self::getDbElapsedSecs(); $queryCount = Profiler::getQueryCount(); - Log::debug(sprintf("Total queries = %d (total sql time = %.2fs)", $queryCount, $totalTime)); + if ($queryCount > 0) { + Log::debug(sprintf("Total queries = %d (total sql time = %.2fs)", $queryCount, $totalTime)); + } } /** @@ -163,7 +175,7 @@ class Profiler * * @param array $infoIndexedByQuery */ - static private function getSqlProfilingQueryBreakdownOutput($infoIndexedByQuery) + private static function getSqlProfilingQueryBreakdownOutput($infoIndexedByQuery) { $output = '
Breakdown by query
'; foreach ($infoIndexedByQuery as $query => $queryInfo) { @@ -184,97 +196,139 @@ class Profiler * Initializes Profiling via XHProf. * See: https://github.com/piwik/piwik/blob/master/tests/README.xhprof.md */ - public static function setupProfilerXHProf($mainRun = false) + public static function setupProfilerXHProf($mainRun = false, $setupDuringTracking = false) { - if(SettingsServer::isTrackerApiRequest()) { + if (!$setupDuringTracking + && SettingsServer::isTrackerApiRequest() + ) { // do not profile Tracker return; } - $path = PIWIK_INCLUDE_PATH . '/tests/lib/xhprof-0.9.4/xhprof_lib/utils/xhprof_runs.php'; - - if(!file_exists($path)) { + if (self::$isXhprofSetup) { return; } - if(!function_exists('xhprof_enable')) { - return; + if (!function_exists('xhprof_enable')) { + $xhProfPath = PIWIK_INCLUDE_PATH . '/vendor/facebook/xhprof/extension/modules/xhprof.so'; + throw new Exception("Cannot find xhprof_enable, make sure to 1) install xhprof: run 'composer install --dev' and build the extension, and 2) add 'extension=$xhProfPath' to your php.ini."); } - if(!is_writable(ini_get("xhprof.output_dir"))) { - throw new \Exception("The profiler output dir '" .ini_get("xhprof.output_dir"). "' should exist and be writable."); + $outputDir = ini_get("xhprof.output_dir"); + if (empty($outputDir)) { + throw new Exception("The profiler output dir is not set. Add 'xhprof.output_dir=...' to your php.ini."); + } + if (!is_writable($outputDir)) { + throw new Exception("The profiler output dir '" . ini_get("xhprof.output_dir") . "' should exist and be writable."); } - require_once $path; - require_once PIWIK_INCLUDE_PATH . '/tests/lib/xhprof-0.9.4/xhprof_lib/utils/xhprof_lib.php'; - if(!function_exists('xhprof_error')) { - function xhprof_error($out) { + if (!function_exists('xhprof_error')) { + function xhprof_error($out) + { echo substr($out, 0, 300) . '...'; } } $currentGitBranch = SettingsPiwik::getCurrentGitBranch(); $profilerNamespace = "piwik"; - if($currentGitBranch != 'master') { - $profilerNamespace .= "." . $currentGitBranch; + if ($currentGitBranch != 'master') { + $profilerNamespace .= "-" . $currentGitBranch; + } + + if ($mainRun) { + self::setProfilingRunIds(array()); } xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY); - if($mainRun) { - self::setProfilingRunIds(array()); - } - - register_shutdown_function(function () use($profilerNamespace, $mainRun) { + register_shutdown_function(function () use ($profilerNamespace, $mainRun) { $xhprofData = xhprof_disable(); - $xhprofRuns = new \XHProfRuns_Default(); + $xhprofRuns = new XHProfRuns_Default(); $runId = $xhprofRuns->save_run($xhprofData, $profilerNamespace); - if(empty($runId)) { + if (empty($runId)) { die('could not write profiler run'); } - $runs = self::getProfilingRunIds(); - $runs[] = $runId; -// $weights = array_fill(0, count($runs), 1); -// $aggregate = xhprof_aggregate_runs($xhprofRuns, $runs, $weights, $profilerNamespace); -// $runId = $xhprofRuns->save_run($aggregate, $profilerNamespace); - if($mainRun) { - $runIds = implode(',', $runs); + $runs = Profiler::getProfilingRunIds(); + array_unshift($runs, $runId); + + if ($mainRun) { + Profiler::aggregateXhprofRuns($runs, $profilerNamespace, $saveTo = $runId); + + $baseUrlStored = SettingsPiwik::getPiwikUrl(); + $out = "\n\n"; $baseUrl = "http://" . @$_SERVER['HTTP_HOST'] . "/" . @$_SERVER['REQUEST_URI']; - $baseUrlStored = SettingsPiwik::getPiwikUrl(); - if(strlen($baseUrlStored) > strlen($baseUrl)) { + if (strlen($baseUrlStored) > strlen($baseUrl)) { $baseUrl = $baseUrlStored; } - $baseUrl = "\n" . $baseUrl - ."tests/lib/xhprof-0.9.4/xhprof_html/?source=$profilerNamespace&run="; + $baseUrl = $baseUrlStored . "vendor/facebook/xhprof/xhprof_html/?source=$profilerNamespace&run=$runId"; - $out .= "Profiler report is available at:"; - $out .= $baseUrl . $runId; - if($runId != $runIds) { - $out .= "\n\nProfiler Report aggregating all runs triggered from this process: "; - $out .= $baseUrl . $runIds; - } + $out .= "Profiler report is available at:\n"; + $out .= "$baseUrl"; $out .= "\n\n"; - echo ($out); + + if (Development::isEnabled()) { + $out .= "WARNING: Development mode is enabled. Many runtime optimizations are not applied in development mode. "; + $out .= "Unless you intend to profile Piwik in development mode, your profile may not be accurate."; + $out .= "\n\n"; + } + + echo $out; } else { - self::setProfilingRunIds($runs); + Profiler::setProfilingRunIds($runs); } }); + + self::$isXhprofSetup = true; } - private static function setProfilingRunIds($ids) + /** + * Aggregates xhprof runs w/o normalizing (xhprof_aggregate_runs will always average data which + * does not fit Piwik's use case). + */ + public static function aggregateXhprofRuns($runIds, $profilerNamespace, $saveToRunId) { - file_put_contents( self::getPathToXHProfRunIds(), json_encode($ids) ); + $xhprofRuns = new XHProfRuns_Default(); + + $aggregatedData = array(); + + foreach ($runIds as $runId) { + $xhprofRunData = $xhprofRuns->get_run($runId, $profilerNamespace, $description); + + foreach ($xhprofRunData as $key => $data) { + if (empty($aggregatedData[$key])) { + $aggregatedData[$key] = $data; + } else { + // don't aggregate main() metrics since only the super run has the correct metrics for the entire run + if ($key == "main()") { + continue; + } + + $aggregatedData[$key]["ct"] += $data["ct"]; // call count + $aggregatedData[$key]["wt"] += $data["wt"]; // incl. wall time + $aggregatedData[$key]["cpu"] += $data["cpu"]; // cpu time + $aggregatedData[$key]["mu"] += $data["mu"]; // memory usage + $aggregatedData[$key]["pmu"] = max($aggregatedData[$key]["pmu"], $data["pmu"]); // peak mem usage + } + } + } + + $xhprofRuns->save_run($aggregatedData, $profilerNamespace, $saveToRunId); + } + + public static function setProfilingRunIds($ids) + { + file_put_contents(self::getPathToXHProfRunIds(), json_encode($ids)); @chmod(self::getPathToXHProfRunIds(), 0777); } - private static function getProfilingRunIds() + public static function getProfilingRunIds() { - $runIds = file_get_contents( self::getPathToXHProfRunIds() ); + $runIds = file_get_contents(self::getPathToXHProfRunIds()); $array = json_decode($runIds, $assoc = true); - if(!is_array($array)) { + if (!is_array($array)) { $array = array(); } return $array; diff --git a/www/analytics/core/ProxyHeaders.php b/www/analytics/core/ProxyHeaders.php index 4a6fa3fd..3cefeb23 100644 --- a/www/analytics/core/ProxyHeaders.php +++ b/www/analytics/core/ProxyHeaders.php @@ -1,6 +1,6 @@ getAssetDirectory() . '/' . basename($file); + // Return 304 if the file has not modified since + if ($modifiedSince === $lastModified) { + Common::sendResponseCode(304); + return; + } - $phpOutputCompressionEnabled = ProxyHttp::isPhpOutputCompressed(); - if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && !$phpOutputCompressionEnabled) { - $acceptEncoding = $_SERVER['HTTP_ACCEPT_ENCODING']; + // if we have to serve the file, serve it now, either in the clear or compressed + if ($byteStart === false) { + $byteStart = 0; + } - if (extension_loaded('zlib') && function_exists('file_get_contents') && function_exists('file_put_contents')) { - if (preg_match('/(?:^|, ?)(deflate)(?:,|$)/', $acceptEncoding, $matches)) { - $encoding = 'deflate'; - $filegz = $compressedFileLocation . '.deflate'; - } else if (preg_match('/(?:^|, ?)((x-)?gzip)(?:,|$)/', $acceptEncoding, $matches)) { - $encoding = $matches[1]; - $filegz = $compressedFileLocation . '.gz'; - } + if ($byteEnd === false) { + $byteEnd = filesize($file); + } - if (!empty($encoding)) { - // compress-on-demand and use cache - if (!file_exists($filegz) || ($fileModifiedTime > @filemtime($filegz))) { - $data = file_get_contents($file); + $compressed = false; + $encoding = ''; + $compressedFileLocation = AssetManager::getInstance()->getAssetDirectory() . '/' . basename($file); - if ($encoding == 'deflate') { - $data = gzdeflate($data, 9); - } else if ($encoding == 'gzip' || $encoding == 'x-gzip') { - $data = gzencode($data, 9); - } + if (!($byteStart == 0 + && $byteEnd == filesize($file)) + ) { + $compressedFileLocation .= ".$byteStart.$byteEnd"; + } - file_put_contents($filegz, $data); - } + $phpOutputCompressionEnabled = self::isPhpOutputCompressed(); + if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && !$phpOutputCompressionEnabled) { + list($encoding, $extension) = self::getCompressionEncodingAcceptedByClient(); + $filegz = $compressedFileLocation . $extension; - $compressed = true; - $file = $filegz; - } - } else { - // manually compressed - $filegz = $compressedFileLocation . '.gz'; - if (preg_match('/(?:^|, ?)((x-)?gzip)(?:,|$)/', $acceptEncoding, $matches) && file_exists($filegz) && ($fileModifiedTime < @filemtime($filegz))) { - $encoding = $matches[1]; - $compressed = true; - $file = $filegz; - } + if (self::canCompressInPhp()) { + if (!empty($encoding)) { + // compress the file if it doesn't exist or is newer than the existing cached file, and cache + // the compressed result + if (self::shouldCompressFile($file, $filegz)) { + self::compressFile($file, $filegz, $encoding, $byteStart, $byteEnd); } + + $compressed = true; + $file = $filegz; + + $byteStart = 0; + $byteEnd = filesize($file); } + } else { + // if a compressed file exists, the file was manually compressed so we just serve that + if ($extension == '.gz' + && !self::shouldCompressFile($file, $filegz) + ) { + $compressed = true; + $file = $filegz; - @header('Last-Modified: ' . $lastModified); - - if (!$phpOutputCompressionEnabled) { - @header('Content-Length: ' . filesize($file)); - } - - if (!empty($contentType)) { - @header('Content-Type: ' . $contentType); - } - - if ($compressed) { - @header('Content-Encoding: ' . $encoding); - } - - if (!_readfile($file)) { - self::setHttpStatus('505 Internal server error'); + $byteStart = 0; + $byteEnd = filesize($file); } } - } else { - self::setHttpStatus('404 Not Found'); + } + + Common::sendHeader('Last-Modified: ' . $lastModified); + + if (!$phpOutputCompressionEnabled) { + Common::sendHeader('Content-Length: ' . ($byteEnd - $byteStart)); + } + + if (!empty($contentType)) { + Common::sendHeader('Content-Type: ' . $contentType); + } + + if ($compressed) { + Common::sendHeader('Content-Encoding: ' . $encoding); + } + + if (!_readfile($file, $byteStart, $byteEnd)) { + Common::sendResponseCode(500); } } @@ -185,7 +193,6 @@ class ProxyHttp !empty($autoAppendFile); } - /** * Workaround IE bug when downloading certain document types over SSL and * cache control headers are present, e.g., @@ -202,31 +209,63 @@ class ProxyHttp public static function overrideCacheControlHeaders($override = null) { if ($override || self::isHttps()) { - @header('Pragma: '); - @header('Expires: '); + Common::sendHeader('Pragma: '); + Common::sendHeader('Expires: '); if (in_array($override, array('public', 'private', 'no-cache', 'no-store'))) { - @header("Cache-Control: $override, must-revalidate"); + Common::sendHeader("Cache-Control: $override, must-revalidate"); } else { - @header('Cache-Control: must-revalidate'); + Common::sendHeader('Cache-Control: must-revalidate'); } } } - /** - * Set response header, e.g., HTTP/1.0 200 Ok - * - * @param string $status Status - * @return bool + * Returns a formatted Expires HTTP header for a certain number of days in the future. The result + * can be used in a call to `header()`. */ - protected static function setHttpStatus($status) + private static function getExpiresHeaderForFutureDay($expireFarFutureDays) { - if (substr_compare(PHP_SAPI, '-fcgi', -5)) { - @header($_SERVER['SERVER_PROTOCOL'] . ' ' . $status); + return "Expires: " . gmdate('D, d M Y H:i:s', time() + 86400 * (int)$expireFarFutureDays) . ' GMT'; + } + + private static function getCompressionEncodingAcceptedByClient() + { + $acceptEncoding = $_SERVER['HTTP_ACCEPT_ENCODING']; + + if (preg_match(self::DEFLATE_ENCODING_REGEX, $acceptEncoding, $matches)) { + return array('deflate', '.deflate'); + } elseif (preg_match(self::GZIP_ENCODING_REGEX, $acceptEncoding, $matches)) { + return array('gzip', '.gz'); } else { - // FastCGI - @header('Status: ' . $status); + return array(false, false); } } + private static function canCompressInPhp() + { + return extension_loaded('zlib') && function_exists('file_get_contents') && function_exists('file_put_contents'); + } + + private static function shouldCompressFile($fileToCompress, $compressedFilePath) + { + $toCompressLastModified = @filemtime($fileToCompress); + $compressedLastModified = @filemtime($compressedFilePath); + + return !file_exists($compressedFilePath) || ($toCompressLastModified > $compressedLastModified); + } + + private static function compressFile($fileToCompress, $compressedFilePath, $compressionEncoding, $byteStart, + $byteEnd) + { + $data = file_get_contents($fileToCompress); + $data = substr($data, $byteStart, $byteEnd - $byteStart); + + if ($compressionEncoding == 'deflate') { + $data = gzdeflate($data, 9); + } elseif ($compressionEncoding == 'gzip' || $compressionEncoding == 'x-gzip') { + $data = gzencode($data, 9); + } + + file_put_contents($compressedFilePath, $data); + } } diff --git a/www/analytics/core/QuickForm2.php b/www/analytics/core/QuickForm2.php index dc2ba9ab..49b21672 100644 --- a/www/analytics/core/QuickForm2.php +++ b/www/analytics/core/QuickForm2.php @@ -1,6 +1,6 @@ _elements as $key => $value) { if ($value->_attributes['name'] == $nameElement) { @@ -86,7 +86,7 @@ abstract class QuickForm2 extends HTML_QuickForm2 } } - function setSelected($nameElement, $value) + public function setSelected($nameElement, $value) { foreach ($this->_elements as $key => $value) { if ($value->_attributes['name'] == $nameElement) { @@ -101,7 +101,7 @@ abstract class QuickForm2 extends HTML_QuickForm2 * @param string $elementName * @return mixed */ - function getSubmitValue($elementName) + public function getSubmitValue($elementName) { $value = $this->getValue(); return isset($value[$elementName]) ? $value[$elementName] : null; @@ -118,7 +118,7 @@ abstract class QuickForm2 extends HTML_QuickForm2 return array_filter($messages); } - static protected $registered = false; + protected static $registered = false; /** * Returns the rendered form as an array. diff --git a/www/analytics/core/RankingQuery.php b/www/analytics/core/RankingQuery.php index de05ab39..cd4f8306 100644 --- a/www/analytics/core/RankingQuery.php +++ b/www/analytics/core/RankingQuery.php @@ -1,6 +1,6 @@ generateQuery($innerQuery); - $data = Db::fetchAll($query, $bind); + $query = $this->generateRankingQuery($innerQuery); + $data = Db::fetchAll($query, $bind); if ($this->columnToMarkExcludedRows !== false) { // split the result into the regular result and the rows with special treatment @@ -268,7 +268,7 @@ class RankingQuery * itself. * @return string The entire ranking query SQL. */ - public function generateQuery($innerQuery) + public function generateRankingQuery($innerQuery) { // +1 to include "Others" $limit = $this->limit + 1; diff --git a/www/analytics/core/Registry.php b/www/analytics/core/Registry.php index 166a48f7..bc80f225 100644 --- a/www/analytics/core/Registry.php +++ b/www/analytics/core/Registry.php @@ -1,27 +1,24 @@ data = array(); - } - public static function isRegistered($key) { return self::getInstance()->hasKey($key); @@ -39,19 +36,28 @@ class Registry extends Singleton public function setKey($key, $value) { - $this->data[$key] = $value; + if ($key === 'auth') { + $key = 'Piwik\Auth'; + } + + StaticContainer::getContainer()->set($key, $value); } public function getKey($key) { - if (!$this->hasKey($key)) { - throw new \Exception(sprintf("Key '%s' doesn't exist in Registry", $key)); + if ($key === 'auth') { + $key = 'Piwik\Auth'; } - return $this->data[$key]; + + return StaticContainer::get($key); } public function hasKey($key) { - return array_key_exists($key, $this->data); + if ($key === 'auth') { + $key = 'Piwik\Auth'; + } + + return StaticContainer::getContainer()->has($key); } } diff --git a/www/analytics/core/ReportRenderer.php b/www/analytics/core/ReportRenderer.php index 4634f919..3fa20aff 100644 --- a/www/analytics/core/ReportRenderer.php +++ b/www/analytics/core/ReportRenderer.php @@ -1,6 +1,6 @@ render($processedReport); - if(empty($reportData)) { + if (empty($reportData)) { $reportData = Piwik::translate('CoreHome_ThereIsNoDataForThisReport'); } @@ -148,4 +158,17 @@ class Csv extends ReportRenderer { return str_replace("_", ".", $uniqueId); } + + /** + * Get report attachments, ex. graph images + * + * @param $report + * @param $processedReports + * @param $prettyDate + * @return array + */ + public function getAttachments($report, $processedReports, $prettyDate) + { + return array(); + } } diff --git a/www/analytics/core/ReportRenderer/Html.php b/www/analytics/core/ReportRenderer/Html.php index 53fbe90a..9915f255 100644 --- a/www/analytics/core/ReportRenderer/Html.php +++ b/www/analytics/core/ReportRenderer/Html.php @@ -1,6 +1,6 @@ assign("reportFontFamily", ReportRenderer::DEFAULT_REPORT_FONT_FAMILY); $view->assign("reportTitleTextColor", ReportRenderer::REPORT_TITLE_TEXT_COLOR); $view->assign("reportTitleTextSize", self::REPORT_TITLE_TEXT_SIZE); $view->assign("reportTextColor", ReportRenderer::REPORT_TEXT_COLOR); @@ -113,7 +115,9 @@ class Html extends ReportRenderer $view->assign("tableHeaderTextColor", ReportRenderer::TABLE_HEADER_TEXT_COLOR); $view->assign("tableCellBorderColor", ReportRenderer::TABLE_CELL_BORDER_COLOR); $view->assign("tableBgColor", ReportRenderer::TABLE_BG_COLOR); + $view->assign("reportTableHeaderTextWeight", self::TABLE_HEADER_TEXT_WEIGHT); $view->assign("reportTableHeaderTextSize", self::REPORT_TABLE_HEADER_TEXT_SIZE); + $view->assign("reportTableHeaderTextTransform", ReportRenderer::TABLE_HEADER_TEXT_TRANSFORM); $view->assign("reportTableRowTextSize", self::REPORT_TABLE_ROW_TEXT_SIZE); $view->assign("reportBackToTopTextSize", self::REPORT_BACK_TO_TOP_TEXT_SIZE); $view->assign("currentPath", SettingsPiwik::getPiwikUrl()); @@ -161,4 +165,56 @@ class Html extends ReportRenderer $this->rendering .= $reportView->render(); } + + public function getAttachments($report, $processedReports, $prettyDate) + { + $additionalFiles = array(); + + foreach ($processedReports as $processedReport) { + if ($processedReport['displayGraph']) { + $additionalFiles[] = $this->getAttachment($report, $processedReport, $prettyDate); + } + } + + return $additionalFiles; + } + + protected function getAttachment($report, $processedReport, $prettyDate) + { + $additionalFile = array(); + + $segment = \Piwik\Plugins\ScheduledReports\API::getSegment($report['idsegment']); + + $segmentName = $segment != null ? sprintf(' (%s)', $segment['name']) : ''; + + $processedReportMetadata = $processedReport['metadata']; + + $additionalFile['filename'] = + sprintf( + '%s - %s - %d - %s %d%s.png', + $processedReportMetadata['name'], + $prettyDate, + $report['idsite'], + Piwik::translate('General_Report'), + $report['idreport'], + $segmentName + ); + + $additionalFile['cid'] = $processedReportMetadata['uniqueId']; + + $additionalFile['content'] = + ReportRenderer::getStaticGraph( + $processedReportMetadata, + Html::IMAGE_GRAPH_WIDTH, + Html::IMAGE_GRAPH_HEIGHT, + $processedReport['evolutionGraph'], + $segment + ); + + $additionalFile['mimeType'] = 'image/png'; + + $additionalFile['encoding'] = \Zend_Mime::ENCODING_BASE64; + + return $additionalFile; + } } diff --git a/www/analytics/core/ReportRenderer/Pdf.php b/www/analytics/core/ReportRenderer/Pdf.php index 66f47df2..59fb8626 100644 --- a/www/analytics/core/ReportRenderer/Pdf.php +++ b/www/analytics/core/ReportRenderer/Pdf.php @@ -1,6 +1,6 @@ reportFont = $reportFont; } public function sendToDisk($filename) { - $filename = ReportRenderer::appendExtension($filename, self::PDF_CONTENT_TYPE); + $filename = ReportRenderer::makeFilenameWithExtension($filename, self::PDF_CONTENT_TYPE); $outputFilename = ReportRenderer::getOutputPath($filename); $this->TCPDF->Output($outputFilename, 'F'); @@ -133,13 +151,13 @@ class Pdf extends ReportRenderer public function sendToBrowserDownload($filename) { - $filename = ReportRenderer::appendExtension($filename, self::PDF_CONTENT_TYPE); + $filename = ReportRenderer::makeFilenameWithExtension($filename, self::PDF_CONTENT_TYPE); $this->TCPDF->Output($filename, 'D'); } public function sendToBrowserInline($filename) { - $filename = ReportRenderer::appendExtension($filename, self::PDF_CONTENT_TYPE); + $filename = ReportRenderer::makeFilenameWithExtension($filename, self::PDF_CONTENT_TYPE); $this->TCPDF->Output($filename, 'I'); } @@ -185,7 +203,6 @@ class Pdf extends ReportRenderer // segment if ($segment != null) { - $this->TCPDF->Ln(); $this->TCPDF->Ln(); $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize - 2); @@ -328,7 +345,7 @@ class Pdf extends ReportRenderer $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]); $this->TCPDF->SetFont(''); - $fill = false; + $fill = true; $url = false; $leftSpacesBeforeLogo = str_repeat(' ', $this->leftSpacesBeforeLogo); @@ -389,7 +406,7 @@ class Pdf extends ReportRenderer if (empty($rowMetrics[$columnId])) { $rowMetrics[$columnId] = 0; } - $this->TCPDF->Cell($this->cellWidth, $this->cellHeight, $rowMetrics[$columnId], 'LR', 0, 'L', $fill); + $this->TCPDF->Cell($this->cellWidth, $this->cellHeight, NumberFormatter::getInstance()->format($rowMetrics[$columnId]), 'LR', 0, 'L', $fill); } } @@ -461,7 +478,7 @@ class Pdf extends ReportRenderer && $columnsCount <= 3 ) { $totalWidth = $this->reportWidthPortrait * 2 / 3; - } else if ($this->orientation == self::LANDSCAPE) { + } elseif ($this->orientation == self::LANDSCAPE) { $totalWidth = $this->reportWidthLandscape; } else { $totalWidth = $this->reportWidthPortrait; @@ -494,12 +511,13 @@ class Pdf extends ReportRenderer $posX = $initPosX; foreach ($this->reportColumns as $columnName) { $columnName = $this->formatText($columnName); + //Label column if ($countColumns == 0) { - $this->TCPDF->MultiCell($this->labelCellWidth, $maxCellHeight, $columnName, 1, 'C', true); + $this->TCPDF->MultiCell($this->labelCellWidth, $maxCellHeight, $columnName, $border = 0, $align = 'L', true); $this->TCPDF->SetXY($posX + $this->labelCellWidth, $posY); } else { - $this->TCPDF->MultiCell($this->cellWidth, $maxCellHeight, $columnName, 1, 'C', true); + $this->TCPDF->MultiCell($this->cellWidth, $maxCellHeight, $columnName, $border = 0, $align = 'L', true); $this->TCPDF->SetXY($posX + $this->cellWidth, $posY); } $countColumns++; @@ -523,4 +541,17 @@ class Pdf extends ReportRenderer $this->TCPDF->Write("1em", $message); $this->TCPDF->Ln(); } + + /** + * Get report attachments, ex. graph images + * + * @param $report + * @param $processedReports + * @param $prettyDate + * @return array + */ + public function getAttachments($report, $processedReports, $prettyDate) + { + return array(); + } } diff --git a/www/analytics/core/ScheduledTask.php b/www/analytics/core/ScheduledTask.php index f126cecb..dd4063ef 100644 --- a/www/analytics/core/ScheduledTask.php +++ b/www/analytics/core/ScheduledTask.php @@ -1,6 +1,6 @@ className = $this->getClassNameFromInstance($objectInstance); - - if ($priority < self::HIGHEST_PRIORITY || $priority > self::LOWEST_PRIORITY) { - throw new Exception("Invalid priority for ScheduledTask '$this->className.$methodName': $priority"); - } - - $this->objectInstance = $objectInstance; - $this->methodName = $methodName; - $this->scheduledTime = $scheduledTime; - $this->methodParameter = $methodParameter; - $this->priority = $priority; - } - - protected function getClassNameFromInstance($_objectInstance) - { - if (is_string($_objectInstance)) { - return $_objectInstance; - } - - $namespaced = get_class($_objectInstance); - $class = explode('\\', $namespaced); - return end($class); - } - - /** - * Returns the object instance that contains the method to execute. Returns a class - * name if the method is static. - * - * @return mixed - */ - public function getObjectInstance() - { - return $this->objectInstance; - } - - /** - * Returns the name of the class that contains the method to execute. - * - * @return string - */ - public function getClassName() - { - return $this->className; - } - - /** - * Returns the name of the method that will be executed. - * - * @return string - */ - public function getMethodName() - { - return $this->methodName; - } - - /** - * Returns the value that will be passed to the method when executed, or `null` if - * no value will be supplied. - * - * @return string|null - */ - public function getMethodParameter() - { - return $this->methodParameter; - } - - /** - * Returns a {@link ScheduledTime} instance that describes when the method should be executed - * and how long before the next execution. - * - * @return ScheduledTime - */ - public function getScheduledTime() - { - return $this->scheduledTime; - } - - /** - * Returns the time in milliseconds when this task will be executed next. - * - * @return int - */ - public function getRescheduledTime() - { - return $this->getScheduledTime()->getRescheduledTime(); - } - - /** - * Returns the task priority. The priority will be an integer whose value is - * between {@link HIGH_PRIORITY} and {@link LOW_PRIORITY}. - * - * @return int - */ - public function getPriority() - { - return $this->priority; - } - - /** - * Returns a unique name for this scheduled task. The name is stored in the DB and is used - * to store a task's previous execution time. The name is created using: - * - * - the name of the class that contains the method to execute, - * - the name of the method to regularly execute, - * - and the value that is passed to the executed task. - * - * @return string - */ - public function getName() - { - return self::getTaskName($this->getClassName(), $this->getMethodName(), $this->getMethodParameter()); - } - - /** - * @ignore - */ - public static function getTaskName($className, $methodName, $methodParameter = null) - { - return $className . '.' . $methodName . ($methodParameter == null ? '' : '_' . $methodParameter); - } } diff --git a/www/analytics/core/ScheduledTaskTimetable.php b/www/analytics/core/ScheduledTaskTimetable.php deleted file mode 100644 index 13e48b8c..00000000 --- a/www/analytics/core/ScheduledTaskTimetable.php +++ /dev/null @@ -1,121 +0,0 @@ -timetable = $unserializedTimetable === false ? array() : $unserializedTimetable; - } - - public function getTimetable() - { - return $this->timetable; - } - - public function setTimetable($timetable) - { - $this->timetable = $timetable; - } - - public function removeInactiveTasks($activeTasks) - { - $activeTaskNames = array(); - foreach ($activeTasks as $task) { - $activeTaskNames[] = $task->getName(); - } - foreach (array_keys($this->timetable) as $taskName) { - if (!in_array($taskName, $activeTaskNames)) { - unset($this->timetable[$taskName]); - } - } - } - - public function getScheduledTaskNames() - { - return array_keys($this->timetable); - } - - public function getScheduledTaskTime($taskName) - { - return isset($this->timetable[$taskName]) ? Date::factory($this->timetable[$taskName]) : false; - } - - /** - * Checks if the task should be executed - * - * Task has to be executed if : - * - the task has already been scheduled once and the current system time is greater than the scheduled time. - * - execution is forced, see $forceTaskExecution - * - * @param string $taskName - * - * @return boolean - */ - public function shouldExecuteTask($taskName) - { - $forceTaskExecution = - (isset($GLOBALS['PIWIK_TRACKER_DEBUG_FORCE_SCHEDULED_TASKS']) && $GLOBALS['PIWIK_TRACKER_DEBUG_FORCE_SCHEDULED_TASKS']) - || DEBUG_FORCE_SCHEDULED_TASKS; - - return $forceTaskExecution || ($this->taskHasBeenScheduledOnce($taskName) && time() >= $this->timetable[$taskName]); - } - - /** - * Checks if a task should be rescheduled - * - * Task has to be rescheduled if : - * - the task has to be executed - * - the task has never been scheduled before - * - * @param string $taskName - * - * @return boolean - */ - public function taskShouldBeRescheduled($taskName) - { - return !$this->taskHasBeenScheduledOnce($taskName) || $this->shouldExecuteTask($taskName); - } - - public function rescheduleTask($task) - { - // update the scheduled time - $this->timetable[$task->getName()] = $task->getRescheduledTime(); - $this->save(); - } - - public function save() - { - Option::set(self::TIMETABLE_OPTION_STRING, serialize($this->timetable)); - } - - public function getScheduledTimeForMethod($className, $methodName, $methodParameter = null) - { - $taskName = ScheduledTask::getTaskName($className, $methodName, $methodParameter); - - return $this->taskHasBeenScheduledOnce($taskName) ? $this->timetable[$taskName] : false; - } - - public function taskHasBeenScheduledOnce($taskName) - { - return isset($this->timetable[$taskName]); - } -} diff --git a/www/analytics/core/ScheduledTime.php b/www/analytics/core/ScheduledTime.php deleted file mode 100644 index 53e1e867..00000000 --- a/www/analytics/core/ScheduledTime.php +++ /dev/null @@ -1,225 +0,0 @@ -= 0` and `< 24`. - * @throws Exception If the current scheduled period is **hourly** or if `$hour` is invalid. - * @api - */ - public function setHour($hour) - { - if (!($hour >= 0 && $hour < 24)) { - throw new Exception ("Invalid hour parameter, must be >=0 and < 24"); - } - - $this->hour = $hour; - } - - /** - * By setting a timezone you make sure the scheduled task will be run at the requested time in the - * given timezone. This is useful for instance in case you want to make sure a task runs at midnight in a website's - * timezone. - * - * @param string $timezone - */ - public function setTimezone($timezone) - { - $this->timezone = $timezone; - } - - protected function adjustTimezone($rescheduledTime) - { - if (is_null($this->timezone)) { - return $rescheduledTime; - } - - $arbitraryDateInUTC = Date::factory('2011-01-01'); - $dateInTimezone = Date::factory($arbitraryDateInUTC, $this->timezone); - - $midnightInTimezone = date('H', $dateInTimezone->getTimestamp()); - - if ($arbitraryDateInUTC->isEarlier($dateInTimezone)) { - $hoursDifference = 0 - $midnightInTimezone; - } else { - $hoursDifference = 24 - $midnightInTimezone; - } - - $hoursDifference = $hoursDifference % 24; - - $rescheduledTime += (3600 * $hoursDifference); - - if ($this->getTime() > $rescheduledTime) { - // make sure the rescheduled date is in the future - $rescheduledTime = (24 * 3600) + $rescheduledTime; - } - - return $rescheduledTime; - } - - /** - * Computes the delta in seconds needed to adjust the rescheduled time to the required hour. - * - * @param int $rescheduledTime The rescheduled time to be adjusted - * @return int adjusted rescheduled time - */ - protected function adjustHour($rescheduledTime) - { - if ($this->hour !== null) { - // Reset the number of minutes and set the scheduled hour to the one specified with setHour() - $rescheduledTime = mktime($this->hour, - 0, - date('s', $rescheduledTime), - date('n', $rescheduledTime), - date('j', $rescheduledTime), - date('Y', $rescheduledTime) - ); - } - return $rescheduledTime; - } - - /** - * Returns a new ScheduledTime instance using a string description of the scheduled period type - * and a string description of the day within the period to execute the task on. - * - * @param string $periodType The scheduled period type. Can be `'hourly'`, `'daily'`, `'weekly'`, or `'monthly'`. - * @param string|int|false $periodDay A string describing the day within the scheduled period to execute - * the task on. Only valid for week and month periods. - * - * If `'weekly'` is supplied for `$periodType`, this should be a day - * of the week, for example, `'monday'` or `'tuesday'`. - * - * If `'monthly'` is supplied for `$periodType`, this can be a numeric - * day in the month or a day in one week of the month. For example, - * `12`, `23`, `'first sunday'` or `'fourth tuesday'`. - * @api - */ - public static function factory($periodType, $periodDay = false) - { - switch ($periodType) { - case 'hourly': - return new Hourly(); - case 'daily': - return new Daily(); - case 'weekly': - $result = new Weekly(); - if($periodDay !== false) { - $result->setDay($periodDay); - } - return $result; - case 'monthly': - $result = new Monthly($periodDay); - if($periodDay !== false) { - if (is_int($periodDay)) { - $result->setDay($periodDay); - } else { - $result->setDayOfWeekFromString($periodDay); - } - } - return $result; - default: - throw new Exception("Unsupported scheduled period type: '$periodType'. Supported values are" - . " 'hourly', 'daily', 'weekly' or 'monthly'."); - } - } -} diff --git a/www/analytics/core/ScheduledTime/Daily.php b/www/analytics/core/Scheduler/Schedule/Daily.php similarity index 83% rename from www/analytics/core/ScheduledTime/Daily.php rename to www/analytics/core/Scheduler/Schedule/Daily.php index 8566ef9b..e459fbfe 100644 --- a/www/analytics/core/ScheduledTime/Daily.php +++ b/www/analytics/core/Scheduler/Schedule/Daily.php @@ -1,22 +1,22 @@ week !== null ) { $newTime = $rescheduledTime + $this->week * 7 * 86400; - while (date("w", $newTime) != $this->dayOfWeek % 7) // modulus for sanity check - { + while (date("w", $newTime) != $this->dayOfWeek % 7) { + // modulus for sanity check + $newTime += 86400; } $scheduledDay = ($newTime - $rescheduledTime) / 86400 + 1; @@ -116,7 +115,7 @@ class Monthly extends ScheduledTime public function setDay($_day) { if (!($_day >= 1 && $_day < 32)) { - throw new Exception ("Invalid day parameter, must be >=1 and < 32"); + throw new Exception("Invalid day parameter, must be >=1 and < 32"); } $this->day = $_day; diff --git a/www/analytics/core/Scheduler/Schedule/Schedule.php b/www/analytics/core/Scheduler/Schedule/Schedule.php new file mode 100644 index 00000000..a150850c --- /dev/null +++ b/www/analytics/core/Scheduler/Schedule/Schedule.php @@ -0,0 +1,224 @@ += 0` and `< 24`. + * @throws Exception If the current scheduled period is **hourly** or if `$hour` is invalid. + * @api + */ + public function setHour($hour) + { + if (!($hour >= 0 && $hour < 24)) { + throw new Exception("Invalid hour parameter, must be >=0 and < 24"); + } + + $this->hour = $hour; + } + + /** + * By setting a timezone you make sure the scheduled task will be run at the requested time in the + * given timezone. This is useful for instance in case you want to make sure a task runs at midnight in a website's + * timezone. + * + * @param string $timezone + */ + public function setTimezone($timezone) + { + $this->timezone = $timezone; + } + + protected function adjustTimezone($rescheduledTime) + { + if (is_null($this->timezone)) { + return $rescheduledTime; + } + + $arbitraryDateInUTC = Date::factory('2011-01-01'); + $dateInTimezone = Date::factory($arbitraryDateInUTC, $this->timezone); + + $midnightInTimezone = date('H', $dateInTimezone->getTimestamp()); + + if ($arbitraryDateInUTC->isEarlier($dateInTimezone)) { + $hoursDifference = 0 - $midnightInTimezone; + } else { + $hoursDifference = 24 - $midnightInTimezone; + } + + $hoursDifference = $hoursDifference % 24; + + $rescheduledTime += (3600 * $hoursDifference); + + if ($this->getTime() > $rescheduledTime) { + // make sure the rescheduled date is in the future + $rescheduledTime = (24 * 3600) + $rescheduledTime; + } + + return $rescheduledTime; + } + + /** + * Computes the delta in seconds needed to adjust the rescheduled time to the required hour. + * + * @param int $rescheduledTime The rescheduled time to be adjusted + * @return int adjusted rescheduled time + */ + protected function adjustHour($rescheduledTime) + { + if ($this->hour !== null) { + // Reset the number of minutes and set the scheduled hour to the one specified with setHour() + $rescheduledTime = mktime($this->hour, + 0, + date('s', $rescheduledTime), + date('n', $rescheduledTime), + date('j', $rescheduledTime), + date('Y', $rescheduledTime) + ); + } + return $rescheduledTime; + } + + /** + * Returns a new Schedule instance using a string description of the scheduled period type + * and a string description of the day within the period to execute the task on. + * + * @param string $periodType The scheduled period type. Can be `'hourly'`, `'daily'`, `'weekly'`, or `'monthly'`. + * @param bool|false|int|string $periodDay A string describing the day within the scheduled period to execute + * the task on. Only valid for week and month periods. + * + * If `'weekly'` is supplied for `$periodType`, this should be a day + * of the week, for example, `'monday'` or `'tuesday'`. + * + * If `'monthly'` is supplied for `$periodType`, this can be a numeric + * day in the month or a day in one week of the month. For example, + * `12`, `23`, `'first sunday'` or `'fourth tuesday'`. + * @return Hourly|Daily|Weekly|Monthly + * @throws Exception + * @api + */ + public static function factory($periodType, $periodDay = false) + { + switch ($periodType) { + case 'hourly': + return new Hourly(); + case 'daily': + return new Daily(); + case 'weekly': + $result = new Weekly(); + if ($periodDay !== false) { + $result->setDay($periodDay); + } + return $result; + case 'monthly': + $result = new Monthly($periodDay); + if ($periodDay !== false) { + if (is_int($periodDay)) { + $result->setDay($periodDay); + } else { + $result->setDayOfWeekFromString($periodDay); + } + } + return $result; + default: + throw new Exception("Unsupported scheduled period type: '$periodType'. Supported values are" + . " 'hourly', 'daily', 'weekly' or 'monthly'."); + } + } +} diff --git a/www/analytics/core/ScheduledTime/Weekly.php b/www/analytics/core/Scheduler/Schedule/Weekly.php similarity index 88% rename from www/analytics/core/ScheduledTime/Weekly.php rename to www/analytics/core/Scheduler/Schedule/Weekly.php index 7a9769ba..09fb5ec7 100644 --- a/www/analytics/core/ScheduledTime/Weekly.php +++ b/www/analytics/core/Scheduler/Schedule/Weekly.php @@ -1,23 +1,22 @@ adjustHour($rescheduledTime); $rescheduledTime = $this->adjustTimezone($rescheduledTime); @@ -65,7 +64,7 @@ class Weekly extends ScheduledTime } if (!($day >= 1 && $day < 8)) { - throw new Exception ("Invalid day parameter, must be >=1 and < 8"); + throw new Exception("Invalid day parameter, must be >=1 and < 8"); } $this->day = $day; diff --git a/www/analytics/core/Scheduler/Scheduler.php b/www/analytics/core/Scheduler/Scheduler.php new file mode 100644 index 00000000..86b8f446 --- /dev/null +++ b/www/analytics/core/Scheduler/Scheduler.php @@ -0,0 +1,241 @@ +hourly('myTask'); // myTask() will be executed once every hour + * } + * public function myTask() + * { + * // do something + * } + * } + * + * **Executing all pending tasks** + * + * $results = $scheduler->run(); + * $task1Result = $results[0]; + * $task1Name = $task1Result['task']; + * $task1Output = $task1Result['output']; + * + * echo "Executed task '$task1Name'. Task output:\n$task1Output"; + */ +class Scheduler +{ + /** + * Is the scheduler running any task. + * @var bool + */ + private $isRunningTask = false; + + /** + * @var Timetable + */ + private $timetable; + + /** + * @var TaskLoader + */ + private $loader; + + /** + * @var LoggerInterface + */ + private $logger; + + public function __construct(TaskLoader $loader, LoggerInterface $logger) + { + $this->timetable = new Timetable(); + $this->loader = $loader; + $this->logger = $logger; + } + + /** + * Executes tasks that are scheduled to run, then reschedules them. + * + * @return array An array describing the results of scheduled task execution. Each element + * in the array will have the following format: + * + * ``` + * array( + * 'task' => 'task name', + * 'output' => '... task output ...' + * ) + * ``` + */ + public function run() + { + $tasks = $this->loader->loadTasks(); + + $this->logger->debug('{count} scheduled tasks loaded', array('count' => count($tasks))); + + // remove from timetable tasks that are not active anymore + $this->timetable->removeInactiveTasks($tasks); + + $this->logger->info("Starting Scheduled tasks... "); + + // for every priority level, starting with the highest and concluding with the lowest + $executionResults = array(); + for ($priority = Task::HIGHEST_PRIORITY; $priority <= Task::LOWEST_PRIORITY; ++$priority) { + $this->logger->debug("Executing tasks with priority {priority}:", array('priority' => $priority)); + + // loop through each task + foreach ($tasks as $task) { + // if the task does not have the current priority level, don't execute it yet + if ($task->getPriority() != $priority) { + continue; + } + + $taskName = $task->getName(); + $shouldExecuteTask = $this->timetable->shouldExecuteTask($taskName); + + if ($this->timetable->taskShouldBeRescheduled($taskName)) { + $rescheduledDate = $this->timetable->rescheduleTask($task); + + $this->logger->debug("Task {task} is scheduled to run again for {date}.", array('task' => $taskName, 'date' => $rescheduledDate)); + } + + if ($shouldExecuteTask) { + $message = $this->executeTask($task); + + $executionResults[] = array('task' => $taskName, 'output' => $message); + } + } + } + + $this->logger->info("done"); + + return $executionResults; + } + + /** + * Run a specific task now. Will ignore the schedule completely. + * + * @param string $taskName + * @return string Task output. + */ + public function runTaskNow($taskName) + { + $tasks = $this->loader->loadTasks(); + + foreach ($tasks as $task) { + if ($task->getName() === $taskName) { + return $this->executeTask($task); + } + } + + throw new \InvalidArgumentException('Task ' . $taskName . ' not found'); + } + + /** + * Determines a task's scheduled time and persists it, overwriting the previous scheduled time. + * + * Call this method if your task's scheduled time has changed due to, for example, an option that + * was changed. + * + * @param Task $task Describes the scheduled task being rescheduled. + * @api + */ + public function rescheduleTask(Task $task) + { + $this->logger->debug('Rescheduling task {task}', array('task' => $task->getName())); + + $this->timetable->rescheduleTask($task); + } + + /** + * Returns true if the scheduler is currently running a task. + * + * @return bool + */ + public function isRunningTask() + { + return $this->isRunningTask; + } + + /** + * Return the next scheduled time given the class and method names of a scheduled task. + * + * @param string $className The name of the class that contains the scheduled task method. + * @param string $methodName The name of the scheduled task method. + * @param string|null $methodParameter Optional method parameter. + * @return mixed int|bool The time in miliseconds when the scheduled task will be executed + * next or false if it is not scheduled to run. + */ + public function getScheduledTimeForMethod($className, $methodName, $methodParameter = null) + { + return $this->timetable->getScheduledTimeForMethod($className, $methodName, $methodParameter); + } + + /** + * Returns the list of the task names. + * + * @return string[] + */ + public function getTaskList() + { + $tasks = $this->loader->loadTasks(); + + return array_map(function (Task $task) { + return $task->getName(); + }, $tasks); + } + + /** + * Executes the given task + * + * @param Task $task + * @return string + */ + private function executeTask($task) + { + $this->logger->info("Scheduler: executing task {taskName}...", array( + 'taskName' => $task->getName(), + )); + + $this->isRunningTask = true; + + $timer = new Timer(); + + try { + $callable = array($task->getObjectInstance(), $task->getMethodName()); + call_user_func($callable, $task->getMethodParameter()); + $message = $timer->__toString(); + } catch (Exception $e) { + $message = 'ERROR: ' . $e->getMessage(); + } + + $this->isRunningTask = false; + + $this->logger->info("Scheduler: finished. {timeElapsed}", array( + 'timeElapsed' => $timer, + )); + + return $message; + } +} diff --git a/www/analytics/core/Scheduler/Task.php b/www/analytics/core/Scheduler/Task.php new file mode 100644 index 00000000..f3c4381a --- /dev/null +++ b/www/analytics/core/Scheduler/Task.php @@ -0,0 +1,200 @@ +className = $this->getClassNameFromInstance($objectInstance); + + if ($priority < self::HIGHEST_PRIORITY || $priority > self::LOWEST_PRIORITY) { + throw new Exception("Invalid priority for ScheduledTask '$this->className.$methodName': $priority"); + } + + $this->objectInstance = $objectInstance; + $this->methodName = $methodName; + $this->scheduledTime = $scheduledTime; + $this->methodParameter = $methodParameter; + $this->priority = $priority; + } + + protected function getClassNameFromInstance($_objectInstance) + { + if (is_string($_objectInstance)) { + return $_objectInstance; + } + + $namespaced = get_class($_objectInstance); + + return $namespaced; + } + + /** + * Returns the object instance that contains the method to execute. Returns a class + * name if the method is static. + * + * @return mixed + */ + public function getObjectInstance() + { + return $this->objectInstance; + } + + /** + * Returns the name of the class that contains the method to execute. + * + * @return string + */ + public function getClassName() + { + return $this->className; + } + + /** + * Returns the name of the method that will be executed. + * + * @return string + */ + public function getMethodName() + { + return $this->methodName; + } + + /** + * Returns the value that will be passed to the method when executed, or `null` if + * no value will be supplied. + * + * @return string|null + */ + public function getMethodParameter() + { + return $this->methodParameter; + } + + /** + * Returns a {@link Schedule} instance that describes when the method should be executed + * and how long before the next execution. + * + * @return \Piwik\Scheduler\Schedule\Schedule + */ + public function getScheduledTime() + { + return $this->scheduledTime; + } + + /** + * Returns the time in milliseconds when this task will be executed next. + * + * @return int + */ + public function getRescheduledTime() + { + return $this->getScheduledTime()->getRescheduledTime(); + } + + /** + * Returns the task priority. The priority will be an integer whose value is + * between {@link HIGH_PRIORITY} and {@link LOW_PRIORITY}. + * + * @return int + */ + public function getPriority() + { + return $this->priority; + } + + /** + * Returns a unique name for this scheduled task. The name is stored in the DB and is used + * to store a task's previous execution time. The name is created using: + * + * - the name of the class that contains the method to execute, + * - the name of the method to regularly execute, + * - and the value that is passed to the executed task. + * + * @return string + */ + public function getName() + { + return self::getTaskName($this->getClassName(), $this->getMethodName(), $this->getMethodParameter()); + } + + /** + * @ignore + */ + public static function getTaskName($className, $methodName, $methodParameter = null) + { + return $className . '.' . $methodName . ($methodParameter == null ? '' : '_' . $methodParameter); + } +} diff --git a/www/analytics/core/Scheduler/TaskLoader.php b/www/analytics/core/Scheduler/TaskLoader.php new file mode 100644 index 00000000..60b9e328 --- /dev/null +++ b/www/analytics/core/Scheduler/TaskLoader.php @@ -0,0 +1,39 @@ +findComponents('Tasks', 'Piwik\Plugin\Tasks'); + + foreach ($pluginTasks as $pluginTask) { + $pluginTask->schedule(); + + foreach ($pluginTask->getScheduledTasks() as $task) { + $tasks[] = $task; + } + } + + return $tasks; + } +} diff --git a/www/analytics/core/Scheduler/Timetable.php b/www/analytics/core/Scheduler/Timetable.php new file mode 100644 index 00000000..c3c01ab1 --- /dev/null +++ b/www/analytics/core/Scheduler/Timetable.php @@ -0,0 +1,133 @@ +timetable = $unserializedTimetable === false ? array() : $unserializedTimetable; + } + + public function getTimetable() + { + return $this->timetable; + } + + public function setTimetable($timetable) + { + $this->timetable = $timetable; + } + + /** + * @param Task[] $activeTasks + */ + public function removeInactiveTasks($activeTasks) + { + $activeTaskNames = array(); + foreach ($activeTasks as $task) { + $activeTaskNames[] = $task->getName(); + } + foreach (array_keys($this->timetable) as $taskName) { + if (!in_array($taskName, $activeTaskNames)) { + unset($this->timetable[$taskName]); + } + } + } + + public function getScheduledTaskNames() + { + return array_keys($this->timetable); + } + + public function getScheduledTaskTime($taskName) + { + return isset($this->timetable[$taskName]) ? Date::factory($this->timetable[$taskName]) : false; + } + + /** + * Checks if the task should be executed + * + * Task has to be executed if : + * - the task has already been scheduled once and the current system time is greater than the scheduled time. + * - execution is forced, see $forceTaskExecution + * + * @param string $taskName + * + * @return boolean + */ + public function shouldExecuteTask($taskName) + { + $forceTaskExecution = (defined('DEBUG_FORCE_SCHEDULED_TASKS') && DEBUG_FORCE_SCHEDULED_TASKS); + + if ($forceTaskExecution) { + return true; + } + + return $this->taskHasBeenScheduledOnce($taskName) && time() >= $this->timetable[$taskName]; + } + + /** + * Checks if a task should be rescheduled + * + * Task has to be rescheduled if : + * - the task has to be executed + * - the task has never been scheduled before + * + * @param string $taskName + * + * @return boolean + */ + public function taskShouldBeRescheduled($taskName) + { + return !$this->taskHasBeenScheduledOnce($taskName) || $this->shouldExecuteTask($taskName); + } + + public function rescheduleTask(Task $task) + { + $rescheduledTime = $task->getRescheduledTime(); + + // update the scheduled time + $this->timetable[$task->getName()] = $rescheduledTime; + $this->save(); + + return Date::factory($rescheduledTime); + } + + public function save() + { + Option::set(self::TIMETABLE_OPTION_STRING, serialize($this->timetable)); + } + + public function getScheduledTimeForMethod($className, $methodName, $methodParameter = null) + { + $taskName = Task::getTaskName($className, $methodName, $methodParameter); + + return $this->taskHasBeenScheduledOnce($taskName) ? $this->timetable[$taskName] : false; + } + + public function taskHasBeenScheduledOnce($taskName) + { + return isset($this->timetable[$taskName]); + } +} diff --git a/www/analytics/core/Segment.php b/www/analytics/core/Segment.php index bf834543..d9f8d163 100644 --- a/www/analytics/core/Segment.php +++ b/www/analytics/core/Segment.php @@ -1,6 +1,6 @@ getSelectQuery( * $select = "table.col1, table2.col2", * $from = array("table", "table2"), @@ -41,15 +44,15 @@ use Piwik\Plugins\API\API; * $orderBy = "table.col1 DESC", * $groupBy = "table2.col2" * ); - * + * * Db::fetchAll($query['sql'], $query['bind']); - * + * * **Creating a _null_ segment** - * + * * $idSites = array(1,2,3); * $segment = new Segment('', $idSites); * // $segment->getSelectQuery will return a query that selects all visits - * + * * @api */ class Segment @@ -57,7 +60,22 @@ class Segment /** * @var SegmentExpression */ - protected $segment = null; + protected $segmentExpression = null; + + /** + * @var string + */ + protected $string = null; + + /** + * @var array + */ + protected $idSites = null; + + /** + * @var LogQueryBuilder + */ + private $segmentQueryBuilder; /** * Truncate the Segments to 8k @@ -66,13 +84,16 @@ class Segment /** * Constructor. - * + * * @param string $segmentCondition The segment condition, eg, `'browserCode=ff;countryCode=CA'`. * @param array $idSites The list of sites the segment will be used with. Some segments are * dependent on the site, such as goal segments. + * @throws */ public function __construct($segmentCondition, $idSites) { + $this->segmentQueryBuilder = StaticContainer::get('Piwik\DataAccess\LogQueryBuilder'); + $segmentCondition = trim($segmentCondition); if (!SettingsPiwik::isSegmentationEnabled() && !empty($segmentCondition) @@ -89,6 +110,35 @@ class Segment } } + private function getAvailableSegments() + { + // segment metadata + if (empty($this->availableSegments)) { + $this->availableSegments = API::getInstance()->getSegmentsMetadata($this->idSites, $_hideImplementationData = false); + } + + return $this->availableSegments; + } + + private function getSegmentByName($name) + { + $segments = $this->getAvailableSegments(); + + foreach ($segments as $segment) { + if ($segment['segment'] == $name && !empty($name)) { + + // check permission + if (isset($segment['permission']) && $segment['permission'] != 1) { + throw new NoAccessException("You do not have enough permission to access the segment " . $name); + } + + return $segment; + } + } + + throw new Exception("Segment '$name' is not a supported segment."); + } + /** * @param $string * @param $idSites @@ -99,13 +149,14 @@ class Segment // As a preventive measure, we restrict the filter size to a safe limit $string = substr($string, 0, self::SEGMENT_TRUNCATE_LIMIT); - $this->string = $string; + $this->string = $string; $this->idSites = $idSites; $segment = new SegmentExpression($string); - $this->segment = $segment; + $this->segmentExpression = $segment; // parse segments $expressions = $segment->parseSubExpressions(); + $expressions = $this->getExpressionsWithUnionsResolved($expressions); // convert segments name to sql segment // check that user is allowed to view this segment @@ -117,68 +168,86 @@ class Segment $expression[SegmentExpression::INDEX_OPERAND] = $cleanedExpression; $cleanedExpressions[] = $expression; } + $segment->setSubExpressionsAfterCleanup($cleanedExpressions); } + private function getExpressionsWithUnionsResolved($expressions) + { + $expressionsWithUnions = array(); + foreach ($expressions as $expression) { + $operand = $expression[SegmentExpression::INDEX_OPERAND]; + $name = $operand[SegmentExpression::INDEX_OPERAND_NAME]; + + $availableSegment = $this->getSegmentByName($name); + + if (!empty($availableSegment['unionOfSegments'])) { + $count = 0; + foreach ($availableSegment['unionOfSegments'] as $segmentNameOfUnion) { + $count++; + $operator = SegmentExpression::BOOL_OPERATOR_OR; // we connect all segments within that union via OR + if ($count === count($availableSegment['unionOfSegments'])) { + $operator = $expression[SegmentExpression::INDEX_BOOL_OPERATOR]; + } + + $operand[SegmentExpression::INDEX_OPERAND_NAME] = $segmentNameOfUnion; + $expressionsWithUnions[] = array( + SegmentExpression::INDEX_BOOL_OPERATOR => $operator, + SegmentExpression::INDEX_OPERAND => $operand + ); + } + } else { + $expressionsWithUnions[] = array( + SegmentExpression::INDEX_BOOL_OPERATOR => $expression[SegmentExpression::INDEX_BOOL_OPERATOR], + SegmentExpression::INDEX_OPERAND => $operand + ); + } + } + + return $expressionsWithUnions; + } + /** * Returns `true` if the segment is empty, `false` if otherwise. */ public function isEmpty() { - return empty($this->string); + return $this->segmentExpression->isEmpty(); } protected $availableSegments = array(); protected function getCleanedExpression($expression) { - if (empty($this->availableSegments)) { - $this->availableSegments = API::getInstance()->getSegmentsMetadata($this->idSites, $_hideImplementationData = false); - } + $name = $expression[SegmentExpression::INDEX_OPERAND_NAME]; + $matchType = $expression[SegmentExpression::INDEX_OPERAND_OPERATOR]; + $value = $expression[SegmentExpression::INDEX_OPERAND_VALUE]; - $name = $expression[0]; - $matchType = $expression[1]; - $value = $expression[2]; - $sqlName = ''; + $segment = $this->getSegmentByName($name); + $sqlName = $segment['sqlSegment']; - foreach ($this->availableSegments as $segment) { - if ($segment['segment'] != $name) { - continue; + if ($matchType != SegmentExpression::MATCH_IS_NOT_NULL_NOR_EMPTY + && $matchType != SegmentExpression::MATCH_IS_NULL_OR_EMPTY) { + + if (isset($segment['sqlFilterValue'])) { + $value = call_user_func($segment['sqlFilterValue'], $value); } - $sqlName = $segment['sqlSegment']; + // apply presentation filter + if (isset($segment['sqlFilter'])) { + $value = call_user_func($segment['sqlFilter'], $value, $segment['sqlSegment'], $matchType, $name); - // check permission - if (isset($segment['permission']) - && $segment['permission'] != 1 - ) { - throw new Exception("You do not have enough permission to access the segment " . $name); - } - - if($matchType != SegmentExpression::MATCH_IS_NOT_NULL_NOR_EMPTY - && $matchType != SegmentExpression::MATCH_IS_NULL_OR_EMPTY) { - - if(isset($segment['sqlFilterValue'])) { - $value = call_user_func($segment['sqlFilterValue'], $value); + if(is_null($value)) { // null is returned in TableLogAction::getIdActionFromSegment() + return array(null, $matchType, null); } - // apply presentation filter - if (isset($segment['sqlFilter'])) { - $value = call_user_func($segment['sqlFilter'], $value, $segment['sqlSegment'], $matchType, $name); - - // sqlFilter-callbacks might return arrays for more complex cases - // e.g. see TableLogAction::getIdActionFromSegment() - if (is_array($value) && isset($value['SQL'])) { - // Special case: returned value is a sub sql expression! - $matchType = SegmentExpression::MATCH_ACTIONS_CONTAINS; - } + // sqlFilter-callbacks might return arrays for more complex cases + // e.g. see TableLogAction::getIdActionFromSegment() + if (is_array($value) && isset($value['SQL'])) { + // Special case: returned value is a sub sql expression! + $matchType = SegmentExpression::MATCH_ACTIONS_CONTAINS; } } - break; - } - - if (empty($sqlName)) { - throw new Exception("Segment '$name' is not a supported segment."); } return array($sqlName, $matchType, $value); @@ -186,7 +255,7 @@ class Segment /** * Returns the segment condition. - * + * * @return string */ public function getString() @@ -197,7 +266,7 @@ class Segment /** * Returns a hash of the segment condition, or the empty string if the segment * condition is empty. - * + * * @return string */ public function getHash() @@ -220,235 +289,31 @@ class Segment * @param array|string $bind (optional) Bind parameters, eg, `array($col1Value, $col2Value)`. * @param false|string $orderBy (optional) Order by clause, eg, `"t1.col1 ASC"`. * @param false|string $groupBy (optional) Group by clause, eg, `"t2.col2"`. + * @param int $limit Limit number of result to $limit + * @param int $offset Specified the offset of the first row to return + * @param int If set to value >= 1 then the Select query (and All inner queries) will be LIMIT'ed by this value. + * Use only when you're not aggregating or it will sample the data. * @return string The entire select query. */ - public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false) + public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false, $limit = 0, $offset = 0) { - if (!is_array($from)) { - $from = array($from); + $segmentExpression = $this->segmentExpression; + + if ($offset > 0) { + $limit = (int) $offset . ', ' . (int) $limit; } - if (!$this->isEmpty()) { - $this->segment->parseSubExpressionsIntoSqlExpressions($from); - - $joins = $this->generateJoins($from); - $from = $joins['sql']; - $joinWithSubSelect = $joins['joinWithSubSelect']; - - $segmentSql = $this->segment->getSql(); - $segmentWhere = $segmentSql['where']; - if (!empty($segmentWhere)) { - if (!empty($where)) { - $where = "( $where ) - AND - ($segmentWhere)"; - } else { - $where = $segmentWhere; - } - } - - $bind = array_merge($bind, $segmentSql['bind']); - } else { - $joins = $this->generateJoins($from); - $from = $joins['sql']; - $joinWithSubSelect = $joins['joinWithSubSelect']; - } - - if ($joinWithSubSelect) { - $sql = $this->buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy); - } else { - $sql = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); - } - return array( - 'sql' => $sql, - 'bind' => $bind - ); + return $this->segmentQueryBuilder->getSelectQueryString($segmentExpression, $select, $from, $where, $bind, + $groupBy, $orderBy, $limit); } /** - * Generate the join sql based on the needed tables - * @param array $tables tables to join - * @throws Exception if tables can't be joined - * @return array - */ - private function generateJoins($tables) - { - $knownTables = array("log_visit", "log_link_visit_action", "log_conversion", "log_conversion_item"); - $visitsAvailable = $actionsAvailable = $conversionsAvailable = $conversionItemAvailable = false; - $joinWithSubSelect = false; - $sql = ''; - - // make sure the tables are joined in the right order - // base table first, then action before conversion - // this way, conversions can be joined on idlink_va - $actionIndex = array_search("log_link_visit_action", $tables); - $conversionIndex = array_search("log_conversion", $tables); - if ($actionIndex > 0 && $conversionIndex > 0 && $actionIndex > $conversionIndex) { - $tables[$actionIndex] = "log_conversion"; - $tables[$conversionIndex] = "log_link_visit_action"; - } - - // same as above: action before visit - $actionIndex = array_search("log_link_visit_action", $tables); - $visitIndex = array_search("log_visit", $tables); - if ($actionIndex > 0 && $visitIndex > 0 && $actionIndex > $visitIndex) { - $tables[$actionIndex] = "log_visit"; - $tables[$visitIndex] = "log_link_visit_action"; - } - - foreach ($tables as $i => $table) { - if (is_array($table)) { - // join condition provided - $alias = isset($table['tableAlias']) ? $table['tableAlias'] : $table['table']; - $sql .= " - LEFT JOIN " . Common::prefixTable($table['table']) . " AS " . $alias - . " ON " . $table['joinOn']; - continue; - } - - if (!in_array($table, $knownTables)) { - throw new Exception("Table '$table' can't be used for segmentation"); - } - - $tableSql = Common::prefixTable($table) . " AS $table"; - - if ($i == 0) { - // first table - $sql .= $tableSql; - } else { - if ($actionsAvailable && $table == "log_conversion") { - // have actions, need conversions => join on idlink_va - $join = "log_conversion.idlink_va = log_link_visit_action.idlink_va " - . "AND log_conversion.idsite = log_link_visit_action.idsite"; - } else if ($actionsAvailable && $table == "log_visit") { - // have actions, need visits => join on idvisit - $join = "log_visit.idvisit = log_link_visit_action.idvisit"; - } else if ($visitsAvailable && $table == "log_link_visit_action") { - // have visits, need actions => we have to use a more complex join - // we don't hande this here, we just return joinWithSubSelect=true in this case - $joinWithSubSelect = true; - $join = "log_link_visit_action.idvisit = log_visit.idvisit"; - } else if ($conversionsAvailable && $table == "log_link_visit_action") { - // have conversions, need actions => join on idlink_va - $join = "log_conversion.idlink_va = log_link_visit_action.idlink_va"; - } else if (($visitsAvailable && $table == "log_conversion") - || ($conversionsAvailable && $table == "log_visit") - ) { - // have visits, need conversion (or vice versa) => join on idvisit - // notice that joining conversions on visits has lower priority than joining it on actions - $join = "log_conversion.idvisit = log_visit.idvisit"; - - // if conversions are joined on visits, we need a complex join - if ($table == "log_conversion") { - $joinWithSubSelect = true; - } - } elseif ($conversionItemAvailable && $table === 'log_visit') { - $join = "log_conversion_item.idvisit = log_visit.idvisit"; - } elseif ($conversionItemAvailable && $table === 'log_link_visit_action') { - $join = "log_conversion_item.idvisit = log_link_visit_action.idvisit"; - } elseif ($conversionItemAvailable && $table === 'log_conversion') { - $join = "log_conversion_item.idvisit = log_conversion.idvisit"; - } else { - throw new Exception("Table '$table' can't be joined for segmentation"); - } - - // the join sql the default way - $sql .= " - LEFT JOIN $tableSql ON $join"; - } - - // remember which tables are available - $visitsAvailable = ($visitsAvailable || $table == "log_visit"); - $actionsAvailable = ($actionsAvailable || $table == "log_link_visit_action"); - $conversionsAvailable = ($conversionsAvailable || $table == "log_conversion"); - $conversionItemAvailable = ($conversionItemAvailable || $table == "log_conversion_item"); - } - - $return = array( - 'sql' => $sql, - 'joinWithSubSelect' => $joinWithSubSelect - ); - return $return; - - } - - /** - * Build select query the normal way - * @param string $select fieldlist to be selected - * @param string $from tablelist to select from - * @param string $where where clause - * @param string $orderBy order by clause - * @param string $groupBy group by clause + * Returns the segment string. + * * @return string */ - private function buildSelectQuery($select, $from, $where, $orderBy, $groupBy) + public function __toString() { - $sql = " - SELECT - $select - FROM - $from"; - - if ($where) { - $sql .= " - WHERE - $where"; - } - - if ($groupBy) { - $sql .= " - GROUP BY - $groupBy"; - } - - if ($orderBy) { - $sql .= " - ORDER BY - $orderBy"; - } - - return $sql; - } - - /** - * Build a select query where actions have to be joined on visits (or conversions) - * In this case, the query gets wrapped in another query so that grouping by visit is possible - * @param string $select - * @param string $from - * @param string $where - * @param string $orderBy - * @param string $groupBy - * @throws Exception - * @return string - */ - private function buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy) - { - $matchTables = "(log_visit|log_conversion_item|log_conversion|log_action)"; - preg_match_all("/". $matchTables ."\.[a-z0-9_\*]+/", $select, $matches); - $neededFields = array_unique($matches[0]); - - if (count($neededFields) == 0) { - throw new Exception("No needed fields found in select expression. " - . "Please use a table prefix."); - } - - $select = preg_replace('/'.$matchTables.'\./', 'log_inner.', $select); - $orderBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $orderBy); - $groupBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $groupBy); - - $from = "( - SELECT - " . implode(", - ", $neededFields) . " - FROM - $from - WHERE - $where - GROUP BY log_visit.idvisit - ) AS log_inner"; - - $where = false; - $query = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); - return $query; + return (string) $this->getString(); } } diff --git a/www/analytics/core/Segment/SegmentExpression.php b/www/analytics/core/Segment/SegmentExpression.php new file mode 100644 index 00000000..605cd136 --- /dev/null +++ b/www/analytics/core/Segment/SegmentExpression.php @@ -0,0 +1,452 @@ +='; + const MATCH_LESS_OR_EQUAL = '<='; + const MATCH_GREATER = '>'; + const MATCH_LESS = '<'; + const MATCH_CONTAINS = '=@'; + const MATCH_DOES_NOT_CONTAIN = '!@'; + const MATCH_STARTS_WITH = '=^'; + const MATCH_ENDS_WITH = '=$'; + + const BOOL_OPERATOR_OR = 'OR'; + const BOOL_OPERATOR_AND = 'AND'; + const BOOL_OPERATOR_END = ''; + + // Note: you can't write this in the API, but access this feature + // via field!= <- IS NOT NULL + // or via field== <- IS NULL / empty + const MATCH_IS_NOT_NULL_NOR_EMPTY = '::NOT_NULL'; + const MATCH_IS_NULL_OR_EMPTY = '::NULL'; + + // Special case, since we look up Page URLs/Page titles in a sub SQL query + const MATCH_ACTIONS_CONTAINS = 'IN'; + + const INDEX_BOOL_OPERATOR = 0; + const INDEX_OPERAND = 1; + + const INDEX_OPERAND_NAME = 0; + const INDEX_OPERAND_OPERATOR = 1; + const INDEX_OPERAND_VALUE = 2; + + const SQL_WHERE_DO_NOT_MATCH_ANY_ROW = "(1 = 0)"; + const SQL_WHERE_MATCHES_ALL_ROWS = "(1 = 1)"; + + public function __construct($string) + { + $this->string = $string; + $this->tree = $this->parseTree(); + } + + public function getSegmentDefinition() + { + return $this->string; + } + + public function isEmpty() + { + return count($this->tree) == 0; + } + + protected $joins = array(); + protected $valuesBind = array(); + protected $parsedTree = array(); + protected $tree = array(); + protected $parsedSubExpressions = array(); + + /** + * Given the array of parsed filters containing, for each filter, + * the boolean operator (AND/OR) and the operand, + * Will return the array where the filters are in SQL representation + * + * @throws Exception + * @return array + */ + public function parseSubExpressions() + { + $parsedSubExpressions = array(); + foreach ($this->tree as $leaf) { + $operand = $leaf[self::INDEX_OPERAND]; + + $operand = urldecode($operand); + + $operator = $leaf[self::INDEX_BOOL_OPERATOR]; + $pattern = '/^(.+?)(' . self::MATCH_EQUAL . '|' + . self::MATCH_NOT_EQUAL . '|' + . self::MATCH_GREATER_OR_EQUAL . '|' + . self::MATCH_GREATER . '|' + . self::MATCH_LESS_OR_EQUAL . '|' + . self::MATCH_LESS . '|' + . self::MATCH_CONTAINS . '|' + . self::MATCH_DOES_NOT_CONTAIN . '|' + . preg_quote(self::MATCH_STARTS_WITH) . '|' + . preg_quote(self::MATCH_ENDS_WITH) + . '){1}(.*)/'; + $match = preg_match($pattern, $operand, $matches); + if ($match == 0) { + throw new Exception('The segment \'' . $operand . '\' is not valid.'); + } + + $leftMember = $matches[1]; + $operation = $matches[2]; + $valueRightMember = urldecode($matches[3]); + + // is null / is not null + if ($valueRightMember === '') { + if ($operation == self::MATCH_NOT_EQUAL) { + $operation = self::MATCH_IS_NOT_NULL_NOR_EMPTY; + } elseif ($operation == self::MATCH_EQUAL) { + $operation = self::MATCH_IS_NULL_OR_EMPTY; + } else { + throw new Exception('The segment \'' . $operand . '\' has no value specified. You can leave this value empty ' . + 'only when you use the operators: ' . self::MATCH_NOT_EQUAL . ' (is not) or ' . self::MATCH_EQUAL . ' (is)'); + } + } + + $parsedSubExpressions[] = array( + self::INDEX_BOOL_OPERATOR => $operator, + self::INDEX_OPERAND => array( + self::INDEX_OPERAND_NAME => $leftMember, + self::INDEX_OPERAND_OPERATOR => $operation, + self::INDEX_OPERAND_VALUE => $valueRightMember, + )); + } + $this->parsedSubExpressions = $parsedSubExpressions; + return $parsedSubExpressions; + } + + /** + * Set the given expression + * @param $parsedSubExpressions + */ + public function setSubExpressionsAfterCleanup($parsedSubExpressions) + { + $this->parsedSubExpressions = $parsedSubExpressions; + } + + /** + * @param array $availableTables + */ + public function parseSubExpressionsIntoSqlExpressions(&$availableTables = array()) + { + $sqlSubExpressions = array(); + $this->valuesBind = array(); + $this->joins = array(); + + foreach ($this->parsedSubExpressions as $leaf) { + $operator = $leaf[self::INDEX_BOOL_OPERATOR]; + $operandDefinition = $leaf[self::INDEX_OPERAND]; + + $operand = $this->getSqlMatchFromDefinition($operandDefinition, $availableTables); + + if ($operand[self::INDEX_OPERAND_OPERATOR] !== null) { + if (is_array($operand[self::INDEX_OPERAND_OPERATOR])) { + $this->valuesBind = array_merge($this->valuesBind, $operand[self::INDEX_OPERAND_OPERATOR]); + } else { + $this->valuesBind[] = $operand[self::INDEX_OPERAND_OPERATOR]; + } + } + + $operand = $operand[self::INDEX_OPERAND_NAME]; + + $sqlSubExpressions[] = array( + self::INDEX_BOOL_OPERATOR => $operator, + self::INDEX_OPERAND => $operand, + ); + } + + $this->tree = $sqlSubExpressions; + } + + /** + * Given an array representing one filter operand ( left member , operation , right member) + * Will return an array containing + * - the SQL substring, + * - the values to bind to this substring + * + * @param array $def + * @param array $availableTables + * @throws Exception + * @return array + */ + protected function getSqlMatchFromDefinition($def, &$availableTables) + { + $field = $def[0]; + $matchType = $def[1]; + $value = $def[2]; + + // Segment::getCleanedExpression() may return array(null, $matchType, null) + $operandWillNotMatchAnyRow = empty($field) && is_null($value); + if($operandWillNotMatchAnyRow) { + if($matchType == self::MATCH_EQUAL) { + // eg. pageUrl==DoesNotExist + // Equal to NULL means it will match none + $sqlExpression = self::SQL_WHERE_DO_NOT_MATCH_ANY_ROW; + } elseif($matchType == self::MATCH_NOT_EQUAL) { + // eg. pageUrl!=DoesNotExist + // Not equal to NULL means it matches all rows + $sqlExpression = self::SQL_WHERE_MATCHES_ALL_ROWS; + } elseif($matchType == self::MATCH_CONTAINS + || $matchType == self::MATCH_DOES_NOT_CONTAIN + || $matchType == self::MATCH_STARTS_WITH + || $matchType == self::MATCH_ENDS_WITH) { + // no action was found for CONTAINS / DOES NOT CONTAIN + // eg. pageUrl=@DoesNotExist -> matches no row + // eg. pageUrl!@DoesNotExist -> matches no rows + $sqlExpression = self::SQL_WHERE_DO_NOT_MATCH_ANY_ROW; + } else { + // it is not expected to reach this code path + throw new Exception("Unexpected match type $matchType for your segment. " . + "Please report this issue to the Piwik team with the segment you are using."); + } + + return array($sqlExpression, $value = null); + } + + $alsoMatchNULLValues = false; + switch ($matchType) { + case self::MATCH_EQUAL: + $sqlMatch = '%s ='; + break; + case self::MATCH_NOT_EQUAL: + $sqlMatch = '%s <>'; + $alsoMatchNULLValues = true; + break; + case self::MATCH_GREATER: + $sqlMatch = '%s >'; + break; + case self::MATCH_LESS: + $sqlMatch = '%s <'; + break; + case self::MATCH_GREATER_OR_EQUAL: + $sqlMatch = '%s >='; + break; + case self::MATCH_LESS_OR_EQUAL: + $sqlMatch = '%s <='; + break; + case self::MATCH_CONTAINS: + $sqlMatch = '%s LIKE'; + $value = '%' . $this->escapeLikeString($value) . '%'; + break; + case self::MATCH_DOES_NOT_CONTAIN: + $sqlMatch = '%s NOT LIKE'; + $value = '%' . $this->escapeLikeString($value) . '%'; + $alsoMatchNULLValues = true; + break; + case self::MATCH_STARTS_WITH: + $sqlMatch = '%s LIKE'; + $value = $this->escapeLikeString($value) . '%'; + break; + case self::MATCH_ENDS_WITH: + $sqlMatch = '%s LIKE'; + $value = '%' . $this->escapeLikeString($value); + break; + + case self::MATCH_IS_NOT_NULL_NOR_EMPTY: + $sqlMatch = '%s IS NOT NULL AND (%s <> \'\' OR %s = 0)'; + $value = null; + break; + + case self::MATCH_IS_NULL_OR_EMPTY: + $sqlMatch = '%s IS NULL OR %s = \'\' '; + $value = null; + break; + + case self::MATCH_ACTIONS_CONTAINS: + // this match type is not accessible from the outside + // (it won't be matched in self::parseSubExpressions()) + // it can be used internally to inject sub-expressions into the query. + // see Segment::getCleanedExpression() + $sqlMatch = '%s IN (' . $value['SQL'] . ')'; + $value = $value['bind']; + break; + default: + throw new Exception("Filter contains the match type '" . $matchType . "' which is not supported"); + break; + } + + // We match NULL values when rows are excluded only when we are not doing a + $alsoMatchNULLValues = $alsoMatchNULLValues && !empty($value); + $sqlMatch = str_replace('%s', $field, $sqlMatch); + + if ($matchType === self::MATCH_ACTIONS_CONTAINS + || is_null($value) + ) { + $sqlExpression = "( $sqlMatch )"; + } else { + if ($alsoMatchNULLValues) { + $sqlExpression = "( $field IS NULL OR $sqlMatch ? )"; + } else { + $sqlExpression = "$sqlMatch ?"; + } + } + + $this->checkFieldIsAvailable($field, $availableTables); + + return array($sqlExpression, $value); + } + + /** + * Check whether the field is available + * If not, add it to the available tables + * + * @param string $field + * @param array $availableTables + */ + private function checkFieldIsAvailable($field, &$availableTables) + { + $fieldParts = explode('.', $field); + + $table = count($fieldParts) == 2 ? $fieldParts[0] : false; + + // remove sql functions from field name + // example: `HOUR(log_visit.visit_last_action_time)` gets `HOUR(log_visit` => remove `HOUR(` + $table = preg_replace('/^[A-Z_]+\(/', '', $table); + $tableExists = !$table || in_array($table, $availableTables); + + if (!$tableExists) { + $availableTables[] = $table; + } + } + + /** + * Escape the characters % and _ in the given string + * @param string $str + * @return string + */ + private function escapeLikeString($str) + { + if (false !== strpos($str, '%')) { + $str = str_replace("%", "\%", $str); + } + + if (false !== strpos($str, '_')) { + $str = str_replace("_", "\_", $str); + } + + return $str; + } + + /** + * Given a filter string, + * will parse it into an array where each row contains the boolean operator applied to it, + * and the operand + * + * @return array + */ + protected function parseTree() + { + $string = $this->string; + if (empty($string)) { + return array(); + } + $tree = array(); + $i = 0; + $length = strlen($string); + $isBackslash = false; + $operand = ''; + while ($i <= $length) { + $char = $string[$i]; + + $isAND = ($char == self::AND_DELIMITER); + $isOR = ($char == self::OR_DELIMITER); + $isEnd = ($length == $i + 1); + + if ($isEnd) { + if ($isBackslash && ($isAND || $isOR)) { + $operand = substr($operand, 0, -1); + } + $operand .= $char; + $tree[] = array(self::INDEX_BOOL_OPERATOR => self::BOOL_OPERATOR_END, self::INDEX_OPERAND => $operand); + break; + } + + if ($isAND && !$isBackslash) { + $tree[] = array(self::INDEX_BOOL_OPERATOR => self::BOOL_OPERATOR_AND, self::INDEX_OPERAND => $operand); + $operand = ''; + } elseif ($isOR && !$isBackslash) { + $tree[] = array(self::INDEX_BOOL_OPERATOR => self::BOOL_OPERATOR_OR, self::INDEX_OPERAND => $operand); + $operand = ''; + } else { + if ($isBackslash && ($isAND || $isOR)) { + $operand = substr($operand, 0, -1); + } + $operand .= $char; + } + $isBackslash = ($char == "\\"); + $i++; + } + return $tree; + } + + /** + * Given the array of parsed boolean logic, will return + * an array containing the full SQL string representing the filter, + * the needed joins and the values to bind to the query + * + * @throws Exception + * @return array SQL Query, Joins and Bind parameters + */ + public function getSql() + { + if ($this->isEmpty()) { + throw new Exception("Invalid segment, please specify a valid segment."); + } + $sql = ''; + $subExpression = false; + foreach ($this->tree as $expression) { + $operator = $expression[self::INDEX_BOOL_OPERATOR]; + $operand = $expression[self::INDEX_OPERAND]; + + if ($operator == self::BOOL_OPERATOR_OR + && !$subExpression + ) { + $sql .= ' ('; + $subExpression = true; + } else { + $sql .= ' '; + } + + $sql .= $operand; + + if ($operator == self::BOOL_OPERATOR_AND + && $subExpression + ) { + $sql .= ')'; + $subExpression = false; + } + + $sql .= " $operator"; + } + if ($subExpression) { + $sql .= ')'; + } + return array( + 'where' => $sql, + 'bind' => $this->valuesBind, + 'join' => implode(' ', $this->joins) + ); + } +} + diff --git a/www/analytics/core/SegmentExpression.php b/www/analytics/core/SegmentExpression.php deleted file mode 100644 index 96cac35c..00000000 --- a/www/analytics/core/SegmentExpression.php +++ /dev/null @@ -1,378 +0,0 @@ -='; - const MATCH_LESS_OR_EQUAL = '<='; - const MATCH_GREATER = '>'; - const MATCH_LESS = '<'; - const MATCH_CONTAINS = '=@'; - const MATCH_DOES_NOT_CONTAIN = '!@'; - - // Note: you can't write this in the API, but access this feature - // via field!= <- IS NOT NULL - // or via field== <- IS NULL / empty - const MATCH_IS_NOT_NULL_NOR_EMPTY = '::NOT_NULL'; - const MATCH_IS_NULL_OR_EMPTY = '::NULL'; - - // Special case, since we look up Page URLs/Page titles in a sub SQL query - const MATCH_ACTIONS_CONTAINS = 'IN'; - - const INDEX_BOOL_OPERATOR = 0; - const INDEX_OPERAND = 1; - - function __construct($string) - { - $this->string = $string; - $this->tree = $this->parseTree(); - } - - protected $joins = array(); - protected $valuesBind = array(); - protected $parsedTree = array(); - protected $tree = array(); - protected $parsedSubExpressions = array(); - - /** - * Given the array of parsed filters containing, for each filter, - * the boolean operator (AND/OR) and the operand, - * Will return the array where the filters are in SQL representation - * - * @throws Exception - * @return array - */ - public function parseSubExpressions() - { - $parsedSubExpressions = array(); - foreach ($this->tree as $id => $leaf) { - $operand = $leaf[self::INDEX_OPERAND]; - - $operand = urldecode($operand); - - $operator = $leaf[self::INDEX_BOOL_OPERATOR]; - $pattern = '/^(.+?)(' . self::MATCH_EQUAL . '|' - . self::MATCH_NOT_EQUAL . '|' - . self::MATCH_GREATER_OR_EQUAL . '|' - . self::MATCH_GREATER . '|' - . self::MATCH_LESS_OR_EQUAL . '|' - . self::MATCH_LESS . '|' - . self::MATCH_CONTAINS . '|' - . self::MATCH_DOES_NOT_CONTAIN - . '){1}(.*)/'; - $match = preg_match($pattern, $operand, $matches); - if ($match == 0) { - throw new Exception('The segment \'' . $operand . '\' is not valid.'); - } - - $leftMember = $matches[1]; - $operation = $matches[2]; - $valueRightMember = urldecode($matches[3]); - - // is null / is not null - if ($valueRightMember === '') { - if ($operation == self::MATCH_NOT_EQUAL) { - $operation = self::MATCH_IS_NOT_NULL_NOR_EMPTY; - } elseif ($operation == self::MATCH_EQUAL) { - $operation = self::MATCH_IS_NULL_OR_EMPTY; - } else { - throw new Exception('The segment \'' . $operand . '\' has no value specified. You can leave this value empty ' . - 'only when you use the operators: ' . self::MATCH_NOT_EQUAL . ' (is not) or ' . self::MATCH_EQUAL . ' (is)'); - } - } - - $parsedSubExpressions[] = array( - self::INDEX_BOOL_OPERATOR => $operator, - self::INDEX_OPERAND => array( - $leftMember, - $operation, - $valueRightMember, - )); - } - $this->parsedSubExpressions = $parsedSubExpressions; - return $parsedSubExpressions; - } - - /** - * Set the given expression - * @param $parsedSubExpressions - */ - public function setSubExpressionsAfterCleanup($parsedSubExpressions) - { - $this->parsedSubExpressions = $parsedSubExpressions; - } - - /** - * @param array $availableTables - */ - public function parseSubExpressionsIntoSqlExpressions(&$availableTables = array()) - { - $sqlSubExpressions = array(); - $this->valuesBind = array(); - $this->joins = array(); - - foreach ($this->parsedSubExpressions as $leaf) { - $operator = $leaf[self::INDEX_BOOL_OPERATOR]; - $operandDefinition = $leaf[self::INDEX_OPERAND]; - - $operand = $this->getSqlMatchFromDefinition($operandDefinition, $availableTables); - - if ($operand[1] !== null) { - $this->valuesBind[] = $operand[1]; - } - $operand = $operand[0]; - $sqlSubExpressions[] = array( - self::INDEX_BOOL_OPERATOR => $operator, - self::INDEX_OPERAND => $operand, - ); - } - - $this->tree = $sqlSubExpressions; - } - - /** - * Given an array representing one filter operand ( left member , operation , right member) - * Will return an array containing - * - the SQL substring, - * - the values to bind to this substring - * - * @param array $def - * @param array $availableTables - * @throws Exception - * @return array - */ - protected function getSqlMatchFromDefinition($def, &$availableTables) - { - $field = $def[0]; - $matchType = $def[1]; - $value = $def[2]; - - $alsoMatchNULLValues = false; - switch ($matchType) { - case self::MATCH_EQUAL: - $sqlMatch = '='; - break; - case self::MATCH_NOT_EQUAL: - $sqlMatch = '<>'; - $alsoMatchNULLValues = true; - break; - case self::MATCH_GREATER: - $sqlMatch = '>'; - break; - case self::MATCH_LESS: - $sqlMatch = '<'; - break; - case self::MATCH_GREATER_OR_EQUAL: - $sqlMatch = '>='; - break; - case self::MATCH_LESS_OR_EQUAL: - $sqlMatch = '<='; - break; - case self::MATCH_CONTAINS: - $sqlMatch = 'LIKE'; - $value = '%' . $this->escapeLikeString($value) . '%'; - break; - case self::MATCH_DOES_NOT_CONTAIN: - $sqlMatch = 'NOT LIKE'; - $value = '%' . $this->escapeLikeString($value) . '%'; - $alsoMatchNULLValues = true; - break; - - case self::MATCH_IS_NOT_NULL_NOR_EMPTY: - $sqlMatch = 'IS NOT NULL AND (' . $field . ' <> \'\' OR ' . $field . ' = 0)'; - $value = null; - break; - - case self::MATCH_IS_NULL_OR_EMPTY: - $sqlMatch = 'IS NULL OR ' . $field . ' = \'\' '; - $value = null; - break; - - case self::MATCH_ACTIONS_CONTAINS: - // this match type is not accessible from the outside - // (it won't be matched in self::parseSubExpressions()) - // it can be used internally to inject sub-expressions into the query. - // see Segment::getCleanedExpression() - $sqlMatch = 'IN (' . $value['SQL'] . ')'; - $value = $this->escapeLikeString($value['bind']); - break; - default: - throw new Exception("Filter contains the match type '" . $matchType . "' which is not supported"); - break; - } - - // We match NULL values when rows are excluded only when we are not doing a - $alsoMatchNULLValues = $alsoMatchNULLValues && !empty($value); - - if ($matchType === self::MATCH_ACTIONS_CONTAINS - || is_null($value) - ) { - $sqlExpression = "( $field $sqlMatch )"; - } else { - if ($alsoMatchNULLValues) { - $sqlExpression = "( $field IS NULL OR $field $sqlMatch ? )"; - } else { - $sqlExpression = "$field $sqlMatch ?"; - } - } - - $this->checkFieldIsAvailable($field, $availableTables); - - return array($sqlExpression, $value); - } - - /** - * Check whether the field is available - * If not, add it to the available tables - * - * @param string $field - * @param array $availableTables - */ - private function checkFieldIsAvailable($field, &$availableTables) - { - $fieldParts = explode('.', $field); - - $table = count($fieldParts) == 2 ? $fieldParts[0] : false; - - // remove sql functions from field name - // example: `HOUR(log_visit.visit_last_action_time)` gets `HOUR(log_visit` => remove `HOUR(` - $table = preg_replace('/^[A-Z_]+\(/', '', $table); - $tableExists = !$table || in_array($table, $availableTables); - - if (!$tableExists) { - $availableTables[] = $table; - } - } - - /** - * Escape the characters % and _ in the given string - * @param string $str - * @return string - */ - private function escapeLikeString($str) - { - $str = str_replace("%", "\%", $str); - $str = str_replace("_", "\_", $str); - return $str; - } - - /** - * Given a filter string, - * will parse it into an array where each row contains the boolean operator applied to it, - * and the operand - * - * @return array - */ - protected function parseTree() - { - $string = $this->string; - if (empty($string)) { - return array(); - } - $tree = array(); - $i = 0; - $length = strlen($string); - $isBackslash = false; - $operand = ''; - while ($i <= $length) { - $char = $string[$i]; - - $isAND = ($char == self::AND_DELIMITER); - $isOR = ($char == self::OR_DELIMITER); - $isEnd = ($length == $i + 1); - - if ($isEnd) { - if ($isBackslash && ($isAND || $isOR)) { - $operand = substr($operand, 0, -1); - } - $operand .= $char; - $tree[] = array(self::INDEX_BOOL_OPERATOR => '', self::INDEX_OPERAND => $operand); - break; - } - - if ($isAND && !$isBackslash) { - $tree[] = array(self::INDEX_BOOL_OPERATOR => 'AND', self::INDEX_OPERAND => $operand); - $operand = ''; - } elseif ($isOR && !$isBackslash) { - $tree[] = array(self::INDEX_BOOL_OPERATOR => 'OR', self::INDEX_OPERAND => $operand); - $operand = ''; - } else { - if ($isBackslash && ($isAND || $isOR)) { - $operand = substr($operand, 0, -1); - } - $operand .= $char; - } - $isBackslash = ($char == "\\"); - $i++; - } - return $tree; - } - - /** - * Given the array of parsed boolean logic, will return - * an array containing the full SQL string representing the filter, - * the needed joins and the values to bind to the query - * - * @throws Exception - * @return array SQL Query, Joins and Bind parameters - */ - public function getSql() - { - if (count($this->tree) == 0) { - throw new Exception("Invalid segment, please specify a valid segment."); - } - $bind = array(); - $sql = ''; - $subExpression = false; - foreach ($this->tree as $expression) { - $operator = $expression[self::INDEX_BOOL_OPERATOR]; - $operand = $expression[self::INDEX_OPERAND]; - - if ($operator == 'OR' - && !$subExpression - ) { - $sql .= ' ('; - $subExpression = true; - } else { - $sql .= ' '; - } - - $sql .= $operand; - - if ($operator == 'AND' - && $subExpression - ) { - $sql .= ')'; - $subExpression = false; - } - - $sql .= " $operator"; - } - if ($subExpression) { - $sql .= ')'; - } - return array( - 'where' => $sql, - 'bind' => $this->valuesBind, - 'join' => implode(' ', $this->joins) - ); - } -} diff --git a/www/analytics/core/Sequence.php b/www/analytics/core/Sequence.php new file mode 100644 index 00000000..7a25c239 --- /dev/null +++ b/www/analytics/core/Sequence.php @@ -0,0 +1,127 @@ +getNextId(); + * $db->insert('anytable', array('id' => $id, '...' => '...')); + */ +class Sequence +{ + const TABLE_NAME = 'sequence'; + /** + * @var string + */ + private $name; + + /** + * @var AdapterInterface + */ + private $db; + + /** + * @var string + */ + private $table; + + /** + * The name of the table or sequence you want to get an id for. + * + * @param string $name eg 'archive_numeric_2014_11' + * @param AdapterInterface $db You can optionally pass a DB adapter to make it work against another database. + * @param string|null $tablePrefix + */ + public function __construct($name, $db = null, $tablePrefix = null) + { + $this->name = $name; + $this->db = $db ?: Db::get(); + $this->table = $this->getTableName($tablePrefix); + } + + /** + * Creates / initializes a new sequence. + * + * @param int $initialValue + * @return int The actually used value to initialize the table. + * + * @throws \Exception in case a sequence having this name already exists. + */ + public function create($initialValue = 0) + { + $initialValue = (int) $initialValue; + + $this->db->insert($this->table, array('name' => $this->name, 'value' => $initialValue)); + + return $initialValue; + } + + /** + * Returns true if the sequence exist. + * + * @return bool + */ + public function exists() + { + $query = $this->db->query('SELECT * FROM ' . $this->table . ' WHERE name = ?', $this->name); + + return $query->rowCount() > 0; + } + + /** + * Get / allocate / reserve a new id for the current sequence. Important: Getting the next id will fail in case + * no such sequence exists. Make sure to create one if needed, see {@link create()}. + * + * @return int + * @throws Exception + */ + public function getNextId() + { + $sql = 'UPDATE ' . $this->table . ' SET value = LAST_INSERT_ID(value + 1) WHERE name = ?'; + + $result = $this->db->query($sql, array($this->name)); + $rowCount = $result->rowCount(); + + if (1 !== $rowCount) { + throw new Exception("Sequence '" . $this->name . "' not found."); + } + + $createdId = $this->db->lastInsertId(); + + return (int) $createdId; + } + + /** + * Returns the current max id. + * @return int + * @internal + */ + public function getCurrentId() + { + $sql = 'SELECT value FROM ' . $this->table . ' WHERE name = ?'; + + $id = $this->db->fetchOne($sql, array($this->name)); + + if (!empty($id) || '0' === $id || 0 === $id) { + return (int) $id; + } + } + + private function getTableName($prefix) + { + return ($prefix !== null) ? $prefix . self::TABLE_NAME : Common::prefixTable(self::TABLE_NAME); + } +} diff --git a/www/analytics/core/Session.php b/www/analytics/core/Session.php index 787affe2..965553b5 100644 --- a/www/analytics/core/Session.php +++ b/www/analytics/core/Session.php @@ -1,6 +1,6 @@ General['session_save_handler'] === 'dbtable' + } elseif ($config->General['session_save_handler'] === 'dbtable' || in_array($currentSaveHandler, array('user', 'mm')) ) { // We consider these to be misconfigurations, in that: @@ -109,19 +113,18 @@ class Session extends Zend_Session } try { - Zend_Session::start(); + parent::start(); register_shutdown_function(array('Zend_Session', 'writeClose'), true); } catch (Exception $e) { - Log::warning('Unable to start session: ' . $e->getMessage()); + Log::error('Unable to start session: ' . $e->getMessage()); $enableDbSessions = ''; if (DbHelper::isInstalled()) { $enableDbSessions = "
If you still experience issues after trying these changes, - we recommend that you enable database session storage."; + we recommend that you enable database session storage."; } - $pathToSessions = Filechecks::getErrorMessageMissingPermissions(Filesystem::getPathToPiwikRoot() . '/tmp/sessions/'); - $pathToSessions = SettingsPiwik::rewriteTmpPathWithHostname($pathToSessions); + $pathToSessions = Filechecks::getErrorMessageMissingPermissions(self::getSessionsDirectory()); $message = sprintf("Error: %s %s %s\n
Debug: the original error was \n%s
", Piwik::translate('General_ExceptionUnableToStartSession'), $pathToSessions, @@ -129,7 +132,10 @@ class Session extends Zend_Session $e->getMessage() ); - Piwik_ExitWithMessage($message); + $ex = new MissingFilePermissionException($message, $e->getCode(), $e); + $ex->setIsHtmlMessage(); + + throw $ex; } } @@ -140,7 +146,11 @@ class Session extends Zend_Session */ public static function getSessionsDirectory() { - $path = PIWIK_USER_PATH . '/tmp/sessions'; - return SettingsPiwik::rewriteTmpPathWithHostname($path); + return StaticContainer::get('path.tmp') . '/sessions'; + } + + public static function close() + { + parent::writeClose(); } } diff --git a/www/analytics/core/Session/SaveHandler/DbTable.php b/www/analytics/core/Session/SaveHandler/DbTable.php index 20494fe9..03b4afa8 100644 --- a/www/analytics/core/Session/SaveHandler/DbTable.php +++ b/www/analytics/core/Session/SaveHandler/DbTable.php @@ -1,6 +1,6 @@ config = $config; $this->maxLifetime = ini_get('session.gc_maxlifetime'); @@ -78,8 +78,9 @@ class DbTable implements Zend_Session_SaveHandler_Interface . ' AND ' . $this->config['modifiedColumn'] . ' + ' . $this->config['lifetimeColumn'] . ' >= ?'; $result = Db::get()->fetchOne($sql, array($id, time())); - if (!$result) + if (!$result) { $result = ''; + } return $result; } @@ -118,8 +119,7 @@ class DbTable implements Zend_Session_SaveHandler_Interface */ public function destroy($id) { - $sql = 'DELETE FROM ' . $this->config['name'] - . ' WHERE ' . $this->config['primary'] . ' = ?'; + $sql = 'DELETE FROM ' . $this->config['name'] . ' WHERE ' . $this->config['primary'] . ' = ?'; Db::get()->query($sql, array($id)); diff --git a/www/analytics/core/Session/SessionNamespace.php b/www/analytics/core/Session/SessionNamespace.php index 8f31a7e5..90a46625 100644 --- a/www/analytics/core/Session/SessionNamespace.php +++ b/www/analytics/core/Session/SessionNamespace.php @@ -1,6 +1,6 @@ findComponents('Settings', 'Piwik\\Plugin\\Settings'); + $byPluginName = array(); - $settings = array(); - - $pluginNames = PluginManager::getInstance()->getLoadedPluginsName(); - foreach ($pluginNames as $pluginName) { - $settings[$pluginName] = self::getPluginSettingsClass($pluginName); + foreach ($settings as $setting) { + $byPluginName[$setting->getPluginName()] = $setting; } - static::$settings = array_filter($settings); + static::$settings = $byPluginName; } return static::$settings; @@ -63,7 +62,14 @@ class Manager */ public static function cleanupPluginSettings($pluginName) { - $settings = self::getPluginSettingsClass($pluginName); + $pluginManager = PluginManager::getInstance(); + + if (!$pluginManager->isPluginLoaded($pluginName)) { + return; + } + + $plugin = $pluginManager->loadPlugin($pluginName); + $settings = $plugin->findComponent('Settings', 'Piwik\\Plugin\\Settings'); if (!empty($settings)) { $settings->removeAllPluginSettings(); @@ -95,39 +101,57 @@ class Manager return $settingsForUser; } - public static function hasPluginSettingsForCurrentUser($pluginName) + public static function hasSystemPluginSettingsForCurrentUser($pluginName) { - $pluginNames = array_keys(static::getPluginSettingsForCurrentUser()); + $pluginNames = static::getPluginNamesHavingSystemSettings(); return in_array($pluginName, $pluginNames); } /** - * Detects whether there are settings for activated plugins available that the current user can change. + * Detects whether there are user settings for activated plugins available that the current user can change. * * @return bool */ - public static function hasPluginsSettingsForCurrentUser() + public static function hasUserPluginsSettingsForCurrentUser() { $settings = static::getPluginSettingsForCurrentUser(); + foreach ($settings as $setting) { + foreach ($setting->getSettingsForCurrentUser() as $set) { + if ($set instanceof UserSetting) { + return true; + } + } + } + + return false; + } + + public static function getPluginNamesHavingSystemSettings() + { + $settings = static::getPluginSettingsForCurrentUser(); + $plugins = array(); + + foreach ($settings as $pluginName => $setting) { + foreach ($setting->getSettingsForCurrentUser() as $set) { + if ($set instanceof SystemSetting) { + $plugins[] = $pluginName; + } + } + } + + return array_unique($plugins); + } + /** + * Detects whether there are system settings for activated plugins available that the current user can change. + * + * @return bool + */ + public static function hasSystemPluginsSettingsForCurrentUser() + { + $settings = static::getPluginNamesHavingSystemSettings(); + return !empty($settings); } - - /** - * Tries to find a settings class for the specified plugin name. Returns null in case the plugin does not specify - * any settings, an instance of the settings class otherwise. - * - * @param string $pluginName - * @return \Piwik\Plugin\Settings|null - */ - private static function getPluginSettingsClass($pluginName) - { - $klassName = 'Piwik\\Plugins\\' . $pluginName . '\\Settings'; - - if (class_exists($klassName) && is_subclass_of($klassName, 'Piwik\\Plugin\\Settings')) { - return new $klassName($pluginName); - } - } - } diff --git a/www/analytics/core/Settings/Setting.php b/www/analytics/core/Settings/Setting.php index c36aa246..bf8947b1 100644 --- a/www/analytics/core/Settings/Setting.php +++ b/www/analytics/core/Settings/Setting.php @@ -1,6 +1,6 @@ 3)`. Attributes will be escaped before outputting. - * + * * @var array */ public $uiControlAttributes = array(); /** * The list of all available values for this setting. If null, the setting can have any value. - * + * * If supplied, this field should be an array mapping available values with their prettified * display value. Eg, if set to `array('nb_visits' => 'Visits', 'nb_actions' => 'Actions')`, * the UI will display **Visits** and **Actions**, and when the user selects one, Piwik will * set the setting to **nb_visits** or **nb_actions** respectively. - * + * * The setting value will be validated if this field is set. If the value is not one of the * available values, an error will be triggered. - * + * * _Note: If a custom validator is supplied (see {@link $validate}), the setting value will * not be validated._ * @@ -63,7 +66,7 @@ abstract class Setting /** * Text that will appear above this setting's section in the _Plugin Settings_ admin page. - * + * * @var null|string */ public $introduction = null; @@ -71,7 +74,7 @@ abstract class Setting /** * Text that will appear directly underneath the setting title in the _Plugin Settings_ admin * page. If set, should be a short description of the setting. - * + * * @var null|string */ public $description = null; @@ -80,20 +83,20 @@ abstract class Setting * Text that will appear next to the setting's section in the _Plugin Settings_ admin page. If set, * it should contain information about the setting that is more specific than a general description, * such as the format of the setting value if it has a special format. - * + * * @var null|string */ public $inlineHelp = null; /** * A closure that does some custom validation on the setting before the setting is persisted. - * + * * The closure should take two arguments: the setting value and the {@link Setting} instance being * validated. If the value is found to be invalid, the closure should throw an exception with * a message that describes the error. - * + * * **Example** - * + * * $setting->validate = function ($value, Setting $setting) { * if ($value > 60) { * throw new \Exception('The time limit is not allowed to be greater than 60 minutes.'); @@ -107,13 +110,13 @@ abstract class Setting /** * A closure that transforms the setting value. If supplied, this closure will be executed after * the setting has been validated. - * + * * _Note: If a transform is supplied, the setting's {@link $type} has no effect. This means the * transformation function will be responsible for casting the setting value to the appropriate * data type._ * * **Example** - * + * * $setting->transform = function ($value, Setting $setting) { * if ($value > 30) { * $value = 30; @@ -128,7 +131,7 @@ abstract class Setting /** * Default value of this setting. - * + * * The default value is not casted to the appropriate data type. This means _**you**_ have to make * sure the value is of the correct type. * @@ -145,12 +148,12 @@ abstract class Setting protected $key; protected $name; - protected $displayedForCurrentUser = false; /** * @var StorageInterface */ private $storage; + protected $pluginName; /** * Constructor. @@ -168,7 +171,7 @@ abstract class Setting /** * Returns the setting's persisted name, eg, `'refreshInterval'`. - * + * * @return string */ public function getName() @@ -177,46 +180,142 @@ abstract class Setting } /** - * Returns `true` if this setting can be displayed for the current user, `false` if otherwise. - * + * Returns `true` if this setting is writable for the current user, `false` if otherwise. In case it returns + * writable for the current user it will be visible in the Plugin settings UI. + * * @return bool */ - public function canBeDisplayedForCurrentUser() + public function isWritableByCurrentUser() { - return $this->displayedForCurrentUser; + return false; + } + + /** + * Returns `true` if this setting can be displayed for the current user, `false` if otherwise. + * + * @return bool + */ + public function isReadableByCurrentUser() + { + return false; } /** * Sets the object used to persist settings. - * - * @return StorageInterface + * + * @param StorageInterface $storage */ public function setStorage(StorageInterface $storage) { $this->storage = $storage; } + /** + * @internal + * @ignore + * @return StorageInterface + */ + public function getStorage() + { + return $this->storage; + } + + /** + * Sets th name of the plugin the setting belongs to + * + * @param string $pluginName + */ + public function setPluginName($pluginName) + { + $this->pluginName = $pluginName; + } + /** * Returns the previously persisted setting value. If no value was set, the default value * is returned. - * + * * @return mixed * @throws \Exception If the current user is not allowed to change the value of this setting. */ public function getValue() { - return $this->storage->getSettingValue($this); + $this->checkHasEnoughReadPermission(); + + return $this->storage->getValue($this); + } + + /** + * Returns the previously persisted setting value. If no value was set, the default value + * is returned. + * + * @return mixed + * @throws \Exception If the current user is not allowed to change the value of this setting. + */ + public function removeValue() + { + $this->checkHasEnoughWritePermission(); + + return $this->storage->deleteValue($this); } /** * Sets and persists this setting's value overwriting any existing value. - * + * * @param mixed $value * @throws \Exception If the current user is not allowed to change the value of this setting. */ public function setValue($value) { - return $this->storage->setSettingValue($this, $value); + $this->validateValue($value); + + if ($this->transform && $this->transform instanceof \Closure) { + $value = call_user_func($this->transform, $value, $this); + } elseif (isset($this->type)) { + settype($value, $this->type); + } + + return $this->storage->setValue($this, $value); + } + + private function validateValue($value) + { + $this->checkHasEnoughWritePermission(); + + if ($this->validate && $this->validate instanceof \Closure) { + call_user_func($this->validate, $value, $this); + } + } + + /** + * @throws \Exception + */ + private function checkHasEnoughWritePermission() + { + // When the request is a Tracker request, allow plugins to write settings + if (SettingsServer::isTrackerApiRequest()) { + return; + } + + if (!$this->isWritableByCurrentUser()) { + $errorMsg = Piwik::translate('CoreAdminHome_PluginSettingChangeNotAllowed', array($this->getName(), $this->pluginName)); + throw new \Exception($errorMsg); + } + } + + /** + * @throws \Exception + */ + private function checkHasEnoughReadPermission() + { + // When the request is a Tracker request, allow plugins to read settings + if (SettingsServer::isTrackerApiRequest()) { + return; + } + + if (!$this->isReadableByCurrentUser()) { + $errorMsg = Piwik::translate('CoreAdminHome_PluginSettingReadNotAllowed', array($this->getName(), $this->pluginName)); + throw new \Exception($errorMsg); + } } /** @@ -231,7 +330,7 @@ abstract class Setting /** * Returns the display order. The lower the return value, the earlier the setting will be displayed. - * + * * @return int */ public function getOrder() diff --git a/www/analytics/core/Settings/Storage.php b/www/analytics/core/Settings/Storage.php new file mode 100644 index 00000000..131c01b1 --- /dev/null +++ b/www/analytics/core/Settings/Storage.php @@ -0,0 +1,148 @@ + [setting-value] ). + * + * @var array + */ + protected $settingsValues = array(); + + // for lazy loading of setting values + private $settingValuesLoaded = false; + + private $pluginName; + + public function __construct($pluginName) + { + $this->pluginName = $pluginName; + } + + /** + * Saves (persists) the current setting values in the database. + */ + public function save() + { + $this->loadSettingsIfNotDoneYet(); + + Option::set($this->getOptionKey(), serialize($this->settingsValues)); + } + + /** + * Removes all settings for this plugin from the database. Useful when uninstalling + * a plugin. + */ + public function deleteAllValues() + { + $this->deleteSettingsFromStorage(); + + $this->settingsValues = array(); + $this->settingValuesLoaded = false; + } + + protected function deleteSettingsFromStorage() + { + Option::delete($this->getOptionKey()); + } + + /** + * Returns the current value for a setting. If no value is stored, the default value + * is be returned. + * + * @param Setting $setting + * @return mixed + * @throws \Exception If the setting does not exist or if the current user is not allowed to change the value + * of this setting. + */ + public function getValue(Setting $setting) + { + $this->loadSettingsIfNotDoneYet(); + + if (array_key_exists($setting->getKey(), $this->settingsValues)) { + return $this->settingsValues[$setting->getKey()]; + } + + return $setting->defaultValue; + } + + /** + * Sets (overwrites) the value of a setting in memory. To persist the change, {@link save()} must be + * called afterwards, otherwise the change has no effect. + * + * Before the setting is changed, the {@link Piwik\Settings\Setting::$validate} and + * {@link Piwik\Settings\Setting::$transform} closures will be invoked (if defined). If there is no validation + * filter, the setting value will be casted to the appropriate data type. + * + * @param Setting $setting + * @param string $value + * @throws \Exception If the setting does not exist or if the current user is not allowed to change the value + * of this setting. + */ + public function setValue(Setting $setting, $value) + { + $this->loadSettingsIfNotDoneYet(); + + $this->settingsValues[$setting->getKey()] = $value; + } + + /** + * Unsets a setting value in memory. To persist the change, {@link save()} must be + * called afterwards, otherwise the change has no effect. + * + * @param Setting $setting + */ + public function deleteValue(Setting $setting) + { + $this->loadSettingsIfNotDoneYet(); + + $key = $setting->getKey(); + + if (array_key_exists($key, $this->settingsValues)) { + unset($this->settingsValues[$key]); + } + } + + public function getOptionKey() + { + return 'Plugin_' . $this->pluginName . '_Settings'; + } + + private function loadSettingsIfNotDoneYet() + { + if ($this->settingValuesLoaded) { + return; + } + + $this->settingValuesLoaded = true; + $this->settingsValues = $this->loadSettings(); + } + + protected function loadSettings() + { + $values = Option::get($this->getOptionKey()); + + if (!empty($values)) { + return unserialize($values); + } + + return array(); + } +} diff --git a/www/analytics/core/Settings/Storage/Factory.php b/www/analytics/core/Settings/Storage/Factory.php new file mode 100644 index 00000000..87d54ad3 --- /dev/null +++ b/www/analytics/core/Settings/Storage/Factory.php @@ -0,0 +1,28 @@ +displayedForCurrentUser = Piwik::hasUserSuperUserAccess(); + $this->writableByCurrentUser = Piwik::hasUserSuperUserAccess(); + $this->readableByCurrentUser = $this->writableByCurrentUser; + } + + /** + * Returns `true` if this setting is writable for the current user, `false` if otherwise. In case it returns + * writable for the current user it will be visible in the Plugin settings UI. + * + * @return bool + */ + public function isWritableByCurrentUser() + { + if ($this->hasConfigValue()) { + return false; + } + + return $this->writableByCurrentUser; + } + + /** + * Returns `true` if this setting can be displayed for the current user, `false` if otherwise. + * + * @return bool + */ + public function isReadableByCurrentUser() + { + return $this->readableByCurrentUser; } /** * Returns the display order. System settings are displayed before user settings. - * + * * @return int */ public function getOrder() { return 30; } + + public function getValue() + { + $defaultValue = parent::getValue(); // we access value first to make sure permissions are checked + + $configValue = $this->getValueFromConfig(); + + if (isset($configValue)) { + $defaultValue = $configValue; + settype($defaultValue, $this->type); + } + + return $defaultValue; + } + + private function hasConfigValue() + { + $value = $this->getValueFromConfig(); + return isset($value); + } + + private function getValueFromConfig() + { + $config = Config::getInstance()->{$this->pluginName}; + + if (!empty($config) && array_key_exists($this->name, $config)) { + return $config[$this->name]; + } + } + } diff --git a/www/analytics/core/Settings/UserSetting.php b/www/analytics/core/Settings/UserSetting.php index caeb918e..aa3a08c4 100644 --- a/www/analytics/core/Settings/UserSetting.php +++ b/www/analytics/core/Settings/UserSetting.php @@ -1,6 +1,6 @@ setUserLogin($userLogin); + } - $this->displayedForCurrentUser = Piwik::isUserHasSomeViewAccess(); + /** + * Returns `true` if this setting can be displayed for the current user, `false` if otherwise. + * + * @return bool + */ + public function isReadableByCurrentUser() + { + return $this->isWritableByCurrentUser(); + } + + /** + * Returns `true` if this setting can be displayed for the current user, `false` if otherwise. + * + * @return bool + */ + public function isWritableByCurrentUser() + { + if (isset($this->hasReadAndWritePermission)) { + return $this->hasReadAndWritePermission; + } + + $this->hasReadAndWritePermission = Piwik::isUserHasSomeViewAccess(); + + return $this->hasReadAndWritePermission; } /** * Returns the display order. User settings are displayed after system settings. - * + * * @return int */ public function getOrder() @@ -100,16 +131,13 @@ class UserSetting extends Setting $pluginsSettings = Manager::getAllPluginSettings(); foreach ($pluginsSettings as $pluginSettings) { - $settings = $pluginSettings->getSettings(); foreach ($settings as $setting) { - if ($setting instanceof UserSetting) { $setting->setUserLogin($userLogin); - $pluginSettings->removeSettingValue($setting); + $setting->removeValue(); } - } $pluginSettings->save(); diff --git a/www/analytics/core/SettingsPiwik.php b/www/analytics/core/SettingsPiwik.php index 969e21a3..b0df27b2 100644 --- a/www/analytics/core/SettingsPiwik.php +++ b/www/analytics/core/SettingsPiwik.php @@ -1,6 +1,6 @@ General['disable_checks_usernames_attributes'] == 0; } - /** - * @see getKnownSegmentsToArchive - * - * @var array - */ - public static $cachedKnownSegmentsToArchive = null; - /** * Returns every stored segment to pre-process for each site during cron archiving. * @@ -55,83 +50,98 @@ class SettingsPiwik */ public static function getKnownSegmentsToArchive() { - if (self::$cachedKnownSegmentsToArchive === null) { - $segments = Config::getInstance()->Segments; - $segmentsToProcess = isset($segments['Segments']) ? $segments['Segments'] : array(); - - /** - * Triggered during the cron archiving process to collect segments that - * should be pre-processed for all websites. The archiving process will be launched - * for each of these segments when archiving data. - * - * This event can be used to add segments to be pre-processed. If your plugin depends - * on data from a specific segment, this event could be used to provide enhanced - * performance. - * - * _Note: If you just want to add a segment that is managed by the user, use the - * SegmentEditor API._ - * - * **Example** - * - * Piwik::addAction('Segments.getKnownSegmentsToArchiveAllSites', function (&$segments) { - * $segments[] = 'country=jp;city=Tokyo'; - * }); - * - * @param array &$segmentsToProcess List of segment definitions, eg, - * - * array( - * 'browserCode=ff;resolution=800x600', - * 'country=jp;city=Tokyo' - * ) - * - * Add segments to this array in your event handler. - */ - Piwik::postEvent('Segments.getKnownSegmentsToArchiveAllSites', array(&$segmentsToProcess)); - - self::$cachedKnownSegmentsToArchive = array_unique($segmentsToProcess); + $cacheId = 'KnownSegmentsToArchive'; + $cache = PiwikCache::getTransientCache(); + if ($cache->contains($cacheId)) { + return $cache->fetch($cacheId); } - return self::$cachedKnownSegmentsToArchive; + $segments = Config::getInstance()->Segments; + $segmentsToProcess = isset($segments['Segments']) ? $segments['Segments'] : array(); + + /** + * Triggered during the cron archiving process to collect segments that + * should be pre-processed for all websites. The archiving process will be launched + * for each of these segments when archiving data. + * + * This event can be used to add segments to be pre-processed. If your plugin depends + * on data from a specific segment, this event could be used to provide enhanced + * performance. + * + * _Note: If you just want to add a segment that is managed by the user, use the + * SegmentEditor API._ + * + * **Example** + * + * Piwik::addAction('Segments.getKnownSegmentsToArchiveAllSites', function (&$segments) { + * $segments[] = 'country=jp;city=Tokyo'; + * }); + * + * @param array &$segmentsToProcess List of segment definitions, eg, + * + * array( + * 'browserCode=ff;resolution=800x600', + * 'country=jp;city=Tokyo' + * ) + * + * Add segments to this array in your event handler. + */ + Piwik::postEvent('Segments.getKnownSegmentsToArchiveAllSites', array(&$segmentsToProcess)); + + $segmentsToProcess = array_unique($segmentsToProcess); + + $cache->save($cacheId, $segmentsToProcess); + return $segmentsToProcess; } /** * Returns the list of stored segments to pre-process for an individual site when executing * cron archiving. - * + * * @param int $idSite The ID of the site to get stored segments for. - * @return string The list of stored segments that apply to the requested site. + * @return string[] The list of stored segments that apply to the requested site. */ public static function getKnownSegmentsToArchiveForSite($idSite) { - $segments = array(); + $cacheId = 'KnownSegmentsToArchiveForSite' . $idSite; + $cache = PiwikCache::getTransientCache(); + if ($cache->contains($cacheId)) { + return $cache->fetch($cacheId); + } + $segments = array(); /** * Triggered during the cron archiving process to collect segments that * should be pre-processed for one specific site. The archiving process will be launched * for each of these segments when archiving data for that one site. - * + * * This event can be used to add segments to be pre-processed for one site. - * + * * _Note: If you just want to add a segment that is managed by the user, you should use the * SegmentEditor API._ - * + * * **Example** - * + * * Piwik::addAction('Segments.getKnownSegmentsToArchiveForSite', function (&$segments, $idSite) { * $segments[] = 'country=jp;city=Tokyo'; * }); - * + * * @param array &$segmentsToProcess List of segment definitions, eg, - * + * * array( * 'browserCode=ff;resolution=800x600', * 'country=JP;city=Tokyo' * ) - * + * * Add segments to this array in your event handler. * @param int $idSite The ID of the site to get segments for. */ Piwik::postEvent('Segments.getKnownSegmentsToArchiveForSite', array(&$segments, $idSite)); + + $segments = array_unique($segments); + + $cache->save($cacheId, $segments); + return $segments; } @@ -159,7 +169,7 @@ class SettingsPiwik $isPiwikCoreDispatching = defined('PIWIK_ENABLE_DISPATCH') && PIWIK_ENABLE_DISPATCH; if (Common::isPhpCliMode() - // in case archive.php is triggered with domain localhost + // in case core:archive command is triggered (often with localhost domain) || SettingsServer::isArchivePhpTriggered() // When someone else than core is dispatching this request then we return the URL as it is read only || !$isPiwikCoreDispatching @@ -169,17 +179,23 @@ class SettingsPiwik $currentUrl = Common::sanitizeInputValue(Url::getCurrentUrlWithoutFileName()); + // when script is called from /misc/cron/archive.php, Piwik URL is /index.php + $currentUrl = str_replace("/misc/cron", "", $currentUrl); + if (empty($url) // if URL changes, always update the cache || $currentUrl != $url ) { - if (strlen($currentUrl) >= strlen('http://a/')) { + $host = Url::getHostFromUrl($url); + + if (strlen($currentUrl) >= strlen('http://a/') + && !Url::isLocalHost($host)) { self::overwritePiwikUrl($currentUrl); } $url = $currentUrl; } - if(ProxyHttp::isHttps()) { + if (ProxyHttp::isHttps()) { $url = str_replace("http://", "https://", $url); } return $url; @@ -191,11 +207,29 @@ class SettingsPiwik */ public static function isPiwikInstalled() { - $config = Config::getInstance()->getLocalConfigPath(); + $config = Config::getInstance()->getLocalPath(); $exists = file_exists($config); // Piwik is installed if the config file is found - return $exists; + if (!$exists) { + return false; + } + + $general = Config::getInstance()->General; + + $isInstallationInProgress = false; + if (array_key_exists('installation_in_progress', $general)) { + $isInstallationInProgress = (bool) $general['installation_in_progress']; + } + if ($isInstallationInProgress) { + return false; + } + + // Check that the database section is really set, ie. file is not empty + if (empty(Config::getInstance()->database['username'])) { + return false; + } + return true; } /** @@ -212,7 +246,7 @@ class SettingsPiwik /** * Returns true if unique visitors should be processed for the given period type. - * + * * Unique visitor processing is controlled by the `[General] enable_processing_unique_visitors_...` * INI config options. By default, unique visitors are processed only for day/week/month periods. * @@ -237,38 +271,31 @@ class SettingsPiwik return $result; } - /** - * If Piwik uses per-domain config file, also make tmp/ folder per-domain - * @param $path - * @return string - * @throws \Exception - */ - public static function rewriteTmpPathWithHostname($path) - { - $tmp = '/tmp/'; - $path = self::rewritePathAppendHostname($path, $tmp); - return $path; - } - /** * If Piwik uses per-domain config file, make sure CustomLogo is unique * @param $path * @return mixed */ - public static function rewriteMiscUserPathWithHostname($path) + public static function rewriteMiscUserPathWithInstanceId($path) { $tmp = 'misc/user/'; - $path = self::rewritePathAppendHostname($path, $tmp); + $path = self::rewritePathAppendPiwikInstanceId($path, $tmp); return $path; } /** * Returns true if the Piwik server appears to be working. * + * If the Piwik server is in an error state (eg. some directories are not writable and Piwik displays error message), + * or if the Piwik server is "offline", + * this will return false.. + * * @param $piwikServerUrl + * @param bool $acceptInvalidSSLCertificates + * @throws Exception * @return bool */ - static public function checkPiwikServerWorking($piwikServerUrl, $acceptInvalidSSLCertificates = false) + public static function checkPiwikServerWorking($piwikServerUrl, $acceptInvalidSSLCertificates = false) { // Now testing if the webserver is running try { @@ -285,9 +312,22 @@ class SettingsPiwik } catch (Exception $e) { $fetched = "ERROR fetching: " . $e->getMessage(); } - $expectedString = 'plugins/CoreHome/images/favicon.ico'; + // this will match when Piwik not installed yet, or favicon not customised + $expectedStringAlt = 'plugins/CoreHome/images/favicon.png'; - if (strpos($fetched, $expectedString) === false) { + // this will match when Piwik is installed and favicon has been customised + $expectedString = 'misc/user/'; + + // see checkPiwikIsNotInstalled() + $expectedStringAlreadyInstalled = 'piwik-is-already-installed'; + + $expectedStringNotFound = strpos($fetched, $expectedString) === false + && strpos($fetched, $expectedStringAlt) === false + && strpos($fetched, $expectedStringAlreadyInstalled) === false; + + $hasError = false !== strpos($fetched, PAGE_TITLE_WHEN_ERROR); + + if ($hasError || $expectedStringNotFound) { throw new Exception("\nPiwik should be running at: " . $piwikServerUrl . " but this URL returned an unexpected response: '" @@ -295,10 +335,21 @@ class SettingsPiwik } } + /** + * Returns true if Piwik is deployed using git + * FAQ: http://piwik.org/faq/how-to-install/faq_18271/ + * + * @return bool + */ + public static function isGitDeployment() + { + return file_exists(PIWIK_INCLUDE_PATH . '/.git/HEAD'); + } + public static function getCurrentGitBranch() { $file = PIWIK_INCLUDE_PATH . '/.git/HEAD'; - if(!file_exists($file)) { + if (!file_exists($file)) { return ''; } $firstLineOfGitHead = file($file); @@ -321,10 +372,10 @@ class SettingsPiwik * @return mixed * @throws \Exception */ - protected static function rewritePathAppendHostname($pathToRewrite, $leadingPathToAppendHostnameTo) + protected static function rewritePathAppendPiwikInstanceId($pathToRewrite, $leadingPathToAppendHostnameTo) { - $hostname = self::getConfigHostname(); - if (empty($hostname)) { + $instanceId = self::getPiwikInstanceId(); + if (empty($instanceId)) { return $pathToRewrite; } @@ -332,7 +383,7 @@ class SettingsPiwik throw new Exception("The path $pathToRewrite was expected to contain the string $leadingPathToAppendHostnameTo"); } - $tmpToReplace = $leadingPathToAppendHostnameTo . $hostname . '/'; + $tmpToReplace = $leadingPathToAppendHostnameTo . $instanceId . '/'; // replace only the latest occurrence (in case path contains twice /tmp) $pathToRewrite = substr_replace($pathToRewrite, $tmpToReplace, $posTmp, strlen($leadingPathToAppendHostnameTo)); @@ -340,18 +391,30 @@ class SettingsPiwik } /** - * @return bool|string + * @throws \Exception + * @return string or False if not set */ - protected static function getConfigHostname() + protected static function getPiwikInstanceId() { - $configByHost = false; - try { - $configByHost = Config::getInstance()->getConfigHostnameIfSet(); - return $configByHost; - } catch (Exception $e) { - // Config file not found + // until Piwik is installed, we use hostname as instance_id + if (!self::isPiwikInstalled() + && Common::isPhpCliMode()) { + // enterprise:install use case + return Config::getHostname(); } - return $configByHost; + + // config.ini.php not ready yet, instance_id will not be set + if (!Config::getInstance()->existsLocalConfig()) { + return false; + } + + $instanceId = @Config::getInstance()->General['instance_id']; + if (!empty($instanceId)) { + return $instanceId; + } + + // do not rewrite the path as Piwik uses the standard config.ini.php file + return false; } /** @@ -367,6 +430,20 @@ class SettingsPiwik */ public static function isHttpsForced() { + if (!SettingsPiwik::isPiwikInstalled()) { + // Only enable this feature after Piwik is already installed + return false; + } return Config::getInstance()->General['force_ssl'] == 1; } + + /** + * Note: this config settig is also checked in the InterSites plugin + * + * @return bool + */ + public static function isSameFingerprintAcrossWebsites() + { + return (bool)Config::getInstance()->Tracker['enable_fingerprinting_across_websites']; + } } diff --git a/www/analytics/core/SettingsServer.php b/www/analytics/core/SettingsServer.php index e666c3fa..d84e3ff4 100644 --- a/www/analytics/core/SettingsServer.php +++ b/www/analytics/core/SettingsServer.php @@ -1,6 +1,6 @@ General['minimum_memory_limit']; if (self::isArchivePhpTriggered()) { - // archive.php: no time limit, high memory limit + // core:archive command: no time limit, high memory limit self::setMaxExecutionTime(0); $minimumMemoryLimitWhenArchiving = Config::getInstance()->General['minimum_memory_limit_when_archiving']; if ($memoryLimit < $minimumMemoryLimitWhenArchiving) { diff --git a/www/analytics/core/Singleton.php b/www/analytics/core/Singleton.php index c1f8682d..46e3c531 100644 --- a/www/analytics/core/Singleton.php +++ b/www/analytics/core/Singleton.php @@ -1,6 +1,6 @@ getName(); - * + * * **Without allocation** - * + * * $name = Site::getNameFor($idSite); - * + * * @api */ class Site @@ -54,7 +57,7 @@ class Site /** * Constructor. - * + * * @param int $idsite The ID of the site we want data for. */ public function __construct($idsite) @@ -62,7 +65,7 @@ class Site $this->id = (int)$idsite; if (!isset(self::$infoSites[$this->id])) { $site = API::getInstance()->getSiteFromId($this->id); - self::setSite($this->id, $site); + self::setSiteFromArray($this->id, $site); } } @@ -71,17 +74,44 @@ class Site * individual site data. * * @param array $sites The array of sites data. Indexed by site ID. eg, - * + * * array('1' => array('name' => 'Site 1', ...), * '2' => array('name' => 'Site 2', ...))` */ public static function setSites($sites) { - foreach($sites as $idsite => $site) { - self::setSite($idsite, $site); + self::triggerSetSitesEvent($sites); + + foreach ($sites as $idsite => $site) { + self::setSiteFromArray($idsite, $site); } } + private static function triggerSetSitesEvent(&$sites) + { + /** + * Triggered so plugins can modify website entities without modifying the database. + * + * This event should **not** be used to add data that is expensive to compute. If you + * need to make HTTP requests or query the database for more information, this is not + * the place to do it. + * + * **Example** + * + * Piwik::addAction('Site.setSites', function (&$sites) { + * foreach ($sites as &$site) { + * $site['name'] .= " (original)"; + * } + * }); + * + * @param array $sites An array of website entities. [Learn more.](/guides/persistence-and-the-mysql-backend#websites-aka-sites) + * + * This is not yet public as it doesn't work 100% accurately. Eg if `setSiteFromArray()` is called directly this event will not be triggered. + * @ignore + */ + Piwik::postEvent('Site.setSites', array(&$sites)); + } + /** * Sets a site information in memory (statically cached). * @@ -92,38 +122,20 @@ class Site * @param $infoSite * @throws Exception if website or idsite is invalid */ - protected static function setSite($idSite, $infoSite) + public static function setSiteFromArray($idSite, $infoSite) { - if(empty($idSite) || empty($infoSite)) { - throw new Exception("An unexpected website was found, check idSite in the request."); + if (empty($idSite) || empty($infoSite)) { + throw new UnexpectedWebsiteFoundException("An unexpected website was found in the request: website id was set to '$idSite' ."); } - /** - * Triggered so plugins can modify website entities without modifying the database. - * - * This event should **not** be used to add data that is expensive to compute. If you - * need to make HTTP requests or query the database for more information, this is not - * the place to do it. - * - * **Example** - * - * Piwik::addAction('Site.setSite', function ($idSite, &$info) { - * $info['name'] .= " (original)"; - * }); - * - * @param int $idSite The ID of the website entity that will be modified. - * @param array $infoSite The website entity. [Learn more.](/guides/persistence-and-the-mysql-backend#websites-aka-sites) - */ - Piwik::postEvent('Site.setSite', array($idSite, &$infoSite)); - self::$infoSites[$idSite] = $infoSite; } /** * Sets the cached Site data with a non-associated array of site data. - * + * * @param array $sites The array of sites data. eg, - * + * * array( * array('idsite' => '1', 'name' => 'Site 1', ...), * array('idsite' => '2', 'name' => 'Site 2', ...), @@ -131,8 +143,15 @@ class Site */ public static function setSitesFromArray($sites) { + self::triggerSetSitesEvent($sites); + foreach ($sites as $site) { - self::setSite($site['idsite'], $site); + $idSite = null; + if (!empty($site['idsite'])) { + $idSite = $site['idsite']; + } + + self::setSiteFromArray($idSite, $site); } } @@ -172,9 +191,9 @@ class Site /** * Returns a string representation of the site this instance references. - * + * * Useful for debugging. - * + * * @return string */ public function __toString() @@ -223,22 +242,31 @@ class Site /** * Returns a site property by name. - * + * * @param string $name Name of the property to return (eg, `'main_url'` or `'name'`). * @return mixed * @throws Exception */ protected function get($name) { + if (!isset(self::$infoSites[$this->id])) { + $site = API::getInstance()->getSiteFromId($this->id); + + if (empty($site)) { + throw new UnexpectedWebsiteFoundException('The requested website id = ' . (int)$this->id . ' couldn\'t be found'); + } + + self::setSiteFromArray($this->id, $site); + } if (!isset(self::$infoSites[$this->id][$name])) { - throw new Exception('The requested website id = ' . (int)$this->id . ' (or its property ' . $name . ') couldn\'t be found'); + throw new Exception("The property $name could not be found on the website ID " . (int)$this->id); } return self::$infoSites[$this->id][$name]; } /** * Returns the website type (by default `"website"`, which means it is a single website). - * + * * @return string */ public function getType() @@ -316,7 +344,7 @@ class Site /** * Returns the site search keyword query parameters for the site. - * + * * @return string * @throws Exception if data for the site cannot be found. */ @@ -327,7 +355,7 @@ class Site /** * Returns the site search category query parameters for the site. - * + * * @return string * @throws Exception if data for the site cannot be found. */ @@ -355,13 +383,13 @@ class Site * @param bool|string $_restrictSitesToLogin Implementation detail. Used only when running as a scheduled task. * @return array An array of valid, unique integers. */ - static public function getIdSitesFromIdSitesString($ids, $_restrictSitesToLogin = false) + public static function getIdSitesFromIdSitesString($ids, $_restrictSitesToLogin = false) { if ($ids === 'all') { return API::getInstance()->getSitesIdWithAtLeastViewAccess($_restrictSitesToLogin); } - if(is_bool($ids)) { + if (is_bool($ids)) { return array(); } if (!is_array($ids)) { @@ -382,10 +410,10 @@ class Site /** * Clears the site data cache. - * + * * See also {@link setSites()} and {@link setSitesFromArray()}. */ - static public function clearCache() + public static function clearCache() { self::$infoSites = array(); } @@ -395,21 +423,17 @@ class Site * site with the specified ID. * * @param int $idsite The ID of the site whose data is being accessed. - * @param bool|string $field The name of the field to get. - * @return array|string + * @param string $field The name of the field to get. + * @return string */ - static protected function getFor($idsite, $field = false) + protected static function getFor($idsite, $field) { - $idsite = (int)$idsite; - if (!isset(self::$infoSites[$idsite])) { $site = API::getInstance()->getSiteFromId($idsite); - self::setSite($idsite, $site); + self::setSiteFromArray($idsite, $site); } - if($field) { - return self::$infoSites[$idsite][$field]; - } - return self::$infoSites[$idsite]; + + return self::$infoSites[$idsite][$field]; } /** @@ -417,7 +441,7 @@ class Site * * @ignore */ - static public function getSites() + public static function getSites() { return self::$infoSites; } @@ -425,9 +449,16 @@ class Site /** * @ignore */ - static public function getSite($id) + public static function getSite($idsite) { - return self::getFor($id); + $idsite = (int)$idsite; + + if (!isset(self::$infoSites[$idsite])) { + $site = API::getInstance()->getSiteFromId($idsite); + self::setSiteFromArray($idsite, $site); + } + + return self::$infoSites[$idsite]; } /** @@ -436,7 +467,7 @@ class Site * @param int $idsite The site ID. * @return string */ - static public function getNameFor($idsite) + public static function getNameFor($idsite) { return self::getFor($idsite, 'name'); } @@ -447,7 +478,7 @@ class Site * @param int $idsite The site ID. * @return string */ - static public function getGroupFor($idsite) + public static function getGroupFor($idsite) { return self::getFor($idsite, 'group'); } @@ -458,7 +489,7 @@ class Site * @param int $idsite The site ID. * @return string */ - static public function getTimezoneFor($idsite) + public static function getTimezoneFor($idsite) { return self::getFor($idsite, 'timezone'); } @@ -469,7 +500,7 @@ class Site * @param $idsite * @return string */ - static public function getTypeFor($idsite) + public static function getTypeFor($idsite) { return self::getFor($idsite, 'type'); } @@ -480,7 +511,7 @@ class Site * @param int $idsite The site ID. * @return string */ - static public function getCreationDateFor($idsite) + public static function getCreationDateFor($idsite) { return self::getFor($idsite, 'ts_created'); } @@ -491,7 +522,7 @@ class Site * @param int $idsite The site ID. * @return string */ - static public function getMainUrlFor($idsite) + public static function getMainUrlFor($idsite) { return self::getFor($idsite, 'main_url'); } @@ -502,7 +533,7 @@ class Site * @param int $idsite The site ID. * @return string */ - static public function isEcommerceEnabledFor($idsite) + public static function isEcommerceEnabledFor($idsite) { return self::getFor($idsite, 'ecommerce') == 1; } @@ -513,7 +544,7 @@ class Site * @param int $idsite The site ID. * @return string */ - static public function isSiteSearchEnabledFor($idsite) + public static function isSiteSearchEnabledFor($idsite) { return self::getFor($idsite, 'sitesearch') == 1; } @@ -524,18 +555,54 @@ class Site * @param int $idsite The site ID. * @return string */ - static public function getCurrencyFor($idsite) + public static function getCurrencyFor($idsite) { return self::getFor($idsite, 'currency'); } + /** + * Returns the currency of the site with the specified ID. + * + * @param int $idsite The site ID. + * @return string + */ + public static function getCurrencySymbolFor($idsite) + { + $currency = self::getCurrencyFor($idsite); + $symbols = self::getCurrencyList(); + + if (isset($symbols[$currency])) { + return $symbols[$currency][0]; + } + + return ''; + } + + + /** + * Returns the list of all known currency symbols. + * + * @return array An array mapping currency codes to their respective currency symbols + * and a description, eg, `array('USD' => array('$', 'US dollar'))`. + * + * @deprecated Use Piwik\Intl\Data\Provider\CurrencyDataProvider instead. + * @see \Piwik\Intl\Data\Provider\CurrencyDataProvider::getCurrencyList() + * @api + */ + public static function getCurrencyList() + { + /** @var CurrencyDataProvider $dataProvider */ + $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\CurrencyDataProvider'); + return $dataProvider->getCurrencyList(); + } + /** * Returns the excluded IP addresses of the site with the specified ID. * * @param int $idsite The site ID. * @return string */ - static public function getExcludedIpsFor($idsite) + public static function getExcludedIpsFor($idsite) { return self::getFor($idsite, 'excluded_ips'); } @@ -546,7 +613,7 @@ class Site * @param int $idsite The site ID. * @return string */ - static public function getExcludedQueryParametersFor($idsite) + public static function getExcludedQueryParametersFor($idsite) { return self::getFor($idsite, 'excluded_parameters'); } diff --git a/www/analytics/core/TCPDF.php b/www/analytics/core/TCPDF.php index 8a2669b5..fbbadbb3 100644 --- a/www/analytics/core/TCPDF.php +++ b/www/analytics/core/TCPDF.php @@ -1,6 +1,6 @@ currentPageNo > 1) { @@ -45,7 +40,7 @@ class TCPDF extends \TCPDF * @param $msg * @throws Exception */ - function Error($msg) + public function Error($msg) { $this->_destroy(true); throw new Exception($msg); @@ -54,7 +49,7 @@ class TCPDF extends \TCPDF /** * Set current page number */ - function setCurrentPageNo() + public function setCurrentPageNo() { if (empty($this->currentPageNo)) { $this->currentPageNo = 1; @@ -73,7 +68,7 @@ class TCPDF extends \TCPDF * @param bool $keepmargins * @param bool $tocpage */ - function AddPage($orientation = '', $format = '', $keepmargins = false, $tocpage = false) + public function AddPage($orientation = '', $format = '', $keepmargins = false, $tocpage = false) { parent::AddPage($orientation); $this->setCurrentPageNo(); @@ -84,7 +79,7 @@ class TCPDF extends \TCPDF * * @param string $footerContent */ - function SetFooterContent($footerContent) + public function SetFooterContent($footerContent) { $this->footerContent = $footerContent; } diff --git a/www/analytics/core/TaskScheduler.php b/www/analytics/core/TaskScheduler.php index dabf4241..46968ad3 100644 --- a/www/analytics/core/TaskScheduler.php +++ b/www/analytics/core/TaskScheduler.php @@ -1,6 +1,6 @@ hourly('myTask'); // myTask() will be executed once every hour + * } + * public function myTask() + * { + * // do something + * } * } - * + * * **Executing all pending tasks** - * + * * $results = TaskScheduler::runTasks(); * $task1Result = $results[0]; * $task1Name = $task1Result['task']; * $task1Output = $task1Result['output']; - * + * * echo "Executed task '$task1Name'. Task output:\n$task1Output"; * - * @method static \Piwik\TaskScheduler getInstance() + * @deprecated Use Piwik\Scheduler\Scheduler instead + * @see \Piwik\Scheduler\Scheduler */ -class TaskScheduler extends Singleton +class TaskScheduler { - const GET_TASKS_EVENT = "TaskScheduler.getScheduledTasks"; - - private $isRunning = false; - - private $timetable = null; - - public function __construct() - { - $this->timetable = new ScheduledTaskTimetable(); - } - /** * Executes tasks that are scheduled to run, then reschedules them. * * @return array An array describing the results of scheduled task execution. Each element * in the array will have the following format: - * + * * ``` * array( * 'task' => 'task name', @@ -76,97 +67,33 @@ class TaskScheduler extends Singleton * ) * ``` */ - static public function runTasks() + public static function runTasks() { - return self::getInstance()->doRunTasks(); - } - - private function doRunTasks() - { - // collect tasks - $tasks = array(); - - /** - * Triggered during scheduled task execution. Collects all the tasks to run. - * - * Subscribe to this event to schedule code execution on an hourly, daily, weekly or monthly - * basis. - * - * **Example** - * - * public function getScheduledTasks(&$tasks) - * { - * $tasks[] = new ScheduledTask( - * 'Piwik\Plugins\CorePluginsAdmin\MarketplaceApiClient', - * 'clearAllCacheEntries', - * null, - * ScheduledTime::factory('daily'), - * ScheduledTask::LOWEST_PRIORITY - * ); - * } - * - * @param ScheduledTask[] &$tasks List of tasks to run periodically. - */ - Piwik::postEvent(self::GET_TASKS_EVENT, array(&$tasks)); - /** @var ScheduledTask[] $tasks */ - - // remove from timetable tasks that are not active anymore - $this->timetable->removeInactiveTasks($tasks); - - // for every priority level, starting with the highest and concluding with the lowest - $executionResults = array(); - for ($priority = ScheduledTask::HIGHEST_PRIORITY; - $priority <= ScheduledTask::LOWEST_PRIORITY; - ++$priority) { - // loop through each task - foreach ($tasks as $task) { - // if the task does not have the current priority level, don't execute it yet - if ($task->getPriority() != $priority) { - continue; - } - - $taskName = $task->getName(); - $shouldExecuteTask = $this->timetable->shouldExecuteTask($taskName); - - if ($this->timetable->taskShouldBeRescheduled($taskName)) { - $this->timetable->rescheduleTask($task); - } - - if ($shouldExecuteTask) { - $this->isRunning = true; - $message = self::executeTask($task); - $this->isRunning = false; - - $executionResults[] = array('task' => $taskName, 'output' => $message); - } - } - } - - return $executionResults; + return self::getInstance()->run(); } /** * Determines a task's scheduled time and persists it, overwriting the previous scheduled time. - * + * * Call this method if your task's scheduled time has changed due to, for example, an option that * was changed. - * - * @param ScheduledTask $task Describes the scheduled task being rescheduled. + * + * @param Task $task Describes the scheduled task being rescheduled. * @api */ - static public function rescheduleTask(ScheduledTask $task) + public static function rescheduleTask(Task $task) { - self::getInstance()->timetable->rescheduleTask($task); + self::getInstance()->rescheduleTask($task); } /** * Returns true if the TaskScheduler is currently running a scheduled task. - * + * * @return bool */ - static public function isTaskBeingExecuted() + public static function isTaskBeingExecuted() { - return self::getInstance()->isRunning; + return self::getInstance()->isRunningTask(); } /** @@ -178,27 +105,16 @@ class TaskScheduler extends Singleton * @return mixed int|bool The time in miliseconds when the scheduled task will be executed * next or false if it is not scheduled to run. */ - static public function getScheduledTimeForMethod($className, $methodName, $methodParameter = null) + public static function getScheduledTimeForMethod($className, $methodName, $methodParameter = null) { - return self::getInstance()->timetable->getScheduledTimeForMethod($className, $methodName, $methodParameter); + return self::getInstance()->getScheduledTimeForMethod($className, $methodName, $methodParameter); } /** - * Executes the given taks - * - * @param ScheduledTask $task - * @return string + * @return Scheduler */ - static private function executeTask($task) + private static function getInstance() { - try { - $timer = new Timer(); - call_user_func(array($task->getObjectInstance(), $task->getMethodName()), $task->getMethodParameter()); - $message = $timer->__toString(); - } catch (Exception $e) { - $message = 'ERROR: ' . $e->getMessage(); - } - - return $message; + return StaticContainer::getContainer()->get('Piwik\Scheduler\Scheduler'); } } diff --git a/www/analytics/core/Theme.php b/www/analytics/core/Theme.php index 48ebbb67..8965a268 100644 --- a/www/analytics/core/Theme.php +++ b/www/analytics/core/Theme.php @@ -1,6 +1,6 @@ theme->getPluginName() . '/' . $jsFile; } return $jsFiles; @@ -97,7 +97,7 @@ class Theme // rewrites images in JS files '~(=)[\s]?[\'"]([^\'"]+[.jpg|.png|.gif|svg]?)[\'"]~', ); - return preg_replace_callback($pattern, array($this,'rewriteAssetPathIfOverridesFound'), $output); + return preg_replace_callback($pattern, array($this, 'rewriteAssetPathIfOverridesFound'), $output); } private function rewriteAssetPathIfOverridesFound($src) @@ -107,18 +107,18 @@ class Theme // Basic health check, we dont replace if not starting with plugins/ $posPluginsInPath = strpos($pathAsset, 'plugins'); - if( $posPluginsInPath !== 0) { + if ($posPluginsInPath !== 0) { return $source; } // or if it's already rewritten - if(strpos($pathAsset, $this->themeName) !== false) { + if (strpos($pathAsset, $this->themeName) !== false) { return $source; } $pathPluginName = substr($pathAsset, strlen('plugins/')); $nextSlash = strpos($pathPluginName, '/'); - if($nextSlash === false) { + if ($nextSlash === false) { return $source; } $pathPluginName = substr($pathPluginName, 0, $nextSlash); @@ -133,11 +133,11 @@ class Theme // Strip trailing query string $fileToCheck = $overridingAsset; $queryStringPos = strpos($fileToCheck, '?'); - if( $queryStringPos !== false) { + if ($queryStringPos !== false) { $fileToCheck = substr($fileToCheck, 0, $queryStringPos); } - if(file_exists($fileToCheck)) { + if (file_exists($fileToCheck)) { return str_replace($pathAsset, $overridingAsset, $source); } return $source; diff --git a/www/analytics/core/Timer.php b/www/analytics/core/Timer.php index 76bbd3a5..3aea3db1 100644 --- a/www/analytics/core/Timer.php +++ b/www/analytics/core/Timer.php @@ -1,6 +1,6 @@ formatter = new Formatter(); + $this->init(); } @@ -56,7 +61,7 @@ class Timer */ public function getMemoryLeak() { - return "Memory delta: " . MetricsFormatter::getPrettySizeFromBytes($this->getMemoryUsage() - $this->memoryStart); + return "Memory delta: " . $this->formatter->getPrettySizeFromBytes($this->getMemoryUsage() - $this->memoryStart); } /** diff --git a/www/analytics/core/Tracker.php b/www/analytics/core/Tracker.php index c84b3c0a..d64f1b7f 100644 --- a/www/analytics/core/Tracker.php +++ b/www/analytics/core/Tracker.php @@ -1,6 +1,6 @@ isInstalled(); + } + + public static function loadTrackerEnvironment() + { + SettingsServer::setIsTrackerApiRequest(); + $GLOBALS['PIWIK_TRACKER_DEBUG'] = self::isDebugEnabled(); + PluginManager::getInstance()->loadTrackerPlugins(); + } + + private function init() + { + $this->handleFatalErrors(); + + if ($this->isDebugModeEnabled()) { + ErrorHandler::registerErrorHandler(); + ExceptionHandler::setUp(); + + Common::printDebug("Debug enabled - Input parameters: "); + Common::printDebug(var_export($_GET, true)); } } - public function clear() + public function isInstalled() { - self::$forcedIpString = null; - self::$forcedDateTime = null; - self::$forcedVisitorId = null; - $this->stateValid = self::STATE_NOTHING_TO_NOTICE; - } - - public static function setForceIp($ipString) - { - self::$forcedIpString = $ipString; - } - - public static function setForceDateTime($dateTime) - { - self::$forcedDateTime = $dateTime; - } - - public static function setForceVisitorId($visitorId) - { - self::$forcedVisitorId = $visitorId; - } - - /** - * Do not load the specified plugins (used during testing, to disable Provider plugin) - * @param array $plugins - */ - static public function setPluginsNotToLoad($plugins) - { - self::$pluginsNotToLoad = $plugins; - } - - /** - * Get list of plugins to not load - * - * @return array - */ - static public function getPluginsNotToLoad() - { - return self::$pluginsNotToLoad; - } - - /** - * Update Tracker config - * - * @param string $name Setting name - * @param mixed $value Value - */ - static private function updateTrackerConfig($name, $value) - { - $section = Config::getInstance()->Tracker; - $section[$name] = $value; - Config::getInstance()->Tracker = $section; - } - - protected function initRequests($args) - { - $rawData = self::getRawBulkRequest(); - if (!empty($rawData)) { - $this->usingBulkTracking = strpos($rawData, '"requests"') || strpos($rawData, "'requests'"); - if ($this->usingBulkTracking) { - return $this->authenticateBulkTrackingRequests($rawData); - } + if (is_null($this->isInstalled)) { + $this->isInstalled = SettingsPiwik::isPiwikInstalled(); } - // Not using bulk tracking - $this->requests = $args ? $args : (!empty($_GET) || !empty($_POST) ? array($_GET + $_POST) : array()); + return $this->isInstalled; } - private static function getRequestsArrayFromBulkRequest($rawData) - { - $rawData = trim($rawData); - $rawData = Common::sanitizeLineBreaks($rawData); - - // POST data can be array of string URLs or array of arrays w/ visit info - $jsonData = json_decode($rawData, $assoc = true); - - $tokenAuth = Common::getRequestVar('token_auth', false, 'string', $jsonData); - - $requests = array(); - if (isset($jsonData['requests'])) { - $requests = $jsonData['requests']; - } - - return array( $requests, $tokenAuth); - } - - private function authenticateBulkTrackingRequests($rawData) - { - list($this->requests, $tokenAuth) = $this->getRequestsArrayFromBulkRequest($rawData); - - if (empty($tokenAuth)) { - throw new Exception( "token_auth must be specified when using Bulk Tracking Import. " - ." See Tracking Doc"); - } - if (!empty($this->requests)) { - - foreach ($this->requests as &$request) { - // if a string is sent, we assume its a URL and try to parse it - if (is_string($request)) { - $params = array(); - - $url = @parse_url($request); - if (!empty($url)) { - @parse_str($url['query'], $params); - $request = $params; - } - } - - $requestObj = new Request($request, $tokenAuth); - $this->loadTrackerPlugins($requestObj); - - // a Bulk Tracking request that is not authenticated should fail - if (!$requestObj->isAuthenticated()) { - throw new Exception(sprintf("token_auth specified does not have Admin permission for idsite=%s", - $requestObj->getIdSite())); - } - - $request = $requestObj; - } - } - - return $tokenAuth; - } - - /** - * Main - tracks the visit/action - * - * @param array $args Optional Request Array - */ - public function main($args = null) + public function main(Handler $handler, RequestSet $requestSet) { try { - $tokenAuth = $this->initRequests($args); - } catch (Exception $ex) { - $this->exitWithException($ex, true); + $this->init(); + $handler->init($this, $requestSet); + $this->track($handler, $requestSet); + } catch (Exception $e) { + $handler->onException($this, $requestSet, $e); } - $this->initOutputBuffer(); + Piwik::postEvent('Tracker.end'); + $response = $handler->finish($this, $requestSet); - if (!empty($this->requests)) { + $this->disconnectDatabase(); - try { - foreach ($this->requests as $params) { - $isAuthenticated = $this->trackRequest($params, $tokenAuth); - } - $this->runScheduledTasksIfAllowed($isAuthenticated); - } catch(DbException $e) { - Common::printDebug($e->getMessage()); - } - } else { - $this->handleEmptyRequest(new Request($_GET + $_POST)); - } - $this->end(); - - $this->flushOutputBuffer(); + return $response; } - protected function initOutputBuffer() + public function track(Handler $handler, RequestSet $requestSet) { - ob_start(); - } - - protected function flushOutputBuffer() - { - ob_end_flush(); - } - - protected function getOutputBuffer() - { - return ob_get_contents(); - } - - - protected function shouldRunScheduledTasks() - { - // don't run scheduled tasks in CLI mode from Tracker, this is the case - // where we bulk load logs & don't want to lose time with tasks - return !Common::isPhpCliMode() - && $this->getState() != self::STATE_LOGGING_DISABLE; - } - - /** - * Tracker requests will automatically trigger the Scheduled tasks. - * This is useful for users who don't setup the cron, - * but still want daily/weekly/monthly PDF reports emailed automatically. - * - * This is similar to calling the API CoreAdminHome.runScheduledTasks (see misc/cron/archive.php) - */ - protected static function runScheduledTasks() - { - $now = time(); - - // Currently, there are no hourly tasks. When there are some, - // this could be too aggressive minimum interval (some hours would be skipped in case of low traffic) - $minimumInterval = Config::getInstance()->Tracker['scheduled_tasks_min_interval']; - - // If the user disabled browser archiving, he has already setup a cron - // To avoid parallel requests triggering the Scheduled Tasks, - // Get last time tasks started executing - $cache = Cache::getCacheGeneral(); - - if ($minimumInterval <= 0 - || empty($cache['isBrowserTriggerEnabled']) - ) { - Common::printDebug("-> Scheduled tasks not running in Tracker: Browser archiving is disabled."); + if (!$this->shouldRecordStatistics()) { return; } - $nextRunTime = $cache['lastTrackerCronRun'] + $minimumInterval; + $requestSet->initRequestsAndTokenAuth(); - if ((isset($GLOBALS['PIWIK_TRACKER_DEBUG_FORCE_SCHEDULED_TASKS']) && $GLOBALS['PIWIK_TRACKER_DEBUG_FORCE_SCHEDULED_TASKS']) - || $cache['lastTrackerCronRun'] === false - || $nextRunTime < $now - ) { - $cache['lastTrackerCronRun'] = $now; - Cache::setCacheGeneral($cache); - self::initCorePiwikInTrackerMode(); - Option::set('lastTrackerCronRun', $cache['lastTrackerCronRun']); - Common::printDebug('-> Scheduled Tasks: Starting...'); - - // save current user privilege and temporarily assume Super User privilege - $isSuperUser = Piwik::hasUserSuperUserAccess(); - - // Scheduled tasks assume Super User is running - Piwik::setUserHasSuperUserAccess(); - - // While each plugins should ensure that necessary languages are loaded, - // we ensure English translations at least are loaded - Translate::loadEnglishTranslation(); - - ob_start(); - CronArchive::$url = SettingsPiwik::getPiwikUrl(); - $cronArchive = new CronArchive(); - $cronArchive->runScheduledTasksInTrackerMode(); - - $resultTasks = ob_get_contents(); - ob_clean(); - - // restore original user privilege - Piwik::setUserHasSuperUserAccess($isSuperUser); - - foreach (explode('', $resultTasks) as $resultTask) { - Common::printDebug(str_replace('
', '', $resultTask));
-            }
-
-            Common::printDebug('Finished Scheduled Tasks.');
-        } else {
-            Common::printDebug("-> Scheduled tasks not triggered.");
+        if ($requestSet->hasRequests()) {
+            $handler->onStartTrackRequests($this, $requestSet);
+            $handler->process($this, $requestSet);
+            $handler->onAllRequestsTracked($this, $requestSet);
         }
-        Common::printDebug("Next run will be from: " . date('Y-m-d H:i:s', $nextRunTime) . ' UTC');
     }
 
-    static public $initTrackerMode = false;
+    /**
+     * @param Request $request
+     * @return array
+     */
+    public function trackRequest(Request $request)
+    {
+        if ($request->isEmptyRequest()) {
+            Common::printDebug("The request is empty");
+        } else {
+            Common::printDebug("Current datetime: " . date("Y-m-d H:i:s", $request->getCurrentTimestamp()));
+
+            $visit = Visit\Factory::make();
+            $visit->setRequest($request);
+            $visit->handle();
+        }
+
+        // increment successfully logged request count. make sure to do this after try-catch,
+        // since an excluded visit is considered 'successfully logged'
+        ++$this->countOfLoggedRequests;
+    }
 
     /**
      * Used to initialize core Piwik components on a piwik.php request
      * Eg. when cache is missed and we will be calling some APIs to generate cache
      */
-    static public function initCorePiwikInTrackerMode()
+    public static function initCorePiwikInTrackerMode()
     {
         if (SettingsServer::isTrackerApiRequest()
             && self::$initTrackerMode === false
         ) {
             self::$initTrackerMode = true;
-            require_once PIWIK_INCLUDE_PATH . '/core/Loader.php';
             require_once PIWIK_INCLUDE_PATH . '/core/Option.php';
 
-            $access = Access::getInstance();
-            $config = Config::getInstance();
+            Access::getInstance();
+            Config::getInstance();
 
             try {
                 Db::get();
@@ -365,497 +164,174 @@ class Tracker
                 Db::createDatabaseObject();
             }
 
-            \Piwik\Plugin\Manager::getInstance()->loadCorePluginsDuringTracker();
+            PluginManager::getInstance()->loadCorePluginsDuringTracker();
         }
     }
 
-    /**
-     * Echos an error message & other information, then exits.
-     *
-     * @param Exception $e
-     * @param bool $authenticated
-     */
-    protected function exitWithException($e, $authenticated = false)
+    public static function restoreTrackerPlugins()
     {
-        if ($this->usingBulkTracking) {
-            // when doing bulk tracking we return JSON so the caller will know how many succeeded
-            $result = array(
-                'status'  => 'error',
-                'tracked' => $this->countOfLoggedRequests
-            );
-            // send error when in debug mode or when authenticated (which happens when doing log importing,
-            if ((isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG'])
-                || $authenticated
-            ) {
-                $result['message'] = $this->getMessageFromException($e);
-            }
-            Common::sendHeader('Content-Type: application/json');
-            echo Common::json_encode($result);
-            exit;
+        if (SettingsServer::isTrackerApiRequest() && Tracker::$initTrackerMode) {
+            Plugin\Manager::getInstance()->loadTrackerPlugins();
         }
+    }
 
-        if (isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) {
-            Common::sendHeader('Content-Type: text/html; charset=utf-8');
-            $trailer = 'Backtrace:
' . $e->getTraceAsString() . '
'; - $headerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Zeitgeist/templates/simpleLayoutHeader.tpl'); - $footerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Zeitgeist/templates/simpleLayoutFooter.tpl'); - $headerPage = str_replace('{$HTML_TITLE}', 'Piwik › Error', $headerPage); + public function getCountOfLoggedRequests() + { + return $this->countOfLoggedRequests; + } - echo $headerPage . '

' . $this->getMessageFromException($e) . '

' . $trailer . $footerPage; - } // If not debug, but running authenticated (eg. during log import) then we display raw errors - elseif ($authenticated) { - Common::sendHeader('Content-Type: text/html; charset=utf-8'); - echo $this->getMessageFromException($e); - } else { - $this->outputTransparentGif(); - } - exit; + public function setCountOfLoggedRequests($numLoggedRequests) + { + $this->countOfLoggedRequests = $numLoggedRequests; + } + + public function hasLoggedRequests() + { + return 0 !== $this->countOfLoggedRequests; } /** - * Returns the date in the "Y-m-d H:i:s" PHP format - * - * @param int $timestamp - * @return string + * @deprecated since 2.10.0 use {@link Date::getDatetimeFromTimestamp()} instead */ public static function getDatetimeFromTimestamp($timestamp) { - return date("Y-m-d H:i:s", $timestamp); + return Date::getDatetimeFromTimestamp($timestamp); } - /** - * Initialization - */ - protected function init(Request $request) + public function isDatabaseConnected() { - $this->loadTrackerPlugins($request); - $this->handleTrackingApi($request); - $this->handleDisabledTracker(); - $this->handleEmptyRequest($request); - - Common::printDebug("Current datetime: " . date("Y-m-d H:i:s", $request->getCurrentTimestamp())); + return !is_null(self::$db); } - /** - * Cleanup - */ - protected function end() + public static function getDatabase() { - if ($this->usingBulkTracking) { - $result = array( - 'status' => 'success', - 'tracked' => $this->countOfLoggedRequests - ); - Common::sendHeader('Content-Type: application/json'); - echo Common::json_encode($result); - exit; - } - switch ($this->getState()) { - case self::STATE_LOGGING_DISABLE: - $this->outputTransparentGif(); - Common::printDebug("Logging disabled, display transparent logo"); - break; - - case self::STATE_EMPTY_REQUEST: - Common::printDebug("Empty request => Piwik page"); - echo "Piwik is a free open source web analytics that lets you keep control of your data."; - break; - - case self::STATE_NOSCRIPT_REQUEST: - case self::STATE_NOTHING_TO_NOTICE: - default: - $this->outputTransparentGif(); - Common::printDebug("Nothing to notice => default behaviour"); - break; - } - Common::printDebug("End of the page."); - - if ($GLOBALS['PIWIK_TRACKER_DEBUG'] === true) { - if (isset(self::$db)) { - self::$db->recordProfiling(); - Profiler::displayDbTrackerProfile(self::$db); + if (is_null(self::$db)) { + try { + self::$db = TrackerDb::connectPiwikTrackerDb(); + } catch (Exception $e) { + throw new DbException($e->getMessage(), $e->getCode()); } } - self::disconnectDatabase(); - } - - /** - * Factory to create database objects - * - * @param array $configDb Database configuration - * @throws Exception - * @return \Piwik\Tracker\Db\Mysqli|\Piwik\Tracker\Db\Pdo\Mysql - */ - public static function factory($configDb) - { - /** - * Triggered before a connection to the database is established by the Tracker. - * - * This event can be used to change the database connection settings used by the Tracker. - * - * @param array $dbInfos Reference to an array containing database connection info, - * including: - * - * - **host**: The host name or IP address to the MySQL database. - * - **username**: The username to use when connecting to the - * database. - * - **password**: The password to use when connecting to the - * database. - * - **dbname**: The name of the Piwik MySQL database. - * - **port**: The MySQL database port to use. - * - **adapter**: either `'PDO_MYSQL'` or `'MYSQLI'` - * - **type**: The MySQL engine to use, for instance 'InnoDB' - */ - Piwik::postEvent('Tracker.getDatabaseConfig', array(&$configDb)); - - switch ($configDb['adapter']) { - case 'PDO\MYSQL': - case 'PDO_MYSQL': // old format pre Piwik 2 - require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Pdo/Mysql.php'; - return new Mysql($configDb); - - case 'MYSQLI': - require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Mysqli.php'; - return new Mysqli($configDb); - } - - throw new Exception('Unsupported database adapter ' . $configDb['adapter']); - } - - public static function connectPiwikTrackerDb() - { - $db = null; - $configDb = Config::getInstance()->database; - - if (!isset($configDb['port'])) { - // before 0.2.4 there is no port specified in config file - $configDb['port'] = '3306'; - } - - $db = Tracker::factory($configDb); - $db->connect(); - - return $db; - } - - protected static function connectDatabaseIfNotConnected() - { - if (!is_null(self::$db)) { - return; - } - - try { - self::$db = self::connectPiwikTrackerDb(); - } catch (Exception $e) { - throw new DbException($e->getMessage(), $e->getCode()); - } - } - - /** - * @return Db - */ - public static function getDatabase() - { - self::connectDatabaseIfNotConnected(); return self::$db; } - public static function disconnectDatabase() + protected function disconnectDatabase() { - if (isset(self::$db)) { + if ($this->isDatabaseConnected()) { // note: I think we do this only for the tests self::$db->disconnect(); self::$db = null; } } - /** - * Returns the Tracker_Visit object. - * This method can be overwritten to use a different Tracker_Visit object - * - * @throws Exception - * @return \Piwik\Tracker\Visit - */ - protected function getNewVisitObject() + // for tests + public static function disconnectCachedDbConnection() { - $visit = null; - - /** - * Triggered before a new **visit tracking object** is created. Subscribers to this - * event can force the use of a custom visit tracking object that extends from - * {@link Piwik\Tracker\VisitInterface}. - * - * @param \Piwik\Tracker\VisitInterface &$visit Initialized to null, but can be set to - * a new visit object. If it isn't modified - * Piwik uses the default class. - */ - Piwik::postEvent('Tracker.makeNewVisitObject', array(&$visit)); - - if (is_null($visit)) { - $visit = new Visit(); - } elseif (!($visit instanceof VisitInterface)) { - throw new Exception("The Visit object set in the plugin must implement VisitInterface"); - } - return $visit; - } - - protected function outputTransparentGif() - { - if (isset($GLOBALS['PIWIK_TRACKER_DEBUG']) - && $GLOBALS['PIWIK_TRACKER_DEBUG'] - ) { - return; - } - - if (strlen($this->getOutputBuffer()) > 0) { - // If there was an error during tracker, return so errors can be flushed - return; - } - $transGifBase64 = "R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="; - Common::sendHeader('Content-Type: image/gif'); - - $this->outputAccessControlHeaders(); - - print(base64_decode($transGifBase64)); - } - - protected function isVisitValid() - { - return $this->stateValid !== self::STATE_LOGGING_DISABLE - && $this->stateValid !== self::STATE_EMPTY_REQUEST; - } - - protected function getState() - { - return $this->stateValid; - } - - protected function setState($value) - { - $this->stateValid = $value; - } - - protected function loadTrackerPlugins(Request $request) - { - // Adding &dp=1 will disable the provider plugin, if token_auth is used (used to speed up bulk imports) - $disableProvider = $request->getParam('dp'); - if (!empty($disableProvider)) { - Tracker::setPluginsNotToLoad(array('Provider')); - } - - try { - $pluginsTracker = \Piwik\Plugin\Manager::getInstance()->loadTrackerPlugins(); - Common::printDebug("Loading plugins: { " . implode(",", $pluginsTracker) . " }"); - } catch (Exception $e) { - Common::printDebug("ERROR: " . $e->getMessage()); - } - } - - protected function handleEmptyRequest(Request $request) - { - $countParameters = $request->getParamsCount(); - if ($countParameters == 0) { - $this->setState(self::STATE_EMPTY_REQUEST); - } - if ($countParameters == 1) { - $this->setState(self::STATE_NOSCRIPT_REQUEST); - } - } - - protected function handleDisabledTracker() - { - $saveStats = Config::getInstance()->Tracker['record_statistics']; - if ($saveStats == 0) { - $this->setState(self::STATE_LOGGING_DISABLE); - } - } - - protected function getTokenAuth() - { - if (!is_null($this->tokenAuth)) { - return $this->tokenAuth; - } - - return Common::getRequestVar('token_auth', false); - } - - /** - * This method allows to set custom IP + server time + visitor ID, when using Tracking API. - * These two attributes can be only set by the Super User (passing token_auth). - */ - protected function handleTrackingApi(Request $request) - { - if (!$request->isAuthenticated()) { - return; - } - - // Custom IP to use for this visitor - $customIp = $request->getParam('cip'); - if (!empty($customIp)) { - $this->setForceIp($customIp); - } - - // Custom server date time to use - $customDatetime = $request->getParam('cdt'); - if (!empty($customDatetime)) { - $this->setForceDateTime($customDatetime); - } - - // Forced Visitor ID to record the visit / action - $customVisitorId = $request->getParam('cid'); - if (!empty($customVisitorId)) { - $this->setForceVisitorId($customVisitorId); + // code redundancy w/ above is on purpose; above disconnectDatabase depends on method that can potentially be overridden + if (!is_null(self::$db)) { + self::$db->disconnect(); + self::$db = null; } } public static function setTestEnvironment($args = null, $requestMethod = null) { if (is_null($args)) { - $postData = self::getRequestsArrayFromBulkRequest(self::getRawBulkRequest()); - $args = $_GET + $postData; + $requests = new Requests(); + $args = $requests->getRequestsArrayFromBulkRequest($requests->getRawBulkRequest()); + $args = $_GET + $args; } + if (is_null($requestMethod) && array_key_exists('REQUEST_METHOD', $_SERVER)) { $requestMethod = $_SERVER['REQUEST_METHOD']; - } else if (is_null($requestMethod)) { + } elseif (is_null($requestMethod)) { $requestMethod = 'GET'; } // Do not run scheduled tasks during tests - self::updateTrackerConfig('scheduled_tasks_min_interval', 0); + if (!defined('DEBUG_FORCE_SCHEDULED_TASKS')) { + TrackerConfig::setConfigValue('scheduled_tasks_min_interval', 0); + } // if nothing found in _GET/_POST and we're doing a POST, assume bulk request. in which case, // we have to bypass authentication if (empty($args) && $requestMethod == 'POST') { - self::updateTrackerConfig('tracking_requests_require_authentication', 0); + TrackerConfig::setConfigValue('tracking_requests_require_authentication', 0); + } + + // Tests can force the use of 3rd party cookie for ID visitor + if (Common::getRequestVar('forceEnableFingerprintingAcrossWebsites', false, null, $args) == 1) { + TrackerConfig::setConfigValue('enable_fingerprinting_across_websites', 1); } // Tests can force the use of 3rd party cookie for ID visitor if (Common::getRequestVar('forceUseThirdPartyCookie', false, null, $args) == 1) { - self::updateTrackerConfig('use_third_party_id_cookie', 1); + TrackerConfig::setConfigValue('use_third_party_id_cookie', 1); } // Tests using window_look_back_for_visitor if (Common::getRequestVar('forceLargeWindowLookBackForVisitor', false, null, $args) == 1 // also look for this in bulk requests (see fake_logs_replay.log) - || strpos( json_encode($args, true), '"forceLargeWindowLookBackForVisitor":"1"' ) !== false) { - self::updateTrackerConfig('window_look_back_for_visitor', 2678400); + || strpos(json_encode($args, true), '"forceLargeWindowLookBackForVisitor":"1"') !== false + ) { + TrackerConfig::setConfigValue('window_look_back_for_visitor', 2678400); } // Tests can force the enabling of IP anonymization if (Common::getRequestVar('forceIpAnonymization', false, null, $args) == 1) { - - self::connectDatabaseIfNotConnected(); + self::getDatabase(); // make sure db is initialized $privacyConfig = new PrivacyManagerConfig(); $privacyConfig->ipAddressMaskLength = 2; \Piwik\Plugins\PrivacyManager\IPAnonymizer::activate(); + + \Piwik\Tracker\Cache::deleteTrackerCache(); + Filesystem::clearPhpCaches(); } - // Custom IP to use for this visitor - $customIp = Common::getRequestVar('cip', false, null, $args); - if (!empty($customIp)) { - self::setForceIp($customIp); - } - - // Custom server date time to use - $customDatetime = Common::getRequestVar('cdt', false, null, $args); - if (!empty($customDatetime)) { - self::setForceDateTime($customDatetime); - } - - // Custom visitor id - $customVisitorId = Common::getRequestVar('cid', false, null, $args); - if (!empty($customVisitorId)) { - self::setForceVisitorId($customVisitorId); - } $pluginsDisabled = array('Provider'); // Disable provider plugin, because it is so slow to do many reverse ip lookups - self::setPluginsNotToLoad($pluginsDisabled); + PluginManager::getInstance()->setTrackerPluginsNotToLoad($pluginsDisabled); } - /** - * Gets the error message to output when a tracking request fails. - * - * @param Exception $e - * @return string - */ - private function getMessageFromException($e) + protected function loadTrackerPlugins() { - // Note: duplicated from FormDatabaseSetup.isAccessDenied - // Avoid leaking the username/db name when access denied - if ($e->getCode() == 1044 || $e->getCode() == 42000) { - return "Error while connecting to the Piwik database - please check your credentials in config/config.ini.php file"; - } else { - return $e->getMessage(); - } - } - - /** - * @param $params - * @param $tokenAuth - * @return array - */ - protected function trackRequest($params, $tokenAuth) - { - if ($params instanceof Request) { - $request = $params; - } else { - $request = new Request($params, $tokenAuth); - } - - $this->init($request); - - $isAuthenticated = $request->isAuthenticated(); - try { - if ($this->isVisitValid()) { - $visit = $this->getNewVisitObject(); - $request->setForcedVisitorId(self::$forcedVisitorId); - $request->setForceDateTime(self::$forcedDateTime); - $request->setForceIp(self::$forcedIpString); - - $visit->setRequest($request); - $visit->handle(); - } else { - Common::printDebug("The request is invalid: empty request, or maybe tracking is disabled in the config.ini.php via record_statistics=0"); - } - } catch (DbException $e) { - Common::printDebug("Exception: " . $e->getMessage()); - $this->exitWithException($e, $isAuthenticated); + $pluginManager = PluginManager::getInstance(); + $pluginsTracker = $pluginManager->loadTrackerPlugins(); + Common::printDebug("Loading plugins: { " . implode(", ", $pluginsTracker) . " }"); } catch (Exception $e) { - $this->exitWithException($e, $isAuthenticated); + Common::printDebug("ERROR: " . $e->getMessage()); } - $this->clear(); - - // increment successfully logged request count. make sure to do this after try-catch, - // since an excluded visit is considered 'successfully logged' - ++$this->countOfLoggedRequests; - return $isAuthenticated; } - - protected function runScheduledTasksIfAllowed($isAuthenticated) + private function handleFatalErrors() + { + register_shutdown_function(function () { + $lastError = error_get_last(); + if (!empty($lastError) && $lastError['type'] == E_ERROR) { + Common::sendResponseCode(500); + } + }); + } + + private static function isDebugEnabled() { - // Do not run schedule task if we are importing logs - // or doing custom tracking (as it could slow down) try { - if (!$isAuthenticated - && $this->shouldRunScheduledTasks() - ) { - self::runScheduledTasks(); + $debug = (bool) TrackerConfig::getConfigValue('debug'); + if ($debug) { + return true; + } + + $debugOnDemand = (bool) TrackerConfig::getConfigValue('debug_on_demand'); + if ($debugOnDemand) { + return (bool) Common::getRequestVar('debug', false); } } catch (Exception $e) { - $this->exitWithException($e); } - } - /** - * @return string - */ - protected static function getRawBulkRequest() - { - return file_get_contents("php://input"); + return false; } } diff --git a/www/analytics/core/Tracker/Action.php b/www/analytics/core/Tracker/Action.php index a1d879b5..b841c210 100644 --- a/www/analytics/core/Tracker/Action.php +++ b/www/analytics/core/Tracker/Action.php @@ -1,6 +1,6 @@ getParam('download'); - if (!empty($downloadUrl)) { - return new ActionClickUrl(self::TYPE_DOWNLOAD, $downloadUrl, $request); + /** @var Action[] $actions */ + $actions = self::getAllActions($request); + + foreach ($actions as $actionType) { + if (empty($action)) { + $action = $actionType; + continue; + } + + $posPrevious = self::getPriority($action); + $posCurrent = self::getPriority($actionType); + + if ($posCurrent > $posPrevious) { + $action = $actionType; + } } - $outlinkUrl = $request->getParam('link'); - if (!empty($outlinkUrl)) { - return new ActionClickUrl(self::TYPE_OUTLINK, $outlinkUrl, $request); - } - - $url = $request->getParam('url'); - - $eventCategory = $request->getParam('e_c'); - $eventAction = $request->getParam('e_a'); - if(strlen($eventCategory) > 0 && strlen($eventAction) > 0 ) { - return new ActionEvent($eventCategory, $eventAction, $url, $request); - } - - $action = new ActionSiteSearch($url, $request); - if ($action->isSearchDetected()) { + if (!empty($action)) { return $action; } - return new ActionPageview($url, $request); + + return new ActionPageview($request); } - /** - * @var Request - */ - protected $request; + private static function getPriority(Action $actionType) + { + $key = array_search($actionType->getActionType(), self::$factoryPriority); - private $idLinkVisitAction; - private $actionIdsCached = array(); - private $actionName; - private $actionType; - private $actionUrl; + if (false === $key) { + return -1; + } + + return $key; + } + + public static function shouldHandle(Request $request) + { + return false; + } + + private static function getAllActions(Request $request) + { + static $actions; + + if (is_null($actions)) { + $actions = Manager::getInstance()->findMultipleComponents('Actions', '\\Piwik\\Tracker\\Action'); + } + + $instances = array(); + + foreach ($actions as $action) { + /** @var \Piwik\Tracker\Action $action */ + if ($action::shouldHandle($request)) { + $instances[] = new $action($request); + } + } + + return $instances; + } public function __construct($type, Request $request) { $this->actionType = $type; - $this->request = $request; + $this->request = $request; } /** @@ -96,6 +160,14 @@ abstract class Action return $this->actionUrl; } + /** + * Returns URL of page being tracked, including all original Query parameters + */ + public function getActionUrlRaw() + { + return $this->rawActionUrl; + } + public function getActionName() { return $this->actionName; @@ -108,8 +180,7 @@ abstract class Action public function getCustomVariables() { - $customVariables = $this->request->getCustomVariables($scope = 'page'); - return $customVariables; + return $this->request->getCustomVariables($scope = 'page'); } // custom_float column @@ -118,24 +189,28 @@ abstract class Action return false; } - protected function setActionName($name) { - $name = PageUrl::cleanupString((string)$name); - $this->actionName = $name; + $this->actionName = PageUrl::cleanupString((string)$name); } protected function setActionUrl($url) { - $urlBefore = $url; + $this->rawActionUrl = PageUrl::getUrlIfLookValid($url); $url = PageUrl::excludeQueryParametersFromUrl($url, $this->request->getIdSite()); - if ($url != $urlBefore) { - Common::printDebug(' Before was "' . $urlBefore . '"'); + $this->actionUrl = PageUrl::getUrlIfLookValid($url); + + if ($url != $this->rawActionUrl) { + Common::printDebug(' Before was "' . $this->rawActionUrl . '"'); Common::printDebug(' After is "' . $url . '"'); } + } + protected function setActionUrlWithoutExcludingParameters($url) + { $url = PageUrl::getUrlIfLookValid($url); + $this->rawActionUrl = $url; $this->actionUrl = $url; } @@ -144,14 +219,26 @@ abstract class Action protected function getUrlAndType() { $url = $this->getActionUrl(); + if (!empty($url)) { // normalize urls by stripping protocol and www $url = PageUrl::normalizeUrl($url); - return array($url['url'], Tracker\Action::TYPE_PAGE_URL, $url['prefixId']); + return array($url['url'], self::TYPE_PAGE_URL, $url['prefixId']); } + return false; } + public function setCustomField($field, $value) + { + $this->customFields[$field] = $value; + } + + public function getCustomFields() + { + return $this->customFields; + } + public function getIdActionUrl() { $idUrl = $this->actionIdsCached['idaction_url']; @@ -159,7 +246,6 @@ abstract class Action return (int)$idUrl; } - public function getIdActionUrlForEntryAndExitIds() { return $this->getIdActionUrl(); @@ -172,9 +258,10 @@ abstract class Action public function getIdActionName() { - if(!isset($this->actionIdsCached['idaction_name'])) { + if (!isset($this->actionIdsCached['idaction_name'])) { return false; } + return $this->actionIdsCached['idaction_name']; } @@ -188,24 +275,17 @@ abstract class Action return $this->idLinkVisitAction; } - public function writeDebugInfo() - { - $type = self::getTypeAsString($this->getActionType()); - Common::printDebug("Action is a $type, - Action name = " . $this->getActionName() . ", - Action URL = " . $this->getActionUrl()); - return true; - } - public static function getTypeAsString($type) { - $class = new \ReflectionClass("\\Piwik\\Tracker\\Action"); + $class = new \ReflectionClass("\\Piwik\\Tracker\\Action"); $constants = $class->getConstants(); $typeId = array_search($type, $constants); - if($typeId === false) { + + if (false === $typeId) { throw new Exception("Unexpected action type " . $type); } + return str_replace('TYPE_', '', $typeId); } @@ -220,13 +300,38 @@ abstract class Action */ public function loadIdsFromLogActionTable() { - if(!empty($this->actionIdsCached)) { + if (!empty($this->actionIdsCached)) { return; } - $actions = $this->getActionsToLookup(); + + /** @var ActionDimension[] $dimensions */ + $dimensions = ActionDimension::getAllDimensions(); + $actions = $this->getActionsToLookup(); + + foreach ($dimensions as $dimension) { + $value = $dimension->onLookupAction($this->request, $this); + + if (false !== $value) { + if (is_float($value)) { + $value = Common::forceDotAsSeparatorForDecimalPoint($value); + } + + $field = $dimension->getColumnName(); + + if (empty($field)) { + $dimensionClass = get_class($dimension); + throw new Exception('Dimension ' . $dimensionClass . ' does not define a field name'); + } + + $actionId = $dimension->getActionId(); + $actions[$field] = array($value, $actionId); + Common::printDebug("$field = $value"); + } + } + $actions = array_filter($actions, 'count'); - if(empty($actions)) { + if (empty($actions)) { return; } @@ -239,72 +344,101 @@ abstract class Action /** * Records in the DB the association between the visit and this action. * - * @param int $idVisit is the ID of the current visit in the DB table log_visit - * @param $visitorIdCookie * @param int $idReferrerActionUrl is the ID of the last action done by the current visit. * @param $idReferrerActionName - * @param int $timeSpentReferrerAction is the number of seconds since the last action was done. - * It is directly related to idReferrerActionUrl. + * @param Visitor $visitor */ - public function record($idVisit, $visitorIdCookie, $idReferrerActionUrl, $idReferrerActionName, $timeSpentReferrerAction) + public function record(Visitor $visitor, $idReferrerActionUrl, $idReferrerActionName) { $this->loadIdsFromLogActionTable(); - $idActionName = in_array($this->getActionType(), array(Tracker\Action::TYPE_PAGE_TITLE, - Tracker\Action::TYPE_PAGE_URL, - Tracker\Action::TYPE_SITE_SEARCH - )) - ? (int)$this->getIdActionName() - : null; - $visitAction = array( - 'idvisit' => $idVisit, - 'idsite' => $this->request->getIdSite(), - 'idvisitor' => $visitorIdCookie, - 'server_time' => Tracker::getDatetimeFromTimestamp($this->request->getCurrentTimestamp()), - 'idaction_url' => $this->getIdActionUrl(), - 'idaction_name' => $idActionName, - 'idaction_url_ref' => $idReferrerActionUrl, - 'idaction_name_ref' => $idReferrerActionName, - 'time_spent_ref_action' => $timeSpentReferrerAction + 'idvisit' => $visitor->getVisitorColumn('idvisit'), + 'idsite' => $this->request->getIdSite(), + 'idvisitor' => $visitor->getVisitorColumn('idvisitor'), + 'idaction_url' => $this->getIdActionUrl(), + 'idaction_url_ref' => $idReferrerActionUrl, + 'idaction_name_ref' => $idReferrerActionName ); - foreach($this->actionIdsCached as $field => $idAction) { - $visitAction[$field] = $idAction; + /** @var ActionDimension[] $dimensions */ + $dimensions = ActionDimension::getAllDimensions(); + + foreach ($dimensions as $dimension) { + $value = $dimension->onNewAction($this->request, $visitor, $this); + + if ($value !== false) { + if (is_float($value)) { + $value = Common::forceDotAsSeparatorForDecimalPoint($value); + } + + $visitAction[$dimension->getColumnName()] = $value; + } + } + + // idaction_name is NULLable. we only set it when applicable + if ($this->isActionHasActionName()) { + $visitAction['idaction_name'] = (int)$this->getIdActionName(); + } + + foreach ($this->actionIdsCached as $field => $idAction) { + $visitAction[$field] = ($idAction === false) ? 0 : $idAction; } $customValue = $this->getCustomFloatValue(); if (!empty($customValue)) { - $visitAction[self::DB_COLUMN_CUSTOM_FLOAT] = $customValue; + $visitAction[self::DB_COLUMN_CUSTOM_FLOAT] = Common::forceDotAsSeparatorForDecimalPoint($customValue); } - $customVariables = $this->getCustomVariables(); - if (!empty($customVariables)) { - Common::printDebug("Page level Custom Variables: "); - Common::printDebug($customVariables); - } + $visitAction = array_merge($visitAction, $this->customFields); - $visitAction = array_merge($visitAction, $customVariables); - $fields = implode(", ", array_keys($visitAction)); - $bind = array_values($visitAction); - $values = Common::getSqlStringFieldsArray($visitAction); + $this->idLinkVisitAction = $this->getModel()->createAction($visitAction); - $sql = "INSERT INTO " . Common::prefixTable('log_link_visit_action') . " ($fields) VALUES ($values)"; - Tracker::getDatabase()->query($sql, $bind); - - $this->idLinkVisitAction = Tracker::getDatabase()->lastInsertId(); $visitAction['idlink_va'] = $this->idLinkVisitAction; Common::printDebug("Inserted new action:"); - Common::printDebug($visitAction); + $visitActionDebug = $visitAction; + $visitActionDebug['idvisitor'] = bin2hex($visitActionDebug['idvisitor']); + Common::printDebug($visitActionDebug); /** * Triggered after successfully persisting a [visit action entity](/guides/persistence-and-the-mysql-backend#visit-actions). - * + * + * This event is deprecated, use [Dimensions](http://developer.piwik.org/guides/dimensions) instead. + * * @param Action $tracker Action The Action tracker instance. * @param array $visitAction The visit action entity that was persisted. Read * [this](/guides/persistence-and-the-mysql-backend#visit-actions) to see what it contains. + * @deprecated */ Piwik::postEvent('Tracker.recordAction', array($trackerAction = $this, $visitAction)); } + + public function writeDebugInfo() + { + $type = self::getTypeAsString($this->getActionType()); + $name = $this->getActionName(); + $url = $this->getActionUrl(); + + Common::printDebug("Action is a $type, + Action name = " . $name . ", + Action URL = " . $url); + + return true; + } + + private function getModel() + { + return new Model(); + } + + /** + * @return bool + */ + private function isActionHasActionName() + { + $types = array(self::TYPE_PAGE_TITLE, self::TYPE_PAGE_URL, self::TYPE_SITE_SEARCH); + + return in_array($this->getActionType(), $types); + } } diff --git a/www/analytics/core/Tracker/ActionClickUrl.php b/www/analytics/core/Tracker/ActionClickUrl.php deleted file mode 100644 index 0eacedf1..00000000 --- a/www/analytics/core/Tracker/ActionClickUrl.php +++ /dev/null @@ -1,63 +0,0 @@ -setActionUrl($url); - } - - protected function getActionsToLookup() - { - return array( - // Note: we do not normalize download/oulink URL - 'idaction_url' => array($this->getActionUrl(), $this->getActionType()) - ); - } - - function writeDebugInfo() - { - parent::writeDebugInfo(); - - if (self::detectActionIsOutlinkOnAliasHost($this, $this->request->getIdSite())) { - Common::printDebug("INFO: The outlink URL host is one of the known host for this website. "); - } - } - - /** - * Detect whether action is an outlink given host aliases - * - * @param Action $action - * @return bool true if the outlink the visitor clicked on points to one of the known hosts for this website - */ - public static function detectActionIsOutlinkOnAliasHost(Action $action, $idSite) - { - if ($action->getActionType() != Action::TYPE_OUTLINK) { - return false; - } - $decodedActionUrl = $action->getActionUrl(); - $actionUrlParsed = @parse_url($decodedActionUrl); - if (!isset($actionUrlParsed['host'])) { - return false; - } - return Visit::isHostKnownAliasHost($actionUrlParsed['host'], $idSite); - } -} diff --git a/www/analytics/core/Tracker/ActionEvent.php b/www/analytics/core/Tracker/ActionEvent.php deleted file mode 100644 index 511cf792..00000000 --- a/www/analytics/core/Tracker/ActionEvent.php +++ /dev/null @@ -1,78 +0,0 @@ -setActionUrl($url); - $this->eventCategory = trim($eventCategory); - $this->eventAction = trim($eventAction); - $this->eventName = trim($request->getParam('e_n')); - $this->eventValue = trim($request->getParam('e_v')); - } - - function getCustomFloatValue() - { - return $this->eventValue; - } - - protected function getActionsToLookup() - { - $actions = array( - 'idaction_url' => $this->getUrlAndType() - ); - - if(strlen($this->eventName) > 0) { - $actions['idaction_name'] = array($this->eventName, Action::TYPE_EVENT_NAME); - } - if(strlen($this->eventCategory) > 0) { - $actions['idaction_event_category'] = array($this->eventCategory, Action::TYPE_EVENT_CATEGORY); - } - if(strlen($this->eventAction) > 0) { - $actions['idaction_event_action'] = array($this->eventAction, Action::TYPE_EVENT_ACTION); - } - return $actions; - } - - // Do not track this Event URL as Entry/Exit Page URL (leave the existing entry/exit) - public function getIdActionUrlForEntryAndExitIds() - { - return false; - } - - // Do not track this Event Name as Entry/Exit Page Title (leave the existing entry/exit) - public function getIdActionNameForEntryAndExitIds() - { - return false; - } - - public function writeDebugInfo() - { - $write = parent::writeDebugInfo(); - if($write) { - Common::printDebug("Event Category = " . $this->eventCategory . ", - Event Action = " . $this->eventAction . ", - Event Name = " . $this->eventName . ", - Event Value = " . $this->getCustomFloatValue()); - } - return $write; - } - -} diff --git a/www/analytics/core/Tracker/ActionPageview.php b/www/analytics/core/Tracker/ActionPageview.php index eae69145..eb98dd4b 100644 --- a/www/analytics/core/Tracker/ActionPageview.php +++ b/www/analytics/core/Tracker/ActionPageview.php @@ -1,6 +1,6 @@ getParam('url'); $this->setActionUrl($url); $actionName = $request->getParam('action_name'); @@ -38,34 +37,54 @@ class ActionPageview extends Action { return array( 'idaction_name' => array($this->getActionName(), Action::TYPE_PAGE_TITLE), - 'idaction_url' => $this->getUrlAndType() + 'idaction_url' => $this->getUrlAndType() ); } - function getCustomFloatValue() + public function getCustomFloatValue() { return $this->request->getPageGenerationTime(); } - protected function cleanupActionName($actionName) + public static function shouldHandle(Request $request) + { + return true; + } + + private function cleanupActionName($actionName) { // get the delimiter, by default '/'; BC, we read the old action_category_delimiter first (see #1067) - $actionCategoryDelimiter = isset(Config::getInstance()->General['action_category_delimiter']) - ? Config::getInstance()->General['action_category_delimiter'] - : Config::getInstance()->General['action_url_category_delimiter']; + $actionCategoryDelimiter = $this->getActionCategoryDelimiter(); // create an array of the categories delimited by the delimiter $split = explode($actionCategoryDelimiter, $actionName); + $split = $this->trimEveryCategory($split); + $split = $this->removeEmptyCategories($split); - // trim every category - $split = array_map('trim', $split); - - // remove empty categories - $split = array_filter($split, 'strlen'); - - // rebuild the name from the array of cleaned categories - $actionName = implode($actionCategoryDelimiter, $split); - return $actionName; + return $this->rebuildNameOfCleanedCategories($actionCategoryDelimiter, $split); } + private function rebuildNameOfCleanedCategories($actionCategoryDelimiter, $split) + { + return implode($actionCategoryDelimiter, $split); + } + + private function removeEmptyCategories($split) + { + return array_filter($split, 'strlen'); + } + + private function trimEveryCategory($split) + { + return array_map('trim', $split); + } + + private function getActionCategoryDelimiter() + { + if (isset(Config::getInstance()->General['action_category_delimiter'])) { + return Config::getInstance()->General['action_category_delimiter']; + } + + return Config::getInstance()->General['action_url_category_delimiter']; + } } diff --git a/www/analytics/core/Tracker/Cache.php b/www/analytics/core/Tracker/Cache.php index edf8185d..b18003aa 100644 --- a/www/analytics/core/Tracker/Cache.php +++ b/www/analytics/core/Tracker/Cache.php @@ -1,6 +1,6 @@ Tracker['tracker_cache_file_ttl']; - self::$trackerCache = new CacheFile('tracker', $ttl); + if (is_null(self::$cache)) { + self::$cache = PiwikCache::getLazyCache(); } - return self::$trackerCache; + + return self::$cache; + } + + private static function getTtl() + { + return Config::getInstance()->Tracker['tracker_cache_file_ttl']; } /** @@ -44,67 +54,68 @@ class Cache * @param int $idSite * @return array */ - static function getCacheWebsiteAttributes($idSite) + public static function getCacheWebsiteAttributes($idSite) { - if($idSite == 'all') { - return array(); - } - $idSite = (int)$idSite; - if($idSite <= 0) { + if ('all' == $idSite) { return array(); } - $cache = self::getInstance(); - if (($cacheContent = $cache->get($idSite)) !== false) { + $idSite = (int) $idSite; + if ($idSite <= 0) { + return array(); + } + + $cache = self::getCache(); + $cacheId = $idSite; + $cacheContent = $cache->fetch($cacheId); + + if (false !== $cacheContent) { return $cacheContent; } Tracker::initCorePiwikInTrackerMode(); - // save current user privilege and temporarily assume Super User privilege - $isSuperUser = Piwik::hasUserSuperUserAccess(); - Piwik::setUserHasSuperUserAccess(); - $content = array(); - - /** - * Triggered to get the attributes of a site entity that might be used by the - * Tracker. - * - * Plugins add new site attributes for use in other tracking events must - * use this event to put those attributes in the Tracker Cache. - * - * **Example** - * - * public function getSiteAttributes($content, $idSite) - * { - * $sql = "SELECT info FROM " . Common::prefixTable('myplugin_extra_site_info') . " WHERE idsite = ?"; - * $content['myplugin_site_data'] = Db::fetchOne($sql, array($idSite)); - * } - * - * @param array &$content Array mapping of site attribute names with values. - * @param int $idSite The site ID to get attributes for. - */ - Piwik::postEvent('Tracker.Cache.getSiteAttributes', array(&$content, $idSite)); - Common::printDebug("Website $idSite tracker cache was re-created."); - - // restore original user privilege - Piwik::setUserHasSuperUserAccess($isSuperUser); + Access::doAsSuperUser(function () use (&$content, $idSite) { + /** + * Triggered to get the attributes of a site entity that might be used by the + * Tracker. + * + * Plugins add new site attributes for use in other tracking events must + * use this event to put those attributes in the Tracker Cache. + * + * **Example** + * + * public function getSiteAttributes($content, $idSite) + * { + * $sql = "SELECT info FROM " . Common::prefixTable('myplugin_extra_site_info') . " WHERE idsite = ?"; + * $content['myplugin_site_data'] = Db::fetchOne($sql, array($idSite)); + * } + * + * @param array &$content Array mapping of site attribute names with values. + * @param int $idSite The site ID to get attributes for. + */ + Piwik::postEvent('Tracker.Cache.getSiteAttributes', array(&$content, $idSite)); + Common::printDebug("Website $idSite tracker cache was re-created."); + }); // if nothing is returned from the plugins, we don't save the content // this is not expected: all websites are expected to have at least one URL if (!empty($content)) { - $cache->set($idSite, $content); + $cache->save($cacheId, $content, self::getTtl()); } + + Tracker::restoreTrackerPlugins(); + return $content; } /** * Clear general (global) cache */ - static public function clearCacheGeneral() + public static function clearCacheGeneral() { - self::getInstance()->delete('general'); + self::getCache()->delete(self::$cacheIdGeneral); } /** @@ -113,12 +124,12 @@ class Cache * * @return array */ - static public function getCacheGeneral() + public static function getCacheGeneral() { - $cache = self::getInstance(); - $cacheId = 'general'; + $cache = self::getCache(); + $cacheContent = $cache->fetch(self::$cacheIdGeneral); - if (($cacheContent = $cache->get($cacheId)) !== false) { + if (false !== $cacheContent) { return $cacheContent; } @@ -131,26 +142,29 @@ class Cache /** * Triggered before the [general tracker cache](/guides/all-about-tracking#the-tracker-cache) * is saved to disk. This event can be used to add extra content to the cache. - * + * * Data that is used during tracking but is expensive to compute/query should be * cached to keep tracking efficient. One example of such data are options * that are stored in the piwik_option table. Querying data for each tracking * request means an extra unnecessary database query for each visitor action. Using * a cache solves this problem. - * + * * **Example** - * + * * public function setTrackerCacheGeneral(&$cacheContent) * { * $cacheContent['MyPlugin.myCacheKey'] = Option::get('MyPlugin_myOption'); * } - * + * * @param array &$cacheContent Array of cached data. Each piece of data must be * mapped by name. */ Piwik::postEvent('Tracker.setTrackerCacheGeneral', array(&$cacheContent)); self::setCacheGeneral($cacheContent); Common::printDebug("General tracker cache was re-created."); + + Tracker::restoreTrackerPlugins(); + return $cacheContent; } @@ -160,12 +174,11 @@ class Cache * @param mixed $value * @return bool */ - static public function setCacheGeneral($value) + public static function setCacheGeneral($value) { - $cache = self::getInstance(); - $cacheId = 'general'; - $cache->set($cacheId, $value); - return true; + $cache = self::getCache(); + + return $cache->save(self::$cacheIdGeneral, $value, self::getTtl()); } /** @@ -173,11 +186,12 @@ class Cache * * @param array|int $idSites Array of idSites to clear cache for */ - static public function regenerateCacheWebsiteAttributes($idSites = array()) + public static function regenerateCacheWebsiteAttributes($idSites = array()) { if (!is_array($idSites)) { $idSites = array($idSites); } + foreach ($idSites as $idSite) { self::deleteCacheWebsiteAttributes($idSite); self::getCacheWebsiteAttributes($idSite); @@ -189,17 +203,16 @@ class Cache * * @param string $idSite (website ID of the site to clear cache for */ - static public function deleteCacheWebsiteAttributes($idSite) + public static function deleteCacheWebsiteAttributes($idSite) { - $idSite = (int)$idSite; - self::getInstance()->delete($idSite); + self::getCache()->delete((int) $idSite); } /** * Deletes all Tracker cache files */ - static public function deleteTrackerCache() + public static function deleteTrackerCache() { - self::getInstance()->deleteAll(); + self::getCache()->flushAll(); } } diff --git a/www/analytics/core/Tracker/Db.php b/www/analytics/core/Tracker/Db.php index 495eb683..e5ec25f5 100644 --- a/www/analytics/core/Tracker/Db.php +++ b/www/analytics/core/Tracker/Db.php @@ -1,6 +1,6 @@ queriesProfiling[$query])) $this->queriesProfiling[$query] = array('sum_time_ms' => 0, 'count' => 0); - $time = $timer->getTimeMs(2); + if (!isset($this->queriesProfiling[$query])) { + $this->queriesProfiling[$query] = array('sum_time_ms' => 0, 'count' => 0); + } + + $time = $timer->getTimeMs(2); $time += $this->queriesProfiling[$query]['sum_time_ms']; $count = $this->queriesProfiling[$query]['count'] + 1; + $this->queriesProfiling[$query] = array('sum_time_ms' => $time, 'count' => $count); } @@ -97,13 +106,13 @@ abstract class Db self::$profiling = false; foreach ($this->queriesProfiling as $query => $info) { - $time = $info['sum_time_ms']; + $time = $info['sum_time_ms']; + $time = Common::forceDotAsSeparatorForDecimalPoint($time); $count = $info['count']; $queryProfiling = "INSERT INTO " . Common::prefixTable('log_profiling') . " (query,count,sum_time_ms) VALUES (?,$count,$time) - ON DUPLICATE KEY - UPDATE count=count+$count,sum_time_ms=sum_time_ms+$time"; + ON DUPLICATE KEY UPDATE count=count+$count,sum_time_ms=sum_time_ms+$time"; $this->query($queryProfiling, array($query)); } @@ -222,4 +231,63 @@ abstract class Db * @return bool True if error number matches; false otherwise */ abstract public function isErrNo($e, $errno); + + /** + * Factory to create database objects + * + * @param array $configDb Database configuration + * @throws Exception + * @return \Piwik\Tracker\Db\Mysqli|\Piwik\Tracker\Db\Pdo\Mysql + */ + public static function factory($configDb) + { + /** + * Triggered before a connection to the database is established by the Tracker. + * + * This event can be used to change the database connection settings used by the Tracker. + * + * @param array $dbInfos Reference to an array containing database connection info, + * including: + * + * - **host**: The host name or IP address to the MySQL database. + * - **username**: The username to use when connecting to the + * database. + * - **password**: The password to use when connecting to the + * database. + * - **dbname**: The name of the Piwik MySQL database. + * - **port**: The MySQL database port to use. + * - **adapter**: either `'PDO\MYSQL'` or `'MYSQLI'` + * - **type**: The MySQL engine to use, for instance 'InnoDB' + */ + Piwik::postEvent('Tracker.getDatabaseConfig', array(&$configDb)); + + switch ($configDb['adapter']) { + case 'PDO\MYSQL': + case 'PDO_MYSQL': // old format pre Piwik 2 + require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Pdo/Mysql.php'; + return new Mysql($configDb); + + case 'MYSQLI': + require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Mysqli.php'; + return new Mysqli($configDb); + } + + throw new Exception('Unsupported database adapter ' . $configDb['adapter']); + } + + public static function connectPiwikTrackerDb() + { + $db = null; + $configDb = Config::getInstance()->database; + + if (!isset($configDb['port'])) { + // before 0.2.4 there is no port specified in config file + $configDb['port'] = '3306'; + } + + $db = self::factory($configDb); + $db->connect(); + + return $db; + } } diff --git a/www/analytics/core/Tracker/Db/DbException.php b/www/analytics/core/Tracker/Db/DbException.php index fc3ffea9..a067f09f 100644 --- a/www/analytics/core/Tracker/Db/DbException.php +++ b/www/analytics/core/Tracker/Db/DbException.php @@ -1,6 +1,6 @@ host = null; $this->port = null; $this->socket = $dbInfo['unix_socket']; - } else if ($dbInfo['port'][0] == '/') { + } elseif ($dbInfo['port'][0] == '/') { $this->host = null; $this->port = null; $this->socket = $dbInfo['port']; } else { $this->host = $dbInfo['host']; - $this->port = $dbInfo['port']; + $this->port = (int)$dbInfo['port']; $this->socket = null; } $this->dbname = $dbInfo['dbname']; @@ -72,7 +73,14 @@ class Mysqli extends Db $timer = $this->initProfiler(); } - $this->connection = mysqli_connect($this->host, $this->username, $this->password, $this->dbname, $this->port, $this->socket); + $this->connection = mysqli_init(); + + // Make sure MySQL returns all matched rows on update queries including + // rows that actually didn't have to be updated because the values didn't + // change. This matches common behaviour among other database systems. + // See #6296 why this is important in tracker + $flags = MYSQLI_CLIENT_FOUND_ROWS; + mysqli_real_connect($this->connection, $this->host, $this->username, $this->password, $this->dbname, $this->port, $this->socket, $flags); if (!$this->connection || mysqli_connect_errno()) { throw new DbException("Connect failed: " . mysqli_connect_error()); } @@ -204,8 +212,8 @@ class Mysqli extends Db return $result; } catch (Exception $e) { throw new DbException("Error query: " . $e->getMessage() . " - In query: $query - Parameters: " . var_export($parameters, true)); + In query: $query + Parameters: " . var_export($parameters, true), $e->getCode()); } } @@ -231,7 +239,7 @@ class Mysqli extends Db { if (!$parameters) { $parameters = array(); - } else if (!is_array($parameters)) { + } elseif (!is_array($parameters)) { $parameters = array($parameters); } @@ -276,4 +284,62 @@ class Mysqli extends Db { return mysqli_affected_rows($this->connection); } + + /** + * Start Transaction + * @return string TransactionID + */ + public function beginTransaction() + { + if (!$this->activeTransaction === false) { + return; + } + + if ($this->connection->autocommit(false)) { + $this->activeTransaction = uniqid(); + return $this->activeTransaction; + } + } + + /** + * Commit Transaction + * @param $xid + * @throws DbException + * @internal param TransactionID $string from beginTransaction + */ + public function commit($xid) + { + if ($this->activeTransaction != $xid || $this->activeTransaction === false) { + return; + } + + $this->activeTransaction = false; + + if (!$this->connection->commit()) { + throw new DbException("Commit failed"); + } + + $this->connection->autocommit(true); + } + + /** + * Rollback Transaction + * @param $xid + * @throws DbException + * @internal param TransactionID $string from beginTransaction + */ + public function rollBack($xid) + { + if ($this->activeTransaction != $xid || $this->activeTransaction === false) { + return; + } + + $this->activeTransaction = false; + + if (!$this->connection->rollback()) { + throw new DbException("Rollback failed"); + } + + $this->connection->autocommit(true); + } } diff --git a/www/analytics/core/Tracker/Db/Pdo/Mysql.php b/www/analytics/core/Tracker/Db/Pdo/Mysql.php index 3a2e04ab..7e4f7458 100644 --- a/www/analytics/core/Tracker/Db/Pdo/Mysql.php +++ b/www/analytics/core/Tracker/Db/Pdo/Mysql.php @@ -1,6 +1,6 @@ dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';unix_socket=' . $dbInfo['unix_socket']; - } else if (!empty($dbInfo['port']) && $dbInfo['port'][0] == '/') { + } elseif (!empty($dbInfo['port']) && $dbInfo['port'][0] == '/') { $this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';unix_socket=' . $dbInfo['port']; } else { $this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';host=' . $dbInfo['host'] . ';port=' . $dbInfo['port']; } + $this->username = $dbInfo['username']; $this->password = $dbInfo['password']; - $this->charset = isset($dbInfo['charset']) ? $dbInfo['charset'] : null; + + if (isset($dbInfo['charset'])) { + $this->charset = $dbInfo['charset']; + $this->dsn .= ';charset=' . $this->charset; + } } public function __destruct() @@ -66,8 +73,17 @@ class Mysql extends Db $timer = $this->initProfiler(); } - $this->connection = @new PDO($this->dsn, $this->username, $this->password, $config = array()); - $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + // Make sure MySQL returns all matched rows on update queries including + // rows that actually didn't have to be updated because the values didn't + // change. This matches common behaviour among other database systems. + // See #6296 why this is important in tracker + $config = array( + PDO::MYSQL_ATTR_FOUND_ROWS => true, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ); + + $this->connection = @new PDO($this->dsn, $this->username, $this->password, $config); + // we may want to setAttribute(PDO::ATTR_TIMEOUT ) to a few seconds (default is 60) in case the DB is locked // the piwik.php would stay waiting for the database... bad! // we delete the password from this object "just in case" it could be printed @@ -192,9 +208,8 @@ class Mysql extends Db } return $sth; } catch (PDOException $e) { - throw new DbException("Error query: " . $e->getMessage() . " - In query: $query - Parameters: " . var_export($parameters, true)); + $message = $e->getMessage() . " In query: $query Parameters: " . var_export($parameters, true); + throw new DbException("Error query: " . $message, (int) $e->getCode()); } } @@ -234,4 +249,58 @@ class Mysql extends Db { return $queryResult->rowCount(); } + + /** + * Start Transaction + * @return string TransactionID + */ + public function beginTransaction() + { + if (!$this->activeTransaction === false) { + return; + } + + if ($this->connection->beginTransaction()) { + $this->activeTransaction = uniqid(); + return $this->activeTransaction; + } + } + + /** + * Commit Transaction + * @param $xid + * @throws DbException + * @internal param TransactionID $string from beginTransaction + */ + public function commit($xid) + { + if ($this->activeTransaction != $xid || $this->activeTransaction === false) { + return; + } + + $this->activeTransaction = false; + + if (!$this->connection->commit()) { + throw new DbException("Commit failed"); + } + } + + /** + * Rollback Transaction + * @param $xid + * @throws DbException + * @internal param TransactionID $string from beginTransaction + */ + public function rollBack($xid) + { + if ($this->activeTransaction != $xid || $this->activeTransaction === false) { + return; + } + + $this->activeTransaction = false; + + if (!$this->connection->rollBack()) { + throw new DbException("Rollback failed"); + } + } } diff --git a/www/analytics/core/Tracker/Db/Pdo/Pgsql.php b/www/analytics/core/Tracker/Db/Pdo/Pgsql.php index 60087c65..b70122aa 100644 --- a/www/analytics/core/Tracker/Db/Pdo/Pgsql.php +++ b/www/analytics/core/Tracker/Db/Pdo/Pgsql.php @@ -1,6 +1,6 @@ request = $request; - $this->init(); - } + if (empty($visitInformation['visit_goal_buyer'])) { + return false; + } - function init() - { - $this->orderId = $this->request->getParam('ec_id'); - $this->isGoalAnOrder = !empty($this->orderId); - $this->idGoal = $this->request->getParam('idgoal'); - $this->requestIsEcommerce = ($this->idGoal == 0); - } + $goalBuyer = $visitInformation['visit_goal_buyer']; + $types = array(GoalManager::TYPE_BUYER_OPEN_CART, GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART); - function getBuyerType($existingType = GoalManager::TYPE_BUYER_NONE) - { // Was there a Cart for this visit prior to the order? - $this->isThereExistingCartInVisit = in_array($existingType, - array(GoalManager::TYPE_BUYER_OPEN_CART, - GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART)); - - if (!$this->requestIsEcommerce) { - return $existingType; - } - if ($this->isGoalAnOrder) { - return self::TYPE_BUYER_ORDERED; - } - // request is Add to Cart - if ($existingType == self::TYPE_BUYER_ORDERED - || $existingType == self::TYPE_BUYER_ORDERED_AND_OPEN_CART - ) { - return self::TYPE_BUYER_ORDERED_AND_OPEN_CART; - } - return self::TYPE_BUYER_OPEN_CART; + return in_array($goalBuyer, $types); } - static public function getGoalDefinitions($idSite) + public static function getGoalDefinitions($idSite) { $websiteAttributes = Cache::getCacheWebsiteAttributes($idSite); + if (isset($websiteAttributes['goals'])) { return $websiteAttributes['goals']; } + return array(); } - static public function getGoalDefinition($idSite, $idGoal) + public static function getGoalDefinition($idSite, $idGoal) { $goals = self::getGoalDefinitions($idSite); + foreach ($goals as $goal) { if ($goal['idgoal'] == $idGoal) { return $goal; } } + throw new Exception('Goal not found'); } - static public function getGoalIds($idSite) + public static function getGoalIds($idSite) { - $goals = self::getGoalDefinitions($idSite); + $goals = self::getGoalDefinitions($idSite); $goalIds = array(); + foreach ($goals as $goal) { $goalIds[] = $goal['idgoal']; } + return $goalIds; } @@ -127,109 +117,124 @@ class GoalManager * @param int $idSite * @param Action $action * @throws Exception - * @return int Number of goals matched + * @return array[] Goals matched */ - function detectGoalsMatchingUrl($idSite, $action) + public function detectGoalsMatchingUrl($idSite, $action) { if (!Common::isGoalPluginEnabled()) { - return false; + return array(); } - $decodedActionUrl = $action->getActionUrl(); - $actionType = $action->getActionType(); $goals = $this->getGoalDefinitions($idSite); + + $convertedGoals = array(); foreach ($goals as $goal) { - $attribute = $goal['match_attribute']; - // if the attribute to match is not the type of the current action - if ( (($attribute == 'url' || $attribute == 'title') && $actionType != Action::TYPE_PAGE_URL) - || ($attribute == 'file' && $actionType != Action::TYPE_DOWNLOAD) - || ($attribute == 'external_website' && $actionType != Action::TYPE_OUTLINK) - || ($attribute == 'manually') - ) { - continue; - } - - $url = $decodedActionUrl; - // Matching on Page Title - if ($attribute == 'title') { - $url = $action->getActionName(); - } - $pattern_type = $goal['pattern_type']; - - $match = $this->isUrlMatchingGoal($goal, $pattern_type, $url); - if ($match) { - $goal['url'] = $decodedActionUrl; - $this->convertedGoals[] = $goal; + $convertedUrl = $this->detectGoalMatch($goal, $action); + if (!empty($convertedUrl)) { + $convertedGoals[] = array('url' => $convertedUrl) + $goal; } } - return count($this->convertedGoals) > 0; + return $convertedGoals; } - function detectGoalId($idSite) + /** + * Detects if an Action matches a given goal. If it does, the URL that triggered the goal + * is returned. Otherwise null is returned. + * + * @param array $goal + * @param Action $action + * @return string|null + */ + public function detectGoalMatch($goal, Action $action) + { + $actionType = $action->getActionType(); + + $attribute = $goal['match_attribute']; + + // if the attribute to match is not the type of the current action + if ((($attribute == 'url' || $attribute == 'title') && $actionType != Action::TYPE_PAGE_URL) + || ($attribute == 'file' && $actionType != Action::TYPE_DOWNLOAD) + || ($attribute == 'external_website' && $actionType != Action::TYPE_OUTLINK) + || ($attribute == 'manually') + || in_array($attribute, array('event_action', 'event_name', 'event_category')) && $actionType != Action::TYPE_EVENT + ) { + return null; + } + + + switch ($attribute) { + case 'title': + // Matching on Page Title + $url = $action->getActionName(); + break; + case 'event_action': + $url = $action->getEventAction(); + break; + case 'event_name': + $url = $action->getEventName(); + break; + case 'event_category': + $url = $action->getEventCategory(); + break; + // url, external_website, file, manually... + default: + $url = $action->getActionUrlRaw(); + break; + } + + $pattern_type = $goal['pattern_type']; + + $match = $this->isUrlMatchingGoal($goal, $pattern_type, $url); + if (!$match) { + return null; + } + + return $action->getActionUrl(); + } + + public function detectGoalId($idSite, Request $request) { if (!Common::isGoalPluginEnabled()) { - return false; + return null; } - $goals = $this->getGoalDefinitions($idSite); - if (!isset($goals[$this->idGoal])) { - return false; - } - $goal = $goals[$this->idGoal]; - $url = $this->request->getParam('url'); + $idGoal = $request->getParam('idgoal'); + + $goals = $this->getGoalDefinitions($idSite); + + if (!isset($goals[$idGoal])) { + return null; + } + + $goal = $goals[$idGoal]; + + $url = $request->getParam('url'); $goal['url'] = PageUrl::excludeQueryParametersFromUrl($url, $idSite); - $goal['revenue'] = $this->getRevenue($this->request->getGoalRevenue($goal['revenue'])); - $this->convertedGoals[] = $goal; - return true; + return $goal; } /** * Records one or several goals matched in this request. * - * @param int $idSite + * @param Visitor $visitor * @param array $visitorInformation * @param array $visitCustomVariables * @param Action $action */ - public function recordGoals($idSite, $visitorInformation, $visitCustomVariables, $action) + public function recordGoals(VisitProperties $visitProperties, Request $request) { - $referrerTimestamp = $this->request->getParam('_refts'); - $referrerUrl = $this->request->getParam('_ref'); - $referrerCampaignName = trim(urldecode($this->request->getParam('_rcn'))); - $referrerCampaignKeyword = trim(urldecode($this->request->getParam('_rck'))); - $browserLanguage = $this->request->getBrowserLanguage(); + $visitorInformation = $visitProperties->getProperties(); + $visitCustomVariables = $request->getMetadata('CustomVariables', 'visitCustomVariables') ?: array(); - $location_country = isset($visitorInformation['location_country']) - ? $visitorInformation['location_country'] - : Common::getCountry( - $browserLanguage, - $enableLanguageToCountryGuess = Config::getInstance()->Tracker['enable_language_to_country_guess'], - $visitorInformation['location_ip'] - ); + /** @var Action $action */ + $action = $request->getMetadata('Actions', 'action'); - $goal = array( - 'idvisit' => $visitorInformation['idvisit'], - 'idsite' => $idSite, - 'idvisitor' => $visitorInformation['idvisitor'], - 'server_time' => Tracker::getDatetimeFromTimestamp($visitorInformation['visit_last_action_time']), - 'location_country' => $location_country, - 'visitor_returning' => $visitorInformation['visitor_returning'], - 'visitor_days_since_first' => $visitorInformation['visitor_days_since_first'], - 'visitor_days_since_order' => $visitorInformation['visitor_days_since_order'], - 'visitor_count_visits' => $visitorInformation['visitor_count_visits'], - ); - - $extraLocationCols = array('location_region', 'location_city', 'location_latitude', 'location_longitude'); - foreach ($extraLocationCols as $col) { - if (isset($visitorInformation[$col])) { - $goal[$col] = $visitorInformation[$col]; - } - } + $goal = $this->getGoalFromVisitor($visitProperties, $request, $action); // Copy Custom Variables from Visit row to the Goal conversion // Otherwise, set the Custom Variables found in the cookie sent with this request $goal += $visitCustomVariables; - $maxCustomVariables = CustomVariables::getMaxCustomVariables(); + $maxCustomVariables = CustomVariables::getNumUsableCustomVariables(); for ($i = 1; $i <= $maxCustomVariables; $i++) { if (isset($visitorInformation['custom_var_k' . $i]) @@ -244,60 +249,12 @@ class GoalManager } } - // Attributing the correct Referrer to this conversion. - // Priority order is as follows: - // 0) In some cases, the campaign is not passed from the JS so we look it up from the current visit - // 1) Campaign name/kwd parsed in the JS - // 2) Referrer URL stored in the _ref cookie - // 3) If no info from the cookie, attribute to the current visit referrer - - // 3) Default values: current referrer - $type = $visitorInformation['referer_type']; - $name = $visitorInformation['referer_name']; - $keyword = $visitorInformation['referer_keyword']; - $time = $visitorInformation['visit_first_action_time']; - - // 0) In some (unknown!?) cases the campaign is not found in the attribution cookie, but the URL ref was found. - // In this case we look up if the current visit is credited to a campaign and will credit this campaign rather than the URL ref (since campaigns have higher priority) - if (empty($referrerCampaignName) - && $type == Common::REFERRER_TYPE_CAMPAIGN - && !empty($name) - ) { - // Use default values per above - } // 1) Campaigns from 1st party cookie - elseif (!empty($referrerCampaignName)) { - $type = Common::REFERRER_TYPE_CAMPAIGN; - $name = $referrerCampaignName; - $keyword = $referrerCampaignKeyword; - $time = $referrerTimestamp; - } // 2) Referrer URL parsing - elseif (!empty($referrerUrl)) { - $referrer = new Referrer(); - $referrer = $referrer->getReferrerInformation($referrerUrl, $currentUrl = '', $idSite); - - // if the parsed referrer is interesting enough, ie. website or search engine - if (in_array($referrer['referer_type'], array(Common::REFERRER_TYPE_SEARCH_ENGINE, Common::REFERRER_TYPE_WEBSITE))) { - $type = $referrer['referer_type']; - $name = $referrer['referer_name']; - $keyword = $referrer['referer_keyword']; - $time = $referrerTimestamp; - } - } - $this->setCampaignValuesToLowercase($type, $name, $keyword); - - $goal += array( - 'referer_type' => $type, - 'referer_name' => $name, - 'referer_keyword' => $keyword, - // this field is currently unused - 'referer_visit_server_date' => date("Y-m-d", $time), - ); - // some goals are converted, so must be ecommerce Order or Cart Update - if ($this->requestIsEcommerce) { - $this->recordEcommerceGoal($goal, $visitorInformation); + $isRequestEcommerce = $request->getMetadata('Ecommerce', 'isRequestEcommerce'); + if ($isRequestEcommerce) { + $this->recordEcommerceGoal($visitProperties, $request, $goal, $action); } else { - $this->recordStandardGoals($goal, $action, $visitorInformation); + $this->recordStandardGoals($visitProperties, $request, $goal, $action); } } @@ -309,10 +266,13 @@ class GoalManager */ protected function getRevenue($revenue) { - if (round($revenue) == $revenue) { - return $revenue; + if (round($revenue) != $revenue) { + $revenue = round($revenue, self::REVENUE_PRECISION); } - return round($revenue, self::REVENUE_PRECISION); + + $revenue = Common::forceDotAsSeparatorForDecimalPoint($revenue); + + return $revenue; } /** @@ -320,92 +280,107 @@ class GoalManager * Will deal with 2 types of conversions: Ecommerce Order and Ecommerce Cart update (Add to cart, Update Cart etc). * * @param array $conversion + * @param Visitor $visitor + * @param Action $action * @param array $visitInformation */ - protected function recordEcommerceGoal($conversion, $visitInformation) + protected function recordEcommerceGoal(VisitProperties $visitProperties, Request $request, $conversion, $action) { - if ($this->isThereExistingCartInVisit) { + $isThereExistingCartInVisit = $request->getMetadata('Goals', 'isThereExistingCartInVisit'); + if ($isThereExistingCartInVisit) { Common::printDebug("There is an existing cart for this visit"); } - if ($this->isGoalAnOrder) { - $conversion['idgoal'] = self::IDGOAL_ORDER; - $conversion['idorder'] = $this->orderId; - $conversion['buster'] = Common::hashStringToInt($this->orderId); - $conversion['revenue_subtotal'] = $this->getRevenue($this->request->getParam('ec_st')); - $conversion['revenue_tax'] = $this->getRevenue($this->request->getParam('ec_tx')); - $conversion['revenue_shipping'] = $this->getRevenue($this->request->getParam('ec_sh')); - $conversion['revenue_discount'] = $this->getRevenue($this->request->getParam('ec_dt')); + $visitor = Visitor::makeFromVisitProperties($visitProperties, $request); + + $isGoalAnOrder = $request->getMetadata('Ecommerce', 'isGoalAnOrder'); + if ($isGoalAnOrder) { $debugMessage = 'The conversion is an Ecommerce order'; + + $orderId = $request->getParam('ec_id'); + + $conversion['idorder'] = $orderId; + $conversion['idgoal'] = self::IDGOAL_ORDER; + $conversion['buster'] = Common::hashStringToInt($orderId); + + $conversionDimensions = ConversionDimension::getAllDimensions(); + $conversion = $this->triggerHookOnDimensions($request, $conversionDimensions, 'onEcommerceOrderConversion', $visitor, $action, $conversion); } // If Cart update, select current items in the previous Cart else { + $debugMessage = 'The conversion is an Ecommerce Cart Update'; + $conversion['buster'] = 0; $conversion['idgoal'] = self::IDGOAL_CART; - $debugMessage = 'The conversion is an Ecommerce Cart Update'; + + $conversionDimensions = ConversionDimension::getAllDimensions(); + $conversion = $this->triggerHookOnDimensions($request, $conversionDimensions, 'onEcommerceCartUpdateConversion', $visitor, $action, $conversion); } - $conversion['revenue'] = $this->getRevenue($this->request->getGoalRevenue($defaultRevenue = 0)); Common::printDebug($debugMessage . ':' . var_export($conversion, true)); // INSERT or Sync items in the Cart / Order for this visit & order - $items = $this->getEcommerceItemsFromRequest(); - if ($items === false) { + $items = $this->getEcommerceItemsFromRequest($request); + + if (false === $items) { return; } $itemsCount = 0; foreach ($items as $item) { - $itemsCount += $item[self::INTERNAL_ITEM_QUANTITY]; + $itemsCount += $item[GoalManager::INTERNAL_ITEM_QUANTITY]; } + $conversion['items'] = $itemsCount; - if($this->isThereExistingCartInVisit) { - $updateWhere = array( - 'idvisit' => $visitInformation['idvisit'], - 'idgoal' => self::IDGOAL_CART, - 'buster' => 0, - ); - $recorded = $this->updateExistingConversion($conversion, $updateWhere); + if ($isThereExistingCartInVisit) { + $recorded = $this->getModel()->updateConversion( + $visitProperties->getProperty('idvisit'), self::IDGOAL_CART, $conversion); } else { - $recorded = $this->insertNewConversion($conversion, $visitInformation); + $recorded = $this->insertNewConversion($conversion, $visitProperties->getProperties(), $request); } if ($recorded) { - $this->recordEcommerceItems($conversion, $items, $visitInformation); + $this->recordEcommerceItems($conversion, $items); } /** * Triggered after successfully persisting an ecommerce conversion. - * + * * _Note: Subscribers should be wary of doing any expensive computation here as it may slow * the tracker down._ - * + * + * This event is deprecated, use [Dimensions](http://developer.piwik.org/guides/dimensions) instead. + * * @param array $conversion The conversion entity that was just persisted. See what information * it contains [here](/guides/persistence-and-the-mysql-backend#conversions). * @param array $visitInformation The visit entity that we are tracking a conversion for. See what * information it contains [here](/guides/persistence-and-the-mysql-backend#visits). + * @deprecated */ - Piwik::postEvent('Tracker.recordEcommerceGoal', array($conversion, $visitInformation)); + Piwik::postEvent('Tracker.recordEcommerceGoal', array($conversion, $visitProperties->getProperties())); } /** * Returns Items read from the request string * @return array|bool */ - protected function getEcommerceItemsFromRequest() + private function getEcommerceItemsFromRequest(Request $request) { - $items = Common::unsanitizeInputValue($this->request->getParam('ec_items')); + $items = $request->getParam('ec_items'); + if (empty($items)) { Common::printDebug("There are no Ecommerce items in the request"); // we still record an Ecommerce order without any item in it return array(); } - $items = Common::json_decode($items, $assoc = true); + if (!is_array($items)) { Common::printDebug("Error while json_decode the Ecommerce items = " . var_export($items, true)); return false; } + $items = Common::unsanitizeInputValues($items); + $cleanedItems = $this->getCleanedEcommerceItems($items); return $cleanedItems; } @@ -425,23 +400,11 @@ class GoalManager $itemInCartBySku[$item[0]] = $item; } - // Select all items currently in the Cart if any - $sql = "SELECT idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted, idorder as idorder_original_value - FROM " . Common::prefixTable('log_conversion_item') . " - WHERE idvisit = ? - AND (idorder = ? OR idorder = ?)"; + $itemsInDb = $this->getModel()->getAllItemsCurrentlyInTheCart($goal, self::ITEM_IDORDER_ABANDONED_CART); - $bind = array($goal['idvisit'], - isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART, - self::ITEM_IDORDER_ABANDONED_CART - ); - - $itemsInDb = Tracker::getDatabase()->fetchAll($sql, $bind); - - Common::printDebug("Items found in current cart, for conversion_item (visit,idorder)=" . var_export($bind, true)); - Common::printDebug($itemsInDb); // Look at which items need to be deleted, which need to be added or updated, based on the SKU $skuFoundInDb = $itemsToUpdate = array(); + foreach ($itemsInDb as $itemInDb) { $skuFoundInDb[] = $itemInDb['idaction_sku']; @@ -492,27 +455,10 @@ class GoalManager $itemsToInsert[] = $item; } } + $this->insertEcommerceItems($goal, $itemsToInsert); } - // In the GET items parameter, each item has the following array of information - const INDEX_ITEM_SKU = 0; - const INDEX_ITEM_NAME = 1; - const INDEX_ITEM_CATEGORY = 2; - const INDEX_ITEM_PRICE = 3; - const INDEX_ITEM_QUANTITY = 4; - - // Used in the array of items, internally to this class - const INTERNAL_ITEM_SKU = 0; - const INTERNAL_ITEM_NAME = 1; - const INTERNAL_ITEM_CATEGORY = 2; - const INTERNAL_ITEM_CATEGORY2 = 3; - const INTERNAL_ITEM_CATEGORY3 = 4; - const INTERNAL_ITEM_CATEGORY4 = 5; - const INTERNAL_ITEM_CATEGORY5 = 6; - const INTERNAL_ITEM_PRICE = 7; - const INTERNAL_ITEM_QUANTITY = 8; - /** * Reads items from the request, then looks up the names from the lookup table * and returns a clean array of items ready for the database. @@ -520,14 +466,15 @@ class GoalManager * @param array $items * @return array $cleanedItems */ - protected function getCleanedEcommerceItems($items) + private function getCleanedEcommerceItems($items) { // Clean up the items array $cleanedItems = array(); foreach ($items as $item) { - $name = $category = $category2 = $category3 = $category4 = $category5 = false; - $price = 0; + $name = $category = $category2 = $category3 = $category4 = $category5 = false; + $price = 0; $quantity = 1; + // items are passed in the request as an array: ( $sku, $name, $category, $price, $quantity ) if (empty($item[self::INDEX_ITEM_SKU])) { continue; @@ -619,6 +566,7 @@ class GoalManager $item[5] = $actionsLookedUp[$index * $columnsInEachRow + 5]; $item[6] = $actionsLookedUp[$index * $columnsInEachRow + 6]; } + return $cleanedItems; } @@ -636,29 +584,23 @@ class GoalManager if (empty($itemsToUpdate)) { return; } + Common::printDebug("Goal data used to update ecommerce items:"); Common::printDebug($goal); foreach ($itemsToUpdate as $item) { $newRow = $this->getItemRowEnriched($goal, $item); Common::printDebug($newRow); - $updateParts = $sqlBind = array(); - foreach ($newRow AS $name => $value) { - $updateParts[] = $name . " = ?"; - $sqlBind[] = $value; - } - $sql = 'UPDATE ' . Common::prefixTable('log_conversion_item') . " - SET " . implode($updateParts, ', ') . " - WHERE idvisit = ? - AND idorder = ? - AND idaction_sku = ?"; - $sqlBind[] = $newRow['idvisit']; - $sqlBind[] = $item['idorder_original_value']; - $sqlBind[] = $newRow['idaction_sku']; - Tracker::getDatabase()->query($sql, $sqlBind); + + $this->getModel()->updateEcommerceItem($item['idorder_original_value'], $newRow); } } + private function getModel() + { + return new Model(); + } + /** * Inserts in the cart in the DB the new items * that were not previously in the cart @@ -673,27 +615,17 @@ class GoalManager if (empty($itemsToInsert)) { return; } + Common::printDebug("Ecommerce items that are added to the cart/order"); Common::printDebug($itemsToInsert); - $sql = "INSERT INTO " . Common::prefixTable('log_conversion_item') . " - (idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted, - idorder, idsite, idvisitor, server_time, idvisit) - VALUES "; - $i = 0; - $bind = array(); + $items = array(); + foreach ($itemsToInsert as $item) { - if ($i > 0) { - $sql .= ','; - } - $newRow = array_values($this->getItemRowEnriched($goal, $item)); - $sql .= " ( " . Common::getSqlStringFieldsArray($newRow) . " ) "; - $i++; - $bind = array_merge($bind, $newRow); + $items[] = $this->getItemRowEnriched($goal, $item); } - Tracker::getDatabase()->query($sql, $bind); - Common::printDebug($sql); - Common::printDebug($bind); + + $this->getModel()->createEcommerceItems($items); } protected function getItemRowEnriched($goal, $item) @@ -706,7 +638,7 @@ class GoalManager 'idaction_category3' => (int)$item[self::INTERNAL_ITEM_CATEGORY3], 'idaction_category4' => (int)$item[self::INTERNAL_ITEM_CATEGORY4], 'idaction_category5' => (int)$item[self::INTERNAL_ITEM_CATEGORY5], - 'price' => $item[self::INTERNAL_ITEM_PRICE], + 'price' => Common::forceDotAsSeparatorForDecimalPoint($item[self::INTERNAL_ITEM_PRICE]), 'quantity' => $item[self::INTERNAL_ITEM_QUANTITY], 'deleted' => isset($item['deleted']) ? $item['deleted'] : 0, //deleted 'idorder' => isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART, //idorder = 0 in log_conversion_item for carts @@ -718,21 +650,34 @@ class GoalManager return $newRow; } + public function getGoalColumn($column) + { + if (array_key_exists($column, $this->currentGoal)) { + return $this->currentGoal[$column]; + } + + return false; + } + /** * Records a standard non-Ecommerce goal in the DB (URL/Title matching), * linking the conversion to the action that triggered it * @param $goal + * @param Visitor $visitor * @param Action $action * @param $visitorInformation */ - protected function recordStandardGoals($goal, $action, $visitorInformation) + protected function recordStandardGoals(VisitProperties $visitProperties, Request $request, $goal, $action) { - foreach ($this->convertedGoals as $convertedGoal) { + $visitor = Visitor::makeFromVisitProperties($visitProperties, $request); + + $convertedGoals = $request->getMetadata('Goals', 'goalsConverted') ?: array(); + foreach ($convertedGoals as $convertedGoal) { + $this->currentGoal = $convertedGoal; Common::printDebug("- Goal " . $convertedGoal['idgoal'] . " matched. Recording..."); $conversion = $goal; $conversion['idgoal'] = $convertedGoal['idgoal']; - $conversion['url'] = $convertedGoal['url']; - $conversion['revenue'] = $this->getRevenue($convertedGoal['revenue']); + $conversion['url'] = $convertedGoal['url']; if (!is_null($action)) { $conversion['idaction_url'] = $action->getIdActionUrl(); @@ -742,18 +687,24 @@ class GoalManager // If multiple Goal conversions per visit, set a cache buster $conversion['buster'] = $convertedGoal['allow_multiple'] == 0 ? '0' - : $visitorInformation['visit_last_action_time']; + : $visitProperties->getProperty('visit_last_action_time'); - $this->insertNewConversion($conversion, $visitorInformation); + $conversionDimensions = ConversionDimension::getAllDimensions(); + $conversion = $this->triggerHookOnDimensions($request, $conversionDimensions, 'onGoalConversion', $visitor, $action, $conversion); + + $this->insertNewConversion($conversion, $visitProperties->getProperties(), $request); /** * Triggered after successfully recording a non-ecommerce conversion. - * + * * _Note: Subscribers should be wary of doing any expensive computation here as it may slow * the tracker down._ - * + * + * This event is deprecated, use [Dimensions](http://developer.piwik.org/guides/dimensions) instead. + * * @param array $conversion The conversion entity that was just persisted. See what information * it contains [here](/guides/persistence-and-the-mysql-backend#conversions). + * @deprecated */ Piwik::postEvent('Tracker.recordStandardGoals', array($conversion)); } @@ -766,34 +717,31 @@ class GoalManager * @param array $visitInformation * @return bool */ - protected function insertNewConversion($conversion, $visitInformation) + protected function insertNewConversion($conversion, $visitInformation, Request $request) { /** * Triggered before persisting a new [conversion entity](/guides/persistence-and-the-mysql-backend#conversions). - * + * * This event can be used to modify conversion information or to add new information to be persisted. - * + * + * This event is deprecated, use [Dimensions](http://developer.piwik.org/guides/dimensions) instead. + * * @param array $conversion The conversion entity. Read [this](/guides/persistence-and-the-mysql-backend#conversions) * to see what it contains. * @param array $visitInformation The visit entity that we are tracking a conversion for. See what * information it contains [here](/guides/persistence-and-the-mysql-backend#visits). * @param \Piwik\Tracker\Request $request An object describing the tracking request being processed. + * @deprecated */ - Piwik::postEvent('Tracker.newConversionInformation', array(&$conversion, $visitInformation, $this->request)); + Piwik::postEvent('Tracker.newConversionInformation', array(&$conversion, $visitInformation, $request)); $newGoalDebug = $conversion; $newGoalDebug['idvisitor'] = bin2hex($newGoalDebug['idvisitor']); Common::printDebug($newGoalDebug); - $fields = implode(", ", array_keys($conversion)); - $bindFields = Common::getSqlStringFieldsArray($conversion); - $sql = 'INSERT IGNORE INTO ' . Common::prefixTable('log_conversion') . " - ($fields) VALUES ($bindFields) "; - $bind = array_values($conversion); - $result = Tracker::getDatabase()->query($sql, $bind); + $wasInserted = $this->getModel()->createConversion($conversion); - // If a record was inserted, we return true - return Tracker::getDatabase()->rowCount($result) > 0; + return $wasInserted; } /** @@ -816,47 +764,6 @@ class GoalManager ); } - protected function updateExistingConversion($newGoal, $updateWhere) - { - $updateParts = $sqlBind = $updateWhereParts = array(); - foreach ($newGoal AS $name => $value) { - $updateParts[] = $name . " = ?"; - $sqlBind[] = $value; - } - foreach ($updateWhere as $name => $value) { - $updateWhereParts[] = $name . " = ?"; - $sqlBind[] = $value; - } - $sql = 'UPDATE ' . Common::prefixTable('log_conversion') . " - SET " . implode($updateParts, ', ') . " - WHERE " . implode($updateWhereParts, ' AND '); - - try { - Tracker::getDatabase()->query($sql, $sqlBind); - } catch(Exception $e){ - Common::printDebug("There was an error while updating the Conversion: " . $e->getMessage()); - return false; - } - return true; - } - - /** - * @param $type - * @param $name - * @param $keyword - */ - protected function setCampaignValuesToLowercase($type, &$name, &$keyword) - { - if ($type === Common::REFERRER_TYPE_CAMPAIGN) { - if (!empty($name)) { - $name = Common::mb_strtolower($name); - } - if (!empty($keyword)) { - $keyword = Common::mb_strtolower($keyword); - } - } - } - /** * @param $goal * @param $pattern_type @@ -865,6 +772,79 @@ class GoalManager * @throws \Exception */ protected function isUrlMatchingGoal($goal, $pattern_type, $url) + { + $url = Common::unsanitizeInputValue($url); + $goal['pattern'] = Common::unsanitizeInputValue($goal['pattern']); + + $match = $this->isGoalPatternMatchingUrl($goal, $pattern_type, $url); + + if (!$match) { + // Users may set Goal matching URL as URL encoded + $goal['pattern'] = urldecode($goal['pattern']); + + $match = $this->isGoalPatternMatchingUrl($goal, $pattern_type, $url); + } + return $match; + } + + /** + * @param ConversionDimension[] $dimensions + * @param string $hook + * @param Visitor $visitor + * @param Action|null $action + * @param array|null $valuesToUpdate If null, $this->visitorInfo will be updated + * + * @return array|null The updated $valuesToUpdate or null if no $valuesToUpdate given + */ + private function triggerHookOnDimensions(Request $request, $dimensions, $hook, $visitor, $action, $valuesToUpdate) + { + foreach ($dimensions as $dimension) { + $value = $dimension->$hook($request, $visitor, $action, $this); + + if (false !== $value) { + if (is_float($value)) { + $value = Common::forceDotAsSeparatorForDecimalPoint($value); + } + + $fieldName = $dimension->getColumnName(); + $visitor->setVisitorColumn($fieldName, $value); + + $valuesToUpdate[$fieldName] = $value; + } + } + + return $valuesToUpdate; + } + + private function getGoalFromVisitor(VisitProperties $visitProperties, Request $request, $action) + { + $goal = array( + 'idvisit' => $visitProperties->getProperty('idvisit'), + 'idvisitor' => $visitProperties->getProperty('idvisitor'), + 'server_time' => Date::getDatetimeFromTimestamp($visitProperties->getProperty('visit_last_action_time')), + ); + + $visitDimensions = VisitDimension::getAllDimensions(); + + $visit = Visitor::makeFromVisitProperties($visitProperties, $request); + foreach ($visitDimensions as $dimension) { + $value = $dimension->onAnyGoalConversion($request, $visit, $action); + if (false !== $value) { + $goal[$dimension->getColumnName()] = $value; + } + } + + return $goal; + } + + /** + * @param $goal + * @param $pattern_type + * @param $url + * @return bool + * @throws Exception + */ + protected function isGoalPatternMatchingUrl($goal, $pattern_type, $url) { switch ($pattern_type) { case 'regex': diff --git a/www/analytics/core/Tracker/Handler.php b/www/analytics/core/Tracker/Handler.php new file mode 100644 index 00000000..ed75104e --- /dev/null +++ b/www/analytics/core/Tracker/Handler.php @@ -0,0 +1,117 @@ +setResponse(new Response()); + } + + public function setResponse($response) + { + $this->response = $response; + } + + public function init(Tracker $tracker, RequestSet $requestSet) + { + $this->response->init($tracker); + } + + public function process(Tracker $tracker, RequestSet $requestSet) + { + foreach ($requestSet->getRequests() as $request) { + $tracker->trackRequest($request); + } + } + + public function onStartTrackRequests(Tracker $tracker, RequestSet $requestSet) + { + } + + public function onAllRequestsTracked(Tracker $tracker, RequestSet $requestSet) + { + $tasks = $this->getScheduledTasksRunner(); + if ($tasks->shouldRun($tracker)) { + $tasks->runScheduledTasks(); + } + } + + private function getScheduledTasksRunner() + { + if (is_null($this->tasksRunner)) { + $this->tasksRunner = new ScheduledTasksRunner(); + } + + return $this->tasksRunner; + } + + /** + * @internal + */ + public function setScheduledTasksRunner(ScheduledTasksRunner $runner) + { + $this->tasksRunner = $runner; + } + + public function onException(Tracker $tracker, RequestSet $requestSet, Exception $e) + { + Common::printDebug("Exception: " . $e->getMessage()); + + $statusCode = 500; + if ($e instanceof UnexpectedWebsiteFoundException) { + $statusCode = 400; + } elseif ($e instanceof InvalidRequestParameterException) { + $statusCode = 400; + } + + $this->response->outputException($tracker, $e, $statusCode); + $this->redirectIfNeeded($requestSet); + } + + public function finish(Tracker $tracker, RequestSet $requestSet) + { + $this->response->outputResponse($tracker); + $this->redirectIfNeeded($requestSet); + return $this->response->getOutput(); + } + + public function getResponse() + { + return $this->response; + } + + protected function redirectIfNeeded(RequestSet $requestSet) + { + $redirectUrl = $requestSet->shouldPerformRedirectToUrl(); + + if (!empty($redirectUrl)) { + Url::redirectToUrl($redirectUrl); + } + } +} diff --git a/www/analytics/core/Tracker/Handler/Factory.php b/www/analytics/core/Tracker/Handler/Factory.php new file mode 100644 index 00000000..63333747 --- /dev/null +++ b/www/analytics/core/Tracker/Handler/Factory.php @@ -0,0 +1,42 @@ +Tracker['cookie_name']; $cookie_path = @Config::getInstance()->Tracker['cookie_path']; @@ -35,7 +35,7 @@ class IgnoreCookie * * @return Cookie */ - static public function getIgnoreCookie() + public static function getIgnoreCookie() { $cookie_name = @Config::getInstance()->Tracker['ignore_visits_cookie_name']; $cookie_path = @Config::getInstance()->Tracker['cookie_path']; @@ -46,7 +46,7 @@ class IgnoreCookie /** * Set ignore (visit) cookie or deletes it if already present */ - static public function setIgnoreCookie() + public static function setIgnoreCookie() { $ignoreCookie = self::getIgnoreCookie(); if ($ignoreCookie->isCookieFound()) { @@ -65,7 +65,7 @@ class IgnoreCookie * * @return bool True if ignore cookie found; false otherwise */ - static public function isIgnoreCookieFound() + public static function isIgnoreCookieFound() { $cookie = self::getIgnoreCookie(); return $cookie->isCookieFound() && $cookie->get('ignore') === '*'; diff --git a/www/analytics/core/Tracker/Model.php b/www/analytics/core/Tracker/Model.php new file mode 100644 index 00000000..afffd5fa --- /dev/null +++ b/www/analytics/core/Tracker/Model.php @@ -0,0 +1,464 @@ +getDb(); + $db->query($sql, $bind); + + $id = $db->lastInsertId(); + + return $id; + } + + public function createConversion($conversion) + { + $fields = implode(", ", array_keys($conversion)); + $bindFields = Common::getSqlStringFieldsArray($conversion); + $table = Common::prefixTable('log_conversion'); + + $sql = "INSERT IGNORE INTO $table ($fields) VALUES ($bindFields) "; + $bind = array_values($conversion); + + $db = $this->getDb(); + $result = $db->query($sql, $bind); + + // If a record was inserted, we return true + return $db->rowCount($result) > 0; + } + + public function updateConversion($idVisit, $idGoal, $newConversion) + { + $updateWhere = array( + 'idvisit' => $idVisit, + 'idgoal' => $idGoal, + 'buster' => 0, + ); + + $updateParts = $sqlBind = $updateWhereParts = array(); + + foreach ($newConversion as $name => $value) { + $updateParts[] = $name . " = ?"; + $sqlBind[] = $value; + } + + foreach ($updateWhere as $name => $value) { + $updateWhereParts[] = $name . " = ?"; + $sqlBind[] = $value; + } + + $parts = implode($updateParts, ', '); + $table = Common::prefixTable('log_conversion'); + + $sql = "UPDATE $table SET $parts WHERE " . implode($updateWhereParts, ' AND '); + + try { + $this->getDb()->query($sql, $sqlBind); + } catch (Exception $e) { + Common::printDebug("There was an error while updating the Conversion: " . $e->getMessage()); + + return false; + } + + return true; + } + + + /** + * Loads the Ecommerce items from the request and records them in the DB + * + * @param array $goal + * @param int $defaultIdOrder + * @throws Exception + * @return array + */ + public function getAllItemsCurrentlyInTheCart($goal, $defaultIdOrder) + { + $sql = "SELECT idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted, idorder as idorder_original_value + FROM " . Common::prefixTable('log_conversion_item') . " + WHERE idvisit = ? AND (idorder = ? OR idorder = ?)"; + + $bind = array( + $goal['idvisit'], + isset($goal['idorder']) ? $goal['idorder'] : $defaultIdOrder, + $defaultIdOrder + ); + + $itemsInDb = $this->getDb()->fetchAll($sql, $bind); + + Common::printDebug("Items found in current cart, for conversion_item (visit,idorder)=" . var_export($bind, true)); + Common::printDebug($itemsInDb); + + return $itemsInDb; + } + + public function createEcommerceItems($ecommerceItems) + { + $sql = "INSERT INTO " . Common::prefixTable('log_conversion_item'); + $i = 0; + $bind = array(); + + foreach ($ecommerceItems as $item) { + if ($i === 0) { + $fields = implode(', ', array_keys($item)); + $sql .= ' (' . $fields . ') VALUES '; + } elseif ($i > 0) { + $sql .= ','; + } + + $newRow = array_values($item); + $sql .= " ( " . Common::getSqlStringFieldsArray($newRow) . " ) "; + $bind = array_merge($bind, $newRow); + $i++; + } + + Common::printDebug($sql); + Common::printDebug($bind); + + try { + $this->getDb()->query($sql, $bind); + } catch (Exception $e) { + if ($e->getCode() == 23000 || + false !== strpos($e->getMessage(), 'Duplicate entry') || + false !== strpos($e->getMessage(), 'Integrity constraint violation')) { + Common::printDebug('Did not create ecommerce item as item was already created'); + } else { + throw $e; + } + } + } + + /** + * Inserts a new action into the log_action table. If there is an existing action that was inserted + * due to another request pre-empting this one, the newly inserted action is deleted. + * + * @param string $name + * @param int $type + * @param int $urlPrefix + * @return int The ID of the action (can be for an existing action or new action). + */ + public function createNewIdAction($name, $type, $urlPrefix) + { + $newActionId = $this->insertNewAction($name, $type, $urlPrefix); + + $realFirstActionId = $this->getIdActionMatchingNameAndType($name, $type); + + // if the inserted action ID is not the same as the queried action ID, then that means we inserted + // a duplicate, so remove it now + if ($realFirstActionId != $newActionId) { + $this->deleteDuplicateAction($newActionId); + } + + return $realFirstActionId; + } + + private function insertNewAction($name, $type, $urlPrefix) + { + $table = Common::prefixTable('log_action'); + $sql = "INSERT INTO $table (name, hash, type, url_prefix) VALUES (?,CRC32(?),?,?)"; + + $db = $this->getDb(); + $db->query($sql, array($name, $name, $type, $urlPrefix)); + + $actionId = $db->lastInsertId(); + + return $actionId; + } + + private function getSqlSelectActionId() + { + // it is possible for multiple actions to exist in the DB (due to rare concurrency issues), so the ORDER BY and + // LIMIT are important + $sql = "SELECT idaction, type, name FROM " . Common::prefixTable('log_action') + . " WHERE " . $this->getSqlConditionToMatchSingleAction() . " " + . "ORDER BY idaction ASC LIMIT 1"; + + return $sql; + } + + public function getIdActionMatchingNameAndType($name, $type) + { + $sql = $this->getSqlSelectActionId(); + $bind = array($name, $name, $type); + + $idAction = $this->getDb()->fetchOne($sql, $bind); + + return $idAction; + } + + /** + * Returns the IDs for multiple actions based on name + type values. + * + * @param array $actionsNameAndType Array like `array( array('name' => '...', 'type' => 1), ... )` + * @return array|false Array of DB rows w/ columns: **idaction**, **type**, **name**. + */ + public function getIdsAction($actionsNameAndType) + { + $sql = "SELECT MIN(idaction) as idaction, type, name FROM " . Common::prefixTable('log_action') + . " WHERE"; + $bind = array(); + + $i = 0; + foreach ($actionsNameAndType as $actionNameType) { + $name = $actionNameType['name']; + + if (empty($name)) { + continue; + } + + if ($i > 0) { + $sql .= " OR"; + } + + $sql .= " " . $this->getSqlConditionToMatchSingleAction() . " "; + + $bind[] = $name; + $bind[] = $name; + $bind[] = $actionNameType['type']; + $i++; + } + + $sql .= " GROUP BY type, hash, name"; + + // Case URL & Title are empty + if (empty($bind)) { + return false; + } + + $actionIds = $this->getDb()->fetchAll($sql, $bind); + + return $actionIds; + } + + public function updateEcommerceItem($originalIdOrder, $newItem) + { + $updateParts = $sqlBind = array(); + foreach ($newItem as $name => $value) { + $updateParts[] = $name . " = ?"; + $sqlBind[] = $value; + } + + $parts = implode($updateParts, ', '); + $table = Common::prefixTable('log_conversion_item'); + + $sql = "UPDATE $table SET $parts WHERE idvisit = ? AND idorder = ? AND idaction_sku = ?"; + + $sqlBind[] = $newItem['idvisit']; + $sqlBind[] = $originalIdOrder; + $sqlBind[] = $newItem['idaction_sku']; + + $this->getDb()->query($sql, $sqlBind); + } + + public function createVisit($visit) + { + $fields = array_keys($visit); + $fields = implode(", ", $fields); + $values = Common::getSqlStringFieldsArray($visit); + $table = Common::prefixTable('log_visit'); + + $sql = "INSERT INTO $table ($fields) VALUES ($values)"; + $bind = array_values($visit); + + $db = $this->getDb(); + $db->query($sql, $bind); + + return $db->lastInsertId(); + } + + public function updateVisit($idSite, $idVisit, $valuesToUpdate) + { + list($updateParts, $sqlBind) = $this->fieldsToQuery($valuesToUpdate); + + $parts = implode($updateParts, ', '); + $table = Common::prefixTable('log_visit'); + + $sqlQuery = "UPDATE $table SET $parts WHERE idsite = ? AND idvisit = ?"; + + $sqlBind[] = $idSite; + $sqlBind[] = $idVisit; + + $db = $this->getDb(); + $result = $db->query($sqlQuery, $sqlBind); + $wasInserted = $db->rowCount($result) != 0; + + if (!$wasInserted) { + Common::printDebug("Visitor with this idvisit wasn't found in the DB."); + Common::printDebug("$sqlQuery --- "); + Common::printDebug($sqlBind); + } + + return $wasInserted; + } + + public function updateAction($idLinkVa, $valuesToUpdate) + { + if (empty($idLinkVa)) { + return; + } + + list($updateParts, $sqlBind) = $this->fieldsToQuery($valuesToUpdate); + + $parts = implode($updateParts, ', '); + $table = Common::prefixTable('log_link_visit_action'); + + $sqlQuery = "UPDATE $table SET $parts WHERE idlink_va = ?"; + + $sqlBind[] = $idLinkVa; + + $db = $this->getDb(); + $result = $db->query($sqlQuery, $sqlBind); + $wasInserted = $db->rowCount($result) != 0; + + if (!$wasInserted) { + Common::printDebug("Action with this idLinkVa wasn't found in the DB."); + Common::printDebug("$sqlQuery --- "); + Common::printDebug($sqlBind); + } + + return $wasInserted; + } + + public function findVisitor($idSite, $configId, $idVisitor, $fieldsToRead, $shouldMatchOneFieldOnly, $isVisitorIdToLookup, $timeLookBack, $timeLookAhead) + { + $selectCustomVariables = ''; + + $selectFields = implode(', ', $fieldsToRead); + + $select = "SELECT $selectFields $selectCustomVariables "; + $from = "FROM " . Common::prefixTable('log_visit'); + + // Two use cases: + // 1) there is no visitor ID so we try to match only on config_id (heuristics) + // Possible causes of no visitor ID: no browser cookie support, direct Tracking API request without visitor ID passed, + // importing server access logs with import_logs.py, etc. + // In this case we use config_id heuristics to try find the visitor in tahhhe past. There is a risk to assign + // this page view to the wrong visitor, but this is better than creating artificial visits. + // 2) there is a visitor ID and we trust it (config setting trust_visitors_cookies, OR it was set using &cid= in tracking API), + // and in these cases, we force to look up this visitor id + $whereCommon = "visit_last_action_time >= ? AND visit_last_action_time <= ? AND idsite = ?"; + $bindSql = array( + $timeLookBack, + $timeLookAhead, + $idSite + ); + + if ($shouldMatchOneFieldOnly && $isVisitorIdToLookup) { + $visitRow = $this->findVisitorByVisitorId($idVisitor, $select, $from, $whereCommon, $bindSql); + } elseif ($shouldMatchOneFieldOnly) { + $visitRow = $this->findVisitorByConfigId($configId, $select, $from, $whereCommon, $bindSql); + } else { + $visitRow = $this->findVisitorByVisitorId($idVisitor, $select, $from, $whereCommon, $bindSql); + + if (empty($visitRow)) { + $whereCommon .= ' AND user_id IS NULL '; + $visitRow = $this->findVisitorByConfigId($configId, $select, $from, $whereCommon, $bindSql); + } + } + + return $visitRow; + } + + private function findVisitorByVisitorId($idVisitor, $select, $from, $where, $bindSql) + { + // will use INDEX index_idsite_idvisitor (idsite, idvisitor) + $where .= ' AND idvisitor = ?'; + $bindSql[] = $idVisitor; + + return $this->fetchVisitor($select, $from, $where, $bindSql); + } + + private function findVisitorByConfigId($configId, $select, $from, $where, $bindSql) + { + // will use INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time) + $where .= ' AND config_id = ?'; + $bindSql[] = $configId; + + return $this->fetchVisitor($select, $from, $where, $bindSql); + } + + private function fetchVisitor($select, $from, $where, $bindSql) + { + $sql = "$select $from WHERE " . $where . " + ORDER BY visit_last_action_time DESC + LIMIT 1"; + + $visitRow = $this->getDb()->fetch($sql, $bindSql); + + return $visitRow; + } + + /** + * Returns true if the site doesn't have log data. + * + * @param int $siteId + * @return bool + */ + public function isSiteEmpty($siteId) + { + $sql = sprintf('SELECT idsite FROM %s WHERE idsite = ? limit 1', Common::prefixTable('log_visit')); + + $result = \Piwik\Db::fetchOne($sql, array($siteId)); + + return $result == null; + } + + private function fieldsToQuery($valuesToUpdate) + { + $updateParts = array(); + $sqlBind = array(); + + foreach ($valuesToUpdate as $name => $value) { + // Case where bind parameters don't work + if ($value === $name . ' + 1') { + //$name = 'visit_total_events' + //$value = 'visit_total_events + 1'; + $updateParts[] = " $name = $value "; + } else { + $updateParts[] = $name . " = ?"; + $sqlBind[] = $value; + } + } + + return array($updateParts, $sqlBind); + } + + private function deleteDuplicateAction($newActionId) + { + $sql = "DELETE FROM " . Common::prefixTable('log_action') . " WHERE idaction = ?"; + + $db = $this->getDb(); + $db->query($sql, array($newActionId)); + } + + private function getDb() + { + return Tracker::getDatabase(); + } + + private function getSqlConditionToMatchSingleAction() + { + return "( hash = CRC32(?) AND name = ? AND type = ? )"; + } +} diff --git a/www/analytics/core/Tracker/PageUrl.php b/www/analytics/core/Tracker/PageUrl.php index c38bae51..1ff3e529 100644 --- a/www/analytics/core/Tracker/PageUrl.php +++ b/www/analytics/core/Tracker/PageUrl.php @@ -1,6 +1,6 @@ $posFirstSemiColon) { $originalUrl = substr_replace($originalUrl, ";", $posQuestionMark, 1); $replace = true; } + if ($replace) { $originalUrl = substr_replace($originalUrl, "?", strpos($originalUrl, ";"), 1); $originalUrl = str_replace(";", "&", $originalUrl); } + return $originalUrl; } @@ -212,10 +227,12 @@ class PageUrl { if (is_string($value)) { $decoded = urldecode($value); - if (@mb_check_encoding($decoded, $encoding)) { + if (function_exists('mb_check_encoding') + && @mb_check_encoding($decoded, $encoding)) { $value = urlencode(mb_convert_encoding($decoded, 'UTF-8', $encoding)); } } + return $value; } @@ -228,6 +245,7 @@ class PageUrl $value = PageUrl::reencodeParameterValue($value, $encoding); } } + return $queryParameters; } @@ -247,14 +265,20 @@ class PageUrl */ public static function reencodeParameters(&$queryParameters, $encoding = false) { - // if query params are encoded w/ non-utf8 characters (due to browser bug or whatever), - // encode to UTF-8. - if ($encoding !== false - && strtolower($encoding) != 'utf-8' - && function_exists('mb_check_encoding') - ) { - $queryParameters = PageUrl::reencodeParametersArray($queryParameters, $encoding); + if (function_exists('mb_check_encoding')) { + // if query params are encoded w/ non-utf8 characters (due to browser bug or whatever), + // encode to UTF-8. + if (strtolower($encoding) != 'utf-8' + && $encoding != false + ) { + Common::printDebug("Encoding page URL query parameters to $encoding."); + + $queryParameters = PageUrl::reencodeParametersArray($queryParameters, $encoding); + } + } else { + Common::printDebug("Page charset supplied in tracking request, but mbstring extension is not available."); } + return $queryParameters; } @@ -263,6 +287,7 @@ class PageUrl $url = Common::unsanitizeInputValue($url); $url = PageUrl::cleanupString($url); $url = PageUrl::convertMatrixUrl($url); + return $url; } @@ -276,6 +301,7 @@ class PageUrl public static function reconstructNormalizedUrl($url, $prefixId) { $map = array_flip(self::$urlPrefixMap); + if ($prefixId !== null && isset($map[$prefixId])) { $fullUrl = $map[$prefixId] . $url; } else { @@ -285,7 +311,8 @@ class PageUrl // Clean up host & hash tags, for URLs $parsedUrl = @parse_url($fullUrl); $parsedUrl = PageUrl::cleanupHostAndHashTag($parsedUrl); - $url = UrlHelper::getParseUrlReverse($parsedUrl); + $url = UrlHelper::getParseUrlReverse($parsedUrl); + if (!empty($url)) { return $url; } @@ -310,6 +337,7 @@ class PageUrl ); } } + return array('url' => $url, 'prefixId' => null); } @@ -319,10 +347,30 @@ class PageUrl if (!UrlHelper::isLookLikeUrl($url)) { Common::printDebug("WARNING: URL looks invalid and is discarded"); - $url = false; - return $url; + + return false; } + return $url; } -} + private static function getExcludedParametersFromWebsite($website) + { + if (isset($website['excluded_parameters'])) { + return $website['excluded_parameters']; + } + + return array(); + } + + public static function urldecodeValidUtf8($value) + { + $value = urldecode($value); + if (function_exists('mb_check_encoding') + && !@mb_check_encoding($value, 'utf-8') + ) { + return urlencode($value); + } + return $value; + } +} diff --git a/www/analytics/core/Tracker/Referrer.php b/www/analytics/core/Tracker/Referrer.php deleted file mode 100644 index 12d8ff74..00000000 --- a/www/analytics/core/Tracker/Referrer.php +++ /dev/null @@ -1,301 +0,0 @@ -idsite = $idSite; - - // default values for the referer_* fields - $referrerUrl = Common::unsanitizeInputValue($referrerUrl); - if (!empty($referrerUrl) - && !UrlHelper::isLookLikeUrl($referrerUrl) - ) { - $referrerUrl = ''; - } - - $currentUrl = PageUrl::cleanupUrl($currentUrl); - - $this->referrerUrl = $referrerUrl; - $this->referrerUrlParse = @parse_url($this->referrerUrl); - $this->currentUrlParse = @parse_url($currentUrl); - $this->typeReferrerAnalyzed = Common::REFERRER_TYPE_DIRECT_ENTRY; - $this->nameReferrerAnalyzed = ''; - $this->keywordReferrerAnalyzed = ''; - $this->referrerHost = ''; - - if (isset($this->referrerUrlParse['host'])) { - $this->referrerHost = $this->referrerUrlParse['host']; - } - - $referrerDetected = $this->detectReferrerCampaign(); - - if (!$referrerDetected) { - if ($this->detectReferrerDirectEntry() - || $this->detectReferrerSearchEngine() - ) { - $referrerDetected = true; - } - } - - if (!empty($this->referrerHost) - && !$referrerDetected - ) { - $this->typeReferrerAnalyzed = Common::REFERRER_TYPE_WEBSITE; - $this->nameReferrerAnalyzed = Common::mb_strtolower($this->referrerHost); - } - - $referrerInformation = array( - 'referer_type' => $this->typeReferrerAnalyzed, - 'referer_name' => $this->nameReferrerAnalyzed, - 'referer_keyword' => $this->keywordReferrerAnalyzed, - 'referer_url' => $this->referrerUrl, - ); - - return $referrerInformation; - } - - /** - * Search engine detection - * @return bool - */ - protected function detectReferrerSearchEngine() - { - $searchEngineInformation = UrlHelper::extractSearchEngineInformationFromUrl($this->referrerUrl); - - /** - * Triggered when detecting the search engine of a referrer URL. - * - * Plugins can use this event to provide custom search engine detection - * logic. - * - * @param array &$searchEngineInformation An array with the following information: - * - * - **name**: The search engine name. - * - **keywords**: The search keywords used. - * - * This parameter is initialized to the results - * of Piwik's default search engine detection - * logic. - * @param string referrerUrl The referrer URL from the tracking request. - */ - Piwik::postEvent('Tracker.detectReferrerSearchEngine', array(&$searchEngineInformation, $this->referrerUrl)); - if ($searchEngineInformation === false) { - return false; - } - $this->typeReferrerAnalyzed = Common::REFERRER_TYPE_SEARCH_ENGINE; - $this->nameReferrerAnalyzed = $searchEngineInformation['name']; - $this->keywordReferrerAnalyzed = $searchEngineInformation['keywords']; - return true; - } - - /** - * @param string $string - * @return bool - */ - protected function detectCampaignFromString($string) - { - foreach ($this->campaignNames as $campaignNameParameter) { - $campaignName = trim(urldecode(UrlHelper::getParameterFromQueryString($string, $campaignNameParameter))); - if (!empty($campaignName)) { - break; - } - } - - if (empty($campaignName)) { - return false; - } - $this->typeReferrerAnalyzed = Common::REFERRER_TYPE_CAMPAIGN; - $this->nameReferrerAnalyzed = $campaignName; - - foreach ($this->campaignKeywords as $campaignKeywordParameter) { - $campaignKeyword = UrlHelper::getParameterFromQueryString($string, $campaignKeywordParameter); - if (!empty($campaignKeyword)) { - $this->keywordReferrerAnalyzed = trim(urldecode($campaignKeyword)); - break; - } - } - return !empty($this->keywordReferrerAnalyzed); - } - - protected function detectReferrerCampaignFromLandingUrl() - { - if (!isset($this->currentUrlParse['query']) - && !isset($this->currentUrlParse['fragment']) - ) { - return false; - } - $campaignParameters = Common::getCampaignParameters(); - $this->campaignNames = $campaignParameters[0]; - $this->campaignKeywords = $campaignParameters[1]; - - $found = false; - - // 1) Detect campaign from query string - if (isset($this->currentUrlParse['query'])) { - $found = $this->detectCampaignFromString($this->currentUrlParse['query']); - } - - // 2) Detect from fragment #hash - if (!$found - && isset($this->currentUrlParse['fragment']) - ) { - $this->detectCampaignFromString($this->currentUrlParse['fragment']); - } - } - - /** - * We have previously tried to detect the campaign variables in the URL - * so at this stage, if the referrer host is the current host, - * or if the referrer host is any of the registered URL for this website, - * it is considered a direct entry - * @return bool - */ - protected function detectReferrerDirectEntry() - { - if (!empty($this->referrerHost)) { - // is the referrer host the current host? - if (isset($this->currentUrlParse['host'])) { - $currentHost = mb_strtolower($this->currentUrlParse['host'], 'UTF-8'); - if ($currentHost == mb_strtolower($this->referrerHost, 'UTF-8')) { - $this->typeReferrerAnalyzed = Common::REFERRER_TYPE_DIRECT_ENTRY; - return true; - } - } - if (Visit::isHostKnownAliasHost($this->referrerHost, $this->idsite)) { - $this->typeReferrerAnalyzed = Common::REFERRER_TYPE_DIRECT_ENTRY; - return true; - } - } - return false; - } - - protected function detectCampaignKeywordFromReferrerUrl() - { - if(!empty($this->nameReferrerAnalyzed) - && !empty($this->keywordReferrerAnalyzed)) { - // keyword is already set, we skip - return true; - } - - // Set the Campaign keyword to the keyword found in the Referrer URL if any - if(!empty($this->nameReferrerAnalyzed)) { - $referrerUrlInfo = UrlHelper::extractSearchEngineInformationFromUrl($this->referrerUrl); - if (!empty($referrerUrlInfo['keywords'])) { - $this->keywordReferrerAnalyzed = $referrerUrlInfo['keywords']; - } - } - - // Set the keyword, to the hostname found, in a Adsense Referrer URL '&url=' parameter - if (empty($this->keywordReferrerAnalyzed) - && !empty($this->referrerUrlParse['query']) - && !empty($this->referrerHost) - && (strpos($this->referrerHost, 'googleads') !== false || strpos($this->referrerHost, 'doubleclick') !== false) - ) { - // This parameter sometimes is found & contains the page with the adsense ad bringing visitor to our site - $value = $this->getParameterValueFromReferrerUrl('url'); - if (!empty($value)) { - $parsedAdsenseReferrerUrl = parse_url($value); - if (!empty($parsedAdsenseReferrerUrl['host'])) { - - if(empty($this->nameReferrerAnalyzed)) { - $type = $this->getParameterValueFromReferrerUrl('ad_type'); - $type = $type ? " ($type)" : ''; - $this->nameReferrerAnalyzed = self::LABEL_ADWORDS_NAME . $type; - $this->typeReferrerAnalyzed = Common::REFERRER_TYPE_CAMPAIGN; - } - $this->keywordReferrerAnalyzed = self::LABEL_PREFIX_ADWORDS_KEYWORD . $parsedAdsenseReferrerUrl['host']; - } - } - } - - } - - /** - * @return string - */ - protected function getParameterValueFromReferrerUrl($adsenseReferrerParameter) - { - $value = trim(urldecode(UrlHelper::getParameterFromQueryString($this->referrerUrlParse['query'], $adsenseReferrerParameter))); - return $value; - } - - /** - * @return bool - */ - protected function detectReferrerCampaign() - { - $this->detectReferrerCampaignFromLandingUrl(); - $this->detectCampaignKeywordFromReferrerUrl(); - - if ($this->typeReferrerAnalyzed != Common::REFERRER_TYPE_CAMPAIGN) { - return false; - } - // if we detected a campaign but there is still no keyword set, we set the keyword to the Referrer host - if(empty($this->keywordReferrerAnalyzed)) { - $this->keywordReferrerAnalyzed = $this->referrerHost; - } - - $this->keywordReferrerAnalyzed = Common::mb_strtolower($this->keywordReferrerAnalyzed); - $this->nameReferrerAnalyzed = Common::mb_strtolower($this->nameReferrerAnalyzed); - return true; - } - -} diff --git a/www/analytics/core/Tracker/Request.php b/www/analytics/core/Tracker/Request.php index df551e7d..b6f31630 100644 --- a/www/analytics/core/Tracker/Request.php +++ b/www/analytics/core/Tracker/Request.php @@ -1,6 +1,6 @@ params = $params; + $this->rawParams = $params; $this->tokenAuth = $tokenAuth; $this->timestamp = time(); - $this->enforcedIp = false; + $this->isEmptyRequest = empty($params); // When the 'url' and referrer url parameter are not given, we might be in the 'Simple Image Tracker' mode. // The URL can default to the Referrer, which will be in this case // the URL of the page containing the Simple Image beacon if (empty($this->params['urlref']) && empty($this->params['url']) + && array_key_exists('HTTP_REFERER', $_SERVER) ) { - $url = @$_SERVER['HTTP_REFERER']; + $url = $_SERVER['HTTP_REFERER']; if (!empty($url)) { $this->params['url'] = $url; } } + + // check for 4byte utf8 characters in url and replace them with � + // @TODO Remove as soon as our database tables use utf8mb4 instead of utf8 + if (array_key_exists('url', $this->params) && preg_match('/[\x{10000}-\x{10FFFF}]/u', $this->params['url'])) { + Common::printDebug("Unsupport character detected. Replacing with \xEF\xBF\xBD"); + $this->params['url'] = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $this->params['url']); + } + } + + /** + * Get the params that were originally passed to the instance. These params do not contain any params that were added + * within this object. + * @return array + */ + public function getRawParams() + { + return $this->rawParams; + } + + public function getTokenAuth() + { + return $this->tokenAuth; } /** @@ -80,21 +123,43 @@ class Request * This method allows to set custom IP + server time + visitor ID, when using Tracking API. * These two attributes can be only set by the Super User (passing token_auth). */ - protected function authenticateTrackingApi($tokenAuthFromBulkRequest) + protected function authenticateTrackingApi($tokenAuth) { - $shouldAuthenticate = Config::getInstance()->Tracker['tracking_requests_require_authentication']; + $shouldAuthenticate = TrackerConfig::getConfigValue('tracking_requests_require_authentication'); + if ($shouldAuthenticate) { - $tokenAuth = $tokenAuthFromBulkRequest ? $tokenAuthFromBulkRequest : Common::getRequestVar('token_auth', false, 'string', $this->params); try { $idSite = $this->getIdSite(); - $this->isAuthenticated = $this->authenticateSuperUserOrAdmin($tokenAuth, $idSite); } catch (Exception $e) { $this->isAuthenticated = false; - } - if (!$this->isAuthenticated) { return; } - Common::printDebug("token_auth is authenticated!"); + + if (empty($tokenAuth)) { + $tokenAuth = Common::getRequestVar('token_auth', false, 'string', $this->params); + } + + $cache = PiwikCache::getTransientCache(); + $cacheKey = 'tracker_request_authentication_' . $idSite . '_' . $tokenAuth; + + if ($cache->contains($cacheKey)) { + Common::printDebug("token_auth is authenticated in cache!"); + $this->isAuthenticated = $cache->fetch($cacheKey); + return; + } + + try { + $this->isAuthenticated = self::authenticateSuperUserOrAdmin($tokenAuth, $idSite); + $cache->save($cacheKey, $this->isAuthenticated); + } catch (Exception $e) { + Common::printDebug("could not authenticate, caught exception: " . $e->getMessage()); + + $this->isAuthenticated = false; + } + + if ($this->isAuthenticated) { + Common::printDebug("token_auth is authenticated!"); + } } else { $this->isAuthenticated = true; Common::printDebug("token_auth authentication not required"); @@ -110,9 +175,11 @@ class Request Piwik::postEvent('Request.initAuthenticationObject'); /** @var \Piwik\Auth $auth */ - $auth = Registry::get('auth'); + $auth = StaticContainer::get('Piwik\Auth'); $auth->setTokenAuth($tokenAuth); $auth->setLogin(null); + $auth->setPassword(null); + $auth->setPasswordHash(null); $access = $auth->authenticate(); if (!empty($access) && $access->hasSuperUserAccess()) { @@ -122,10 +189,12 @@ class Request // Now checking the list of admin token_auth cached in the Tracker config file if (!empty($idSite) && $idSite > 0) { $website = Cache::getCacheWebsiteAttributes($idSite); - if (array_key_exists('admin_token_auth', $website) && in_array($tokenAuth, $website['admin_token_auth'])) { + + if (array_key_exists('admin_token_auth', $website) && in_array((string) $tokenAuth, $website['admin_token_auth'])) { return true; } } + Common::printDebug("WARNING! token_auth = $tokenAuth is not valid, Super User / Admin was NOT authenticated"); return false; @@ -137,13 +206,17 @@ class Request public function getDaysSinceFirstVisit() { $cookieFirstVisitTimestamp = $this->getParam('_idts'); + if (!$this->isTimestampValid($cookieFirstVisitTimestamp)) { $cookieFirstVisitTimestamp = $this->getCurrentTimestamp(); } + $daysSinceFirstVisit = round(($this->getCurrentTimestamp() - $cookieFirstVisitTimestamp) / 86400, $precision = 0); + if ($daysSinceFirstVisit < 0) { $daysSinceFirstVisit = 0; } + return $daysSinceFirstVisit; } @@ -154,12 +227,14 @@ class Request { $daysSinceLastOrder = false; $lastOrderTimestamp = $this->getParam('_ects'); + if ($this->isTimestampValid($lastOrderTimestamp)) { $daysSinceLastOrder = round(($this->getCurrentTimestamp() - $lastOrderTimestamp) / 86400, $precision = 0); if ($daysSinceLastOrder < 0) { $daysSinceLastOrder = 0; } } + return $daysSinceLastOrder; } @@ -170,12 +245,14 @@ class Request { $daysSinceLastVisit = 0; $lastVisitTimestamp = $this->getParam('_viewts'); + if ($this->isTimestampValid($lastVisitTimestamp)) { $daysSinceLastVisit = round(($this->getCurrentTimestamp() - $lastVisitTimestamp) / 86400, $precision = 0); if ($daysSinceLastVisit < 0) { $daysSinceLastVisit = 0; } } + return $daysSinceLastVisit; } @@ -211,6 +288,15 @@ class Request 'i' => (string)Common::getRequestVar('m', $this->getCurrentDate("i"), 'int', $this->params), 's' => (string)Common::getRequestVar('s', $this->getCurrentDate("s"), 'int', $this->params) ); + if($localTimes['h'] < 0 || $localTimes['h'] > 23) { + $localTimes['h'] = 0; + } + if($localTimes['i'] < 0 || $localTimes['i'] > 59) { + $localTimes['i'] = 0; + } + if($localTimes['s'] < 0 || $localTimes['s'] > 59) { + $localTimes['s'] = 0; + } foreach ($localTimes as $k => $time) { if (strlen($time) == 1) { $localTimes[$k] = '0' . $time; @@ -252,75 +338,176 @@ class Request 'urlref' => array('', 'string'), 'res' => array(self::UNKNOWN_RESOLUTION, 'string'), 'idgoal' => array(-1, 'int'), + 'ping' => array(0, 'int'), // other 'bots' => array(0, 'int'), 'dp' => array(0, 'int'), - 'rec' => array(false, 'int'), + 'rec' => array(0, 'int'), 'new_visit' => array(0, 'int'), // Ecommerce - 'ec_id' => array(false, 'string'), + 'ec_id' => array('', 'string'), 'ec_st' => array(false, 'float'), 'ec_tx' => array(false, 'float'), 'ec_sh' => array(false, 'float'), 'ec_dt' => array(false, 'float'), - 'ec_items' => array('', 'string'), + 'ec_items' => array('', 'json'), // Events - 'e_c' => array(false, 'string'), - 'e_a' => array(false, 'string'), - 'e_n' => array(false, 'string'), + 'e_c' => array('', 'string'), + 'e_a' => array('', 'string'), + 'e_n' => array('', 'string'), 'e_v' => array(false, 'float'), // some visitor attributes can be overwritten - 'cip' => array(false, 'string'), - 'cdt' => array(false, 'string'), - 'cid' => array(false, 'string'), + 'cip' => array('', 'string'), + 'cdt' => array('', 'string'), + 'cid' => array('', 'string'), + 'uid' => array('', 'string'), // Actions / pages - 'cs' => array(false, 'string'), + 'cs' => array('', 'string'), 'download' => array('', 'string'), 'link' => array('', 'string'), 'action_name' => array('', 'string'), 'search' => array('', 'string'), - 'search_cat' => array(false, 'string'), + 'search_cat' => array('', 'string'), 'search_count' => array(-1, 'int'), 'gt_ms' => array(-1, 'int'), + + // Content + 'c_p' => array('', 'string'), + 'c_n' => array('', 'string'), + 'c_t' => array('', 'string'), + 'c_i' => array('', 'string'), ); + if (isset($this->paramsCache[$name])) { + return $this->paramsCache[$name]; + } + if (!isset($supportedParams[$name])) { throw new Exception("Requested parameter $name is not a known Tracking API Parameter."); } + $paramDefaultValue = $supportedParams[$name][0]; $paramType = $supportedParams[$name][1]; - $value = Common::getRequestVar($name, $paramDefaultValue, $paramType, $this->params); + if ($this->hasParam($name)) { + $this->paramsCache[$name] = Common::getRequestVar($name, $paramDefaultValue, $paramType, $this->params); + } else { + $this->paramsCache[$name] = $paramDefaultValue; + } - return $value; + return $this->paramsCache[$name]; + } + + public function setParam($name, $value) + { + $this->params[$name] = $value; + unset($this->paramsCache[$name]); + + if ($name === 'cdt') { + $this->cdtCache = null; + } + } + + private function hasParam($name) + { + return isset($this->params[$name]); + } + + public function getParams() + { + return $this->params; } public function getCurrentTimestamp() { + if (!isset($this->cdtCache)) { + $this->cdtCache = $this->getCustomTimestamp(); + } + + if (!empty($this->cdtCache)) { + return $this->cdtCache; + } + return $this->timestamp; } - protected function isTimestampValid($time) + public function setCurrentTimestamp($timestamp) { - return $time <= $this->getCurrentTimestamp() - && $time > $this->getCurrentTimestamp() - 10 * 365 * 86400; + $this->timestamp = $timestamp; + } + + protected function getCustomTimestamp() + { + if (!$this->hasParam('cdt')) { + return false; + } + + $cdt = $this->getParam('cdt'); + + if (empty($cdt)) { + return false; + } + + if (!is_numeric($cdt)) { + $cdt = strtotime($cdt); + } + + if (!$this->isTimestampValid($cdt, $this->timestamp)) { + Common::printDebug(sprintf("Datetime %s is not valid", date("Y-m-d H:i:m", $cdt))); + return false; + } + + // If timestamp in the past, token_auth is required + $timeFromNow = $this->timestamp - $cdt; + $isTimestampRecent = $timeFromNow < self::CUSTOM_TIMESTAMP_DOES_NOT_REQUIRE_TOKENAUTH_WHEN_NEWER_THAN; + + if (!$isTimestampRecent) { + if (!$this->isAuthenticated()) { + Common::printDebug(sprintf("Custom timestamp is %s seconds old, requires &token_auth...", $timeFromNow)); + Common::printDebug("WARN: Tracker API 'cdt' was used with invalid token_auth"); + return false; + } + } + + return $cdt; + } + + /** + * Returns true if the timestamp is valid ie. timestamp is sometime in the last 10 years and is not in the future. + * + * @param $time int Timestamp to test + * @param $now int Current timestamp + * @return bool + */ + protected function isTimestampValid($time, $now = null) + { + if (empty($now)) { + $now = $this->getCurrentTimestamp(); + } + + return $time <= $now + && $time > $now - 10 * 365 * 86400; } public function getIdSite() { + if (isset($this->idSiteCache)) { + return $this->idSiteCache; + } + $idSite = Common::getRequestVar('idsite', 0, 'int', $this->params); /** * Triggered when obtaining the ID of the site we are tracking a visit for. - * + * * This event can be used to change the site ID so data is tracked for a different * website. - * + * * @param int &$idSite Initialized to the value of the **idsite** query parameter. If a * subscriber sets this variable, the value it uses must be greater * than 0. @@ -328,18 +515,41 @@ class Request * request. */ Piwik::postEvent('Tracker.Request.getIdSite', array(&$idSite, $this->params)); + if ($idSite <= 0) { - throw new Exception('Invalid idSite: \'' . $idSite . '\''); + throw new UnexpectedWebsiteFoundException('Invalid idSite: \'' . $idSite . '\''); } + + $this->idSiteCache = $idSite; + return $idSite; } public function getUserAgent() { - $default = @$_SERVER['HTTP_USER_AGENT']; - return Common::getRequestVar('ua', is_null($default) ? false : $default, 'string', $this->params); + $default = false; + + if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) { + $default = $_SERVER['HTTP_USER_AGENT']; + } + + return Common::getRequestVar('ua', $default, 'string', $this->params); } + public function getCustomVariablesInVisitScope() + { + return $this->getCustomVariables('visit'); + } + + public function getCustomVariablesInPageScope() + { + return $this->getCustomVariables('page'); + } + + /** + * @deprecated since Piwik 2.10.0. Use Request::getCustomVariablesInPageScope() or Request::getCustomVariablesInVisitScope() instead. + * When we "remove" this method we will only set visibility to "private" and pass $parameter = _cvar|cvar as an argument instead of $scope + */ public function getCustomVariables($scope) { if ($scope == 'visit') { @@ -348,14 +558,19 @@ class Request $parameter = 'cvar'; } - $customVar = Common::unsanitizeInputValues(Common::getRequestVar($parameter, '', 'json', $this->params)); + $cvar = Common::getRequestVar($parameter, '', 'json', $this->params); + $customVar = Common::unsanitizeInputValues($cvar); + if (!is_array($customVar)) { return array(); } + $customVariables = array(); - $maxCustomVars = CustomVariables::getMaxCustomVariables(); + $maxCustomVars = CustomVariables::getNumUsableCustomVariables(); + foreach ($customVar as $id => $keyValue) { $id = (int)$id; + if ($id < 1 || $id > $maxCustomVars || count($keyValue) != 2 @@ -364,16 +579,15 @@ class Request Common::printDebug("Invalid custom variables detected (id=$id)"); continue; } + if (strlen($keyValue[1]) == 0) { $keyValue[1] = ""; } // We keep in the URL when Custom Variable have empty names // and values, as it means they can be deleted server side - $key = self::truncateCustomVariable($keyValue[0]); - $value = self::truncateCustomVariable($keyValue[1]); - $customVariables['custom_var_k' . $id] = $key; - $customVariables['custom_var_v' . $id] = $value; + $customVariables['custom_var_k' . $id] = self::truncateCustomVariable($keyValue[0]); + $customVariables['custom_var_v' . $id] = self::truncateCustomVariable($keyValue[1]); } return $customVariables; @@ -397,6 +611,7 @@ class Request if (!$this->shouldUseThirdPartyCookie()) { return; } + Common::printDebug("We manage the cookie..."); $cookie = $this->makeThirdPartyCookie(); @@ -417,37 +632,53 @@ class Request protected function getCookieName() { - return Config::getInstance()->Tracker['cookie_name']; + return TrackerConfig::getConfigValue('cookie_name'); } protected function getCookieExpire() { - return $this->getCurrentTimestamp() + Config::getInstance()->Tracker['cookie_expire']; + return $this->getCurrentTimestamp() + TrackerConfig::getConfigValue('cookie_expire'); } protected function getCookiePath() { - return Config::getInstance()->Tracker['cookie_path']; + return TrackerConfig::getConfigValue('cookie_path'); } /** - * Is the request for a known VisitorId, based on 1st party, 3rd party (optional) cookies or Tracking API forced Visitor ID + * Returns the ID from the request in this order: + * return from a given User ID, + * or from a Tracking API forced Visitor ID, + * or from a Visitor ID from 3rd party (optional) cookies, + * or from a given Visitor Id from 1st party? + * * @throws Exception */ public function getVisitorId() { $found = false; - // Was a Visitor ID "forced" (@see Tracking API setVisitorId()) for this request? - $idVisitor = $this->getForcedVisitorId(); - if (!empty($idVisitor)) { - if (strlen($idVisitor) != Tracker::LENGTH_HEX_ID_STRING) { - throw new Exception("Visitor ID (cid) $idVisitor must be " . Tracker::LENGTH_HEX_ID_STRING . " characters long"); - } - Common::printDebug("Request will be recorded for this idvisitor = " . $idVisitor); + // If User ID is set it takes precedence + $userId = $this->getForcedUserId(); + if ($userId) { + $userIdHashed = $this->getUserIdHashed($userId); + $idVisitor = $this->truncateIdAsVisitorId($userIdHashed); + Common::printDebug("Request will be recorded for this user_id = " . $userId . " (idvisitor = $idVisitor)"); $found = true; } + // Was a Visitor ID "forced" (@see Tracking API setVisitorId()) for this request? + if (!$found) { + $idVisitor = $this->getForcedVisitorId(); + if (!empty($idVisitor)) { + if (strlen($idVisitor) != Tracker::LENGTH_HEX_ID_STRING) { + throw new InvalidRequestParameterException("Visitor ID (cid) $idVisitor must be " . Tracker::LENGTH_HEX_ID_STRING . " characters long"); + } + Common::printDebug("Request will be recorded for this idvisitor = " . $idVisitor); + $found = true; + } + } + // - If set to use 3rd party cookies for Visit ID, read the cookie if (!$found) { // - By default, reads the first party cookie ID @@ -462,6 +693,7 @@ class Request } } } + // If a third party cookie was not found, we default to the first party cookie if (!$found) { $idVisitor = Common::getRequestVar('_id', '', 'string', $this->params); @@ -469,78 +701,34 @@ class Request } if ($found) { - $truncated = substr($idVisitor, 0, Tracker::LENGTH_HEX_ID_STRING); + $truncated = $this->truncateIdAsVisitorId($idVisitor); $binVisitorId = @Common::hex2bin($truncated); if (!empty($binVisitorId)) { return $binVisitorId; } } + return false; } public function getIp() { - if (!empty($this->enforcedIp)) { - $ipString = $this->enforcedIp; - } else { - $ipString = IP::getIpFromHeader(); - } - $ip = IP::P2N($ipString); - return $ip; + return IPUtils::stringToBinaryIP($this->getIpString()); } - public function setForceIp($ip) + public function getForcedUserId() { - if (!empty($ip)) { - $this->enforcedIp = $ip; + $userId = $this->getParam('uid'); + if (strlen($userId) > 0) { + return $userId; } - } - public function setForceDateTime($dateTime) - { - if (!is_numeric($dateTime)) { - $dateTime = strtotime($dateTime); - } - if (!empty($dateTime)) { - $this->timestamp = $dateTime; - } - } - - public function setForcedVisitorId($visitorId) - { - if (!empty($visitorId)) { - $this->forcedVisitorId = $visitorId; - } + return false; } public function getForcedVisitorId() { - return $this->forcedVisitorId; - } - - public function overrideLocation(&$visitorInfo) - { - if (!$this->isAuthenticated()) { - return; - } - - // check for location override query parameters (ie, lat, long, country, region, city) - static $locationOverrideParams = array( - 'country' => array('string', 'location_country'), - 'region' => array('string', 'location_region'), - 'city' => array('string', 'location_city'), - 'lat' => array('float', 'location_latitude'), - 'long' => array('float', 'location_longitude'), - ); - foreach ($locationOverrideParams as $queryParamName => $info) { - list($type, $visitorInfoKey) = $info; - - $value = Common::getRequestVar($queryParamName, false, $type, $this->params); - if (!empty($value)) { - $visitorInfo[$visitorInfoKey] = $value; - } - } - return; + return $this->getParam('cid'); } public function getPlugins() @@ -553,9 +741,9 @@ class Request return $plugins; } - public function getParamsCount() + public function isEmptyRequest() { - return count($this->params); + return $this->isEmptyRequest; } const GENERATION_TIME_MS_MAXIMUM = 3600000; // 1 hour @@ -568,6 +756,71 @@ class Request ) { return (int)$generationTime; } + return false; } + + /** + * @param $idVisitor + * @return string + */ + private function truncateIdAsVisitorId($idVisitor) + { + return substr($idVisitor, 0, Tracker::LENGTH_HEX_ID_STRING); + } + + /** + * Matches implementation of PiwikTracker::getUserIdHashed + * + * @param $userId + * @return string + */ + public function getUserIdHashed($userId) + { + return substr(sha1($userId), 0, 16); + } + + /** + * @return mixed|string + * @throws Exception + */ + public function getIpString() + { + $cip = $this->getParam('cip'); + + if (empty($cip)) { + return IP::getIpFromHeader(); + } + + if (!$this->isAuthenticated()) { + Common::printDebug("WARN: Tracker API 'cip' was used with invalid token_auth"); + return IP::getIpFromHeader(); + } + + return $cip; + } + + /** + * Set a request metadata value. + * + * @param string $pluginName eg, `'Actions'`, `'Goals'`, `'YourPlugin'` + * @param string $key + * @param mixed $value + */ + public function setMetadata($pluginName, $key, $value) + { + $this->requestMetadata[$pluginName][$key] = $value; + } + + /** + * Get a request metadata value. Returns `null` if none exists. + * + * @param string $pluginName eg, `'Actions'`, `'Goals'`, `'YourPlugin'` + * @param string $key + * @return mixed + */ + public function getMetadata($pluginName, $key) + { + return isset($this->requestMetadata[$pluginName][$key]) ? $this->requestMetadata[$pluginName][$key] : null; + } } diff --git a/www/analytics/core/Tracker/RequestProcessor.php b/www/analytics/core/Tracker/RequestProcessor.php new file mode 100644 index 00000000..8d50d3b2 --- /dev/null +++ b/www/analytics/core/Tracker/RequestProcessor.php @@ -0,0 +1,174 @@ +visitorInfo` will be empty. + * + * @param VisitProperties $visitProperties + * @param Request $request + * @return bool If `true` the tracking request will be aborted. + */ + public function processRequestParams(VisitProperties $visitProperties, Request $request) + { + return false; + } + + /** + * This is the third method called when processing a tracker request. + * + * Derived classes should use this method to set request metadata that needs request metadata + * from other plugins, or to override request metadata from other plugins to change + * tracking behavior. + * + * When this method is called, you can assume all available request metadata from all plugins + * will be initialized (but not at their final value). Also, `$visitProperties->visitorInfo` + * will contain the values of the visitor's last known visit (if any). + * + * @param VisitProperties $visitProperties + * @param Request $request + * @return bool If `true` the tracking request will be aborted. + */ + public function afterRequestProcessed(VisitProperties $visitProperties, Request $request) + { + return false; + } + + /** + * This method is called before recording a new visit. You can set/change visit information here + * to change what gets inserted into `log_visit`. + * + * Only implement this method if you cannot use a Dimension for the same thing. + * + * @param VisitProperties $visitProperties + * @param Request $request + */ + public function onNewVisit(VisitProperties $visitProperties, Request $request) + { + // empty + } + + /** + * This method is called before updating an existing visit. You can set/change visit information + * here to change what gets recorded in `log_visit`. + * + * Only implement this method if you cannot use a Dimension for the same thing. + * + * @param array &$valuesToUpdate + * @param VisitProperties $visitProperties + * @param Request $request + */ + public function onExistingVisit(&$valuesToUpdate, VisitProperties $visitProperties, Request $request) + { + // empty + } + + /** + * This method is called last. Derived classes should use this method to insert log data. They + * should also only read request metadata, and not set it. + * + * When this method is called, you can assume all request metadata have their final values. Also, + * `$visitProperties->visitorInfo` will contain the properties of the visitor's current visit (in + * other words, the values in the array were persisted to the DB before this method was called). + * + * @param VisitProperties $visitProperties + * @param Request $request + */ + public function recordLogs(VisitProperties $visitProperties, Request $request) + { + // empty + } +} diff --git a/www/analytics/core/Tracker/RequestSet.php b/www/analytics/core/Tracker/RequestSet.php new file mode 100644 index 00000000..3d3c626e --- /dev/null +++ b/www/analytics/core/Tracker/RequestSet.php @@ -0,0 +1,255 @@ +requests = array(); + + foreach ($requests as $request) { + if (empty($request) && !is_array($request)) { + continue; + } + + if (!$request instanceof Request) { + $request = new Request($request, $this->getTokenAuth()); + } + + $this->requests[] = $request; + } + } + + public function setTokenAuth($tokenAuth) + { + $this->tokenAuth = $tokenAuth; + } + + public function getNumberOfRequests() + { + if (is_array($this->requests)) { + return count($this->requests); + } + + return 0; + } + + public function getRequests() + { + if (!$this->areRequestsInitialized()) { + return array(); + } + + return $this->requests; + } + + public function getTokenAuth() + { + if (!is_null($this->tokenAuth)) { + return $this->tokenAuth; + } + + return Common::getRequestVar('token_auth', false); + } + + private function areRequestsInitialized() + { + return !is_null($this->requests); + } + + public function initRequestsAndTokenAuth() + { + if ($this->areRequestsInitialized()) { + return; + } + + /** + * Triggered when detecting tracking requests. A plugin can use this event to set + * requests that should be tracked by calling the {@link RequestSet::setRequests()} method. + * For example the BulkTracking plugin uses this event to detect tracking requests and auth token based on + * a sent JSON instead of default $_GET+$_POST. It would allow you for example to track requests based on + * XML or you could import tracking requests stored in a file. + * + * @param \Piwik\Tracker\RequestSet &$requestSet Call {@link setRequests()} to initialize requests and + * {@link setTokenAuth()} to set a detected auth token. + * + * @ignore This event is not public yet as the RequestSet API is not really stable yet + */ + Piwik::postEvent('Tracker.initRequestSet', array($this)); + + if (!$this->areRequestsInitialized()) { + $this->requests = array(); + + if (!empty($_GET) || !empty($_POST)) { + $this->setRequests(array($_GET + $_POST)); + } + } + } + + public function hasRequests() + { + return !empty($this->requests); + } + + protected function getRedirectUrl() + { + return Common::getRequestVar('redirecturl', false, 'string'); + } + + protected function hasRedirectUrl() + { + $redirectUrl = $this->getRedirectUrl(); + + return !empty($redirectUrl); + } + + protected function getAllSiteIdsWithinRequest() + { + if (empty($this->requests)) { + return array(); + } + + $siteIds = array(); + foreach ($this->requests as $request) { + $siteIds[] = (int) $request->getIdSite(); + } + + return array_values(array_unique($siteIds)); + } + + // TODO maybe move to reponse? or somewhere else? not sure where! + public function shouldPerformRedirectToUrl() + { + if (!$this->hasRedirectUrl()) { + return false; + } + + if (!$this->hasRequests()) { + return false; + } + + $redirectUrl = $this->getRedirectUrl(); + $host = Url::getHostFromUrl($redirectUrl); + + if (empty($host)) { + return false; + } + + $urls = new SiteUrls(); + $siteUrls = $urls->getAllCachedSiteUrls(); + $siteIds = $this->getAllSiteIdsWithinRequest(); + + foreach ($siteIds as $siteId) { + if (empty($siteUrls[$siteId])) { + $siteUrls[$siteId] = array(); + } + + if (Url::isHostInUrls($host, $siteUrls[$siteId])) { + return $redirectUrl; + } + } + + return false; + } + + public function getState() + { + $requests = array( + 'requests' => array(), + 'env' => $this->getEnvironment(), + 'tokenAuth' => $this->getTokenAuth(), + 'time' => time() + ); + + foreach ($this->getRequests() as $request) { + $requests['requests'][] = $request->getRawParams(); + } + + return $requests; + } + + public function restoreState($state) + { + $backupEnv = $this->getCurrentEnvironment(); + + $this->setEnvironment($state['env']); + $this->setTokenAuth($state['tokenAuth']); + + $this->restoreEnvironment(); + $this->setRequests($state['requests']); + + foreach ($this->getRequests() as $request) { + $request->setCurrentTimestamp($state['time']); + } + + $this->resetEnvironment($backupEnv); + } + + public function rememberEnvironment() + { + $this->setEnvironment($this->getEnvironment()); + } + + public function setEnvironment($env) + { + $this->env = $env; + } + + protected function getEnvironment() + { + if (!empty($this->env)) { + return $this->env; + } + + return $this->getCurrentEnvironment(); + } + + public function restoreEnvironment() + { + if (empty($this->env)) { + return; + } + + $this->resetEnvironment($this->env); + } + + private function resetEnvironment($env) + { + $_SERVER = $env['server']; + } + + private function getCurrentEnvironment() + { + return array( + 'server' => $_SERVER + ); + } +} diff --git a/www/analytics/core/Tracker/Response.php b/www/analytics/core/Tracker/Response.php new file mode 100644 index 00000000..2b4a6f3b --- /dev/null +++ b/www/analytics/core/Tracker/Response.php @@ -0,0 +1,182 @@ +isDebugModeEnabled()) { + $this->timer = new Timer(); + + TrackerDb::enableProfiling(); + } + } + + public function getOutput() + { + $this->outputAccessControlHeaders(); + + if (is_null($this->content) && ob_get_level() > 0) { + $this->content = ob_get_clean(); + } + + return $this->content; + } + + /** + * Echos an error message & other information, then exits. + * + * @param Tracker $tracker + * @param Exception $e + * @param int $statusCode eg 500 + */ + public function outputException(Tracker $tracker, Exception $e, $statusCode) + { + Common::sendResponseCode($statusCode); + $this->logExceptionToErrorLog($e); + + if ($tracker->isDebugModeEnabled()) { + Common::sendHeader('Content-Type: text/html; charset=utf-8'); + $trailer = 'Backtrace:
' . $e->getTraceAsString() . '
'; + $headerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Morpheus/templates/simpleLayoutHeader.tpl'); + $footerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Morpheus/templates/simpleLayoutFooter.tpl'); + $headerPage = str_replace('{$HTML_TITLE}', 'Piwik › Error', $headerPage); + + echo $headerPage . '

' . $this->getMessageFromException($e) . '

' . $trailer . $footerPage; + } else { + $this->outputApiResponse($tracker); + } + } + + public function outputResponse(Tracker $tracker) + { + if (!$tracker->shouldRecordStatistics()) { + $this->outputApiResponse($tracker); + Common::printDebug("Logging disabled, display transparent logo"); + } elseif (!$tracker->hasLoggedRequests()) { + if (!$this->isHttpGetRequest() || !empty($_GET) || !empty($_POST)) { + Common::sendResponseCode(400); + } + Common::printDebug("Empty request => Piwik page"); + echo "Piwik is a free/libre web analytics that lets you keep control of your data."; + } else { + $this->outputApiResponse($tracker); + Common::printDebug("Nothing to notice => default behaviour"); + } + + Common::printDebug("End of the page."); + + if ($tracker->isDebugModeEnabled() + && $tracker->isDatabaseConnected() + && TrackerDb::isProfilingEnabled()) { + $db = Tracker::getDatabase(); + $db->recordProfiling(); + Profiler::displayDbTrackerProfile($db); + } + + if ($tracker->isDebugModeEnabled()) { + Common::printDebug($_COOKIE); + Common::printDebug((string)$this->timer); + } + } + + private function outputAccessControlHeaders() + { + if (!$this->isHttpGetRequest()) { + $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*'; + Common::sendHeader('Access-Control-Allow-Origin: ' . $origin); + Common::sendHeader('Access-Control-Allow-Credentials: true'); + } + } + + private function isHttpGetRequest() + { + $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + + return strtoupper($requestMethod) === 'GET'; + } + + private function getOutputBuffer() + { + return ob_get_contents(); + } + + protected function hasAlreadyPrintedOutput() + { + return strlen($this->getOutputBuffer()) > 0; + } + + private function outputApiResponse(Tracker $tracker) + { + if ($tracker->isDebugModeEnabled()) { + return; + } + + if ($this->hasAlreadyPrintedOutput()) { + return; + } + + $request = $_GET + $_POST; + + if (array_key_exists('send_image', $request) && $request['send_image'] === '0') { + Common::sendResponseCode(204); + return; + } + + $this->outputTransparentGif(); + } + + private function outputTransparentGif() + { + $transGifBase64 = "R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="; + Common::sendHeader('Content-Type: image/gif'); + + echo base64_decode($transGifBase64); + } + + /** + * Gets the error message to output when a tracking request fails. + * + * @param Exception $e + * @return string + */ + protected function getMessageFromException($e) + { + // Note: duplicated from FormDatabaseSetup.isAccessDenied + // Avoid leaking the username/db name when access denied + if ($e->getCode() == 1044 || $e->getCode() == 42000) { + return "Error while connecting to the Piwik database - please check your credentials in config/config.ini.php file"; + } + + if (Common::isPhpCliMode()) { + return $e->getMessage() . "\n" . $e->getTraceAsString(); + } + + return $e->getMessage(); + } + + protected function logExceptionToErrorLog(Exception $e) + { + error_log(sprintf("Error in Piwik (tracker): %s", str_replace("\n", " ", $this->getMessageFromException($e)))); + } +} diff --git a/www/analytics/core/Tracker/ScheduledTasksRunner.php b/www/analytics/core/Tracker/ScheduledTasksRunner.php new file mode 100644 index 00000000..6c1f230b --- /dev/null +++ b/www/analytics/core/Tracker/ScheduledTasksRunner.php @@ -0,0 +1,86 @@ +shouldRecordStatistics(); + } + + /** + * Tracker requests will automatically trigger the Scheduled tasks. + * This is useful for users who don't setup the cron, + * but still want daily/weekly/monthly PDF reports emailed automatically. + * + * This is similar to calling the API CoreAdminHome.runScheduledTasks + */ + public function runScheduledTasks() + { + $now = time(); + + // Currently, there are no hourly tasks. When there are some, + // this could be too aggressive minimum interval (some hours would be skipped in case of low traffic) + $minimumInterval = TrackerConfig::getConfigValue('scheduled_tasks_min_interval'); + + // If the user disabled browser archiving, he has already setup a cron + // To avoid parallel requests triggering the Scheduled Tasks, + // Get last time tasks started executing + $cache = Cache::getCacheGeneral(); + + if ($minimumInterval <= 0 + || empty($cache['isBrowserTriggerEnabled']) + ) { + Common::printDebug("-> Scheduled tasks not running in Tracker: Browser archiving is disabled."); + return; + } + + $nextRunTime = $cache['lastTrackerCronRun'] + $minimumInterval; + + if ((defined('DEBUG_FORCE_SCHEDULED_TASKS') && DEBUG_FORCE_SCHEDULED_TASKS) + || $cache['lastTrackerCronRun'] === false + || $nextRunTime < $now + ) { + $cache['lastTrackerCronRun'] = $now; + Cache::setCacheGeneral($cache); + + Option::set('lastTrackerCronRun', $cache['lastTrackerCronRun']); + Common::printDebug('-> Scheduled Tasks: Starting...'); + + $invokeScheduledTasksUrl = "?module=API&format=csv&convertToUnicode=0&method=CoreAdminHome.runScheduledTasks&trigger=archivephp"; + + $cliMulti = new CliMulti(); + $cliMulti->runAsSuperUser(); + $responses = $cliMulti->request(array($invokeScheduledTasksUrl)); + $resultTasks = reset($responses); + + Common::printDebug($resultTasks); + + Common::printDebug('Finished Scheduled Tasks.'); + } else { + Common::printDebug("-> Scheduled tasks not triggered."); + } + + Common::printDebug("Next run will be from: " . date('Y-m-d H:i:s', $nextRunTime) . ' UTC'); + } +} diff --git a/www/analytics/core/Tracker/Settings.php b/www/analytics/core/Tracker/Settings.php new file mode 100644 index 00000000..36aeb917 --- /dev/null +++ b/www/analytics/core/Tracker/Settings.php @@ -0,0 +1,126 @@ +isSameFingerprintsAcrossWebsites = $isSameFingerprintsAcrossWebsites; + } + + public function getConfigId(Request $request, $ipAddress) + { + list($plugin_Flash, $plugin_Java, $plugin_Director, $plugin_Quicktime, $plugin_RealPlayer, $plugin_PDF, + $plugin_WindowsMedia, $plugin_Gears, $plugin_Silverlight, $plugin_Cookie) = $request->getPlugins(); + + $userAgent = $request->getUserAgent(); + + $deviceDetector = DeviceDetectorFactory::getInstance($userAgent); + $aBrowserInfo = $deviceDetector->getClient(); + + if ($aBrowserInfo['type'] != 'browser') { + // for now only track browsers + unset($aBrowserInfo); + } + + $browserName = !empty($aBrowserInfo['short_name']) ? $aBrowserInfo['short_name'] : 'UNK'; + $browserVersion = !empty($aBrowserInfo['version']) ? $aBrowserInfo['version'] : ''; + + if ($deviceDetector->isBot()) { + $os = self::OS_BOT; + } else { + $os = $deviceDetector->getOS(); + $os = empty($os['short_name']) ? 'UNK' : $os['short_name']; + } + + $browserLang = substr($request->getBrowserLanguage(), 0, 20); // limit the length of this string to match db + + return $this->getConfigHash( + $request, + $os, + $browserName, + $browserVersion, + $plugin_Flash, + $plugin_Java, + $plugin_Director, + $plugin_Quicktime, + $plugin_RealPlayer, + $plugin_PDF, + $plugin_WindowsMedia, + $plugin_Gears, + $plugin_Silverlight, + $plugin_Cookie, + $ipAddress, + $browserLang); + } + + /** + * Returns a 64-bit hash that attemps to identify a user. + * Maintaining some privacy by default, eg. prevents the merging of several Piwik serve together for matching across instances.. + * + * @param $os + * @param $browserName + * @param $browserVersion + * @param $plugin_Flash + * @param $plugin_Java + * @param $plugin_Director + * @param $plugin_Quicktime + * @param $plugin_RealPlayer + * @param $plugin_PDF + * @param $plugin_WindowsMedia + * @param $plugin_Gears + * @param $plugin_Silverlight + * @param $plugin_Cookie + * @param $ip + * @param $browserLang + * @return string + */ + protected function getConfigHash(Request $request, $os, $browserName, $browserVersion, $plugin_Flash, $plugin_Java, + $plugin_Director, $plugin_Quicktime, $plugin_RealPlayer, $plugin_PDF, + $plugin_WindowsMedia, $plugin_Gears, $plugin_Silverlight, $plugin_Cookie, $ip, + $browserLang) + { + // prevent the config hash from being the same, across different Piwik instances + // (limits ability of different Piwik instances to cross-match users) + $salt = SettingsPiwik::getSalt(); + + $configString = + $os + . $browserName . $browserVersion + . $plugin_Flash . $plugin_Java . $plugin_Director . $plugin_Quicktime . $plugin_RealPlayer . $plugin_PDF + . $plugin_WindowsMedia . $plugin_Gears . $plugin_Silverlight . $plugin_Cookie + . $ip + . $browserLang + . $salt; + + if (!$this->isSameFingerprintsAcrossWebsites) { + $configString .= $request->getIdSite(); + } + + $hash = md5($configString, $raw_output = true); + + return substr($hash, 0, Tracker::LENGTH_BINARY_ID); + } +} diff --git a/www/analytics/core/Tracker/SettingsStorage.php b/www/analytics/core/Tracker/SettingsStorage.php new file mode 100644 index 00000000..a69e0355 --- /dev/null +++ b/www/analytics/core/Tracker/SettingsStorage.php @@ -0,0 +1,58 @@ +getOptionKey(); + $cache = $this->getCache(); + + if ($cache->contains($cacheId)) { + $settings = $cache->fetch($cacheId); + } else { + $settings = parent::loadSettings(); + + $cache->save($cacheId, $settings); + } + + return $settings; + } + + public function save() + { + parent::save(); + self::clearCache(); + } + + private function getCache() + { + return self::buildCache($this->getOptionKey()); + } + + public static function clearCache() + { + Cache::deleteTrackerCache(); + self::buildCache()->flushAll(); + } + + private static function buildCache() + { + return PiwikCache::getEagerCache(); + } +} diff --git a/www/analytics/core/Tracker/TableLogAction.php b/www/analytics/core/Tracker/TableLogAction.php index 1eb237d3..9552e7ae 100644 --- a/www/analytics/core/Tracker/TableLogAction.php +++ b/www/analytics/core/Tracker/TableLogAction.php @@ -1,6 +1,6 @@ query($sql, array($name, $name, $type, $urlPrefix)); - $actionId = Tracker::getDatabase()->lastInsertId(); - - $inserted[$fieldName] = $actionId; + $actionId = self::getModel()->createNewIdAction($name, $type, $urlPrefix); Common::printDebug("Recorded a new action (" . Action::getTypeAsString($type) . ") in the lookup table: " . $name . " (idaction = " . $actionId . ")"); + + $inserted[$fieldName] = $actionId; } + return $inserted; } + private static function getModel() + { + return new Model(); + } + private static function queryIdsAction($actionsNameAndType) { - $sql = TableLogAction::getSqlSelectActionId(); - $bind = array(); - $i = 0; + $toQuery = array(); foreach ($actionsNameAndType as &$actionNameType) { list($name, $type, $urlPrefix) = $actionNameType; - if (empty($name)) { - continue; - } - if ($i > 0) { - $sql .= " OR ( hash = CRC32(?) AND name = ? AND type = ? ) "; - } - $bind[] = $name; - $bind[] = $name; - $bind[] = $type; - $i++; + $toQuery[] = array('name' => $name, 'type' => $type); } - // Case URL & Title are empty - if (empty($bind)) { - return false; - } - $actionIds = Tracker::getDatabase()->fetchAll($sql, $bind); + + $actionIds = self::getModel()->getIdsAction($toQuery); + return $actionIds; } @@ -151,6 +131,7 @@ class TableLogAction // For the Actions found in the lookup table, add the idaction in the array, // If not found in lookup table, queue for INSERT $fieldNamesToInsert = $fieldNameToActionId = array(); + foreach ($actionsNameAndType as $fieldName => &$actionNameType) { @list($name, $type, $urlPrefix) = $actionNameType; if (empty($name)) { @@ -173,10 +154,10 @@ class TableLogAction $fieldNamesToInsert[] = $fieldName; } } + return array($fieldNameToActionId, $fieldNamesToInsert); } - /** * Convert segment expression to an action ID or an SQL expression. * @@ -193,34 +174,37 @@ class TableLogAction */ public static function getIdActionFromSegment($valueToMatch, $sqlField, $matchType, $segmentName) { - $actionType = self::guessActionTypeFromSegment($segmentName); - - if ($actionType == Action::TYPE_PAGE_URL) { - // for urls trim protocol and www because it is not recorded in the db - $valueToMatch = preg_replace('@^http[s]?://(www\.)?@i', '', $valueToMatch); - } - $valueToMatch = Common::sanitizeInputValue(Common::unsanitizeInputValue($valueToMatch)); - - if ($matchType == SegmentExpression::MATCH_EQUAL - || $matchType == SegmentExpression::MATCH_NOT_EQUAL - ) { - $idAction = self::getIdActionMatchingNameAndType($valueToMatch, $actionType); - // if the action is not found, we hack -100 to ensure it tries to match against an integer - // otherwise binding idaction_name to "false" returns some rows for some reasons (in case &segment=pageTitle==Větrnásssssss) - if (empty($idAction)) { - $idAction = -100; + if ($segmentName === 'actionType') { + $actionType = (int) $valueToMatch; + $valueToMatch = array(); + $sql = 'SELECT idaction FROM ' . Common::prefixTable('log_action') . ' WHERE type = ' . $actionType . ' )'; + } else { + $actionType = self::guessActionTypeFromSegment($segmentName); + if ($actionType == Action::TYPE_PAGE_URL) { + // for urls trim protocol and www because it is not recorded in the db + $valueToMatch = preg_replace('@^http[s]?://(www\.)?@i', '', $valueToMatch); } - return $idAction; + + $valueToMatch = self::normaliseActionString($actionType, $valueToMatch); + if ($matchType == SegmentExpression::MATCH_EQUAL + || $matchType == SegmentExpression::MATCH_NOT_EQUAL + ) { + $idAction = self::getModel()->getIdActionMatchingNameAndType($valueToMatch, $actionType); + // Action is not found (eg. &segment=pageTitle==Větrnásssssss) + if (empty($idAction)) { + $idAction = null; + } + return $idAction; + } + + // "name contains $string" match can match several idaction so we cannot return yet an idaction + // special case + $sql = self::getSelectQueryWhereNameContains($matchType, $actionType); } - // "name contains $string" match can match several idaction so we cannot return yet an idaction - // special case - $sql = TableLogAction::getSelectQueryWhereNameContains($matchType, $actionType); - return array( - // mark that the returned value is an sql-expression instead of a literal value - 'SQL' => $sql, - 'bind' => $valueToMatch, - ); + + $cache = StaticContainer::get('Piwik\Tracker\TableLogAction\Cache'); + return $cache->getIdActionFromSegment($valueToMatch, $sql); } /** @@ -231,11 +215,18 @@ class TableLogAction private static function guessActionTypeFromSegment($segmentName) { $exactMatch = array( - 'eventAction' => Action::TYPE_EVENT_ACTION, - 'eventCategory' => Action::TYPE_EVENT_CATEGORY, - 'eventName' => Action::TYPE_EVENT_NAME, + 'outlinkUrl' => Action::TYPE_OUTLINK, + 'downloadUrl' => Action::TYPE_DOWNLOAD, + 'eventAction' => Action::TYPE_EVENT_ACTION, + 'eventCategory' => Action::TYPE_EVENT_CATEGORY, + 'eventName' => Action::TYPE_EVENT_NAME, + 'contentPiece' => Action::TYPE_CONTENT_PIECE, + 'contentTarget' => Action::TYPE_CONTENT_TARGET, + 'contentName' => Action::TYPE_CONTENT_NAME, + 'contentInteraction' => Action::TYPE_CONTENT_INTERACTION, ); - if(!empty($exactMatch[$segmentName])) { + + if (!empty($exactMatch[$segmentName])) { return $exactMatch[$segmentName]; } @@ -253,5 +244,40 @@ class TableLogAction } } -} + /** + * This function will sanitize or not if it's needed for the specified action type + * + * URLs (Download URL, Outlink URL) are stored raw (unsanitized) + * while other action types are stored Sanitized + * + * @param $actionType + * @param $actionString + * @return string + */ + private static function normaliseActionString($actionType, $actionString) + { + $actionString = Common::unsanitizeInputValue($actionString); + if (self::isActionTypeStoredUnsanitized($actionType)) { + return $actionString; + } + + return Common::sanitizeInputValue($actionString); + } + + /** + * @param $actionType + * @return bool + */ + private static function isActionTypeStoredUnsanitized($actionType) + { + $actionsTypesStoredUnsanitized = array( + $actionType == Action::TYPE_DOWNLOAD, + $actionType == Action::TYPE_OUTLINK, + $actionType == Action::TYPE_PAGE_URL, + $actionType == Action::TYPE_CONTENT, + ); + + return in_array($actionType, $actionsTypesStoredUnsanitized); + } +} diff --git a/www/analytics/core/Tracker/TableLogAction/Cache.php b/www/analytics/core/Tracker/TableLogAction/Cache.php new file mode 100644 index 00000000..ba078c57 --- /dev/null +++ b/www/analytics/core/Tracker/TableLogAction/Cache.php @@ -0,0 +1,160 @@ +isEnabled = (bool)$config->General['enable_segments_subquery_cache']; + $this->limitActionIds = $config->General['segments_subquery_cache_limit']; + $this->lifetime = $config->General['segments_subquery_cache_ttl']; + $this->logger = $logger; + $this->cache = $cache; + } + + /** + * @param $valueToMatch + * @param $sql + * @return array|null + * @throws \Exception + */ + public function getIdActionFromSegment($valueToMatch, $sql) + { + if (!$this->isEnabled) { + return array( + // mark that the returned value is an sql-expression instead of a literal value + 'SQL' => $sql, + 'bind' => $valueToMatch, + ); + } + + $ids = self::getIdsFromCache($valueToMatch, $sql); + + if(is_null($ids)) { + // Too Big To Cache, issue SQL as subquery instead + return array( + 'SQL' => $sql, + 'bind' => $valueToMatch, + ); + } + + if(count($ids) == 0) { + return null; + } + + + $sql = Common::getSqlStringFieldsArray($ids); + $bind = $ids; + + return array( + // mark that the returned value is an sql-expression instead of a literal value + 'SQL' => $sql, + 'bind' => $bind, + ); + } + + + /** + * @param $valueToMatch + * @param $sql + * @return array of IDs, or null if the returnset is too big to cache + */ + private function getIdsFromCache($valueToMatch, $sql) + { + $cacheKey = $this->getCacheKey($valueToMatch, $sql); + + if ($this->cache->contains($cacheKey) === true) { // TODO: hits + $this->logger->debug("Segment subquery cache HIT (for '$valueToMatch' and SQL '$sql)"); + return $this->cache->fetch($cacheKey); + } + + $ids = $this->fetchActionIdsFromDb($valueToMatch, $sql); + + if($this->isTooBigToCache($ids)) { + $this->logger->debug("Segment subquery cache SKIPPED SAVE (too many IDs returned by subquery: %s ids)'", array(count($ids))); + $this->cache->save($cacheKey, $ids = null, $this->lifetime); + return null; + } + + $this->cache->save($cacheKey, $ids, $this->lifetime); + $this->logger->debug("Segment subquery cache SAVE (for '$valueToMatch' and SQL '$sql')'"); + + return $ids; + } + + /** + * @param $valueToMatch + * @param $sql + * @return string + * @throws + */ + private function getCacheKey($valueToMatch, $sql) + { + if(is_array($valueToMatch)) { + throw new \Exception("value to match is an array: this is not expected"); + } + + $uniqueKey = md5($sql . $valueToMatch); + $cacheKey = 'TableLogAction.getIdActionFromSegment.' . $uniqueKey; + return $cacheKey; + } + + /** + * @param $valueToMatch + * @param $sql + * @return array|null + * @throws \Exception + */ + private function fetchActionIdsFromDb($valueToMatch, $sql) + { + $idActions = \Piwik\Db::fetchAll($sql, $valueToMatch); + + $ids = array(); + foreach ($idActions as $idAction) { + $ids[] = $idAction['idaction']; + } + + return $ids; + } + + /** + * @param $ids + * @return bool + */ + private function isTooBigToCache($ids) + { + return count($ids) > $this->limitActionIds; + } +} \ No newline at end of file diff --git a/www/analytics/core/Tracker/TrackerCodeGenerator.php b/www/analytics/core/Tracker/TrackerCodeGenerator.php new file mode 100644 index 00000000..c985db88 --- /dev/null +++ b/www/analytics/core/Tracker/TrackerCodeGenerator.php @@ -0,0 +1,199 @@ +getJavascriptTagOptions($idSite, $mergeSubdomains, $mergeAliasUrls); + } + $maxCustomVars = CustomVariables::getNumUsableCustomVariables(); + + if ($visitorCustomVariables && count($visitorCustomVariables) > 0) { + $options .= ' // you can set up to ' . $maxCustomVars . ' custom variables for each visitor' . "\n"; + $index = 1; + foreach ($visitorCustomVariables as $visitorCustomVariable) { + if (empty($visitorCustomVariable)) { + continue; + } + + $options .= sprintf( + ' _paq.push(["setCustomVariable", %d, %s, %s, "visit"]);%s', + $index++, + json_encode($visitorCustomVariable[0]), + json_encode($visitorCustomVariable[1]), + "\n" + ); + } + } + if ($pageCustomVariables && count($pageCustomVariables) > 0) { + $options .= ' // you can set up to ' . $maxCustomVars . ' custom variables for each action (page view, download, click, site search)' . "\n"; + $index = 1; + foreach ($pageCustomVariables as $pageCustomVariable) { + if (empty($pageCustomVariable)) { + continue; + } + $options .= sprintf( + ' _paq.push(["setCustomVariable", %d, %s, %s, "page"]);%s', + $index++, + json_encode($pageCustomVariable[0]), + json_encode($pageCustomVariable[1]), + "\n" + ); + } + } + if ($customCampaignNameQueryParam) { + $options .= ' _paq.push(["setCampaignNameKey", ' + . json_encode($customCampaignNameQueryParam) . ']);' . "\n"; + } + if ($customCampaignKeywordParam) { + $options .= ' _paq.push(["setCampaignKeywordKey", ' + . json_encode($customCampaignKeywordParam) . ']);' . "\n"; + } + if ($doNotTrack) { + $options .= ' _paq.push(["setDoNotTrack", true]);' . "\n"; + } + if ($disableCookies) { + $options .= ' _paq.push(["disableCookies"]);' . "\n"; + } + + $codeImpl = array( + 'idSite' => $idSite, + // TODO why sanitizeInputValue() and not json_encode? + 'piwikUrl' => Common::sanitizeInputValue($piwikUrl), + 'options' => $options, + 'optionsBeforeTrackerUrl' => $optionsBeforeTrackerUrl, + 'protocol' => '//' + ); + $parameters = compact('mergeSubdomains', 'groupPageTitlesByDomain', 'mergeAliasUrls', 'visitorCustomVariables', + 'pageCustomVariables', 'customCampaignNameQueryParam', 'customCampaignKeywordParam', + 'doNotTrack'); + + /** + * Triggered when generating JavaScript tracking code server side. Plugins can use + * this event to customise the JavaScript tracking code that is displayed to the + * user. + * + * @param array &$codeImpl An array containing snippets of code that the event handler + * can modify. Will contain the following elements: + * + * - **idSite**: The ID of the site being tracked. + * - **piwikUrl**: The tracker URL to use. + * - **options**: A string of JavaScript code that customises + * the JavaScript tracker. + * - **optionsBeforeTrackerUrl**: A string of Javascript code that customises + * the JavaScript tracker inside of anonymous function before + * adding setTrackerUrl into paq. + * - **protocol**: Piwik url protocol. + * + * The **httpsPiwikUrl** element can be set if the HTTPS + * domain is different from the normal domain. + * @param array $parameters The parameters supplied to `TrackerCodeGenerator::generate()`. + */ + Piwik::postEvent('Piwik.getJavascriptCode', array(&$codeImpl, $parameters)); + + $setTrackerUrl = 'var u="' . $codeImpl['protocol'] . '{$piwikUrl}/";'; + + if (!empty($codeImpl['httpsPiwikUrl'])) { + $setTrackerUrl = 'var u=((document.location.protocol === "https:") ? "https://{$httpsPiwikUrl}/" : "http://{$piwikUrl}/");'; + $codeImpl['httpsPiwikUrl'] = rtrim($codeImpl['httpsPiwikUrl'], "/"); + } + $codeImpl = array('setTrackerUrl' => htmlentities($setTrackerUrl)) + $codeImpl; + + foreach ($codeImpl as $keyToReplace => $replaceWith) { + $jsCode = str_replace('{$' . $keyToReplace . '}', $replaceWith, $jsCode); + } + + return $jsCode; + } + + private function getJavascriptTagOptions($idSite, $mergeSubdomains, $mergeAliasUrls) + { + try { + $websiteUrls = APISitesManager::getInstance()->getSiteUrlsFromId($idSite); + } catch (\Exception $e) { + return ''; + } + // We need to parse_url to isolate hosts + $websiteHosts = array(); + $firstHost = null; + foreach ($websiteUrls as $site_url) { + $referrerParsed = parse_url($site_url); + + if (!isset($firstHost)) { + $firstHost = $referrerParsed['host']; + } + + $url = $referrerParsed['host']; + if (!empty($referrerParsed['path'])) { + $url .= $referrerParsed['path']; + } + $websiteHosts[] = $url; + } + $options = ''; + if ($mergeSubdomains && !empty($firstHost)) { + $options .= ' _paq.push(["setCookieDomain", "*.' . $firstHost . '"]);' . "\n"; + } + if ($mergeAliasUrls && !empty($websiteHosts)) { + $urls = '["*.' . implode('","*.', $websiteHosts) . '"]'; + $options .= ' _paq.push(["setDomains", ' . $urls . ']);' . "\n"; + } + return $options; + } +} diff --git a/www/analytics/core/Tracker/TrackerConfig.php b/www/analytics/core/Tracker/TrackerConfig.php new file mode 100644 index 00000000..537dc8f0 --- /dev/null +++ b/www/analytics/core/Tracker/TrackerConfig.php @@ -0,0 +1,39 @@ +Tracker = $section; + } + + public static function getConfigValue($name) + { + $config = self::getConfig(); + return $config[$name]; + } + + private static function getConfig() + { + return Config::getInstance()->Tracker; + } +} diff --git a/www/analytics/core/Tracker/Visit.php b/www/analytics/core/Tracker/Visit.php index 1cd7d7c9..80d8d136 100644 --- a/www/analytics/core/Tracker/Visit.php +++ b/www/analytics/core/Tracker/Visit.php @@ -1,6 +1,6 @@ requestProcessors = $requestProcessors->getRequestProcessors(); + $this->visitorRecognizer = StaticContainer::get('Piwik\Tracker\VisitorRecognizer'); + $this->visitProperties = null; + $this->userSettings = StaticContainer::get('Piwik\Tracker\Settings'); + $this->invalidator = StaticContainer::get('Piwik\Archive\ArchiveInvalidator'); + } /** * @param Request $request @@ -78,73 +115,42 @@ class Visit implements VisitInterface */ public function handle() { - // the IP is needed by isExcluded() and GoalManager->recordGoals() - $ip = $this->request->getIp(); - $this->visitorInfo['location_ip'] = $ip; + foreach ($this->requestProcessors as $processor) { + Common::printDebug("Executing " . get_class($processor) . "::manipulateRequest()..."); - $excluded = new VisitExcluded($this->request, $ip); - if ($excluded->isExcluded()) { - return; + $processor->manipulateRequest($this->request); } - /** - * Triggered after visits are tested for exclusion so plugins can modify the IP address - * persisted with a visit. - * - * This event is primarily used by the **PrivacyManager** plugin to anonymize IP addresses. - * - * @param string &$ip The visitor's IP address. - */ - Piwik::postEvent('Tracker.setVisitorIp', array(&$this->visitorInfo['location_ip'])); + $this->visitProperties = new VisitProperties(); - $this->visitorCustomVariables = $this->request->getCustomVariables($scope = 'visit'); - if (!empty($this->visitorCustomVariables)) { - Common::printDebug("Visit level Custom Variables: "); - Common::printDebug($this->visitorCustomVariables); - } + foreach ($this->requestProcessors as $processor) { + Common::printDebug("Executing " . get_class($processor) . "::processRequestParams()..."); - $this->goalManager = new GoalManager($this->request); - - $visitIsConverted = false; - $action = null; - - $requestIsManualGoalConversion = ($this->goalManager->idGoal > 0); - $requestIsEcommerce = $this->goalManager->requestIsEcommerce; - if ($requestIsEcommerce) { - $someGoalsConverted = true; - - // Mark the visit as Converted only if it is an order (not for a Cart update) - if ($this->goalManager->isGoalAnOrder) { - $visitIsConverted = true; + $abort = $processor->processRequestParams($this->visitProperties, $this->request); + if ($abort) { + Common::printDebug("-> aborting due to processRequestParams method"); + return; } - } // this request is from the JS call to piwikTracker.trackGoal() - elseif ($requestIsManualGoalConversion) { - $someGoalsConverted = $this->goalManager->detectGoalId($this->request->getIdSite()); - $visitIsConverted = $someGoalsConverted; - // if we find a idgoal in the URL, but then the goal is not valid, this is most likely a fake request - if (!$someGoalsConverted) { - throw new \Exception('Invalid goal tracking request for goal id = ' . $this->goalManager->idGoal); + } + + $isNewVisit = $this->request->getMetadata('CoreHome', 'isNewVisit'); + if (!$isNewVisit) { + $isNewVisit = $this->triggerPredicateHookOnDimensions($this->getAllVisitDimensions(), 'shouldForceNewVisit'); + $this->request->setMetadata('CoreHome', 'isNewVisit', $isNewVisit); + } + + foreach ($this->requestProcessors as $processor) { + Common::printDebug("Executing " . get_class($processor) . "::afterRequestProcessed()..."); + + $abort = $processor->afterRequestProcessed($this->visitProperties, $this->request); + if ($abort) { + Common::printDebug("-> aborting due to afterRequestProcessed method"); + return; } - } // normal page view, potentially triggering a URL matching goal - else { - $action = Action::factory($this->request); - - $action->writeDebugInfo(); - - $someGoalsConverted = $this->goalManager->detectGoalsMatchingUrl($this->request->getIdSite(), $action); - $visitIsConverted = $someGoalsConverted; - - $action->loadIdsFromLogActionTable(); } - // the visitor and session - $this->recognizeTheVisitor(); + $isNewVisit = $this->request->getMetadata('CoreHome', 'isNewVisit'); - $isLastActionInTheSameVisit = $this->isLastActionInTheSameVisit(); - - if (!$isLastActionInTheSameVisit) { - Common::printDebug("Visitor detected, but last action was more than 30 minutes ago..."); - } // Known visit when: // ( - the visitor has the Piwik cookie with the idcookie ID used by Piwik to match the visitor // OR @@ -152,38 +158,11 @@ class Visit implements VisitInterface // ) // AND // - the last page view for this visitor was less than 30 minutes ago @see isLastActionInTheSameVisit() - if ($this->isVisitorKnown() - && $isLastActionInTheSameVisit - ) { - $idReferrerActionUrl = $this->visitorInfo['visit_exit_idaction_url']; - $idReferrerActionName = $this->visitorInfo['visit_exit_idaction_name']; + if (!$isNewVisit) { try { - $this->handleExistingVisit($action, $visitIsConverted); - if (!is_null($action)) { - $action->record($this->visitorInfo['idvisit'], - $this->visitorInfo['idvisitor'], - $idReferrerActionUrl, - $idReferrerActionName, - $this->visitorInfo['time_spent_ref_action'] - ); - } + $this->handleExistingVisit($this->request->getMetadata('Goals', 'visitIsConverted')); } catch (VisitorNotFoundInDb $e) { - - // There is an edge case when: - // - two manual goal conversions happen in the same second - // - which result in handleExistingVisit throwing the exception - // because the UPDATE didn't affect any rows (one row was found, but not updated since no field changed) - // - the exception is caught here and will result in a new visit incorrectly - // In this case, we cancel the current conversion to be recorded: - if ($requestIsManualGoalConversion - || $requestIsEcommerce - ) { - $someGoalsConverted = $visitIsConverted = false; - } // When the row wasn't found in the logs, and this is a pageview or - // goal matching URL, we force a new visitor - else { - $this->visitorKnown = false; - } + $this->request->setMetadata('CoreHome', 'visitorNotFoundInDb', true); // TODO: perhaps we should just abort here? } } @@ -191,29 +170,20 @@ class Visit implements VisitInterface // - the visitor has the Piwik cookie but the last action was performed more than 30 min ago @see isLastActionInTheSameVisit() // - the visitor doesn't have the Piwik cookie, and couldn't be matched in @see recognizeTheVisitor() // - the visitor does have the Piwik cookie but the idcookie and idvisit found in the cookie didn't match to any existing visit in the DB - if (!$this->isVisitorKnown() - || !$isLastActionInTheSameVisit - ) { - $this->handleNewVisit($action, $visitIsConverted); - if (!is_null($action)) { - $action->record($this->visitorInfo['idvisit'], $this->visitorInfo['idvisitor'], 0, 0, 0); - } + if ($isNewVisit) { + $this->handleNewVisit($this->request->getMetadata('Goals', 'visitIsConverted')); } // update the cookie with the new visit information - $this->request->setThirdPartyCookie($this->visitorInfo['idvisitor']); + $this->request->setThirdPartyCookie($this->visitProperties->getProperty('idvisitor')); - // record the goals if applicable - if ($someGoalsConverted) { - $this->goalManager->recordGoals( - $this->request->getIdSite(), - $this->visitorInfo, - $this->visitorCustomVariables, - $action - ); + foreach ($this->requestProcessors as $processor) { + Common::printDebug("Executing " . get_class($processor) . "::recordLogs()..."); + + $processor->recordLogs($this->visitProperties, $this->request); } - unset($this->goalManager); - unset($action); + + $this->markArchivedReportsAsInvalidIfArchiveAlreadyFinished(); } /** @@ -222,39 +192,48 @@ class Visit implements VisitInterface * 1) Insert the new action * 2) Update the visit information * + * @param Visitor $visitor * @param Action $action * @param $visitIsConverted * @throws VisitorNotFoundInDb */ - protected function handleExistingVisit($action, $visitIsConverted) + protected function handleExistingVisit($visitIsConverted) { - Common::printDebug("Visit is known (IP = " . IP::N2P($this->getVisitorIp()) . ")"); + Common::printDebug("Visit is known (IP = " . IPUtils::binaryToStringIP($this->getVisitorIp()) . ")"); - $valuesToUpdate = $this->getExistingVisitFieldsToUpdate($action, $visitIsConverted); + // TODO it should be its own dimension + $this->visitProperties->setProperty('time_spent_ref_action', $this->getTimeSpentReferrerAction()); - $this->visitorInfo['time_spent_ref_action'] = $this->getTimeSpentReferrerAction(); - - $this->request->overrideLocation($valuesToUpdate); + $valuesToUpdate = $this->getExistingVisitFieldsToUpdate($visitIsConverted); // update visitorInfo - foreach ($valuesToUpdate AS $name => $value) { - $this->visitorInfo[$name] = $value; + foreach ($valuesToUpdate as $name => $value) { + $this->visitProperties->setProperty($name, $value); } /** * Triggered before a [visit entity](/guides/persistence-and-the-mysql-backend#visits) is updated when * tracking an action for an existing visit. - * + * * This event can be used to modify the visit properties that will be updated before the changes * are persisted. - * + * + * This event is deprecated, use [Dimensions](http://developer.piwik.org/guides/dimensions) instead. + * * @param array &$valuesToUpdate Visit entity properties that will be updated. * @param array $visit The entire visit entity. Read [this](/guides/persistence-and-the-mysql-backend#visits) * to see what it contains. + * @deprecated */ - Piwik::postEvent('Tracker.existingVisitInformation', array(&$valuesToUpdate, $this->visitorInfo)); + Piwik::postEvent('Tracker.existingVisitInformation', array(&$valuesToUpdate, $this->visitProperties->getProperties())); + + foreach ($this->requestProcessors as $processor) { + $processor->onExistingVisit($valuesToUpdate, $this->visitProperties, $this->request); + } $this->updateExistingVisit($valuesToUpdate); + + $this->visitProperties->setProperty('visit_last_action_time', $this->request->getCurrentTimestamp()); } /** @@ -262,12 +241,15 @@ class Visit implements VisitInterface */ protected function getTimeSpentReferrerAction() { - $timeSpent = $this->request->getCurrentTimestamp() - $this->visitorInfo['visit_last_action_time']; - if ($timeSpent < 0 - || $timeSpent > Config::getInstance()->Tracker['visit_standard_length'] - ) { + $timeSpent = $this->request->getCurrentTimestamp() - + $this->visitProperties->getProperty('visit_last_action_time'); + if ($timeSpent < 0) { $timeSpent = 0; } + $visitStandardLength = $this->getVisitStandardLength(); + if ($timeSpent > $visitStandardLength) { + $timeSpent = $visitStandardLength; + } return $timeSpent; } @@ -278,58 +260,58 @@ class Visit implements VisitInterface * * 2) Insert the visit information * + * @param Visitor $visitor * @param Action $action * @param bool $visitIsConverted */ - protected function handleNewVisit($action, $visitIsConverted) + protected function handleNewVisit($visitIsConverted) { - Common::printDebug("New Visit (IP = " . IP::N2P($this->getVisitorIp()) . ")"); + Common::printDebug("New Visit (IP = " . IPUtils::binaryToStringIP($this->getVisitorIp()) . ")"); - $this->visitorInfo = $this->getNewVisitorInformation($action); + $this->setNewVisitorInformation(); - // Add Custom variable key,value to the visitor array - $this->visitorInfo = array_merge($this->visitorInfo, $this->visitorCustomVariables); + $dimensions = $this->getAllVisitDimensions(); - $this->visitorInfo['visit_goal_converted'] = $visitIsConverted ? 1 : 0; + $this->triggerHookOnDimensions($dimensions, 'onNewVisit'); - $this->visitorInfo['referer_name'] = substr($this->visitorInfo['referer_name'], 0, 70); - $this->visitorInfo['referer_keyword'] = substr($this->visitorInfo['referer_keyword'], 0, 255); - $this->visitorInfo['config_resolution'] = substr($this->visitorInfo['config_resolution'], 0, 9); + if ($visitIsConverted) { + $this->triggerHookOnDimensions($dimensions, 'onConvertedVisit'); + } + + $properties = &$this->visitProperties->getProperties(); /** * Triggered before a new [visit entity](/guides/persistence-and-the-mysql-backend#visits) is persisted. - * + * * This event can be used to modify the visit entity or add new information to it before it is persisted. * The UserCountry plugin, for example, uses this event to add location information for each visit. * + * This event is deprecated, use [Dimensions](http://developer.piwik.org/guides/dimensions) instead. + * * @param array &$visit The visit entity. Read [this](/guides/persistence-and-the-mysql-backend#visits) to see * what information it contains. * @param \Piwik\Tracker\Request $request An object describing the tracking request being processed. + * + * @deprecated */ - Piwik::postEvent('Tracker.newVisitorInformation', array(&$this->visitorInfo, $this->request)); + Piwik::postEvent('Tracker.newVisitorInformation', array(&$properties, $this->request)); + + foreach ($this->requestProcessors as $processor) { + $processor->onNewVisit($this->visitProperties, $this->request); + } - $this->request->overrideLocation($this->visitorInfo); $this->printVisitorInformation(); - $idVisit = $this->insertNewVisit( $this->visitorInfo ); - - $this->visitorInfo['idvisit'] = $idVisit; - $this->visitorInfo['visit_first_action_time'] = $this->request->getCurrentTimestamp(); - $this->visitorInfo['visit_last_action_time'] = $this->request->getCurrentTimestamp(); + $idVisit = $this->insertNewVisit($this->visitProperties->getProperties()); + $this->visitProperties->setProperty('idvisit', $idVisit); + $this->visitProperties->setProperty('visit_first_action_time', $this->request->getCurrentTimestamp()); + $this->visitProperties->setProperty('visit_last_action_time', $this->request->getCurrentTimestamp()); } - static private function cleanupVisitTotalTime($t) + private function getModel() { - $t = (int)$t; - if ($t < 0) { - $t = 0; - } - $smallintMysqlLimit = 65534; - if ($t > $smallintMysqlLimit) { - $t = $smallintMysqlLimit; - } - return $t; + return new Model(); } /** @@ -339,25 +321,28 @@ class Visit implements VisitInterface */ protected function getVisitorIdcookie() { - if ($this->isVisitorKnown()) { - return $this->visitorInfo['idvisitor']; + $isKnown = $this->request->getMetadata('CoreHome', 'isVisitorKnown'); + if ($isKnown) { + return $this->visitProperties->getProperty('idvisitor'); } + // If the visitor had a first party ID cookie, then we use this value - if (!empty($this->visitorInfo['idvisitor']) - && strlen($this->visitorInfo['idvisitor']) == Tracker::LENGTH_BINARY_ID + $idVisitor = $this->visitProperties->getProperty('idvisitor'); + if (!empty($idVisitor) + && Tracker::LENGTH_BINARY_ID == strlen($this->visitProperties->getProperty('idvisitor')) ) { - return $this->visitorInfo['idvisitor']; + return $this->visitProperties->getProperty('idvisitor'); } + return Common::hex2bin($this->generateUniqueVisitorId()); } /** * @return string returns random 16 chars hex string */ - static public function generateUniqueVisitorId() + public static function generateUniqueVisitorId() { - $uniqueId = substr(Common::generateUniqId(), 0, Tracker::LENGTH_HEX_ID_STRING); - return $uniqueId; + return substr(Common::generateUniqId(), 0, Tracker::LENGTH_HEX_ID_STRING); } /** @@ -367,378 +352,43 @@ class Visit implements VisitInterface */ protected function getVisitorIp() { - return $this->visitorInfo['location_ip']; + return $this->visitProperties->getProperty('location_ip'); } /** - * This methods tries to see if the visitor has visited the website before. + * Gets the UserSettings object * - * We have to split the visitor into one of the category - * - Known visitor - * - New visitor + * @return Settings */ - protected function recognizeTheVisitor() + protected function getSettingsObject() { - $this->visitorKnown = false; - - $userInfo = $this->getUserSettingsInformation(); - $configId = $userInfo['config_id']; - - $idVisitor = $this->request->getVisitorId(); - $isVisitorIdToLookup = !empty($idVisitor); - - if ($isVisitorIdToLookup) { - $this->visitorInfo['idvisitor'] = $idVisitor; - Common::printDebug("Matching visitors with: visitorId=" . bin2hex($this->visitorInfo['idvisitor']) . " OR configId=" . bin2hex($configId)); - } else { - Common::printDebug("Visitor doesn't have the piwik cookie..."); - } - - $selectCustomVariables = ''; - // No custom var were found in the request, so let's copy the previous one in a potential conversion later - if (!$this->visitorCustomVariables) { - $maxCustomVariables = CustomVariables::getMaxCustomVariables(); - - for ($index = 1; $index <= $maxCustomVariables; $index++) { - $selectCustomVariables .= ', custom_var_k' . $index . ', custom_var_v' . $index; - } - } - - $persistedVisitAttributes = $this->getVisitFieldsPersist(); - - $selectFields = implode(", ", $persistedVisitAttributes); - - $select = "SELECT - visit_last_action_time, - visit_first_action_time, - $selectFields - $selectCustomVariables - "; - $from = "FROM " . Common::prefixTable('log_visit'); - - list($timeLookBack, $timeLookAhead) = $this->getWindowLookupThisVisit(); - - $shouldMatchOneFieldOnly = $this->shouldLookupOneVisitorFieldOnly($isVisitorIdToLookup); - - // Two use cases: - // 1) there is no visitor ID so we try to match only on config_id (heuristics) - // Possible causes of no visitor ID: no browser cookie support, direct Tracking API request without visitor ID passed, - // importing server access logs with import_logs.py, etc. - // In this case we use config_id heuristics to try find the visitor in tahhhe past. There is a risk to assign - // this page view to the wrong visitor, but this is better than creating artificial visits. - // 2) there is a visitor ID and we trust it (config setting trust_visitors_cookies, OR it was set using &cid= in tracking API), - // and in these cases, we force to look up this visitor id - $whereCommon = "visit_last_action_time >= ? AND visit_last_action_time <= ? AND idsite = ?"; - $bindSql = array( - $timeLookBack, - $timeLookAhead, - $this->request->getIdSite() - ); - - if ($shouldMatchOneFieldOnly) { - if ($isVisitorIdToLookup) { - $whereCommon .= ' AND idvisitor = ?'; - $bindSql[] = $this->visitorInfo['idvisitor']; - } else { - $whereCommon .= ' AND config_id = ?'; - $bindSql[] = $configId; - } - - $sql = "$select - $from - WHERE " . $whereCommon . " - ORDER BY visit_last_action_time DESC - LIMIT 1"; - } // We have a config_id AND a visitor_id. We match on either of these. - // Why do we also match on config_id? - // we do not trust the visitor ID only. Indeed, some browsers, or browser addons, - // cause the visitor id from the 1st party cookie to be different on each page view! - // It is not acceptable to create a new visit every time such browser does a page view, - // so we also backup by searching for matching config_id. - // We use a UNION here so that each sql query uses its own INDEX - else { - // will use INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time) - $where = ' AND config_id = ?'; - $bindSql[] = $configId; - $sqlConfigId = "$select , - 0 as priority - $from - WHERE $whereCommon $where - ORDER BY visit_last_action_time DESC - LIMIT 1 - "; - - // will use INDEX index_idsite_idvisitor (idsite, idvisitor) - $bindSql[] = $timeLookBack; - $bindSql[] = $timeLookAhead; - $bindSql[] = $this->request->getIdSite(); - $where = ' AND idvisitor = ?'; - $bindSql[] = $this->visitorInfo['idvisitor']; - $sqlVisitorId = "$select , - 1 as priority - $from - WHERE $whereCommon $where - ORDER BY visit_last_action_time DESC - LIMIT 1 - "; - - // We join both queries and favor the one matching the visitor_id if it did match - $sql = " ( $sqlConfigId ) - UNION - ( $sqlVisitorId ) - ORDER BY priority DESC - LIMIT 1"; - } - - $visitRow = Tracker::getDatabase()->fetch($sql, $bindSql); - - $isNewVisitForced = $this->request->getParam('new_visit'); - $isNewVisitForced = !empty($isNewVisitForced); - $newVisitEnforcedAPI = $isNewVisitForced - && ($this->request->isAuthenticated() - || !Config::getInstance()->Tracker['new_visit_api_requires_admin']); - $enforceNewVisit = $newVisitEnforcedAPI || Config::getInstance()->Debug['tracker_always_new_visitor']; - - if (!$enforceNewVisit - && $visitRow - && count($visitRow) > 0 - ) { - // These values will be used throughout the request - $this->visitorInfo['visit_last_action_time'] = strtotime($visitRow['visit_last_action_time']); - $this->visitorInfo['visit_first_action_time'] = strtotime($visitRow['visit_first_action_time']); - - foreach($persistedVisitAttributes as $field) { - $this->visitorInfo[$field] = $visitRow[$field]; - } - - // Custom Variables copied from Visit in potential later conversion - if (!empty($selectCustomVariables)) { - $maxCustomVariables = CustomVariables::getMaxCustomVariables(); - for ($i = 1; $i <= $maxCustomVariables; $i++) { - if (isset($visitRow['custom_var_k' . $i]) - && strlen($visitRow['custom_var_k' . $i]) - ) { - $this->visitorInfo['custom_var_k' . $i] = $visitRow['custom_var_k' . $i]; - } - if (isset($visitRow['custom_var_v' . $i]) - && strlen($visitRow['custom_var_v' . $i]) - ) { - $this->visitorInfo['custom_var_v' . $i] = $visitRow['custom_var_v' . $i]; - } - } - } - - $this->visitorKnown = true; - Common::printDebug("The visitor is known (idvisitor = " . bin2hex($this->visitorInfo['idvisitor']) . ", - config_id = " . bin2hex($configId) . ", - idvisit = {$this->visitorInfo['idvisit']}, - last action = " . date("r", $this->visitorInfo['visit_last_action_time']) . ", - first action = " . date("r", $this->visitorInfo['visit_first_action_time']) . ", - visit_goal_buyer' = " . $this->visitorInfo['visit_goal_buyer'] . ")"); - //Common::printDebug($this->visitorInfo); - } else { - Common::printDebug("The visitor was not matched with an existing visitor..."); - } - } - - /** - * By default, we look back 30 minutes to find a previous visitor (for performance reasons). - * In some cases, it is useful to look back and count unique visitors more accurately. You can set custom lookback window in - * [Tracker] window_look_back_for_visitor - * - * The returned value is the window range (Min, max) that the matched visitor should fall within - * - * @return array( datetimeMin, datetimeMax ) - */ - protected function getWindowLookupThisVisit() - { - $visitStandardLength = Config::getInstance()->Tracker['visit_standard_length']; - $lookBackNSecondsCustom = Config::getInstance()->Tracker['window_look_back_for_visitor']; - - $lookAheadNSeconds = $visitStandardLength; - $lookBackNSeconds = $visitStandardLength; - if ($lookBackNSecondsCustom > $lookBackNSeconds) { - $lookBackNSeconds = $lookBackNSecondsCustom; - } - - $timeLookBack = date('Y-m-d H:i:s', $this->request->getCurrentTimestamp() - $lookBackNSeconds); - $timeLookAhead = date('Y-m-d H:i:s', $this->request->getCurrentTimestamp() + $lookAheadNSeconds); - - return array($timeLookBack, $timeLookAhead); - } - - protected function shouldLookupOneVisitorFieldOnly($isVisitorIdToLookup) - { - // This setting would be enabled for Intranet websites, to ensure that visitors using all the same computer config, same IP - // are not counted as 1 visitor. In this case, we want to enforce and trust the visitor ID from the cookie. - $trustCookiesOnly = Config::getInstance()->Tracker['trust_visitors_cookies']; - - // If a &cid= was set, we force to select this visitor (or create a new one) - $isForcedVisitorIdMustMatch = ($this->request->getForcedVisitorId() != null); - - $shouldMatchOneFieldOnly = (($isVisitorIdToLookup && $trustCookiesOnly) - || $isForcedVisitorIdMustMatch - || !$isVisitorIdToLookup); - return $shouldMatchOneFieldOnly; - } - - /** - * Gets the UserSettings information and returns them in an array of name => value - * - * @return array - */ - protected function getUserSettingsInformation() - { - // we already called this method before, simply returns the result - if (is_array($this->userSettingsInformation)) { - return $this->userSettingsInformation; - } - - list($plugin_Flash, $plugin_Java, $plugin_Director, $plugin_Quicktime, $plugin_RealPlayer, $plugin_PDF, - $plugin_WindowsMedia, $plugin_Gears, $plugin_Silverlight, $plugin_Cookie) = $this->request->getPlugins(); - - $resolution = $this->request->getParam('res'); - $userAgent = $this->request->getUserAgent(); - - $deviceDetector = new DeviceDetector($userAgent); - $deviceDetector->parse(); - $aBrowserInfo = $deviceDetector->getBrowser(); - - $browserName = !empty($aBrowserInfo['short_name']) ? $aBrowserInfo['short_name'] : 'UNK'; - $browserVersion = !empty($aBrowserInfo['version']) ? $aBrowserInfo['version'] : ''; - - $os = $deviceDetector->getOS(); - $os = empty($os['short_name']) ? 'UNK' : $os['short_name']; - - $browserLang = substr($this->request->getBrowserLanguage(), 0, 20); // limit the length of this string to match db - $configurationHash = $this->getConfigHash( - $os, - $browserName, - $browserVersion, - $plugin_Flash, - $plugin_Java, - $plugin_Director, - $plugin_Quicktime, - $plugin_RealPlayer, - $plugin_PDF, - $plugin_WindowsMedia, - $plugin_Gears, - $plugin_Silverlight, - $plugin_Cookie, - $this->getVisitorIp(), - $browserLang); - - $this->userSettingsInformation = array( - 'config_id' => $configurationHash, - 'config_os' => $os, - 'config_browser_name' => $browserName, - 'config_browser_version' => $browserVersion, - 'config_resolution' => $resolution, - 'config_pdf' => $plugin_PDF, - 'config_flash' => $plugin_Flash, - 'config_java' => $plugin_Java, - 'config_director' => $plugin_Director, - 'config_quicktime' => $plugin_Quicktime, - 'config_realplayer' => $plugin_RealPlayer, - 'config_windowsmedia' => $plugin_WindowsMedia, - 'config_gears' => $plugin_Gears, - 'config_silverlight' => $plugin_Silverlight, - 'config_cookie' => $plugin_Cookie, - 'location_browser_lang' => $browserLang, - ); - return $this->userSettingsInformation; - } - - /** - * Returns true if the last action was done during the last 30 minutes - * @return bool - */ - protected function isLastActionInTheSameVisit() - { - return isset($this->visitorInfo['visit_last_action_time']) - && ($this->visitorInfo['visit_last_action_time'] - > ($this->request->getCurrentTimestamp() - Config::getInstance()->Tracker['visit_standard_length'])); - } - - /** - * Returns true if the recognizeTheVisitor() method did recognize the visitor - * @return bool - */ - protected function isVisitorKnown() - { - return $this->visitorKnown === true; - } - - /** - * Returns a 64-bit hash of all the configuration settings - * @param $os - * @param $browserName - * @param $browserVersion - * @param $plugin_Flash - * @param $plugin_Java - * @param $plugin_Director - * @param $plugin_Quicktime - * @param $plugin_RealPlayer - * @param $plugin_PDF - * @param $plugin_WindowsMedia - * @param $plugin_Gears - * @param $plugin_Silverlight - * @param $plugin_Cookie - * @param $ip - * @param $browserLang - * @return string - */ - protected function getConfigHash($os, $browserName, $browserVersion, $plugin_Flash, $plugin_Java, $plugin_Director, $plugin_Quicktime, $plugin_RealPlayer, $plugin_PDF, $plugin_WindowsMedia, $plugin_Gears, $plugin_Silverlight, $plugin_Cookie, $ip, $browserLang) - { - $hash = md5($os . $browserName . $browserVersion . $plugin_Flash . $plugin_Java . $plugin_Director . $plugin_Quicktime . $plugin_RealPlayer . $plugin_PDF . $plugin_WindowsMedia . $plugin_Gears . $plugin_Silverlight . $plugin_Cookie . $ip . $browserLang, $raw_output = true); - return substr($hash, 0, Tracker::LENGTH_BINARY_ID); - } - - /** - * Returns either - * - "-1" for a known visitor - * - at least 16 char identifier in hex @see Common::generateUniqId() - * @return int|string - */ - protected function getVisitorUniqueId() - { - if ($this->isVisitorKnown()) { - return -1; - } - return Common::generateUniqId(); + return $this->userSettings; } // is the referrer host any of the registered URLs for this website? - static public function isHostKnownAliasHost($urlHost, $idSite) + public static function isHostKnownAliasHost($urlHost, $idSite) { $websiteData = Cache::getCacheWebsiteAttributes($idSite); + if (isset($websiteData['hosts'])) { $canonicalHosts = array(); foreach ($websiteData['hosts'] as $host) { - $canonicalHosts[] = str_replace('www.', '', mb_strtolower($host, 'UTF-8')); + $canonicalHosts[] = self::toCanonicalHost($host); } - $canonicalHost = str_replace('www.', '', mb_strtolower($urlHost, 'UTF-8')); + + $canonicalHost = self::toCanonicalHost($urlHost); if (in_array($canonicalHost, $canonicalHosts)) { return true; } } + return false; } - /** - * @return mixed - */ - protected function insertNewVisit($visit) + private static function toCanonicalHost($host) { - $fields = implode(", ", array_keys($visit)); - $values = Common::getSqlStringFieldsArray($visit); - - $sql = "INSERT INTO " . Common::prefixTable('log_visit') . " ($fields) VALUES ($values)"; - $bind = array_values($visit); - Tracker::getDatabase()->query($sql, $bind); - - $idVisit = Tracker::getDatabase()->lastInsertId(); - return $idVisit; + $hostLower = Common::mb_strtolower($host); + return str_replace('www.', '', $hostLower); } /** @@ -747,264 +397,208 @@ class Visit implements VisitInterface */ protected function updateExistingVisit($valuesToUpdate) { - $sqlQuery = "UPDATE " . Common::prefixTable('log_visit') . " - SET %s - WHERE idsite = ? - AND idvisit = ?"; - // build sql query - $updateParts = $sqlBind = array(); - foreach ($valuesToUpdate AS $name => $value) { - // Case where bind parameters don't work - if(strpos($value, $name) !== false) { - //$name = 'visit_total_events' - //$value = 'visit_total_events + 1'; - $updateParts[] = " $name = $value "; - } else { - $updateParts[] = $name . " = ?"; - $sqlBind[] = $value; - } - } - $sqlQuery = sprintf($sqlQuery, implode($updateParts, ', ') ); - array_push($sqlBind, $this->request->getIdSite(), (int)$this->visitorInfo['idvisit']); + $idSite = $this->request->getIdSite(); + $idVisit = (int)$this->visitProperties->getProperty('idvisit'); - $result = Tracker::getDatabase()->query($sqlQuery, $sqlBind); - - $this->visitorInfo['visit_last_action_time'] = $this->request->getCurrentTimestamp(); + $wasInserted = $this->getModel()->updateVisit($idSite, $idVisit, $valuesToUpdate); // Debug output if (isset($valuesToUpdate['idvisitor'])) { $valuesToUpdate['idvisitor'] = bin2hex($valuesToUpdate['idvisitor']); } - Common::printDebug('Updating existing visit: ' . var_export($valuesToUpdate, true)); - if (Tracker::getDatabase()->rowCount($result) == 0) { - Common::printDebug("Visitor with this idvisit wasn't found in the DB."); - Common::printDebug("$sqlQuery --- "); - Common::printDebug($sqlBind); + if ($wasInserted) { + Common::printDebug('Updated existing visit: ' . var_export($valuesToUpdate, true)); + } else { throw new VisitorNotFoundInDb( - "The visitor with idvisitor=" . bin2hex($this->visitorInfo['idvisitor']) . " and idvisit=" . $this->visitorInfo['idvisit'] + "The visitor with idvisitor=" . bin2hex($this->visitProperties->getProperty('idvisitor')) + . " and idvisit=" . @$this->visitProperties->getProperty('idvisit') . " wasn't found in the DB, we fallback to a new visitor"); } } - protected function printVisitorInformation() + private function printVisitorInformation() { - $debugVisitInfo = $this->visitorInfo; + $debugVisitInfo = $this->visitProperties->getProperties(); $debugVisitInfo['idvisitor'] = bin2hex($debugVisitInfo['idvisitor']); $debugVisitInfo['config_id'] = bin2hex($debugVisitInfo['config_id']); + $debugVisitInfo['location_ip'] = IPUtils::binaryToStringIP($debugVisitInfo['location_ip']); Common::printDebug($debugVisitInfo); } - protected function getNewVisitorInformation($action) + private function setNewVisitorInformation() { - $actionType = $idActionName = $idActionUrl = false; - if($action) { - $idActionUrl = $action->getIdActionUrlForEntryAndExitIds(); - $idActionName = $action->getIdActionNameForEntryAndExitIds(); - $actionType = $action->getActionType(); - } + $idVisitor = $this->getVisitorIdcookie(); + $visitorIp = $this->getVisitorIp(); + $configId = $this->request->getMetadata('CoreHome', 'visitorId'); - $daysSinceFirstVisit = $this->request->getDaysSinceFirstVisit(); - $visitCount = $this->request->getVisitCount(); - $daysSinceLastVisit = $this->request->getDaysSinceLastVisit(); + $this->visitProperties->clearProperties(); - $daysSinceLastOrder = $this->request->getDaysSinceLastOrder(); - $isReturningCustomer = ($daysSinceLastOrder !== false); - - if ($daysSinceLastOrder === false) { - $daysSinceLastOrder = 0; - } - - // User settings - $userInfo = $this->getUserSettingsInformation(); - - // Referrer data - $referrer = new Referrer(); - $referrerUrl = $this->request->getParam('urlref'); - $currentUrl = $this->request->getParam('url'); - $referrerInfo = $referrer->getReferrerInformation($referrerUrl, $currentUrl, $this->request->getIdSite()); - - $visitorReturning = $isReturningCustomer - ? 2 /* Returning customer */ - : ($visitCount > 1 || $this->isVisitorKnown() || $daysSinceLastVisit > 0 - ? 1 /* Returning */ - : 0 /* New */); - $defaultTimeOnePageVisit = Config::getInstance()->Tracker['default_time_one_page_visit']; - - return array( - 'idsite' => $this->request->getIdSite(), - 'visitor_localtime' => $this->request->getLocalTime(), - 'idvisitor' => $this->getVisitorIdcookie(), - 'visitor_returning' => $visitorReturning, - 'visitor_count_visits' => $visitCount, - 'visitor_days_since_last' => $daysSinceLastVisit, - 'visitor_days_since_order' => $daysSinceLastOrder, - 'visitor_days_since_first' => $daysSinceFirstVisit, - 'visit_first_action_time' => Tracker::getDatetimeFromTimestamp($this->request->getCurrentTimestamp()), - 'visit_last_action_time' => Tracker::getDatetimeFromTimestamp($this->request->getCurrentTimestamp()), - 'visit_entry_idaction_url' => (int)$idActionUrl, - 'visit_entry_idaction_name' => (int)$idActionName, - 'visit_exit_idaction_url' => (int)$idActionUrl, - 'visit_exit_idaction_name' => (int)$idActionName, - 'visit_total_actions' => in_array($actionType, - array(Action::TYPE_PAGE_URL, - Action::TYPE_DOWNLOAD, - Action::TYPE_OUTLINK, - Action::TYPE_SITE_SEARCH, - Action::TYPE_EVENT)) - ? 1 : 0, // if visit starts with something else (e.g. ecommerce order), don't record as an action - 'visit_total_searches' => $actionType == Action::TYPE_SITE_SEARCH ? 1 : 0, - 'visit_total_events' => $actionType == Action::TYPE_EVENT ? 1 : 0, - 'visit_total_time' => self::cleanupVisitTotalTime($defaultTimeOnePageVisit), - 'visit_goal_buyer' => $this->goalManager->getBuyerType(), - 'referer_type' => $referrerInfo['referer_type'], - 'referer_name' => $referrerInfo['referer_name'], - 'referer_url' => $referrerInfo['referer_url'], - 'referer_keyword' => $referrerInfo['referer_keyword'], - 'config_id' => $userInfo['config_id'], - 'config_os' => $userInfo['config_os'], - 'config_browser_name' => $userInfo['config_browser_name'], - 'config_browser_version' => $userInfo['config_browser_version'], - 'config_resolution' => $userInfo['config_resolution'], - 'config_pdf' => $userInfo['config_pdf'], - 'config_flash' => $userInfo['config_flash'], - 'config_java' => $userInfo['config_java'], - 'config_director' => $userInfo['config_director'], - 'config_quicktime' => $userInfo['config_quicktime'], - 'config_realplayer' => $userInfo['config_realplayer'], - 'config_windowsmedia' => $userInfo['config_windowsmedia'], - 'config_gears' => $userInfo['config_gears'], - 'config_silverlight' => $userInfo['config_silverlight'], - 'config_cookie' => $userInfo['config_cookie'], - 'location_ip' => $this->getVisitorIp(), - 'location_browser_lang' => $userInfo['location_browser_lang'], - ); + $this->visitProperties->setProperty('idvisitor', $idVisitor); + $this->visitProperties->setProperty('config_id', $configId); + $this->visitProperties->setProperty('location_ip', $visitorIp); } /** * Gather fields=>values that needs to be updated for the existing visit in log_visit * - * @param $action * @param $visitIsConverted * @return array */ - protected function getExistingVisitFieldsToUpdate($action, $visitIsConverted) + private function getExistingVisitFieldsToUpdate($visitIsConverted) { $valuesToUpdate = array(); - if ($action) { - $idActionUrl = $action->getIdActionUrlForEntryAndExitIds(); - $idActionName = $action->getIdActionNameForEntryAndExitIds(); - $actionType = $action->getActionType(); + $valuesToUpdate = $this->setIdVisitorForExistingVisit($valuesToUpdate); - if ($idActionName !== false) { - $valuesToUpdate['visit_exit_idaction_name'] = $idActionName; - } + $dimensions = $this->getAllVisitDimensions(); + $valuesToUpdate = $this->triggerHookOnDimensions($dimensions, 'onExistingVisit', $valuesToUpdate); - $incrementActions = false; - if ($idActionUrl !== false) { - $valuesToUpdate['visit_exit_idaction_url'] = $idActionUrl; - $incrementActions = true; - } - if ($actionType == Action::TYPE_SITE_SEARCH) { - $valuesToUpdate['visit_total_searches'] = 'visit_total_searches + 1'; - $incrementActions = true; - } else if ($actionType == Action::TYPE_EVENT) { - $valuesToUpdate['visit_total_events'] = 'visit_total_events + 1'; - $incrementActions = true; - } - - if ($incrementActions) { - $valuesToUpdate['visit_total_actions'] = 'visit_total_actions + 1'; - } - } - - $datetimeServer = Tracker::getDatetimeFromTimestamp($this->request->getCurrentTimestamp()); - $valuesToUpdate['visit_last_action_time'] = $datetimeServer; - - // Add 1 so it's always > 0 - $visitTotalTime = 1 + $this->request->getCurrentTimestamp() - $this->visitorInfo['visit_first_action_time']; - $valuesToUpdate['visit_total_time'] = self::cleanupVisitTotalTime($visitTotalTime); - - // Goal conversion if ($visitIsConverted) { - $valuesToUpdate['visit_goal_converted'] = 1; - // If a pageview and goal conversion in the same second, with previously a goal conversion recorded - // the request would not "update" the row since all values are the same as previous - // therefore the request below throws exception, instead we make sure the UPDATE will affect the row - $valuesToUpdate['visit_total_time'] = self::cleanupVisitTotalTime( - $valuesToUpdate['visit_total_time'] - + $this->goalManager->idGoal - // +2 to offset idgoal=-1 and idgoal=0 - + 2); - } - - // Might update the idvisitor when it was forced or overwritten for this visit - if (strlen($this->visitorInfo['idvisitor']) == Tracker::LENGTH_BINARY_ID) { - $valuesToUpdate['idvisitor'] = $this->visitorInfo['idvisitor']; - } - - // Ecommerce buyer status - $visitEcommerceStatus = $this->goalManager->getBuyerType($this->visitorInfo['visit_goal_buyer']); - - if($visitEcommerceStatus != GoalManager::TYPE_BUYER_NONE - // only update if the value has changed (prevents overwriting the value in case a request has updated it in the meantime) - && $visitEcommerceStatus != $this->visitorInfo['visit_goal_buyer']) { - $valuesToUpdate['visit_goal_buyer'] = $visitEcommerceStatus; + $valuesToUpdate = $this->triggerHookOnDimensions($dimensions, 'onConvertedVisit', $valuesToUpdate); } // Custom Variables overwrite previous values on each page view - $valuesToUpdate = array_merge($valuesToUpdate, $this->visitorCustomVariables); return $valuesToUpdate; } /** - * @return array + * @param VisitDimension[] $dimensions + * @param string $hook + * @param Visitor $visitor + * @param Action|null $action + * @param array|null $valuesToUpdate If null, $this->visitorInfo will be updated + * + * @return array|null The updated $valuesToUpdate or null if no $valuesToUpdate given */ - public static function getVisitFieldsPersist() + private function triggerHookOnDimensions($dimensions, $hook, $valuesToUpdate = null) { - $fields = array( - 'idvisitor', - 'idvisit', - 'visit_exit_idaction_url', - 'visit_exit_idaction_name', - 'visitor_returning', - 'visitor_days_since_first', - 'visitor_days_since_order', - 'visitor_count_visits', - 'visit_goal_buyer', + $visitor = $this->makeVisitorFacade(); - 'location_country', - 'location_region', - 'location_city', - 'location_latitude', - 'location_longitude', + /** @var Action $action */ + $action = $this->request->getMetadata('Actions', 'action'); - 'referer_name', - 'referer_keyword', - 'referer_type', - ); + foreach ($dimensions as $dimension) { + $value = $dimension->$hook($this->request, $visitor, $action); - /** - * Triggered when checking if the current action being tracked belongs to an existing visit. - * - * This event collects a list of [visit entity]() properties that should be loaded when reading - * the existing visit. Properties that appear in this list will be available in other tracking - * events such as {@hook Tracker.newConversionInformation} and {@hook Tracker.newVisitorInformation}. - * - * Plugins can use this event to load additional visit entity properties for later use during tracking. - * When you add fields to this $fields array, they will be later available in Tracker.newConversionInformation - * - * **Example** - * - * Piwik::addAction('Tracker.getVisitFieldsToPersist', function (&$fields) { - * $fields[] = 'custom_visit_property'; - * }); - * - * @param array &$fields The list of visit properties to load. - */ - Piwik::postEvent('Tracker.getVisitFieldsToPersist', array(&$fields)); + if ($value !== false) { + $fieldName = $dimension->getColumnName(); + $visitor->setVisitorColumn($fieldName, $value); - return $fields; + if (is_float($value)) { + $value = Common::forceDotAsSeparatorForDecimalPoint($value); + } + + if ($valuesToUpdate !== null) { + $valuesToUpdate[$fieldName] = $value; + } else { + $this->visitProperties->setProperty($fieldName, $value); + } + } + } + + return $valuesToUpdate; + } + + private function triggerPredicateHookOnDimensions($dimensions, $hook) + { + $visitor = $this->makeVisitorFacade(); + + /** @var Action $action */ + $action = $this->request->getMetadata('Actions', 'action'); + + foreach ($dimensions as $dimension) { + if ($dimension->$hook($this->request, $visitor, $action)) { + return true; + } + } + return false; + } + + protected function getAllVisitDimensions() + { + if (is_null(self::$dimensions)) { + self::$dimensions = VisitDimension::getAllDimensions(); + + $dimensionNames = array(); + foreach (self::$dimensions as $dimension) { + $dimensionNames[] = $dimension->getColumnName(); + } + + Common::printDebug("Following dimensions have been collected from plugins: " . implode(", ", + $dimensionNames)); + } + + return self::$dimensions; + } + + private function getVisitStandardLength() + { + return Config::getInstance()->Tracker['visit_standard_length']; + } + + /** + * @param $visitor + * @param $valuesToUpdate + * @return mixed + */ + private function setIdVisitorForExistingVisit($valuesToUpdate) + { + // Might update the idvisitor when it was forced or overwritten for this visit + if (strlen($this->visitProperties->getProperty('idvisitor')) == Tracker::LENGTH_BINARY_ID) { + $binIdVisitor = $this->visitProperties->getProperty('idvisitor'); + $valuesToUpdate['idvisitor'] = $binIdVisitor; + } + + // User ID takes precedence and overwrites idvisitor value + $userId = $this->request->getForcedUserId(); + if ($userId) { + $userIdHash = $this->request->getUserIdHashed($userId); + $binIdVisitor = Common::hex2bin($userIdHash); + $this->visitProperties->setProperty('idvisitor', $binIdVisitor); + $valuesToUpdate['idvisitor'] = $binIdVisitor; + } + return $valuesToUpdate; + } + + protected function insertNewVisit($visit) + { + return $this->getModel()->createVisit($visit); + } + + private function markArchivedReportsAsInvalidIfArchiveAlreadyFinished() + { + $idSite = (int)$this->request->getIdSite(); + $time = $this->request->getCurrentTimestamp(); + + $timezone = $this->getTimezoneForSite($idSite); + + if (!isset($timezone)) { + return; + } + + $date = Date::factory((int)$time, $timezone); + + if (!$date->isToday()) { // we don't have to handle in case date is in future as it is not allowed by tracker + $this->invalidator->rememberToInvalidateArchivedReportsLater($idSite, $date); + } + } + + private function getTimezoneForSite($idSite) + { + try { + $site = Cache::getCacheWebsiteAttributes($idSite); + } catch (UnexpectedWebsiteFoundException $e) { + return null; + } + + if (!empty($site['timezone'])) { + return $site['timezone']; + } + } + + private function makeVisitorFacade() + { + return Visitor::makeFromVisitProperties($this->visitProperties, $this->request); } } diff --git a/www/analytics/core/Tracker/Visit/Factory.php b/www/analytics/core/Tracker/Visit/Factory.php new file mode 100644 index 00000000..9dbb8632 --- /dev/null +++ b/www/analytics/core/Tracker/Visit/Factory.php @@ -0,0 +1,48 @@ +getSpammerListFromCache(); + + $referrerUrl = $request->getParam('urlref'); + + foreach ($spammers as $spammerHost) { + if (stripos($referrerUrl, $spammerHost) !== false) { + Common::printDebug('Referrer URL is a known spam: ' . $spammerHost); + return true; + } + } + + return false; + } + + private function getSpammerListFromCache() + { + $cache = Cache::getEagerCache(); + $cacheId = 'ReferrerSpamFilter-' . self::OPTION_STORAGE_NAME; + + if ($cache->contains($cacheId)) { + $list = $cache->fetch($cacheId); + } else { + $list = $this->loadSpammerList(); + $cache->save($cacheId, $list); + } + + if(!is_array($list)) { + Common::printDebug('Warning: could not read list of spammers from cache.'); + return array(); + } + return $list; + } + + private function loadSpammerList() + { + if ($this->spammerList !== null) { + return $this->spammerList; + } + + // Read first from the auto-updated list in database + $list = Option::get(self::OPTION_STORAGE_NAME); + + if ($list) { + $this->spammerList = unserialize($list); + } else { + // Fallback to reading the bundled list + $file = PIWIK_VENDOR_PATH . '/piwik/referrer-spam-blacklist/spammers.txt'; + $this->spammerList = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + } + + return $this->spammerList; + } +} diff --git a/www/analytics/core/Tracker/Visit/VisitProperties.php b/www/analytics/core/Tracker/Visit/VisitProperties.php new file mode 100644 index 00000000..567d9bd0 --- /dev/null +++ b/www/analytics/core/Tracker/Visit/VisitProperties.php @@ -0,0 +1,73 @@ +visitInfo[$name]) ? $this->visitInfo[$name] : null; + } + + /** + * Returns all visit properties by reference. + * + * @return array + */ + public function &getProperties() + { + return $this->visitInfo; + } + + /** + * Sets a visit property. + * + * @param string $name The property name. + * @param mixed $value The property value. + */ + public function setProperty($name, $value) + { + $this->visitInfo[$name] = $value; + } + + /** + * Unsets all visit properties. + */ + public function clearProperties() + { + $this->visitInfo = array(); + } + + /** + * Sets all visit properties. + * + * @param array $properties + */ + public function setProperties($properties) + { + $this->visitInfo = $properties; + } +} diff --git a/www/analytics/core/Tracker/VisitExcluded.php b/www/analytics/core/Tracker/VisitExcluded.php index 25d05250..a644d447 100644 --- a/www/analytics/core/Tracker/VisitExcluded.php +++ b/www/analytics/core/Tracker/VisitExcluded.php @@ -1,6 +1,6 @@ spamFilter = new ReferrerSpamFilter(); + + if (false === $ip) { $ip = $request->getIp(); } - if ($userAgent === false) { + if (false === $userAgent) { $userAgent = $request->getUserAgent(); } - $this->request = $request; - $this->idSite = $request->getIdSite(); + $this->request = $request; + $this->idSite = $request->getIdSite(); $this->userAgent = $userAgent; $this->ip = $ip; } @@ -72,9 +83,9 @@ class VisitExcluded /** * Triggered on every tracking request. - * + * * This event can be used to tell the Tracker not to record this particular action or visit. - * + * * @param bool &$excluded Whether the request should be excluded or not. Initialized * to `false`. Event subscribers should set it to `true` in * order to exclude the request. @@ -110,6 +121,22 @@ class VisitExcluded } } + // Check if Referrer URL is a known spam + if (!$excluded) { + $excluded = $this->isReferrerSpamExcluded(); + if ($excluded) { + Common::printDebug("Referrer URL is blacklisted as spam."); + } + } + + // Check if request URL is excluded + if (!$excluded) { + $excluded = $this->isUrlExcluded(); + if ($excluded) { + Common::printDebug("Unknown URL is not allowed to track."); + } + } + if (!$excluded) { if ($this->isPrefetchDetected()) { $excluded = true; @@ -138,37 +165,53 @@ class VisitExcluded * As a result, these sophisticated bots exhibit characteristics of * browsers (cookies enabled, executing JavaScript, etc). * + * @see \DeviceDetector\Parser\Bot + * * @return boolean */ protected function isNonHumanBot() { $allowBots = $this->request->getParam('bots'); - return !$allowBots - // Seen in the wild - && (strpos($this->userAgent, 'Googlebot') !== false // Googlebot - || strpos($this->userAgent, 'Google Web Preview') !== false // Google Instant - || strpos($this->userAgent, 'AdsBot-Google') !== false // Google Adwords landing pages - || strpos($this->userAgent, 'Google Page Speed Insights') !== false // #4049 - || strpos($this->userAgent, 'Google (+https://developers.google.com') !== false // Google Snippet https://developers.google.com/+/web/snippet/ - || strpos($this->userAgent, 'facebookexternalhit') !== false // http://www.facebook.com/externalhit_uatext.php - || strpos($this->userAgent, 'baidu') !== false // Baidu - || strpos($this->userAgent, 'bingbot') !== false // Bingbot - || strpos($this->userAgent, 'YottaaMonitor') !== false // Yottaa - || strpos($this->userAgent, 'CloudFlare') !== false // CloudFlare-AlwaysOnline + $deviceDetector = DeviceDetectorFactory::getInstance($this->userAgent); - // Added as they are popular bots - || strpos($this->userAgent, 'pingdom') !== false // pingdom - || strpos($this->userAgent, 'yandex') !== false // yandex - || strpos($this->userAgent, 'exabot') !== false // Exabot - || strpos($this->userAgent, 'sogou') !== false // Sogou - || strpos($this->userAgent, 'soso') !== false // Soso - || IP::isIpInRange($this->ip, $this->getBotIpRanges())); + return !$allowBots + && ($deviceDetector->isBot() || $this->isIpInRange()); } - protected function getBotIpRanges() + private function isIpInRange() { - return array( + $cache = PiwikCache::getTransientCache(); + + $ip = IP::fromBinaryIP($this->ip); + $key = 'VisitExcludedIsIpInRange' . $ip->toString(); + + if ($cache->contains($key)) { + $isInRanges = $cache->fetch($key); + } else { + if ($this->isChromeDataSaverUsed($ip)) { + $isInRanges = false; + } else { + $isInRanges = $ip->isInRanges($this->getBotIpRanges()); + } + + $cache->save($key, $isInRanges); + } + + return $isInRanges; + } + + private function isChromeDataSaverUsed(IP $ip) + { + // see https://github.com/piwik/piwik/issues/7733 + return !empty($_SERVER['HTTP_VIA']) + && false !== strpos(strtolower($_SERVER['HTTP_VIA']), 'chrome-compression-proxy') + && $ip->isInRanges($this->getGoogleBotIpRanges()); + } + + protected function getBotIpRanges() + { + return array_merge($this->getGoogleBotIpRanges(), array( // Live/Bing/MSN '64.4.0.0/18', '65.52.0.0/14', @@ -180,12 +223,29 @@ class VisitExcluded '207.68.192.0/20', '131.253.26.0/20', '131.253.24.0/20', + // Yahoo '72.30.198.0/20', '72.30.196.0/20', '98.137.207.0/20', // Chinese bot hammering websites '1.202.218.8' + )); + } + + private function getGoogleBotIpRanges() + { + return array( + '216.239.32.0/19', + '64.233.160.0/19', + '66.249.80.0/20', + '72.14.192.0/18', + '209.85.128.0/17', + '66.102.0.0/20', + '74.125.0.0/16', + '64.18.0.0/20', + '207.126.144.0/20', + '173.194.0.0/16' ); } @@ -199,6 +259,7 @@ class VisitExcluded Common::printDebug('Piwik ignore cookie was found, visit not tracked.'); return true; } + return false; } @@ -210,12 +271,39 @@ class VisitExcluded protected function isVisitorIpExcluded() { $websiteAttributes = Cache::getCacheWebsiteAttributes($this->idSite); + if (!empty($websiteAttributes['excluded_ips'])) { - if (IP::isIpInRange($this->ip, $websiteAttributes['excluded_ips'])) { - Common::printDebug('Visitor IP ' . IP::N2P($this->ip) . ' is excluded from being tracked'); + $ip = IP::fromBinaryIP($this->ip); + if ($ip->isInRanges($websiteAttributes['excluded_ips'])) { + Common::printDebug('Visitor IP ' . $ip->toString() . ' is excluded from being tracked'); return true; } } + + return false; + } + + /** + * Checks if request URL is excluded + * @return bool + */ + protected function isUrlExcluded() + { + $site = Cache::getCacheWebsiteAttributes($this->idSite); + + if (!empty($site['exclude_unknown_urls']) && !empty($site['urls'])) { + $url = $this->request->getParam('url'); + $parsedUrl = parse_url($url); + + $trackingUrl = new SiteUrls(); + $urls = $trackingUrl->groupUrlsByHost(array($this->idSite => $site['urls'])); + + $idSites = $trackingUrl->getIdSitesMatchingUrl($parsedUrl, $urls); + $isUrlExcluded = !isset($idSites) || !in_array($this->idSite, $idSites); + + return $isUrlExcluded; + } + return false; } @@ -231,6 +319,7 @@ class VisitExcluded protected function isUserAgentExcluded() { $websiteAttributes = Cache::getCacheWebsiteAttributes($this->idSite); + if (!empty($websiteAttributes['excluded_user_agents'])) { foreach ($websiteAttributes['excluded_user_agents'] as $excludedUserAgent) { // if the excluded user agent string part is in this visit's user agent, this visit should be excluded @@ -239,6 +328,17 @@ class VisitExcluded } } } + return false; } + + /** + * Returns true if the Referrer is a known spammer. + * + * @return bool + */ + protected function isReferrerSpamExcluded() + { + return $this->spamFilter->isSpam($this->request); + } } diff --git a/www/analytics/core/Tracker/VisitInterface.php b/www/analytics/core/Tracker/VisitInterface.php index 250d5565..a7022072 100644 --- a/www/analytics/core/Tracker/VisitInterface.php +++ b/www/analytics/core/Tracker/VisitInterface.php @@ -1,6 +1,6 @@ visitProperties = $visitProperties; + $this->setIsVisitorKnown($isVisitorKnown); + } + + public static function makeFromVisitProperties(VisitProperties $visitProperties, Request $request) + { + $isKnown = $request->getMetadata('CoreHome', 'isVisitorKnown'); + return new Visitor($visitProperties, $isKnown); + } + + public function setVisitorColumn($column, $value) + { + $this->visitProperties->setProperty($column, $value); + } + + public function getVisitorColumn($column) + { + if (array_key_exists($column, $this->visitProperties->getProperties())) { + return $this->visitProperties->getProperty($column); + } + + return false; + } + + public function isVisitorKnown() + { + return $this->visitorKnown === true; + } + + public function isNewVisit() + { + return !$this->isVisitorKnown(); + } + + private function setIsVisitorKnown($isVisitorKnown) + { + return $this->visitorKnown = $isVisitorKnown; + } +} \ No newline at end of file diff --git a/www/analytics/core/Tracker/VisitorNotFoundInDb.php b/www/analytics/core/Tracker/VisitorNotFoundInDb.php index e3dfce50..5cbf7919 100644 --- a/www/analytics/core/Tracker/VisitorNotFoundInDb.php +++ b/www/analytics/core/Tracker/VisitorNotFoundInDb.php @@ -1,6 +1,6 @@ trustCookiesOnly = $trustCookiesOnly; + $this->visitStandardLength = $visitStandardLength; + $this->lookBackNSecondsCustom = $lookbackNSecondsCustom; + $this->trackerAlwaysNewVisitor = $trackerAlwaysNewVisitor; + + $this->model = $model; + $this->eventDispatcher = $eventDispatcher; + } + + public function findKnownVisitor($configId, VisitProperties $visitProperties, Request $request) + { + $idSite = $request->getIdSite(); + $idVisitor = $request->getVisitorId(); + + $isVisitorIdToLookup = !empty($idVisitor); + + if ($isVisitorIdToLookup) { + $visitProperties->setProperty('idvisitor', $idVisitor); + Common::printDebug("Matching visitors with: visitorId=" . bin2hex($idVisitor) . " OR configId=" . bin2hex($configId)); + } else { + Common::printDebug("Visitor doesn't have the piwik cookie..."); + } + + $persistedVisitAttributes = $this->getVisitFieldsPersist(); + + $shouldMatchOneFieldOnly = $this->shouldLookupOneVisitorFieldOnly($isVisitorIdToLookup, $request); + list($timeLookBack, $timeLookAhead) = $this->getWindowLookupThisVisit($request); + + $visitRow = $this->model->findVisitor($idSite, $configId, $idVisitor, $persistedVisitAttributes, $shouldMatchOneFieldOnly, $isVisitorIdToLookup, $timeLookBack, $timeLookAhead); + + $isNewVisitForced = $request->getParam('new_visit'); + $isNewVisitForced = !empty($isNewVisitForced); + $enforceNewVisit = $isNewVisitForced || $this->trackerAlwaysNewVisitor; + + if (!$enforceNewVisit + && $visitRow + && count($visitRow) > 0 + ) { + + // These values will be used throughout the request + foreach ($persistedVisitAttributes as $field) { + $visitProperties->setProperty($field, $visitRow[$field]); + } + + $visitProperties->setProperty('visit_last_action_time', strtotime($visitRow['visit_last_action_time'])); + $visitProperties->setProperty('visit_first_action_time', strtotime($visitRow['visit_first_action_time'])); + + // Custom Variables copied from Visit in potential later conversion + if (!empty($numCustomVarsToRead)) { + for ($i = 1; $i <= $numCustomVarsToRead; $i++) { + if (isset($visitRow['custom_var_k' . $i]) + && strlen($visitRow['custom_var_k' . $i]) + ) { + $visitProperties->setProperty('custom_var_k' . $i, $visitRow['custom_var_k' . $i]); + } + if (isset($visitRow['custom_var_v' . $i]) + && strlen($visitRow['custom_var_v' . $i]) + ) { + $visitProperties->setProperty('custom_var_v' . $i, $visitRow['custom_var_v' . $i]); + } + } + } + + Common::printDebug("The visitor is known (idvisitor = " . bin2hex($visitProperties->getProperty('idvisitor')) . ", + config_id = " . bin2hex($configId) . ", + idvisit = {$visitProperties->getProperty('idvisit')}, + last action = " . date("r", $visitProperties->getProperty('visit_last_action_time')) . ", + first action = " . date("r", $visitProperties->getProperty('visit_first_action_time')) . ", + visit_goal_buyer' = " . $visitProperties->getProperty('visit_goal_buyer') . ")"); + + return true; + } else { + Common::printDebug("The visitor was not matched with an existing visitor..."); + + return false; + } + } + + protected function shouldLookupOneVisitorFieldOnly($isVisitorIdToLookup, Request $request) + { + $isForcedUserIdMustMatch = (false !== $request->getForcedUserId()); + + if ($isForcedUserIdMustMatch) { + // if &iud was set, we must try and match both idvisitor and config_id + return false; + } + + // This setting would be enabled for Intranet websites, to ensure that visitors using all the same computer config, same IP + // are not counted as 1 visitor. In this case, we want to enforce and trust the visitor ID from the cookie. + if ($isVisitorIdToLookup && $this->trustCookiesOnly) { + return true; + } + + // If a &cid= was set, we force to select this visitor (or create a new one) + $isForcedVisitorIdMustMatch = ($request->getForcedVisitorId() != null); + + if ($isForcedVisitorIdMustMatch) { + return true; + } + + if (!$isVisitorIdToLookup) { + return true; + } + + return false; + } + + /** + * By default, we look back 30 minutes to find a previous visitor (for performance reasons). + * In some cases, it is useful to look back and count unique visitors more accurately. You can set custom lookback window in + * [Tracker] window_look_back_for_visitor + * + * The returned value is the window range (Min, max) that the matched visitor should fall within + * + * @return array( datetimeMin, datetimeMax ) + */ + protected function getWindowLookupThisVisit(Request $request) + { + $lookAheadNSeconds = $this->visitStandardLength; + $lookBackNSeconds = $this->visitStandardLength; + if ($this->lookBackNSecondsCustom > $lookBackNSeconds) { + $lookBackNSeconds = $this->lookBackNSecondsCustom; + } + + $timeLookBack = date('Y-m-d H:i:s', $request->getCurrentTimestamp() - $lookBackNSeconds); + $timeLookAhead = date('Y-m-d H:i:s', $request->getCurrentTimestamp() + $lookAheadNSeconds); + + return array($timeLookBack, $timeLookAhead); + } + + /** + * @return array + */ + private function getVisitFieldsPersist() + { + if (is_null($this->visitFieldsToSelect)) { + $fields = array( + 'idvisitor', + 'idvisit', + 'user_id', + + 'visit_exit_idaction_url', + 'visit_exit_idaction_name', + 'visitor_returning', + 'visitor_days_since_first', + 'visitor_days_since_order', + 'visitor_count_visits', + 'visit_goal_buyer', + + 'location_country', + 'location_region', + 'location_city', + 'location_latitude', + 'location_longitude', + + 'referer_name', + 'referer_keyword', + 'referer_type', + ); + + $dimensions = VisitDimension::getAllDimensions(); + + foreach ($dimensions as $dimension) { + if ($dimension->hasImplementedEvent('onExistingVisit')) { + $fields[] = $dimension->getColumnName(); + } + + foreach ($dimension->getRequiredVisitFields() as $field) { + $fields[] = $field; + } + } + + /** + * This event collects a list of [visit entity](/guides/persistence-and-the-mysql-backend#visits) properties that should be loaded when reading + * the existing visit. Properties that appear in this list will be available in other tracking + * events such as 'onExistingVisit'. + * + * Plugins can use this event to load additional visit entity properties for later use during tracking. + * + * This event is deprecated, use [Dimensions](http://developer.piwik.org/guides/dimensions) instead. + * + * @deprecated + */ + $this->eventDispatcher->postEvent('Tracker.getVisitFieldsToPersist', array(&$fields)); + + array_unshift($fields, 'visit_first_action_time'); + array_unshift($fields, 'visit_last_action_time'); + + for ($index = 1; $index <= CustomVariables::getNumUsableCustomVariables(); $index++) { + $fields[] = 'custom_var_k' . $index; + $fields[] = 'custom_var_v' . $index; + } + + $this->visitFieldsToSelect = array_unique($fields); + } + + return $this->visitFieldsToSelect; + } +} \ No newline at end of file diff --git a/www/analytics/core/Translate.php b/www/analytics/core/Translate.php index fecfbdf9..5b46a2e7 100644 --- a/www/analytics/core/Translate.php +++ b/www/analytics/core/Translate.php @@ -1,6 +1,6 @@ loadPluginTranslations($language); } /** @@ -57,41 +61,14 @@ class Translate */ public static function loadCoreTranslation($language = false) { - if (empty($language)) { - $language = self::getLanguageToLoad(); - } - if (self::$loadedLanguage == $language) { - return; - } - self::loadCoreTranslationFile($language); - } - - private static function loadCoreTranslationFile($language) - { - if(empty($language)) { - return; - } - $path = PIWIK_INCLUDE_PATH . '/lang/' . $language . '.json'; - if (!Filesystem::isValidFilename($language) || !is_readable($path)) { - throw new Exception(Piwik::translate('General_ExceptionLanguageFileNotFound', array($language))); - } - $data = file_get_contents($path); - $translations = json_decode($data, true); - self::mergeTranslationArray($translations); - self::setLocale(); - self::$loadedLanguage = $language; + self::getTranslator()->addDirectory(PIWIK_INCLUDE_PATH . '/lang'); } + /** + * @deprecated + */ public static function mergeTranslationArray($translation) { - if (!isset($GLOBALS['Piwik_translations'])) { - $GLOBALS['Piwik_translations'] = array(); - } - if (empty($translation)) { - return; - } - // we could check that no string overlap here - $GLOBALS['Piwik_translations'] = array_replace_recursive($GLOBALS['Piwik_translations'], $translation); } /** @@ -100,52 +77,27 @@ class Translate */ public static function getLanguageToLoad() { - if (is_null(self::$languageToLoad)) { - $lang = Common::getRequestVar('language', '', 'string'); - - /** - * Triggered when the current user's language is requested. - * - * By default the current language is determined by the **language** query - * parameter. Plugins can override this logic by subscribing to this event. - * - * **Example** - * - * public function getLanguage(&$lang) - * { - * $client = new My3rdPartyAPIClient(); - * $thirdPartyLang = $client->getLanguageForUser(Piwik::getCurrentUserLogin()); - * - * if (!empty($thirdPartyLang)) { - * $lang = $thirdPartyLang; - * } - * } - * - * @param string &$lang The language that should be used for the current user. Will be - * initialized to the value of the **language** query parameter. - */ - Piwik::postEvent('User.getLanguage', array(&$lang)); - - self::$languageToLoad = $lang; - } - - return self::$languageToLoad; + return self::getTranslator()->getCurrentLanguage(); } /** Reset the cached language to load. Used in tests. */ public static function reset() { - self::$languageToLoad = null; + self::getTranslator()->reset(); } + /** + * Either the name of the currently loaded language such as 'en' or 'de' or null if no language is loaded at all. + * @return bool|string + */ public static function getLanguageLoaded() { - return self::$loadedLanguage; + return self::getTranslator()->getCurrentLanguage(); } public static function getLanguageDefault() { - return Config::getInstance()->General['default_language']; + return self::getTranslator()->getDefaultLanguage(); } /** @@ -153,63 +105,25 @@ class Translate */ public static function getJavascriptTranslations() { - $translations = & $GLOBALS['Piwik_translations']; + return self::getTranslator()->getJavascriptTranslations(); + } - $clientSideTranslations = array(); - foreach (self::getClientSideTranslationKeys() as $key) { - list($plugin, $stringName) = explode("_", $key, 2); - $clientSideTranslations[$key] = $translations[$plugin][$stringName]; - } - - $js = 'var translations = ' . Common::json_encode($clientSideTranslations) . ';'; - $js .= "\n" . 'if(typeof(piwik_translations) == \'undefined\') { var piwik_translations = new Object; }' . - 'for(var i in translations) { piwik_translations[i] = translations[i];} '; - return $js; + public static function findTranslationKeyForTranslation($translation) + { + return self::getTranslator()->findTranslationKeyForTranslation($translation); } /** - * Returns the list of client side translations by key. These translations will be outputted - * to the translation JavaScript. + * @return Translator */ - private static function getClientSideTranslationKeys() + private static function getTranslator() { - $result = array(); - - /** - * Triggered before generating the JavaScript code that allows i18n strings to be used - * in the browser. - * - * Plugins should subscribe to this event to specify which translations - * should be available to JavaScript. - * - * Event handlers should add whole translation keys, ie, keys that include the plugin name. - * - * **Example** - * - * public function getClientSideTranslationKeys(&$result) - * { - * $result[] = "MyPlugin_MyTranslation"; - * } - * - * @param array &$result The whole list of client side translation keys. - */ - Piwik::postEvent('Translate.getClientSideTranslationKeys', array(&$result)); - - $result = array_unique($result); - - return $result; + return StaticContainer::get('Piwik\Translation\Translator'); } - /** - * Set locale - * - * @see http://php.net/setlocale - */ - private static function setLocale() + public static function loadAllTranslations() { - $locale = $GLOBALS['Piwik_translations']['General']['Locale']; - $locale_variant = str_replace('UTF-8', 'UTF8', $locale); - setlocale(LC_ALL, $locale, $locale_variant); - setlocale(LC_CTYPE, ''); + self::loadCoreTranslation(); + Manager::getInstance()->loadPluginTranslations(); } } diff --git a/www/analytics/core/Translate/Validate/CoreTranslations.php b/www/analytics/core/Translate/Validate/CoreTranslations.php deleted file mode 100644 index 96ea1da7..00000000 --- a/www/analytics/core/Translate/Validate/CoreTranslations.php +++ /dev/null @@ -1,101 +0,0 @@ -baseTranslations = $baseTranslations; - } - - /** - * Validates the given translations - * * There need to be more than 250 translations presen - * * Locale, TranslatorName and TranslatorEmail needs to be set in plugin General - * * LayoutDirection needs to be ltr or rtl if present - * * Locale must be valid (format, language & country) - * - * @param array $translations - * - * @return boolean - */ - public function isValid($translations) - { - $this->message = null; - - if (250 > count($translations, COUNT_RECURSIVE)) { - $this->message = self::ERRORSTATE_MINIMUMTRANSLATIONS; - return false; - } - - if (empty($translations['General']['Locale'])) { - $this->message = self::ERRORSTATE_LOCALEREQUIRED; - return false; - } - - if (empty($translations['General']['TranslatorName'])) { - $this->message = self::ERRORSTATE_TRANSLATORINFOREQUIRED; - return false; - } - - if (empty($translations['General']['TranslatorEmail'])) { - $this->message = self::ERRORSTATE_TRANSLATOREMAILREQUIRED; - return false; - } - - if (!empty($translations['General']['LayoutDirection']) && - !in_array($translations['General']['LayoutDirection'], array('ltr', 'rtl')) - ) { - $this->message = self::ERRORSTATE_LAYOUTDIRECTIONINVALID; - return false; - } - - $allLanguages = Common::getLanguagesList(); - $allCountries = Common::getCountriesList(); - - if (!preg_match('/^([a-z]{2})_([A-Z]{2})\.UTF-8$/', $translations['General']['Locale'], $matches)) { - $this->message = self::ERRORSTATE_LOCALEINVALID; - return false; - } else if (!array_key_exists($matches[1], $allLanguages)) { - $this->message = self::ERRORSTATE_LOCALEINVALIDLANGUAGE; - return false; - } else if (!array_key_exists(strtolower($matches[2]), $allCountries)) { - $this->message = self::ERRORSTATE_LOCALEINVALIDCOUNTRY; - return false; - } - - return true; - } -} diff --git a/www/analytics/core/Translation/Loader/DevelopmentLoader.php b/www/analytics/core/Translation/Loader/DevelopmentLoader.php new file mode 100644 index 00000000..a75af4bd --- /dev/null +++ b/www/analytics/core/Translation/Loader/DevelopmentLoader.php @@ -0,0 +1,72 @@ +loader = $loader; + } + + /** + * {@inheritdoc} + */ + public function load($language, array $directories) + { + if ($language !== self::LANGUAGE_ID) { + return $this->loader->load($language, $directories); + } + + return $this->getDevelopmentTranslations($directories); + } + + private function getDevelopmentTranslations(array $directories) + { + $fallbackTranslations = $this->loader->load($this->fallbackLanguage, $directories); + + $translations = array(); + + foreach ($fallbackTranslations as $section => $sectionFallbackTranslations) { + $translationIds = array_keys($sectionFallbackTranslations); + $sectionTranslations = $this->prefixTranslationsWithSection($section, $translationIds); + + $translations[$section] = array_combine($translationIds, $sectionTranslations); + } + + return $translations; + } + + private function prefixTranslationsWithSection($section, $translationIds) + { + return array_map(function ($translation) use ($section) { + return $section . '_' . $translation; + }, $translationIds); + } +} diff --git a/www/analytics/core/Translation/Loader/JsonFileLoader.php b/www/analytics/core/Translation/Loader/JsonFileLoader.php new file mode 100644 index 00000000..802bcc62 --- /dev/null +++ b/www/analytics/core/Translation/Loader/JsonFileLoader.php @@ -0,0 +1,64 @@ +loadFile($filename) + ); + } + + return $translations; + } + + private function loadFile($filename) + { + $data = file_get_contents($filename); + $translations = json_decode($data, true); + + if (is_null($translations) && Common::hasJsonErrorOccurred()) { + throw new \Exception(sprintf( + 'Not able to load translation file %s: %s', + $filename, + Common::getLastJsonError() + )); + } + + if (!is_array($translations)) { + return array(); + } + + return $translations; + } +} diff --git a/www/analytics/core/Translation/Loader/LoaderCache.php b/www/analytics/core/Translation/Loader/LoaderCache.php new file mode 100644 index 00000000..5448e1ae --- /dev/null +++ b/www/analytics/core/Translation/Loader/LoaderCache.php @@ -0,0 +1,65 @@ +loader = $loader; + $this->cache = $cache; + } + + /** + * {@inheritdoc} + */ + public function load($language, array $directories) + { + if (empty($language)) { + return array(); + } + + $cacheKey = $this->getCacheKey($language, $directories); + + $translations = $this->cache->fetch($cacheKey); + + if (empty($translations) || !is_array($translations)) { + $translations = $this->loader->load($language, $directories); + + $this->cache->save($cacheKey, $translations, 43200); // ttl=12hours + } + + return $translations; + } + + private function getCacheKey($language, array $directories) + { + $cacheKey = 'Translations-' . $language . '-'; + + // in case loaded plugins change (ie Tests vs Tracker vs UI etc) + $cacheKey .= sha1(implode('', $directories)); + + return $cacheKey; + } +} diff --git a/www/analytics/core/Translation/Loader/LoaderInterface.php b/www/analytics/core/Translation/Loader/LoaderInterface.php new file mode 100644 index 00000000..9aae71d4 --- /dev/null +++ b/www/analytics/core/Translation/Loader/LoaderInterface.php @@ -0,0 +1,23 @@ +username = $username; + $this->password = $password; + $this->projectSlug = $project; + } + + /** + * Returns all resources available on Transifex project + * + * @return array + */ + public function getAvailableResources() + { + $cache = Cache::getTransientCache(); + $cacheId = 'transifex_resources_' . $this->projectSlug; + $resources = $cache->fetch($cacheId); + + if (empty($resources)) { + $apiPath = 'project/' . $this->projectSlug . '/resources'; + $resources = $this->getApiResults($apiPath); + $cache->save($cacheId, $resources); + } + + return $resources; + } + + /** + * Checks if the given resource exists in Transifex project + * + * @param string $resource + * @return bool + */ + public function resourceExists($resource) + { + $resources = $this->getAvailableResources(); + foreach ($resources as $res) { + if ($res->slug == $resource) { + return true; + } + } + return false; + } + + /** + * Returns all language codes the transifex project is available for + * + * @return array + * @throws AuthenticationFailedException + * @throws Exception + */ + public function getAvailableLanguageCodes() + { + $cache = Cache::getTransientCache(); + $cacheId = 'transifex_languagescodes_' . $this->projectSlug; + $languageCodes = $cache->fetch($cacheId); + + if (empty($languageCodes)) { + $apiData = $this->getApiResults('project/' . $this->projectSlug . '/languages'); + foreach ($apiData as $languageData) { + $languageCodes[] = $languageData->language_code; + } + $cache->save($cacheId, $languageCodes); + } + return $languageCodes; + } + + /** + * Returns statistic data for the given resource + * + * @param string $resource e.g. piwik-base, piwik-plugin-api,... + * @return array + * @throws AuthenticationFailedException + * @throws Exception + */ + public function getStatistics($resource) + { + return $this->getApiResults('project/' . $this->projectSlug . '/resource/' . $resource . '/stats/'); + } + + /** + * Return the translations for the given resource and language + * + * @param string $resource e.g. piwik-base, piwik-plugin-api,... + * @param string $language e.g. de, pt_BR, hy,... + * @param bool $raw if true plain response wil be returned (unparsed json) + * @return mixed + * @throws AuthenticationFailedException + * @throws Exception + */ + public function getTranslations($resource, $language, $raw = false) + { + if ($this->resourceExists($resource)) { + $apiPath = 'project/' . $this->projectSlug . '/resource/' . $resource . '/translation/' . $language . '/?mode=onlytranslated&file'; + return $this->getApiResults($apiPath, $raw); + } + return null; + } + + /** + * Returns response for API request with given path + * + * @param $apiPath + * @param bool $raw + * @return mixed + * @throws AuthenticationFailedException + * @throws Exception + */ + protected function getApiResults($apiPath, $raw = false) + { + $apiUrl = $this->apiUrl . $apiPath; + + $response = Http::sendHttpRequest($apiUrl, 1000, null, null, 5, false, false, true, 'GET', $this->username, $this->password); + + $httpStatus = $response['status']; + $response = $response['data']; + + if ($httpStatus == 401) { + throw new AuthenticationFailedException(); + } elseif ($httpStatus != 200) { + throw new Exception('Error while getting API results', $httpStatus); + } + + return $raw ? $response : json_decode($response); + } +} diff --git a/www/analytics/core/Translation/Translator.php b/www/analytics/core/Translation/Translator.php new file mode 100644 index 00000000..af2ae597 --- /dev/null +++ b/www/analytics/core/Translation/Translator.php @@ -0,0 +1,271 @@ +loader = $loader; + $this->currentLanguage = $this->getDefaultLanguage(); + + if ($directories === null) { + // TODO should be moved out of this class + $directories = array(PIWIK_INCLUDE_PATH . '/lang'); + } + $this->directories = $directories; + } + + /** + * Returns an internationalized string using a translation ID. If a translation + * cannot be found for the ID, the ID is returned. + * + * @param string $translationId Translation ID, eg, `General_Date`. + * @param array|string|int $args `sprintf` arguments to be applied to the internationalized + * string. + * @param string|null $language Optionally force the language. + * @return string The translated string or `$translationId`. + * @api + */ + public function translate($translationId, $args = array(), $language = null) + { + $args = is_array($args) ? $args : array($args); + + if (strpos($translationId, "_") !== false) { + list($plugin, $key) = explode("_", $translationId, 2); + $language = is_string($language) ? $language : $this->currentLanguage; + + $translationId = $this->getTranslation($translationId, $language, $plugin, $key); + } + + if (count($args) == 0) { + return $translationId; + } + return vsprintf($translationId, $args); + } + + /** + * @return string + */ + public function getCurrentLanguage() + { + return $this->currentLanguage; + } + + /** + * @param string $language + */ + public function setCurrentLanguage($language) + { + if (!$language) { + $language = $this->getDefaultLanguage(); + } + + $this->currentLanguage = $language; + } + + /** + * @return string The default configured language. + */ + public function getDefaultLanguage() + { + $generalSection = Config::getInstance()->General; + + // the config may not be available (for example, during environment setup), so we default to 'en' + // if the config cannot be found. + return @$generalSection['default_language'] ?: 'en'; + } + + /** + * Generate javascript translations array + */ + public function getJavascriptTranslations() + { + $clientSideTranslations = array(); + foreach ($this->getClientSideTranslationKeys() as $id) { + list($plugin, $key) = explode('_', $id, 2); + $clientSideTranslations[$id] = $this->getTranslation($id, $this->currentLanguage, $plugin, $key); + } + + $js = 'var translations = ' . json_encode($clientSideTranslations) . ';'; + $js .= "\n" . 'if (typeof(piwik_translations) == \'undefined\') { var piwik_translations = new Object; }' . + 'for(var i in translations) { piwik_translations[i] = translations[i];} '; + return $js; + } + + /** + * Returns the list of client side translations by key. These translations will be outputted + * to the translation JavaScript. + */ + private function getClientSideTranslationKeys() + { + $result = array(); + + /** + * Triggered before generating the JavaScript code that allows i18n strings to be used + * in the browser. + * + * Plugins should subscribe to this event to specify which translations + * should be available to JavaScript. + * + * Event handlers should add whole translation keys, ie, keys that include the plugin name. + * + * **Example** + * + * public function getClientSideTranslationKeys(&$result) + * { + * $result[] = "MyPlugin_MyTranslation"; + * } + * + * @param array &$result The whole list of client side translation keys. + */ + Piwik::postEvent('Translate.getClientSideTranslationKeys', array(&$result)); + + $result = array_unique($result); + + return $result; + } + + /** + * Add a directory containing translations. + * + * @param string $directory + */ + public function addDirectory($directory) + { + if (isset($this->directories[$directory])) { + return; + } + // index by name to avoid duplicates + $this->directories[$directory] = $directory; + + // clear currently loaded translations to force reloading them + $this->translations = array(); + } + + /** + * Should be used by tests only, and this method should eventually be removed. + */ + public function reset() + { + $this->currentLanguage = $this->getDefaultLanguage(); + $this->directories = array(); + $this->translations = array(); + } + + /** + * @param string $translation + * @return null|string + */ + public function findTranslationKeyForTranslation($translation) + { + foreach ($this->getAllTranslations() as $key => $translations) { + $possibleKey = array_search($translation, $translations); + if (!empty($possibleKey)) { + return $key . '_' . $possibleKey; + } + } + + return null; + } + + /** + * Returns all the translation messages loaded. + * + * @return array + */ + public function getAllTranslations() + { + $this->loadTranslations($this->currentLanguage); + + if (!isset($this->translations[$this->currentLanguage])) { + return array(); + } + + return $this->translations[$this->currentLanguage]; + } + + private function getTranslation($id, $lang, $plugin, $key) + { + $this->loadTranslations($lang); + + if (isset($this->translations[$lang][$plugin]) + && isset($this->translations[$lang][$plugin][$key]) + ) { + return $this->translations[$lang][$plugin][$key]; + } + + /** + * Fallback for keys moved to new Intl plugin to avoid untranslated string in non core plugins + * @todo remove this in Piwik 3.0 + */ + if ($plugin != 'Intl') { + if (isset($this->translations[$lang]['Intl']) + && isset($this->translations[$lang]['Intl'][$key]) + ) { + return $this->translations[$lang]['Intl'][$key]; + } + } + + // fallback + if ($lang !== $this->fallback) { + return $this->getTranslation($id, $this->fallback, $plugin, $key); + } + + return $id; + } + + private function loadTranslations($language) + { + if (empty($language) || isset($this->translations[$language])) { + return; + } + + $this->translations[$language] = $this->loader->load($language, $this->directories); + } +} diff --git a/www/analytics/core/Twig.php b/www/analytics/core/Twig.php old mode 100644 new mode 100755 index 16348e52..c3b6ba74 --- a/www/analytics/core/Twig.php +++ b/www/analytics/core/Twig.php @@ -1,6 +1,6 @@ getDefaultThemeLoader(); - $this->addPluginNamespaces($loader); - // If theme != default we need to chain - $chainLoader = new Twig_Loader_Chain(array($loader)); + //get current theme + $manager = Plugin\Manager::getInstance(); + $theme = $manager->getThemeEnabled(); + $loaders = array(); + + $this->formatter = new Formatter(); + + //create loader for custom theme to overwrite twig templates + if ($theme && $theme->getPluginName() != \Piwik\Plugin\Manager::DEFAULT_THEME) { + $customLoader = $this->getCustomThemeLoader($theme); + if ($customLoader) { + //make it possible to overwrite plugin templates + $this->addCustomPluginNamespaces($customLoader, $theme->getPluginName()); + $loaders[] = $customLoader; + } + } + + $loaders[] = $loader; + + $chainLoader = new Twig_Loader_Chain($loaders); // Create new Twig Environment and set cache dir - $templatesCompiledPath = PIWIK_USER_PATH . '/tmp/templates_c'; - $templatesCompiledPath = SettingsPiwik::rewriteTmpPathWithHostname($templatesCompiledPath); + $templatesCompiledPath = StaticContainer::get('path.tmp') . '/templates_c'; $this->twig = new Twig_Environment($chainLoader, array( @@ -62,11 +83,19 @@ class Twig $this->addFilter_sumTime(); $this->addFilter_money(); $this->addFilter_truncate(); - $this->addFilter_notificiation(); + $this->addFilter_notification(); $this->addFilter_percentage(); + $this->addFilter_percent(); + $this->addFilter_percentEvolution(); + $this->addFilter_piwikProAdLink(); + $this->addFilter_piwikProOnPremisesAdLink(); + $this->addFilter_piwikProCloudAdLink(); $this->addFilter_prettyDate(); + $this->addFilter_safeDecodeRaw(); + $this->addFilter_number(); $this->twig->addFilter(new Twig_SimpleFilter('implode', 'implode')); $this->twig->addFilter(new Twig_SimpleFilter('ucwords', 'ucwords')); + $this->twig->addFilter(new Twig_SimpleFilter('lcfirst', 'lcfirst')); $this->addFunction_includeAssets(); $this->addFunction_linkTo(); @@ -76,6 +105,43 @@ class Twig $this->addFunction_getJavascriptTranslations(); $this->twig->addTokenParser(new RenderTokenParser()); + + $this->addTest_false(); + $this->addTest_true(); + $this->addTest_emptyString(); + } + + private function addTest_false() + { + $test = new Twig_SimpleTest( + 'false', + function ($value) { + return false === $value; + } + ); + $this->twig->addTest($test); + } + + private function addTest_true() + { + $test = new Twig_SimpleTest( + 'true', + function ($value) { + return true === $value; + } + ); + $this->twig->addTest($test); + } + + private function addTest_emptyString() + { + $test = new Twig_SimpleTest( + 'emptyString', + function ($value) { + return '' === $value; + } + ); + $this->twig->addTest($test); } protected function addFunction_getJavascriptTranslations() @@ -126,7 +192,7 @@ class Twig // make the first value the string that will get output in the template // plugins can modify this string $str = ''; - $params = array_merge( array( &$str ), $params); + $params = array_merge(array( &$str ), $params); Piwik::postEvent($eventName, $params); return $str; @@ -164,12 +230,29 @@ class Twig return $themeLoader; } + /** + * create template loader for a custom theme + * @param \Piwik\Plugin $theme + * @return \Twig_Loader_Filesystem + */ + protected function getCustomThemeLoader(Plugin $theme) + { + if (!file_exists(sprintf("%s/plugins/%s/templates/", PIWIK_INCLUDE_PATH, $theme->getPluginName()))) { + return false; + } + $themeLoader = new Twig_Loader_Filesystem(array( + sprintf("%s/plugins/%s/templates/", PIWIK_INCLUDE_PATH, $theme->getPluginName()) + )); + + return $themeLoader; + } + public function getTwigEnvironment() { return $this->twig; } - protected function addFilter_notificiation() + protected function addFilter_notification() { $twigEnv = $this->getTwigEnvironment(); $notificationFunction = new Twig_SimpleFilter('notification', function ($message, $options) use ($twigEnv) { @@ -198,10 +281,22 @@ class Twig $this->twig->addFilter($notificationFunction); } + protected function addFilter_safeDecodeRaw() + { + $rawSafeDecoded = new Twig_SimpleFilter('rawSafeDecoded', function ($string) { + $string = str_replace('+', '%2B', $string); + $string = str_replace(' ', html_entity_decode(' '), $string); + + return SafeDecodeLabel::decodeLabelSafe($string); + + }, array('is_safe' => array('all'))); + $this->twig->addFilter($rawSafeDecoded); + } + protected function addFilter_prettyDate() { $prettyDate = new Twig_SimpleFilter('prettyDate', function ($dateString, $period) { - return Range::factory($period, $dateString)->getLocalizedShortString(); + return Period\Factory::build($period, $dateString)->getLocalizedShortString(); }); $this->twig->addFilter($prettyDate); } @@ -209,11 +304,78 @@ class Twig protected function addFilter_percentage() { $percentage = new Twig_SimpleFilter('percentage', function ($string, $totalValue, $precision = 1) { - return Piwik::getPercentageSafe($string, $totalValue, $precision) . '%'; + return NumberFormatter::getInstance()->formatPercent(Piwik::getPercentageSafe($string, $totalValue, $precision), $precision); }); $this->twig->addFilter($percentage); } + protected function addFilter_percent() + { + $percentage = new Twig_SimpleFilter('percent', function ($string, $precision = 1) { + return NumberFormatter::getInstance()->formatPercent($string, $precision); + }); + $this->twig->addFilter($percentage); + } + + protected function addFilter_percentEvolution() + { + $percentage = new Twig_SimpleFilter('percentEvolution', function ($string) { + return NumberFormatter::getInstance()->formatPercentEvolution($string); + }); + $this->twig->addFilter($percentage); + } + + protected function addFilter_piwikProAdLink() + { + $ads = $this->getPiwikProAdvertising(); + $piwikProAd = new Twig_SimpleFilter('piwikProCampaignParameters', function ($url, $campaignName, $campaignMedium, $campaignContent = '') use ($ads) { + $url = $ads->addPromoCampaignParametersToUrl($url, $campaignName, $campaignMedium, $campaignContent); + return $url; + }); + $this->twig->addFilter($piwikProAd); + } + + protected function addFilter_piwikProOnPremisesAdLink() + { + $twigEnv = $this->getTwigEnvironment(); + $ads = $this->getPiwikProAdvertising(); + $piwikProAd = new Twig_SimpleFilter('piwikProOnPremisesPromoUrl', function ($medium, $content = '') use ($twigEnv, $ads) { + + $url = $ads->getPromoUrlForOnPremises($medium, $content); + + return twig_escape_filter($twigEnv, $url, 'html_attr'); + + }, array('is_safe' => array('html_attr'))); + $this->twig->addFilter($piwikProAd); + } + + protected function addFilter_piwikProCloudAdLink() + { + $twigEnv = $this->getTwigEnvironment(); + $ads = $this->getPiwikProAdvertising(); + $piwikProAd = new Twig_SimpleFilter('piwikProCloudPromoUrl', function ($medium, $content = '') use ($twigEnv, $ads) { + + $url = $ads->getPromoUrlForCloud($medium, $content); + + return twig_escape_filter($twigEnv, $url, 'html_attr'); + + }, array('is_safe' => array('html_attr'))); + $this->twig->addFilter($piwikProAd); + } + + private function getPiwikProAdvertising() + { + return StaticContainer::get('Piwik\PiwikPro\Advertising'); + } + + protected function addFilter_number() + { + $formatter = new Twig_SimpleFilter('number', function ($string, $minFractionDigits = 0, $maxFractionDigits = 0) { + return NumberFormatter::getInstance()->format($string, $minFractionDigits, $maxFractionDigits); + }); + $this->twig->addFilter($formatter); + } + protected function addFilter_truncate() { $truncateFilter = new Twig_SimpleFilter('truncate', function ($string, $size) { @@ -229,21 +391,24 @@ class Twig protected function addFilter_money() { - $moneyFilter = new Twig_SimpleFilter('money', function ($amount) { + $formatter = $this->formatter; + $moneyFilter = new Twig_SimpleFilter('money', function ($amount) use ($formatter) { if (func_num_args() != 2) { throw new Exception('the money modifier expects one parameter: the idSite.'); } $idSite = func_get_args(); $idSite = $idSite[1]; - return MetricsFormatter::getPrettyMoney($amount, $idSite); + $currencySymbol = Site::getCurrencySymbolFor($idSite); + return NumberFormatter::getInstance()->formatCurrency($amount, $currencySymbol, GoalManager::REVENUE_PRECISION); }); $this->twig->addFilter($moneyFilter); } protected function addFilter_sumTime() { - $sumtimeFilter = new Twig_SimpleFilter('sumtime', function ($numberOfSeconds) { - return MetricsFormatter::getPrettyTimeFromSeconds($numberOfSeconds); + $formatter = $this->formatter; + $sumtimeFilter = new Twig_SimpleFilter('sumtime', function ($numberOfSeconds) use ($formatter) { + return $formatter->getPrettyTimeFromSeconds($numberOfSeconds, true); }); $this->twig->addFilter($sumtimeFilter); } @@ -289,6 +454,22 @@ class Twig } } + /** + * + * Plugin-Templates can be overwritten by putting identically named templates in plugins/[theme]/templates/plugins/[plugin]/ + * + */ + private function addCustomPluginNamespaces(Twig_Loader_Filesystem $loader, $pluginName) + { + $plugins = \Piwik\Plugin\Manager::getInstance()->getAllPluginsNames(); + foreach ($plugins as $name) { + $path = sprintf("%s/plugins/%s/templates/plugins/%s/", PIWIK_INCLUDE_PATH, $pluginName, $name); + if (is_dir($path)) { + $loader->addPath(PIWIK_INCLUDE_PATH . '/plugins/' . $pluginName . '/templates/plugins/'. $name, $name); + } + } + } + /** * Prepend relative paths with absolute Piwik path * diff --git a/www/analytics/core/Unzip.php b/www/analytics/core/Unzip.php index 2ba8cfcd..ffef2add 100644 --- a/www/analytics/core/Unzip.php +++ b/www/analytics/core/Unzip.php @@ -1,22 +1,20 @@ tarArchive = new Archive_Tar($filename, $compression); - } - - /** - * Extracts the contents of the tar file to $pathExtracted. - * - * @param string $pathExtracted Directory to extract into. - * @return bool true if successful, false if otherwise. - */ - public function extract($pathExtracted) - { - return $this->tarArchive->extract($pathExtracted); - } - - /** - * Extracts one file held in a tar archive and returns the deflated file - * as a string. - * - * @param string $inArchivePath Path to file in the tar archive. - * @return bool true if successful, false if otherwise. - */ - public function extractInString($inArchivePath) - { - return $this->tarArchive->extractInString($inArchivePath); - } - - /** - * Lists the files held in the tar archive. - * - * @return array List of paths describing everything held in the tar archive. - */ - public function listContent() - { - return $this->tarArchive->listContent(); - } - - /** - * Get error status string for the latest error. - * - * @return string - */ - public function errorInfo() - { - return $this->tarArchive->error_object->getMessage(); - } -} diff --git a/www/analytics/core/Unzip/UncompressInterface.php b/www/analytics/core/Unzip/UncompressInterface.php deleted file mode 100644 index d3dccf20..00000000 --- a/www/analytics/core/Unzip/UncompressInterface.php +++ /dev/null @@ -1,39 +0,0 @@ -filename = $filename; - $this->ziparchive = new \ZipArchive; - if ($this->ziparchive->open($filename) !== true) { - throw new Exception('Error opening ' . $filename); - } - } - - /** - * Extract files from archive to target directory - * - * @param string $pathExtracted Absolute path of target directory - * @return mixed Array of filenames if successful; or 0 if an error occurred - */ - public function extract($pathExtracted) - { - if (substr_compare($pathExtracted, '/', -1)) - $pathExtracted .= '/'; - - $fileselector = array(); - $list = array(); - $count = $this->ziparchive->numFiles; - if ($count === 0) { - return 0; - } - - for ($i = 0; $i < $count; $i++) { - $entry = $this->ziparchive->statIndex($i); - - $filename = str_replace('\\', '/', $entry['name']); - $parts = explode('/', $filename); - - if (!strncmp($filename, '/', 1) || - array_search('..', $parts) !== false || - strpos($filename, ':') !== false - ) { - return 0; - } - $fileselector[] = $entry['name']; - $list[] = array( - 'filename' => $pathExtracted . $entry['name'], - 'stored_filename' => $entry['name'], - 'size' => $entry['size'], - 'compressed_size' => $entry['comp_size'], - 'mtime' => $entry['mtime'], - 'index' => $i, - 'crc' => $entry['crc'], - ); - } - - $res = $this->ziparchive->extractTo($pathExtracted, $fileselector); - if ($res === false) - return 0; - return $list; - } - - /** - * Get error status string for the latest error - * - * @return string - */ - public function errorInfo() - { - static $statusStrings = array( - \ZIPARCHIVE::ER_OK => 'No error', - \ZIPARCHIVE::ER_MULTIDISK => 'Multi-disk zip archives not supported', - \ZIPARCHIVE::ER_RENAME => 'Renaming temporary file failed', - \ZIPARCHIVE::ER_CLOSE => 'Closing zip archive failed', - \ZIPARCHIVE::ER_SEEK => 'Seek error', - \ZIPARCHIVE::ER_READ => 'Read error', - \ZIPARCHIVE::ER_WRITE => 'Write error', - \ZIPARCHIVE::ER_CRC => 'CRC error', - \ZIPARCHIVE::ER_ZIPCLOSED => 'Containing zip archive was closed', - \ZIPARCHIVE::ER_NOENT => 'No such file', - \ZIPARCHIVE::ER_EXISTS => 'File already exists', - \ZIPARCHIVE::ER_OPEN => 'Can\'t open file', - \ZIPARCHIVE::ER_TMPOPEN => 'Failure to create temporary file', - \ZIPARCHIVE::ER_ZLIB => 'Zlib error', - \ZIPARCHIVE::ER_MEMORY => 'Malloc failure', - \ZIPARCHIVE::ER_CHANGED => 'Entry has been changed', - \ZIPARCHIVE::ER_COMPNOTSUPP => 'Compression method not supported', - \ZIPARCHIVE::ER_EOF => 'Premature EOF', - \ZIPARCHIVE::ER_INVAL => 'Invalid argument', - \ZIPARCHIVE::ER_NOZIP => 'Not a zip archive', - \ZIPARCHIVE::ER_INTERNAL => 'Internal error', - \ZIPARCHIVE::ER_INCONS => 'Zip archive inconsistent', - \ZIPARCHIVE::ER_REMOVE => 'Can\'t remove file', - \ZIPARCHIVE::ER_DELETED => 'Entry has been deleted', - ); - - if (isset($statusStrings[$this->ziparchive->status])) { - $statusString = $statusStrings[$this->ziparchive->status]; - } else { - $statusString = 'Unknown status'; - } - return $statusString . '(' . $this->ziparchive->status . ')'; - } -} diff --git a/www/analytics/core/UpdateCheck.php b/www/analytics/core/UpdateCheck.php index 76b3a248..b176ed87 100644 --- a/www/analytics/core/UpdateCheck.php +++ b/www/analytics/core/UpdateCheck.php @@ -1,6 +1,6 @@ Version::VERSION, - 'php_version' => PHP_VERSION, - 'url' => Url::getCurrentUrlWithoutQueryString(), - 'trigger' => Common::getRequestVar('module', '', 'string'), - 'timezone' => API::getInstance()->getDefaultTimezone(), - ); - $url = Config::getInstance()->General['api_service_url'] - . '/1.0/getLatestVersion/' - . '?' . http_build_query($parameters, '', '&'); - $timeout = self::SOCKET_TIMEOUT; - - if (@Config::getInstance()->Debug['allow_upgrades_to_beta']) { - $url = 'http://builds.piwik.org/LATEST_BETA'; - } - - try { - $latestVersion = Http::sendHttpRequest($url, $timeout); - if (!preg_match('~^[0-9][0-9a-zA-Z_.-]*$~D', $latestVersion)) { - $latestVersion = ''; - } - } catch (Exception $e) { - // e.g., disable_functions = fsockopen; allow_url_open = Off + $latestVersion = self::getLatestAvailableVersionNumber(); + $latestVersion = trim((string) $latestVersion); + if (!preg_match('~^[0-9][0-9a-zA-Z_.-]*$~D', $latestVersion)) { $latestVersion = ''; } + Option::set(self::LATEST_VERSION, $latestVersion); } } + /** + * Get the latest available version number for the currently active release channel. Eg '2.15.0-b4' or '2.15.0'. + * Should return a semantic version number in format MAJOR.MINOR.PATCH (http://semver.org/). + * Returns an empty string in case one cannot connect to the remote server. + * @return string + */ + private static function getLatestAvailableVersionNumber() + { + $channel = StaticContainer::get('\Piwik\Plugin\ReleaseChannels')->getActiveReleaseChannel(); + $url = $channel->getUrlToCheckForLatestAvailableVersion(); + + try { + $latestVersion = Http::sendHttpRequest($url, self::SOCKET_TIMEOUT); + } catch (\Exception $e) { + // e.g., disable_functions = fsockopen; allow_url_open = Off + $latestVersion = ''; + } + + return $latestVersion; + } + /** * Returns the latest available version number. Does not perform a check whether a later version is available. * diff --git a/www/analytics/core/UpdateCheck/ReleaseChannel.php b/www/analytics/core/UpdateCheck/ReleaseChannel.php new file mode 100644 index 00000000..5f204074 --- /dev/null +++ b/www/analytics/core/UpdateCheck/ReleaseChannel.php @@ -0,0 +1,73 @@ +pathUpdateFileCore = PIWIK_INCLUDE_PATH . '/core/Updates/'; - $this->pathUpdateFilePlugins = PIWIK_INCLUDE_PATH . '/plugins/%s/Updates/'; + $this->pathUpdateFileCore = $pathUpdateFileCore ?: PIWIK_INCLUDE_PATH . '/core/Updates/'; + $this->pathUpdateFilePlugins = $pathUpdateFilePlugins ?: PIWIK_INCLUDE_PATH . '/plugins/%s/Updates/'; + $this->columnsUpdater = $columnsUpdater ?: new Columns\Updater(); + + self::$activeInstance = $this; } /** - * Add component to check + * Adds an UpdateObserver to the internal list of listeners. * - * @param string $name - * @param string $version + * @param UpdateObserver $listener */ - public function addComponentToCheck($name, $version) + public function addUpdateObserver(UpdateObserver $listener) { - $this->componentsToCheck[$name] = $version; + $this->updateObservers[] = $listener; } /** - * Record version of successfully completed component update + * Marks a component as successfully updated to a specific version in the database. Sets an option + * that looks like `"version_$componentName"`. * - * @param string $name - * @param string $version + * @param string $name The component name. Eg, a plugin name, `'core'` or dimension column name. + * @param string $version The component version (should use semantic versioning). */ - public static function recordComponentSuccessfullyUpdated($name, $version) + public function markComponentSuccessfullyUpdated($name, $version) { try { Option::set(self::getNameInOptionTable($name), $version, $autoLoad = 1); @@ -58,23 +91,58 @@ class Updater } /** - * Returns the flag name to use in the option table to record current schema version - * @param string $name - * @return string + * Marks a component as successfully uninstalled. Deletes an option + * that looks like `"version_$componentName"`. + * + * @param string $name The component name. Eg, a plugin name, `'core'` or dimension column name. */ - private static function getNameInOptionTable($name) + public function markComponentSuccessfullyUninstalled($name) { - return 'version_' . $name; + try { + Option::delete(self::getNameInOptionTable($name)); + } catch (\Exception $e) { + // case when the option table is not yet created (before 0.2.10) + } + } + + /** + * Returns the currently installed version of a Piwik component. + * + * @param string $name The component name. Eg, a plugin name, `'core'` or dimension column name. + * @return string A semantic version. + * @throws \Exception + */ + public function getCurrentComponentVersion($name) + { + try { + $currentVersion = Option::get(self::getNameInOptionTable($name)); + } catch (\Exception $e) { + // mysql error 1146: table doesn't exist + if (Db::get()->isErrNo($e, '1146')) { + // case when the option table is not yet created (before 0.2.10) + $currentVersion = false; + } else { + // failed for some other reason + throw $e; + } + } + + return $currentVersion; } /** * Returns a list of components (core | plugin) that need to run through the upgrade process. * + * @param string[] $componentsToCheck An array mapping component names to the latest locally available version. + * If the version is later than the currently installed version, the component + * must be upgraded. + * + * Example: `array('core' => '2.11.0')` * @return array( componentName => array( file1 => version1, [...]), [...]) */ - public function getComponentsWithUpdateFile() + public function getComponentsWithUpdateFile($componentsToCheck) { - $this->componentsWithNewVersion = $this->getComponentsWithNewVersion(); + $this->componentsWithNewVersion = $this->getComponentsWithNewVersion($componentsToCheck); $this->componentsWithUpdateFile = $this->loadComponentsWithUpdateFile(); return $this->componentsWithUpdateFile; } @@ -87,8 +155,7 @@ class Updater */ public function hasNewVersion($componentName) { - return isset($this->componentsWithNewVersion) && - isset($this->componentsWithNewVersion[$componentName]); + return isset($this->componentsWithNewVersion[$componentName]); } /** @@ -111,6 +178,8 @@ class Updater public function getSqlQueriesToExecute() { $queries = array(); + $classNames = array(); + foreach ($this->componentsWithUpdateFile as $componentName => $componentUpdateInfo) { foreach ($componentUpdateInfo as $file => $fileVersion) { require_once $file; // prefixed by PIWIK_INCLUDE_PATH @@ -119,21 +188,25 @@ class Updater if (!class_exists($className, false)) { throw new \Exception("The class $className was not found in $file"); } - $queriesForComponent = call_user_func(array($className, 'getSql')); + + if (in_array($className, $classNames)) { + continue; // prevent from getting updates from Piwik\Columns\Updater multiple times + } + + $classNames[] = $className; + + $update = StaticContainer::getContainer()->make($className); + $queriesForComponent = call_user_func(array($update, 'getMigrationQueries'), $this); foreach ($queriesForComponent as $query => $error) { $queries[] = $query . ';'; } $this->hasMajorDbUpdate = $this->hasMajorDbUpdate || call_user_func(array($className, 'isMajorUpdate')); } - // unfortunately had to extract this query from the Option class - $queries[] = 'UPDATE `' . Common::prefixTable('option') . '` '. - 'SET option_value = \'' . $fileVersion . '\' '. - 'WHERE option_name = \'' . self::getNameInOptionTable($componentName) . '\';'; } return $queries; } - private function getUpdateClassName($componentName, $fileVersion) + public function getUpdateClassName($componentName, $fileVersion) { $suffix = strtolower(str_replace(array('-', '.'), '_', $fileVersion)); $className = 'Updates_' . $suffix; @@ -141,6 +214,11 @@ class Updater if ($componentName == 'core') { return '\\Piwik\\Updates\\' . $className; } + + if (ColumnUpdater::isDimensionComponent($componentName)) { + return '\\Piwik\\Columns\\Updater'; + } + return '\\Piwik\\Plugins\\' . $componentName . '\\' . $className; } @@ -154,26 +232,46 @@ class Updater public function update($componentName) { $warningMessages = array(); + + $this->executeListenerHook('onComponentUpdateStarting', array($componentName)); + foreach ($this->componentsWithUpdateFile[$componentName] as $file => $fileVersion) { try { require_once $file; // prefixed by PIWIK_INCLUDE_PATH $className = $this->getUpdateClassName($componentName, $fileVersion); - if (class_exists($className, false)) { - // update() - call_user_func(array($className, 'update')); + if (!in_array($className, $this->updatedClasses) + && class_exists($className, false) + ) { + $this->executeListenerHook('onComponentUpdateFileStarting', array($componentName, $file, $className, $fileVersion)); + + $this->executeSingleUpdateClass($className); + + $this->executeListenerHook('onComponentUpdateFileFinished', array($componentName, $file, $className, $fileVersion)); + + // makes sure to call Piwik\Columns\Updater only once as one call updates all dimensions at the same + // time for better performance + $this->updatedClasses[] = $className; } - self::recordComponentSuccessfullyUpdated($componentName, $fileVersion); + $this->markComponentSuccessfullyUpdated($componentName, $fileVersion); } catch (UpdaterErrorException $e) { + $this->executeListenerHook('onError', array($componentName, $fileVersion, $e)); + throw $e; } catch (\Exception $e) { $warningMessages[] = $e->getMessage(); + + $this->executeListenerHook('onWarning', array($componentName, $fileVersion, $e)); } } - // to debug, create core/Updates/X.php, update the core/Version.php, throw an Exception in the try, and comment the following line - self::recordComponentSuccessfullyUpdated($componentName, $this->componentsWithNewVersion[$componentName][self::INDEX_NEW_VERSION]); + // to debug, create core/Updates/X.php, update the core/Version.php, throw an Exception in the try, and comment the following lines + $updatedVersion = $this->componentsWithNewVersion[$componentName][self::INDEX_NEW_VERSION]; + $this->markComponentSuccessfullyUpdated($componentName, $updatedVersion); + + $this->executeListenerHook('onComponentUpdateFinished', array($componentName, $updatedVersion, $warningMessages)); + return $warningMessages; } @@ -185,29 +283,34 @@ class Updater private function loadComponentsWithUpdateFile() { $componentsWithUpdateFile = array(); + foreach ($this->componentsWithNewVersion as $name => $versions) { $currentVersion = $versions[self::INDEX_CURRENT_VERSION]; $newVersion = $versions[self::INDEX_NEW_VERSION]; if ($name == 'core') { $pathToUpdates = $this->pathUpdateFileCore . '*.php'; + } elseif (ColumnUpdater::isDimensionComponent($name)) { + $componentsWithUpdateFile[$name][PIWIK_INCLUDE_PATH . '/core/Columns/Updater.php'] = $newVersion; } else { $pathToUpdates = sprintf($this->pathUpdateFilePlugins, $name) . '*.php'; } - $files = _glob($pathToUpdates); - if ($files == false) { - $files = array(); - } + if (!empty($pathToUpdates)) { + $files = _glob($pathToUpdates); + if ($files == false) { + $files = array(); + } - foreach ($files as $file) { - $fileVersion = basename($file, '.php'); - if ( // if the update is from a newer version - version_compare($currentVersion, $fileVersion) == -1 - // but we don't execute updates from non existing future releases - && version_compare($fileVersion, $newVersion) <= 0 - ) { - $componentsWithUpdateFile[$name][$file] = $fileVersion; + foreach ($files as $file) { + $fileVersion = basename($file, '.php'); + if (// if the update is from a newer version + version_compare($currentVersion, $fileVersion) == -1 + // but we don't execute updates from non existing future releases + && version_compare($fileVersion, $newVersion) <= 0 + ) { + $componentsWithUpdateFile[$name][$file] = $fileVersion; + } } } @@ -216,67 +319,245 @@ class Updater uasort($componentsWithUpdateFile[$name], "version_compare"); } else { // there are no update file => nothing to do, update to the new version is successful - self::recordComponentSuccessfullyUpdated($name, $newVersion); + $this->markComponentSuccessfullyUpdated($name, $newVersion); } } + return $componentsWithUpdateFile; } /** * Construct list of outdated components * + * @param string[] $componentsToCheck An array mapping component names to the latest locally available version. + * If the version is later than the currently installed version, the component + * must be upgraded. + * + * Example: `array('core' => '2.11.0')` * @throws \Exception * @return array array( componentName => array( oldVersion, newVersion), [...]) */ - public function getComponentsWithNewVersion() + public function getComponentsWithNewVersion($componentsToCheck) { $componentsToUpdate = array(); // we make sure core updates are processed before any plugin updates - if (isset($this->componentsToCheck['core'])) { - $coreVersions = $this->componentsToCheck['core']; - unset($this->componentsToCheck['core']); - $this->componentsToCheck = array_merge(array('core' => $coreVersions), $this->componentsToCheck); + if (isset($componentsToCheck['core'])) { + $coreVersions = $componentsToCheck['core']; + unset($componentsToCheck['core']); + $componentsToCheck = array_merge(array('core' => $coreVersions), $componentsToCheck); } - foreach ($this->componentsToCheck as $name => $version) { - try { - $currentVersion = Option::get(self::getNameInOptionTable($name)); - } catch (\Exception $e) { - // mysql error 1146: table doesn't exist - if (Db::get()->isErrNo($e, '1146')) { - // case when the option table is not yet created (before 0.2.10) - $currentVersion = false; - } else { - // failed for some other reason - throw $e; - } - } - if ($currentVersion === false) { - if ($name === 'core') { - // This should not happen - $currentVersion = Version::VERSION; - } else { - // When plugins have been installed since Piwik 2.0 this should not happen - // We "fix" the data for any plugin that may have been ported from Piwik 1.x - $currentVersion = $version; - } - self::recordComponentSuccessfullyUpdated($name, $currentVersion); + $recordedCoreVersion = $this->getCurrentComponentVersion('core'); + if (empty($recordedCoreVersion)) { + // This should not happen + $recordedCoreVersion = Version::VERSION; + $this->markComponentSuccessfullyUpdated('core', $recordedCoreVersion); + } + + foreach ($componentsToCheck as $name => $version) { + $currentVersion = $this->getCurrentComponentVersion($name); + + if (ColumnUpdater::isDimensionComponent($name)) { + $isComponentOutdated = $currentVersion !== $version; + } else { + // note: when versionCompare == 1, the version in the DB is newer, we choose to ignore + $isComponentOutdated = version_compare($currentVersion, $version) == -1; } - $versionCompare = version_compare($currentVersion, $version); - if ($versionCompare == -1) { + if ($isComponentOutdated || $currentVersion === false) { $componentsToUpdate[$name] = array( self::INDEX_CURRENT_VERSION => $currentVersion, self::INDEX_NEW_VERSION => $version ); - } else if ($versionCompare == 1) { - // the version in the DB is newest.. we choose to ignore } } + return $componentsToUpdate; } + /** + * Updates multiple components, while capturing & returning errors and warnings. + * + * @param string[] $componentsWithUpdateFile Component names mapped with arrays of update files. Same structure + * as the result of `getComponentsWithUpdateFile()`. + * @return array Information about the update process, including: + * + * * **warnings**: The list of warnings that occurred during the update process. + * * **errors**: The list of updater exceptions thrown during individual component updates. + * * **coreError**: True if an exception was thrown while updating core. + * * **deactivatedPlugins**: The list of plugins that were deactivated due to an error in the + * update process. + */ + public function updateComponents($componentsWithUpdateFile) + { + $warnings = array(); + $errors = array(); + $deactivatedPlugins = array(); + $coreError = false; + + if (!empty($componentsWithUpdateFile)) { + $currentAccess = Access::getInstance(); + $hasSuperUserAccess = $currentAccess->hasSuperUserAccess(); + + if (!$hasSuperUserAccess) { + $currentAccess->setSuperUserAccess(true); + } + + // if error in any core update, show message + help message + EXIT + // if errors in any plugins updates, show them on screen, disable plugins that errored + CONTINUE + // if warning in any core update or in any plugins update, show message + CONTINUE + // if no error or warning, success message + CONTINUE + foreach ($componentsWithUpdateFile as $name => $filenames) { + try { + $warnings = array_merge($warnings, $this->update($name)); + } catch (UpdaterErrorException $e) { + $errors[] = $e->getMessage(); + if ($name == 'core') { + $coreError = true; + break; + } elseif (\Piwik\Plugin\Manager::getInstance()->isPluginActivated($name)) { + \Piwik\Plugin\Manager::getInstance()->deactivatePlugin($name); + $deactivatedPlugins[] = $name; + } + } + } + + if (!$hasSuperUserAccess) { + $currentAccess->setSuperUserAccess(false); + } + } + + Filesystem::deleteAllCacheOnUpdate(); + + $result = array( + 'warnings' => $warnings, + 'errors' => $errors, + 'coreError' => $coreError, + 'deactivatedPlugins' => $deactivatedPlugins + ); + + /** + * Triggered after Piwik has been updated. + */ + Piwik::postEvent('CoreUpdater.update.end'); + + return $result; + } + + /** + * Returns any updates that should occur for core and all plugins that are both loaded and + * installed. Also includes updates required for dimensions. + * + * @return string[]|null Returns the result of `getComponentsWithUpdateFile()`. + */ + public function getComponentUpdates() + { + $componentsToCheck = array( + 'core' => Version::VERSION + ); + + $manager = \Piwik\Plugin\Manager::getInstance(); + $plugins = $manager->getLoadedPlugins(); + foreach ($plugins as $pluginName => $plugin) { + if ($manager->isPluginInstalled($pluginName)) { + $componentsToCheck[$pluginName] = $plugin->getVersion(); + } + } + + $columnsVersions = $this->columnsUpdater->getAllVersions($this); + foreach ($columnsVersions as $component => $version) { + $componentsToCheck[$component] = $version; + } + + $componentsWithUpdateFile = $this->getComponentsWithUpdateFile($componentsToCheck); + + if (count($componentsWithUpdateFile) == 0) { + $this->columnsUpdater->onNoUpdateAvailable($columnsVersions); + + if (!$this->hasNewVersion('core')) { + return null; + } + } + + return $componentsWithUpdateFile; + } + + /** + * Execute multiple migration queries from a single Update file. + * + * @param string $file The path to the Updates file. + * @param array $migrationQueries An array mapping SQL queries w/ one or more MySQL errors to ignore. + */ + public function executeMigrationQueries($file, $migrationQueries) + { + foreach ($migrationQueries as $update => $ignoreError) { + $this->executeSingleMigrationQuery($update, $ignoreError, $file); + } + } + + /** + * Execute a single migration query from an update file. + * + * @param string $migrationQuerySql The SQL to execute. + * @param int|int[]|null An optional error code or list of error codes to ignore. + * @param string $file The path to the Updates file. + */ + public function executeSingleMigrationQuery($migrationQuerySql, $errorToIgnore, $file) + { + try { + $this->executeListenerHook('onStartExecutingMigrationQuery', array($file, $migrationQuerySql)); + + Db::exec($migrationQuerySql); + } catch (\Exception $e) { + $this->handleUpdateQueryError($e, $migrationQuerySql, $errorToIgnore, $file); + } + + $this->executeListenerHook('onFinishedExecutingMigrationQuery', array($file, $migrationQuerySql)); + } + + /** + * Handle an update query error. + * + * @param \Exception $e The error that occurred. + * @param string $updateSql The SQL that was executed. + * @param int|int[]|null An optional error code or list of error codes to ignore. + * @param string $file The path to the Updates file. + * @throws \Exception + */ + public function handleUpdateQueryError(\Exception $e, $updateSql, $errorToIgnore, $file) + { + if (($errorToIgnore === false) + || !self::isDbErrorOneOf($e, $errorToIgnore) + ) { + $message = $file . ":\nError trying to execute the query '" . $updateSql . "'.\nThe error was: " . $e->getMessage(); + throw new UpdaterErrorException($message); + } + } + + private function executeListenerHook($hookName, $arguments) + { + foreach ($this->updateObservers as $listener) { + call_user_func_array(array($listener, $hookName), $arguments); + } + } + + private function executeSingleUpdateClass($className) + { + $update = StaticContainer::getContainer()->make($className); + try { + call_user_func(array($update, 'doUpdate'), $this); + } catch (\Exception $e) { + // if an Update file executes PHP statements directly, DB exceptions be handled by executeSingleMigrationQuery, so + // make sure to check for them here + if ($e instanceof Zend_Db_Exception) { + throw new UpdaterErrorException($e->getMessage(), $e->getCode(), $e); + } else { + throw $e; + } + } + } + /** * Performs database update(s) * @@ -284,11 +565,9 @@ class Updater * @param array $sqlarray An array of SQL queries to be executed * @throws UpdaterErrorException */ - static function updateDatabase($file, $sqlarray) + public static function updateDatabase($file, $sqlarray) { - foreach ($sqlarray as $update => $ignoreError) { - self::executeMigrationQuery($update, $ignoreError, $file); - } + self::$activeInstance->executeMigrationQueries($file, $sqlarray); } /** @@ -300,11 +579,7 @@ class Updater */ public static function executeMigrationQuery($updateSql, $errorToIgnore, $file) { - try { - Db::exec($updateSql); - } catch (\Exception $e) { - self::handleQueryError($e, $updateSql, $errorToIgnore, $file); - } + self::$activeInstance->executeSingleMigrationQuery($updateSql, $errorToIgnore, $file); } /** @@ -314,15 +589,61 @@ class Updater * @param string $updateSql Update SQL query. * @param int|false $errorToIgnore A MySQL error code to ignore. * @param string $file The Update file that's calling this method. + * @throws UpdaterErrorException */ public static function handleQueryError($e, $updateSql, $errorToIgnore, $file) { - if (($errorToIgnore === false) - || !Db::get()->isErrNo($e, $errorToIgnore) - ) { - $message = $file . ":\nError trying to execute the query '" . $updateSql . "'.\nThe error was: " . $e->getMessage(); - throw new UpdaterErrorException($message); + self::$activeInstance->handleQueryError($e, $updateSql, $errorToIgnore, $file); + } + + /** + * Record version of successfully completed component update + * + * @param string $name + * @param string $version + */ + public static function recordComponentSuccessfullyUpdated($name, $version) + { + self::$activeInstance->markComponentSuccessfullyUpdated($name, $version); + } + + /** + * Retrieve the current version of a recorded component + * @param string $name + * @return false|string + * @throws \Exception + */ + public static function getCurrentRecordedComponentVersion($name) + { + return self::$activeInstance->getCurrentComponentVersion($name); + } + + /** + * Returns whether an exception is a DB error with a code in the $errorCodesToIgnore list. + * + * @param int $error + * @param int|int[] $errorCodesToIgnore + * @return boolean + */ + public static function isDbErrorOneOf($error, $errorCodesToIgnore) + { + $errorCodesToIgnore = is_array($errorCodesToIgnore) ? $errorCodesToIgnore : array($errorCodesToIgnore); + foreach ($errorCodesToIgnore as $code) { + if (Db::get()->isErrNo($error, $code)) { + return true; + } } + return false; + } + + /** + * Returns the flag name to use in the option table to record current schema version + * @param string $name + * @return string + */ + private static function getNameInOptionTable($name) + { + return 'version_' . $name; } } diff --git a/www/analytics/core/Updater/UpdateObserver.php b/www/analytics/core/Updater/UpdateObserver.php new file mode 100644 index 00000000..e639ecdb --- /dev/null +++ b/www/analytics/core/Updater/UpdateObserver.php @@ -0,0 +1,114 @@ +executeMigrationQueries(__FILE__, $this->getMigrationQueries()); + * } * * @example core/Updates/0.4.2.php */ abstract class Updates { /** - * Return SQL to be executed in this update - * - * @return array( - * 'ALTER .... ' => '1234', // if the query fails, it will be ignored if the error code is 1234 - * 'ALTER .... ' => false, // if an error occurs, the update will stop and fail - * // and user will have to manually run the query - * ) + * @deprecated since v2.12.0 use getMigrationQueries() instead */ - static function getSql() + public static function getSql() { return array(); } /** - * Incremental version update + * @deprecated since v2.12.0 use doUpdate() instead */ - static function update() + public static function update() { } + /** + * Return SQL to be executed in this update. + * + * SQL queries should be defined here, instead of in `doUpdate()`, since this method is used + * in the `core:update` command when displaying the queries an update will run. If you execute + * queries directly in `doUpdate()`, they won't be displayed to the user. + * + * @param Updater $updater + * @return array ``` + * array( + * 'ALTER .... ' => '1234', // if the query fails, it will be ignored if the error code is 1234 + * 'ALTER .... ' => false, // if an error occurs, the update will stop and fail + * // and user will have to manually run the query + * ) + * ``` + * @api + */ + public function getMigrationQueries(Updater $updater) + { + return static::getSql(); + } + + /** + * Perform the incremental version update. + * + * This method should preform all updating logic. If you define queries in an overridden `getMigrationQueries()` + * method, you must call {@link Updater::executeMigrationQueries()} here. + * + * See {@link Updates} for an example. + * + * @param Updater $updater + * @api + */ + public function doUpdate(Updater $updater) + { + static::update(); + } + /** * Tell the updater that this is a major update. * Leads to a more visible notice. * + * NOTE to release manager: Remember to mention in the Changelog + * that this update contains major DB upgrades and will take some time! + * * @return bool */ - static function isMajorUpdate() + public static function isMajorUpdate() { return false; } /** - * Helper method to enable maintenance mode during large updates + * Enables maintenance mode. Should be used for updates where Piwik will be unavailable + * for a large amount of time. */ - static function enableMaintenanceMode() + public static function enableMaintenanceMode() { $config = Config::getInstance(); - $config->init(); $tracker = $config->Tracker; $tracker['record_statistics'] = 0; @@ -67,12 +118,11 @@ abstract class Updates } /** - * Helper method to disable maintenance mode after large updates + * Helper method to disable maintenance mode after large updates. */ - static function disableMaintenanceMode() + public static function disableMaintenanceMode() { $config = Config::getInstance(); - $config->init(); $tracker = $config->Tracker; $tracker['record_statistics'] = 1; @@ -88,7 +138,6 @@ abstract class Updates public static function deletePluginFromConfigFile($pluginToDelete) { $config = Config::getInstance(); - $config->init(); if (isset($config->Plugins['Plugins'])) { $plugins = $config->Plugins['Plugins']; if (($key = array_search($pluginToDelete, $plugins)) !== false) { diff --git a/www/analytics/core/Updates/0.2.10.php b/www/analytics/core/Updates/0.2.10.php index bb3bba97..a7ae47c5 100644 --- a/www/analytics/core/Updates/0.2.10.php +++ b/www/analytics/core/Updates/0.2.10.php @@ -1,6 +1,6 @@ false, + )' => 1050, // 0.1.7 [463] - 'ALTER IGNORE TABLE `' . Common::prefixTable('log_visit') . '` - CHANGE `location_provider` `location_provider` VARCHAR( 100 ) DEFAULT NULL' => '1054', + 'ALTER TABLE `' . Common::prefixTable('log_visit') . '` + CHANGE `location_provider` `location_provider` VARCHAR( 100 ) DEFAULT NULL' => 1054, // 0.1.7 [470] 'ALTER TABLE `' . Common::prefixTable('logger_api_call') . '` CHANGE `parameter_names_default_values` `parameter_names_default_values` TEXT, CHANGE `parameter_values` `parameter_values` TEXT, - CHANGE `returned_value` `returned_value` TEXT' => false, + CHANGE `returned_value` `returned_value` TEXT' => array(1054, 1146), 'ALTER TABLE `' . Common::prefixTable('logger_error') . '` - CHANGE `message` `message` TEXT' => false, + CHANGE `message` `message` TEXT' => array(1054, 1146), 'ALTER TABLE `' . Common::prefixTable('logger_exception') . '` - CHANGE `message` `message` TEXT' => false, + CHANGE `message` `message` TEXT' => array(1054, 1146), 'ALTER TABLE `' . Common::prefixTable('logger_message') . '` - CHANGE `message` `message` TEXT' => false, + CHANGE `message` `message` TEXT' => 1054, // 0.2.2 [489] - 'ALTER IGNORE TABLE `' . Common::prefixTable('site') . '` - CHANGE `feedburnerName` `feedburnerName` VARCHAR( 100 ) DEFAULT NULL' => '1054', + 'ALTER TABLE `' . Common::prefixTable('site') . '` + CHANGE `feedburnerName` `feedburnerName` VARCHAR( 100 ) DEFAULT NULL' => 1054, ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); - - $obsoleteFile = '/plugins/ExamplePlugin/API.php'; - if (file_exists(PIWIK_INCLUDE_PATH . $obsoleteFile)) { - @unlink(PIWIK_INCLUDE_PATH . $obsoleteFile); - } + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); $obsoleteDirectories = array( '/plugins/AdminHome', diff --git a/www/analytics/core/Updates/0.2.12.php b/www/analytics/core/Updates/0.2.12.php index ff8fa18b..de9eb2a0 100644 --- a/www/analytics/core/Updates/0.2.12.php +++ b/www/analytics/core/Updates/0.2.12.php @@ -1,6 +1,6 @@ false, 'ALTER TABLE `' . Common::prefixTable('log_visit') . '` - DROP `config_color_depth`' => false, + DROP `config_color_depth`' => 1091, // 0.2.12 [673] // Note: requires INDEX privilege - 'DROP INDEX index_idaction ON `' . Common::prefixTable('log_action') . '`' => '1091', + 'DROP INDEX index_idaction ON `' . Common::prefixTable('log_action') . '`' => 1091, ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.2.13.php b/www/analytics/core/Updates/0.2.13.php index 93228ad6..6f13d753 100644 --- a/www/analytics/core/Updates/0.2.13.php +++ b/www/analytics/core/Updates/0.2.13.php @@ -1,6 +1,6 @@ false, @@ -27,12 +27,12 @@ class Updates_0_2_13 extends Updates option_value LONGTEXT NOT NULL , autoload TINYINT NOT NULL DEFAULT '1', PRIMARY KEY ( option_name ) - )" => false, + )" => 1050, ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.2.24.php b/www/analytics/core/Updates/0.2.24.php index e627f64f..970f961f 100644 --- a/www/analytics/core/Updates/0.2.24.php +++ b/www/analytics/core/Updates/0.2.24.php @@ -1,6 +1,6 @@ false, + ON ' . Common::prefixTable('log_action') . ' (type, name(15))' => 1072, 'CREATE INDEX index_idsite_date - ON ' . Common::prefixTable('log_visit') . ' (idsite, visit_server_date)' => false, - 'DROP INDEX index_idsite ON ' . Common::prefixTable('log_visit') => false, - 'DROP INDEX index_visit_server_date ON ' . Common::prefixTable('log_visit') => false, + ON ' . Common::prefixTable('log_visit') . ' (idsite, visit_server_date)' => 1072, + 'DROP INDEX index_idsite ON ' . Common::prefixTable('log_visit') => 1091, + 'DROP INDEX index_visit_server_date ON ' . Common::prefixTable('log_visit') => 1091, ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.2.27.php b/www/analytics/core/Updates/0.2.27.php index 4b505194..b18ea37e 100644 --- a/www/analytics/core/Updates/0.2.27.php +++ b/www/analytics/core/Updates/0.2.27.php @@ -1,6 +1,6 @@ false, + ADD `visit_goal_converted` VARCHAR( 1 ) NOT NULL AFTER `visit_total_time`' => 1060, // 0.2.27 [826] - 'ALTER IGNORE TABLE `' . Common::prefixTable('log_visit') . '` - CHANGE `visit_goal_converted` `visit_goal_converted` TINYINT(1) NOT NULL' => false, + 'ALTER TABLE `' . Common::prefixTable('log_visit') . '` + CHANGE `visit_goal_converted` `visit_goal_converted` TINYINT(1) NOT NULL' => 1060, 'CREATE TABLE `' . Common::prefixTable('goal') . "` ( `idsite` int(11) NOT NULL, @@ -38,7 +38,7 @@ class Updates_0_2_27 extends Updates `revenue` float NOT NULL, `deleted` tinyint(4) NOT NULL default '0', PRIMARY KEY (`idsite`,`idgoal`) - )" => false, + )" => 1050, 'CREATE TABLE `' . Common::prefixTable('log_conversion') . '` ( `idvisit` int(10) unsigned NOT NULL, @@ -49,7 +49,6 @@ class Updates_0_2_27 extends Updates `idaction` int(11) NOT NULL, `idlink_va` int(11) NOT NULL, `referer_idvisit` int(10) unsigned default NULL, - `referer_visit_server_date` date default NULL, `referer_type` int(10) unsigned default NULL, `referer_name` varchar(70) default NULL, `referer_keyword` varchar(255) default NULL, @@ -61,21 +60,21 @@ class Updates_0_2_27 extends Updates `revenue` float default NULL, PRIMARY KEY (`idvisit`,`idgoal`), KEY `index_idsite_date` (`idsite`,`visit_server_date`) - )' => false, + )' => 1050, ); $tables = DbHelper::getTablesInstalled(); foreach ($tables as $tableName) { if (preg_match('/archive_/', $tableName) == 1) { - $sqlarray['CREATE INDEX index_all ON ' . $tableName . ' (`idsite`,`date1`,`date2`,`name`,`ts_archived`)'] = false; + $sqlarray['CREATE INDEX index_all ON ' . $tableName . ' (`idsite`,`date1`,`date2`,`name`,`ts_archived`)'] = 1072; } } return $sqlarray; } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.2.32.php b/www/analytics/core/Updates/0.2.32.php index 7f697cc4..9d844224 100644 --- a/www/analytics/core/Updates/0.2.32.php +++ b/www/analytics/core/Updates/0.2.32.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.2.33.php b/www/analytics/core/Updates/0.2.33.php index 0b4d281f..40cb9251 100644 --- a/www/analytics/core/Updates/0.2.33.php +++ b/www/analytics/core/Updates/0.2.33.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.2.34.php b/www/analytics/core/Updates/0.2.34.php deleted file mode 100644 index 49c128f1..00000000 --- a/www/analytics/core/Updates/0.2.34.php +++ /dev/null @@ -1,28 +0,0 @@ -getAllSitesId(); - Cache::regenerateCacheWebsiteAttributes($allSiteIds); - } -} diff --git a/www/analytics/core/Updates/0.2.35.php b/www/analytics/core/Updates/0.2.35.php index 4c0c53ff..01d5854b 100644 --- a/www/analytics/core/Updates/0.2.35.php +++ b/www/analytics/core/Updates/0.2.35.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.2.37.php b/www/analytics/core/Updates/0.2.37.php index 47c8e78d..8fbcbc00 100644 --- a/www/analytics/core/Updates/0.2.37.php +++ b/www/analytics/core/Updates/0.2.37.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.4.1.php b/www/analytics/core/Updates/0.4.1.php index b4562d4f..077e5ef6 100644 --- a/www/analytics/core/Updates/0.4.1.php +++ b/www/analytics/core/Updates/0.4.1.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.4.2.php b/www/analytics/core/Updates/0.4.2.php index 15039367..d567fe04 100644 --- a/www/analytics/core/Updates/0.4.2.php +++ b/www/analytics/core/Updates/0.4.2.php @@ -1,6 +1,6 @@ '1060', + ADD `config_java` TINYINT(1) NOT NULL AFTER `config_flash`' => 1060, 'ALTER TABLE `' . Common::prefixTable('log_visit') . '` - ADD `config_quicktime` TINYINT(1) NOT NULL AFTER `config_director`' => '1060', + ADD `config_quicktime` TINYINT(1) NOT NULL AFTER `config_director`' => 1060, 'ALTER TABLE `' . Common::prefixTable('log_visit') . '` ADD `config_gears` TINYINT(1) NOT NULL AFTER `config_windowsmedia`, - ADD `config_silverlight` TINYINT(1) NOT NULL AFTER `config_gears`' => false, + ADD `config_silverlight` TINYINT(1) NOT NULL AFTER `config_gears`' => 1060, ); } // when restoring (possibly) previousy dropped columns, ignore mysql code error 1060: duplicate column - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.4.4.php b/www/analytics/core/Updates/0.4.4.php index 44cc1690..4aff5254 100644 --- a/www/analytics/core/Updates/0.4.4.php +++ b/www/analytics/core/Updates/0.4.4.php @@ -1,6 +1,6 @@ false, + SET location_ip=location_ip+CAST(POW(2,32) AS UNSIGNED) WHERE location_ip < 0' => false, 'ALTER TABLE `' . Common::prefixTable('log_visit') . '` - CHANGE `location_ip` `location_ip` BIGINT UNSIGNED NOT NULL' => false, + CHANGE `location_ip` `location_ip` BIGINT UNSIGNED NOT NULL' => 1054, 'UPDATE `' . Common::prefixTable('logger_api_call') . '` - SET caller_ip=caller_ip+CAST(POW(2,32) AS UNSIGNED) WHERE caller_ip < 0' => false, + SET caller_ip=caller_ip+CAST(POW(2,32) AS UNSIGNED) WHERE caller_ip < 0' => 1146, 'ALTER TABLE `' . Common::prefixTable('logger_api_call') . '` - CHANGE `caller_ip` `caller_ip` BIGINT UNSIGNED' => false, + CHANGE `caller_ip` `caller_ip` BIGINT UNSIGNED' => 1146, ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.5.4.php b/www/analytics/core/Updates/0.5.4.php index 235391db..59c787c1 100644 --- a/www/analytics/core/Updates/0.5.4.php +++ b/www/analytics/core/Updates/0.5.4.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.5.5.php b/www/analytics/core/Updates/0.5.5.php index aaacf0e9..4334a34d 100644 --- a/www/analytics/core/Updates/0.5.5.php +++ b/www/analytics/core/Updates/0.5.5.php @@ -1,6 +1,6 @@ '1091', - 'CREATE INDEX index_idsite_date_config ON ' . Common::prefixTable('log_visit') . ' (idsite, visit_server_date, config_md5config(8))' => '1061', + 'DROP INDEX index_idsite_date ON ' . Common::prefixTable('log_visit') => 1091, + 'CREATE INDEX index_idsite_date_config ON ' . Common::prefixTable('log_visit') . ' (idsite, visit_server_date, config_md5config(8))' => array(1061,1072), ); $tables = DbHelper::getTablesInstalled(); foreach ($tables as $tableName) { if (preg_match('/archive_/', $tableName) == 1) { - $sqlarray['DROP INDEX index_all ON ' . $tableName] = '1091'; + $sqlarray['DROP INDEX index_all ON ' . $tableName] = 1091; } if (preg_match('/archive_numeric_/', $tableName) == 1) { - $sqlarray['CREATE INDEX index_idsite_dates_period ON ' . $tableName . ' (idsite, date1, date2, period)'] = '1061'; + $sqlarray['CREATE INDEX index_idsite_dates_period ON ' . $tableName . ' (idsite, date1, date2, period)'] = 1061; } } return $sqlarray; } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); - + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.5.php b/www/analytics/core/Updates/0.5.php index f90f7f48..00c6ed7a 100644 --- a/www/analytics/core/Updates/0.5.php +++ b/www/analytics/core/Updates/0.5.php @@ -1,6 +1,6 @@ '1060', - 'ALTER TABLE ' . Common::prefixTable('log_visit') . ' CHANGE visit_exit_idaction visit_exit_idaction_url INTEGER(11) NOT NULL;' => '1054', - 'ALTER TABLE ' . Common::prefixTable('log_visit') . ' CHANGE visit_entry_idaction visit_entry_idaction_url INTEGER(11) NOT NULL;' => '1054', - 'ALTER TABLE ' . Common::prefixTable('log_link_visit_action') . ' CHANGE `idaction_ref` `idaction_url_ref` INTEGER(10) UNSIGNED NOT NULL;' => '1054', - 'ALTER TABLE ' . Common::prefixTable('log_link_visit_action') . ' CHANGE `idaction` `idaction_url` INTEGER(10) UNSIGNED NOT NULL;' => '1054', - 'ALTER TABLE ' . Common::prefixTable('log_link_visit_action') . ' ADD COLUMN `idaction_name` INTEGER(10) UNSIGNED AFTER `idaction_url_ref`;' => '1060', - 'ALTER TABLE ' . Common::prefixTable('log_conversion') . ' CHANGE `idaction` `idaction_url` INTEGER(11) UNSIGNED NOT NULL;' => '1054', + 'ALTER TABLE ' . Common::prefixTable('log_action') . ' ADD COLUMN `hash` INTEGER(10) UNSIGNED NOT NULL AFTER `name`;' => 1060, + 'ALTER TABLE ' . Common::prefixTable('log_visit') . ' CHANGE visit_exit_idaction visit_exit_idaction_url INTEGER(11) NOT NULL;' => 1054, + 'ALTER TABLE ' . Common::prefixTable('log_visit') . ' CHANGE visit_entry_idaction visit_entry_idaction_url INTEGER(11) NOT NULL;' => 1054, + 'ALTER TABLE ' . Common::prefixTable('log_link_visit_action') . ' CHANGE `idaction_ref` `idaction_url_ref` INTEGER(10) UNSIGNED NOT NULL;' => 1054, + 'ALTER TABLE ' . Common::prefixTable('log_link_visit_action') . ' CHANGE `idaction` `idaction_url` INTEGER(10) UNSIGNED NOT NULL;' => 1054, + 'ALTER TABLE ' . Common::prefixTable('log_link_visit_action') . ' ADD COLUMN `idaction_name` INTEGER(10) UNSIGNED AFTER `idaction_url_ref`;' => 1060, + 'ALTER TABLE ' . Common::prefixTable('log_conversion') . ' CHANGE `idaction` `idaction_url` INTEGER(11) UNSIGNED NOT NULL;' => 1054, 'UPDATE ' . Common::prefixTable('log_action') . ' SET `hash` = CRC32(name);' => false, - 'CREATE INDEX index_type_hash ON ' . Common::prefixTable('log_action') . ' (type, hash);' => '1061', - 'DROP INDEX index_type_name ON ' . Common::prefixTable('log_action') . ';' => '1091', + 'CREATE INDEX index_type_hash ON ' . Common::prefixTable('log_action') . ' (type, hash);' => 1061, + 'DROP INDEX index_type_name ON ' . Common::prefixTable('log_action') . ';' => 1091, ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.6-rc1.php b/www/analytics/core/Updates/0.6-rc1.php index 43ab8e4e..1c6a4f31 100644 --- a/www/analytics/core/Updates/0.6-rc1.php +++ b/www/analytics/core/Updates/0.6-rc1.php @@ -1,6 +1,6 @@ false, - 'ALTER TABLE ' . Common::prefixTable('site') . ' CHANGE ts_created ts_created TIMESTAMP NULL' => false, - 'ALTER TABLE ' . Common::prefixTable('site') . ' ADD `timezone` VARCHAR( 50 ) NOT NULL AFTER `ts_created` ;' => false, - 'UPDATE ' . Common::prefixTable('site') . ' SET `timezone` = "' . $defaultTimezone . '";' => false, - 'ALTER TABLE ' . Common::prefixTable('site') . ' ADD currency CHAR( 3 ) NOT NULL AFTER `timezone` ;' => false, - 'UPDATE ' . Common::prefixTable('site') . ' SET `currency` = "' . $defaultCurrency . '";' => false, - 'ALTER TABLE ' . Common::prefixTable('site') . ' ADD `excluded_ips` TEXT NOT NULL AFTER `currency` ;' => false, - 'ALTER TABLE ' . Common::prefixTable('site') . ' ADD excluded_parameters VARCHAR( 255 ) NOT NULL AFTER `excluded_ips` ;' => false, - 'ALTER TABLE ' . Common::prefixTable('log_visit') . ' ADD INDEX `index_idsite_datetime_config` ( `idsite` , `visit_last_action_time` , `config_md5config` ( 8 ) ) ;' => false, - 'ALTER TABLE ' . Common::prefixTable('log_visit') . ' ADD INDEX index_idsite_idvisit (idsite, idvisit) ;' => false, - 'ALTER TABLE ' . Common::prefixTable('log_conversion') . ' DROP INDEX index_idsite_date' => false, - 'ALTER TABLE ' . Common::prefixTable('log_conversion') . ' DROP visit_server_date;' => false, - 'ALTER TABLE ' . Common::prefixTable('log_conversion') . ' ADD INDEX index_idsite_datetime ( `idsite` , `server_time` )' => false, + 'ALTER TABLE ' . Common::prefixTable('user') . ' CHANGE date_registered date_registered TIMESTAMP NULL' => 1054, + 'ALTER TABLE ' . Common::prefixTable('site') . ' CHANGE ts_created ts_created TIMESTAMP NULL' => 1054, + 'ALTER TABLE ' . Common::prefixTable('site') . ' ADD `timezone` VARCHAR( 50 ) NOT NULL AFTER `ts_created` ;' => 1060, + 'UPDATE ' . Common::prefixTable('site') . ' SET `timezone` = "' . $defaultTimezone . '";' => 1060, + 'ALTER TABLE ' . Common::prefixTable('site') . ' ADD currency CHAR( 3 ) NOT NULL AFTER `timezone` ;' => 1060, + 'UPDATE ' . Common::prefixTable('site') . ' SET `currency` = "' . $defaultCurrency . '";' => 1060, + 'ALTER TABLE ' . Common::prefixTable('site') . ' ADD `excluded_ips` TEXT NOT NULL AFTER `currency` ;' => 1060, + 'ALTER TABLE ' . Common::prefixTable('site') . ' ADD excluded_parameters VARCHAR( 255 ) NOT NULL AFTER `excluded_ips` ;' => 1060, + 'ALTER TABLE ' . Common::prefixTable('log_visit') . ' ADD INDEX `index_idsite_datetime_config` ( `idsite` , `visit_last_action_time` , `config_md5config` ( 8 ) ) ;' => array(1061, 1072), + 'ALTER TABLE ' . Common::prefixTable('log_visit') . ' ADD INDEX index_idsite_idvisit (idsite, idvisit) ;' => array(1061, 1072), + 'ALTER TABLE ' . Common::prefixTable('log_conversion') . ' DROP INDEX index_idsite_date' => 1091, + 'ALTER TABLE ' . Common::prefixTable('log_conversion') . ' DROP visit_server_date;' => 1091, + 'ALTER TABLE ' . Common::prefixTable('log_conversion') . ' ADD INDEX index_idsite_datetime ( `idsite` , `server_time` )' => array(1072, 1061), ); } - static function update() + public function doUpdate(Updater $updater) { // first we disable the plugins and keep an array of warnings messages $pluginsToDisableMessage = array( - 'SearchEnginePosition' => "SearchEnginePosition plugin was disabled, because it is not compatible with the new Piwik 0.6. \n You can download the latest version of the plugin, compatible with Piwik 0.6.\nClick here.", - 'GeoIP' => "GeoIP plugin was disabled, because it is not compatible with the new Piwik 0.6. \nYou can download the latest version of the plugin, compatible with Piwik 0.6.\nClick here." + 'SearchEnginePosition' => "SearchEnginePosition plugin was disabled, because it is not compatible with the new Piwik 0.6. \n You can download the latest version of the plugin, compatible with Piwik 0.6.\nClick here.", + 'GeoIP' => "GeoIP plugin was disabled, because it is not compatible with the new Piwik 0.6. \nYou can download the latest version of the plugin, compatible with Piwik 0.6.\nClick here." ); $disabledPlugins = array(); foreach ($pluginsToDisableMessage as $pluginToDisable => $warningMessage) { @@ -54,7 +54,7 @@ class Updates_0_6_rc1 extends Updates } // Run the SQL - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); // Outputs warning message, pointing users to the plugin download page if (!empty($disabledPlugins)) { diff --git a/www/analytics/core/Updates/0.6.2.php b/www/analytics/core/Updates/0.6.2.php deleted file mode 100644 index 15236ed0..00000000 --- a/www/analytics/core/Updates/0.6.2.php +++ /dev/null @@ -1,47 +0,0 @@ -getAllSitesId(); - Cache::regenerateCacheWebsiteAttributes($allSiteIds); - } -} diff --git a/www/analytics/core/Updates/0.6.3.php b/www/analytics/core/Updates/0.6.3.php index 87ef2d27..7716fb7c 100644 --- a/www/analytics/core/Updates/0.6.3.php +++ b/www/analytics/core/Updates/0.6.3.php @@ -1,6 +1,6 @@ false, + CHANGE `location_ip` `location_ip` INT UNSIGNED NOT NULL' => 1054, 'ALTER TABLE `' . Common::prefixTable('logger_api_call') . '` - CHANGE `caller_ip` `caller_ip` INT UNSIGNED' => false, + CHANGE `caller_ip` `caller_ip` INT UNSIGNED' => array(1054, 1146), ); } - static function update() + public function doUpdate(Updater $updater) { $config = Config::getInstance(); $dbInfos = $config->database; @@ -44,6 +44,6 @@ class Updates_0_6_3 extends Updates } } - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.7.php b/www/analytics/core/Updates/0.7.php index 3c6daad6..212ab2d1 100644 --- a/www/analytics/core/Updates/0.7.php +++ b/www/analytics/core/Updates/0.7.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/0.9.1.php b/www/analytics/core/Updates/0.9.1.php index 5b11099b..323ef219 100644 --- a/www/analytics/core/Updates/0.9.1.php +++ b/www/analytics/core/Updates/0.9.1.php @@ -1,6 +1,6 @@ false, 'UPDATE `' . Common::prefixTable('option') . '` - SET option_value = "UTC" - WHERE option_name = "SitesManager_DefaultTimezone" + SET option_value = "UTC" + WHERE option_name = "SitesManager_DefaultTimezone" AND option_value IN (' . $timezoneList . ')' => false, ); } - static function update() + public function doUpdate(Updater $updater) { if (SettingsServer::isTimezoneSupportEnabled()) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } } diff --git a/www/analytics/core/Updates/1.1.php b/www/analytics/core/Updates/1.1.php index df58645c..e977099d 100644 --- a/www/analytics/core/Updates/1.1.php +++ b/www/analytics/core/Updates/1.1.php @@ -1,6 +1,6 @@ activatePlugin('MobileMessaging'); diff --git a/www/analytics/core/Updates/1.10.1.php b/www/analytics/core/Updates/1.10.1.php index e6051a7b..d6cc58b1 100755 --- a/www/analytics/core/Updates/1.10.1.php +++ b/www/analytics/core/Updates/1.10.1.php @@ -1,6 +1,6 @@ activatePlugin('Overlay'); diff --git a/www/analytics/core/Updates/1.10.2-b1.php b/www/analytics/core/Updates/1.10.2-b1.php index bf58008b..fcf35d96 100755 --- a/www/analytics/core/Updates/1.10.2-b1.php +++ b/www/analytics/core/Updates/1.10.2-b1.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/1.10.2-b2.php b/www/analytics/core/Updates/1.10.2-b2.php index de177432..9a2b1c34 100644 --- a/www/analytics/core/Updates/1.10.2-b2.php +++ b/www/analytics/core/Updates/1.10.2-b2.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/1.11-b1.php b/www/analytics/core/Updates/1.11-b1.php index dc118714..ffc222ad 100644 --- a/www/analytics/core/Updates/1.11-b1.php +++ b/www/analytics/core/Updates/1.11-b1.php @@ -1,6 +1,6 @@ activatePlugin('UserCountryMap'); diff --git a/www/analytics/core/Updates/1.12-b1.php b/www/analytics/core/Updates/1.12-b1.php index 5318a7cb..9c2271be 100644 --- a/www/analytics/core/Updates/1.12-b1.php +++ b/www/analytics/core/Updates/1.12-b1.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } - } diff --git a/www/analytics/core/Updates/1.12-b15.php b/www/analytics/core/Updates/1.12-b15.php index 96a0696c..8769f981 100644 --- a/www/analytics/core/Updates/1.12-b15.php +++ b/www/analytics/core/Updates/1.12-b15.php @@ -1,6 +1,6 @@ activatePlugin('SegmentEditor'); diff --git a/www/analytics/core/Updates/1.12-b16.php b/www/analytics/core/Updates/1.12-b16.php index d1090674..26fde0ba 100644 --- a/www/analytics/core/Updates/1.12-b16.php +++ b/www/analytics/core/Updates/1.12-b16.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/1.2-rc1.php b/www/analytics/core/Updates/1.2-rc1.php index cb46b767..db09aae1 100644 --- a/www/analytics/core/Updates/1.2-rc1.php +++ b/www/analytics/core/Updates/1.2-rc1.php @@ -1,6 +1,6 @@ array(1054, 1091), + 'ALTER TABLE `' . Common::prefixTable('log_visit') . '` ADD `visit_entry_idaction_name` INT UNSIGNED NOT NULL AFTER `visit_entry_idaction_url`, ADD `visit_exit_idaction_name` INT UNSIGNED NOT NULL AFTER `visit_exit_idaction_url`, - CHANGE `visit_exit_idaction_url` `visit_exit_idaction_url` INT UNSIGNED NOT NULL, + CHANGE `visit_exit_idaction_url` `visit_exit_idaction_url` INT UNSIGNED NOT NULL, CHANGE `visit_entry_idaction_url` `visit_entry_idaction_url` INT UNSIGNED NOT NULL, CHANGE `referer_type` `referer_type` TINYINT UNSIGNED NULL DEFAULT NULL, - ADD `idvisitor` BINARY(8) NOT NULL AFTER `idsite`, ADD visitor_count_visits SMALLINT(5) UNSIGNED NOT NULL AFTER `visitor_returning`, ADD visitor_days_since_last SMALLINT(5) UNSIGNED NOT NULL, - ADD visitor_days_since_first SMALLINT(5) UNSIGNED NOT NULL, - ADD `config_id` BINARY(8) NOT NULL AFTER `config_md5config` - ' => false, + ADD visitor_days_since_first SMALLINT(5) UNSIGNED NOT NULL + ' => 1060, 'ALTER TABLE `' . Common::prefixTable('log_visit') . '` ADD custom_var_k1 VARCHAR(100) DEFAULT NULL, ADD custom_var_v1 VARCHAR(100) DEFAULT NULL, @@ -49,19 +51,23 @@ class Updates_1_2_rc1 extends Updates ADD custom_var_v5 VARCHAR(100) DEFAULT NULL ' => 1060, 'ALTER TABLE `' . Common::prefixTable('log_link_visit_action') . '` - ADD `idsite` INT( 10 ) UNSIGNED NOT NULL AFTER `idlink_va` , - ADD `server_time` DATETIME AFTER `idsite`, + ADD `idsite` INT( 10 ) UNSIGNED NOT NULL AFTER `idlink_va` , ADD `idvisitor` BINARY(8) NOT NULL AFTER `idsite`, - ADD `idaction_name_ref` INT UNSIGNED NOT NULL AFTER `idaction_name`, + ADD `idaction_name_ref` INT UNSIGNED NOT NULL AFTER `idaction_name` + ' => 1060, + 'ALTER TABLE `' . Common::prefixTable('log_link_visit_action') . '` + ADD `server_time` DATETIME AFTER `idsite`, ADD INDEX `index_idsite_servertime` ( `idsite` , `server_time` ) - ' => false, + ' => 1060, 'ALTER TABLE `' . Common::prefixTable('log_conversion') . '` DROP `referer_idvisit`, - ADD `idvisitor` BINARY(8) NOT NULL AFTER `idsite`, + ADD `idvisitor` BINARY(8) NOT NULL AFTER `idsite` + ' => array(1060, 1091), + 'ALTER TABLE `' . Common::prefixTable('log_conversion') . '` ADD visitor_count_visits SMALLINT(5) UNSIGNED NOT NULL, ADD visitor_days_since_first SMALLINT(5) UNSIGNED NOT NULL - ' => false, + ' => 1060, 'ALTER TABLE `' . Common::prefixTable('log_conversion') . '` ADD custom_var_k1 VARCHAR(100) DEFAULT NULL, ADD custom_var_v1 VARCHAR(100) DEFAULT NULL, @@ -73,36 +79,36 @@ class Updates_1_2_rc1 extends Updates ADD custom_var_v4 VARCHAR(100) DEFAULT NULL, ADD custom_var_k5 VARCHAR(100) DEFAULT NULL, ADD custom_var_v5 VARCHAR(100) DEFAULT NULL - ' => 1060, + ' => array(1060, 1061), // Migrate 128bits IDs inefficiently stored as 8bytes (256 bits) into 64bits 'UPDATE ' . Common::prefixTable('log_visit') . ' SET idvisitor = binary(unhex(substring(visitor_idcookie,1,16))), config_id = binary(unhex(substring(config_md5config,1,16))) - ' => false, + ' => 1054, 'UPDATE ' . Common::prefixTable('log_conversion') . ' SET idvisitor = binary(unhex(substring(visitor_idcookie,1,16))) - ' => false, + ' => 1054, // Drop migrated fields 'ALTER TABLE `' . Common::prefixTable('log_visit') . '` - DROP visitor_idcookie, + DROP visitor_idcookie, DROP config_md5config - ' => false, + ' => 1091, 'ALTER TABLE `' . Common::prefixTable('log_conversion') . '` DROP visitor_idcookie - ' => false, + ' => 1091, // Recreate INDEX on new field 'ALTER TABLE `' . Common::prefixTable('log_visit') . '` ADD INDEX `index_idsite_datetime_config` (idsite, visit_last_action_time, config_id) - ' => false, + ' => 1061, // Backfill action logs as best as we can 'UPDATE ' . Common::prefixTable('log_link_visit_action') . ' as action, ' . Common::prefixTable('log_visit') . ' as visit - SET action.idsite = visit.idsite, - action.server_time = visit.visit_last_action_time, + SET action.idsite = visit.idsite, + action.server_time = visit.visit_last_action_time, action.idvisitor = visit.idvisitor WHERE action.idvisit=visit.idvisit ' => false, @@ -112,18 +118,18 @@ class Updates_1_2_rc1 extends Updates ' => false, // New index used max once per request, in case this table grows significantly in the future - 'ALTER TABLE `' . Common::prefixTable('option') . '` ADD INDEX ( `autoload` ) ' => false, + 'ALTER TABLE `' . Common::prefixTable('option') . '` ADD INDEX ( `autoload` ) ' => 1061, // new field for websites - 'ALTER TABLE `' . Common::prefixTable('site') . '` ADD `group` VARCHAR( 250 ) NOT NULL' => false, + 'ALTER TABLE `' . Common::prefixTable('site') . '` ADD `group` VARCHAR( 250 ) NOT NULL' => 1060, ); } - static function update() + public function doUpdate(Updater $updater) { // first we disable the plugins and keep an array of warnings messages $pluginsToDisableMessage = array( - 'GeoIP' => "GeoIP plugin was disabled, because it is not compatible with the new Piwik 1.2. \nYou can download the latest version of the plugin, compatible with Piwik 1.2.\nClick here.", + 'GeoIP' => "GeoIP plugin was disabled, because it is not compatible with the new Piwik 1.2. \nYou can download the latest version of the plugin, compatible with Piwik 1.2.\nClick here.", 'EntryPage' => "EntryPage plugin is not compatible with this version of Piwik, it was disabled.", ); $disabledPlugins = array(); @@ -135,7 +141,7 @@ class Updates_1_2_rc1 extends Updates } // Run the SQL - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); // Outputs warning message, pointing users to the plugin download page if (!empty($disabledPlugins)) { @@ -144,7 +150,5 @@ class Updates_1_2_rc1 extends Updates implode('
  • ', $disabledPlugins) . "
  • "); } - } } - diff --git a/www/analytics/core/Updates/1.2-rc2.php b/www/analytics/core/Updates/1.2-rc2.php index 02cb3208..3d15a5a5 100644 --- a/www/analytics/core/Updates/1.2-rc2.php +++ b/www/analytics/core/Updates/1.2-rc2.php @@ -1,6 +1,6 @@ activatePlugin('CustomVariables'); @@ -23,4 +24,3 @@ class Updates_1_2_rc2 extends Updates } } } - diff --git a/www/analytics/core/Updates/1.2.3.php b/www/analytics/core/Updates/1.2.3.php index cf5c4559..3f640d20 100644 --- a/www/analytics/core/Updates/1.2.3.php +++ b/www/analytics/core/Updates/1.2.3.php @@ -1,6 +1,6 @@ database['dbname'] . '` DEFAULT CHARACTER SET utf8' => false, + 'ALTER DATABASE `' . Config::getInstance()->database['dbname'] . '` DEFAULT CHARACTER SET utf8' => false, // Various performance improvements schema updates 'ALTER TABLE `' . Common::prefixTable('log_visit') . '` DROP INDEX index_idsite_datetime_config, DROP INDEX index_idsite_idvisit, ADD INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time), - ADD INDEX index_idsite_datetime (idsite, visit_last_action_time)' => false, + ADD INDEX index_idsite_datetime (idsite, visit_last_action_time)' => array(1061, 1091), ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } - diff --git a/www/analytics/core/Updates/1.2.5-rc1.php b/www/analytics/core/Updates/1.2.5-rc1.php index 09c4dea6..d58c772c 100644 --- a/www/analytics/core/Updates/1.2.5-rc1.php +++ b/www/analytics/core/Updates/1.2.5-rc1.php @@ -1,6 +1,6 @@ false, + ADD `allow_multiple` tinyint(4) NOT NULL AFTER case_sensitive' => 1060, 'ALTER TABLE `' . Common::prefixTable('log_conversion') . '` ADD buster int unsigned NOT NULL AFTER revenue, DROP PRIMARY KEY, - ADD PRIMARY KEY (idvisit, idgoal, buster)' => false, + ADD PRIMARY KEY (idvisit, idgoal, buster)' => 1060, ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } - diff --git a/www/analytics/core/Updates/1.2.5-rc7.php b/www/analytics/core/Updates/1.2.5-rc7.php index c4a51afc..79c5b0a5 100644 --- a/www/analytics/core/Updates/1.2.5-rc7.php +++ b/www/analytics/core/Updates/1.2.5-rc7.php @@ -1,6 +1,6 @@ false, + ADD INDEX index_idsite_idvisitor (idsite, idvisitor)' => 1061, ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } - - diff --git a/www/analytics/core/Updates/1.4-rc1.php b/www/analytics/core/Updates/1.4-rc1.php index d8cc1397..6218f3cf 100644 --- a/www/analytics/core/Updates/1.4-rc1.php +++ b/www/analytics/core/Updates/1.4-rc1.php @@ -1,6 +1,6 @@ '42S22', + SET format = "pdf"' => '42S22', 'ALTER TABLE `' . Common::prefixTable('pdf') . '` ADD COLUMN `format` VARCHAR(10)' => '42S22', ); } - static function update() + public function doUpdate(Updater $updater) { try { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } catch (\Exception $e) { } } diff --git a/www/analytics/core/Updates/1.4-rc2.php b/www/analytics/core/Updates/1.4-rc2.php index 1f2ba56b..aa0b12cc 100644 --- a/www/analytics/core/Updates/1.4-rc2.php +++ b/www/analytics/core/Updates/1.4-rc2.php @@ -1,6 +1,6 @@ false, + "SET sql_mode=''" => false, // this converts the 32-bit UNSIGNED INT column to a 16 byte VARBINARY; // _but_ MySQL does string conversion! (e.g., integer 1 is converted to 49 -- the ASCII code for "1") 'ALTER TABLE ' . Common::prefixTable('log_visit') . ' MODIFY location_ip VARBINARY(16) NOT NULL' => false, 'ALTER TABLE ' . Common::prefixTable('logger_api_call') . ' - MODIFY caller_ip VARBINARY(16) NOT NULL' => false, + MODIFY caller_ip VARBINARY(16) NOT NULL' => 1146, // fortunately, 2^32 is 10 digits long and fits in the VARBINARY(16) without truncation; // to fix this, we cast to an integer, convert to hex, pad out leading zeros, and unhex it 'UPDATE ' . Common::prefixTable('log_visit') . " SET location_ip = UNHEX(LPAD(HEX(CONVERT(location_ip, UNSIGNED)), 8, '0'))" => false, 'UPDATE ' . Common::prefixTable('logger_api_call') . " - SET caller_ip = UNHEX(LPAD(HEX(CONVERT(caller_ip, UNSIGNED)), 8, '0'))" => false, + SET caller_ip = UNHEX(LPAD(HEX(CONVERT(caller_ip, UNSIGNED)), 8, '0'))" => 1146, ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/1.5-b1.php b/www/analytics/core/Updates/1.5-b1.php index 0ccd81e0..e481d1e8 100644 --- a/www/analytics/core/Updates/1.5-b1.php +++ b/www/analytics/core/Updates/1.5-b1.php @@ -1,6 +1,6 @@ false, + ) DEFAULT CHARSET=utf8 ' => 1050, - 'ALTER IGNORE TABLE `' . Common::prefixTable('log_visit') . '` + 'ALTER TABLE `' . Common::prefixTable('log_visit') . '` ADD visitor_days_since_order SMALLINT(5) UNSIGNED NOT NULL AFTER visitor_days_since_last, - ADD visit_goal_buyer TINYINT(1) NOT NULL AFTER visit_goal_converted' => false, + ADD visit_goal_buyer TINYINT(1) NOT NULL AFTER visit_goal_converted' => 1060, - 'ALTER IGNORE TABLE `' . Common::prefixTable('log_conversion') . '` - ADD visitor_days_since_order SMALLINT(5) UNSIGNED NOT NULL AFTER visitor_days_since_first, + 'ALTER TABLE `' . $logConversionTable . '` + ADD visitor_days_since_order SMALLINT(5) UNSIGNED NOT NULL AFTER visitor_days_since_first' => 1060, + 'ALTER TABLE `' . $logConversionTable . '` ADD idorder varchar(100) default NULL AFTER buster, ADD items SMALLINT UNSIGNED DEFAULT NULL, ADD revenue_subtotal float default NULL, ADD revenue_tax float default NULL, ADD revenue_shipping float default NULL, ADD revenue_discount float default NULL, - ADD UNIQUE KEY unique_idsite_idorder (idsite, idorder), - MODIFY idgoal int(10) NOT NULL' => false, + MODIFY idgoal int(10) NOT NULL' => 1060, + 'ALTER TABLE `' . Common::prefixTable('log_conversion') . '` + ADD UNIQUE KEY unique_idsite_idorder (idsite, idorder)' => 1061, ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/1.5-b2.php b/www/analytics/core/Updates/1.5-b2.php index 00c748f8..affb2e61 100644 --- a/www/analytics/core/Updates/1.5-b2.php +++ b/www/analytics/core/Updates/1.5-b2.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/1.5-b3.php b/www/analytics/core/Updates/1.5-b3.php index 781056db..02a8fd16 100644 --- a/www/analytics/core/Updates/1.5-b3.php +++ b/www/analytics/core/Updates/1.5-b3.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/1.5-b4.php b/www/analytics/core/Updates/1.5-b4.php index 32c37bf2..359dd55b 100644 --- a/www/analytics/core/Updates/1.5-b4.php +++ b/www/analytics/core/Updates/1.5-b4.php @@ -1,6 +1,6 @@ false, + ADD ecommerce TINYINT DEFAULT 0' => 1060, ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/1.5-b5.php b/www/analytics/core/Updates/1.5-b5.php index 5e4b10fa..d79f18b3 100644 --- a/www/analytics/core/Updates/1.5-b5.php +++ b/www/analytics/core/Updates/1.5-b5.php @@ -1,6 +1,6 @@ false, + ) DEFAULT CHARSET=utf8' => 1050, ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/1.5-rc6.php b/www/analytics/core/Updates/1.5-rc6.php index 56e87e64..9a588745 100644 --- a/www/analytics/core/Updates/1.5-rc6.php +++ b/www/analytics/core/Updates/1.5-rc6.php @@ -1,6 +1,6 @@ activatePlugin('PrivacyManager'); @@ -23,4 +24,3 @@ class Updates_1_5_rc6 extends Updates } } } - diff --git a/www/analytics/core/Updates/1.6-b1.php b/www/analytics/core/Updates/1.6-b1.php index 2b73ed95..69e9e724 100644 --- a/www/analytics/core/Updates/1.6-b1.php +++ b/www/analytics/core/Updates/1.6-b1.php @@ -1,6 +1,6 @@ false, + ADD idaction_category5 INTEGER(10) UNSIGNED NOT NULL' => 1060, 'ALTER TABLE `' . Common::prefixTable('log_visit') . '` CHANGE custom_var_k1 custom_var_k1 VARCHAR(200) DEFAULT NULL, CHANGE custom_var_v1 custom_var_v1 VARCHAR(200) DEFAULT NULL, @@ -61,8 +61,8 @@ class Updates_1_6_b1 extends Updates ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/1.6-rc1.php b/www/analytics/core/Updates/1.6-rc1.php index 8255786c..44e1dab8 100644 --- a/www/analytics/core/Updates/1.6-rc1.php +++ b/www/analytics/core/Updates/1.6-rc1.php @@ -1,6 +1,6 @@ activatePlugin('ImageGraph'); @@ -23,4 +24,3 @@ class Updates_1_6_rc1 extends Updates } } } - diff --git a/www/analytics/core/Updates/1.7-b1.php b/www/analytics/core/Updates/1.7-b1.php index 04e45c9c..fb10fca8 100644 --- a/www/analytics/core/Updates/1.7-b1.php +++ b/www/analytics/core/Updates/1.7-b1.php @@ -1,6 +1,6 @@ false, + ADD COLUMN `aggregate_reports_format` TINYINT(1) NOT NULL AFTER `reports`' => 1060, 'UPDATE `' . Common::prefixTable('pdf') . '` SET `aggregate_reports_format` = 1' => false, ); } - static function update() + public function doUpdate(Updater $updater) { try { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } catch (\Exception $e) { } } diff --git a/www/analytics/core/Updates/1.7.2-rc5.php b/www/analytics/core/Updates/1.7.2-rc5.php index 19388cc9..69359d95 100644 --- a/www/analytics/core/Updates/1.7.2-rc5.php +++ b/www/analytics/core/Updates/1.7.2-rc5.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } catch (\Exception $e) { } } diff --git a/www/analytics/core/Updates/1.7.2-rc7.php b/www/analytics/core/Updates/1.7.2-rc7.php index ac1871de..0a731d21 100755 --- a/www/analytics/core/Updates/1.7.2-rc7.php +++ b/www/analytics/core/Updates/1.7.2-rc7.php @@ -1,6 +1,6 @@ false, + ADD `name` VARCHAR( 100 ) NULL DEFAULT NULL AFTER `iddashboard`' => 1060, ); } - static function update() + public function doUpdate(Updater $updater) { try { $dashboards = Db::fetchAll('SELECT * FROM `' . Common::prefixTable('user_dashboard') . '`'); - foreach ($dashboards AS $dashboard) { + foreach ($dashboards as $dashboard) { $idDashboard = $dashboard['iddashboard']; $login = $dashboard['login']; $layout = $dashboard['layout']; @@ -38,7 +38,7 @@ class Updates_1_7_2_rc7 extends Updates $layout = str_replace("\\\"", "\"", $layout); Db::query('UPDATE `' . Common::prefixTable('user_dashboard') . '` SET layout = ? WHERE iddashboard = ? AND login = ?', array($layout, $idDashboard, $login)); } - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } catch (\Exception $e) { } } diff --git a/www/analytics/core/Updates/1.8.3-b1.php b/www/analytics/core/Updates/1.8.3-b1.php index 85f9f669..fb2bfaa4 100644 --- a/www/analytics/core/Updates/1.8.3-b1.php +++ b/www/analytics/core/Updates/1.8.3-b1.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); if (!\Piwik\Plugin\Manager::getInstance()->isPluginLoaded('ScheduledReports')) { return; } @@ -60,8 +60,7 @@ class Updates_1_8_3_b1 extends Updates // - delete Common::prefixTable('pdf') $reports = Db::fetchAll('SELECT * FROM `' . Common::prefixTable('pdf') . '`'); - foreach ($reports AS $report) { - + foreach ($reports as $report) { $idreport = $report['idreport']; $idsite = $report['idsite']; $login = $report['login']; @@ -98,8 +97,8 @@ class Updates_1_8_3_b1 extends Updates is_null($period) ? ScheduledReports::DEFAULT_PERIOD : $period, ScheduledReports::EMAIL_TYPE, is_null($format) ? ScheduledReports::DEFAULT_REPORT_FORMAT : $format, - Common::json_encode(preg_split('/,/', $reports)), - Common::json_encode($parameters), + json_encode(preg_split('/,/', $reports)), + json_encode($parameters), $ts_created, $ts_last_sent, $deleted @@ -110,6 +109,5 @@ class Updates_1_8_3_b1 extends Updates Db::query('DROP TABLE `' . Common::prefixTable('pdf') . '`'); } catch (\Exception $e) { } - } } diff --git a/www/analytics/core/Updates/1.8.4-b1.php b/www/analytics/core/Updates/1.8.4-b1.php index 97e70daa..05fa418e 100644 --- a/www/analytics/core/Updates/1.8.4-b1.php +++ b/www/analytics/core/Updates/1.8.4-b1.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); self::disableMaintenanceMode(); } catch (\Exception $e) { self::disableMaintenanceMode(); diff --git a/www/analytics/core/Updates/1.9-b16.php b/www/analytics/core/Updates/1.9-b16.php index 3c82828d..ccb324e1 100755 --- a/www/analytics/core/Updates/1.9-b16.php +++ b/www/analytics/core/Updates/1.9-b16.php @@ -1,6 +1,6 @@ false, - 'ALTER TABLE `' . Common::prefixTable('log_visit') . '` ADD visit_total_searches SMALLINT(5) UNSIGNED NOT NULL AFTER `visit_total_actions`' => 1060, @@ -46,9 +45,8 @@ class Updates_1_9_b16 extends Updates ); } - static function update() + public function doUpdate(Updater $updater) { - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } - diff --git a/www/analytics/core/Updates/1.9-b19.php b/www/analytics/core/Updates/1.9-b19.php index d2496cbb..5e6a67c3 100755 --- a/www/analytics/core/Updates/1.9-b19.php +++ b/www/analytics/core/Updates/1.9-b19.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); try { \Piwik\Plugin\Manager::getInstance()->activatePlugin('Transitions'); @@ -40,4 +39,3 @@ class Updates_1_9_b19 extends Updates } } } - diff --git a/www/analytics/core/Updates/1.9-b9.php b/www/analytics/core/Updates/1.9-b9.php index dce8c9e2..84c8abf3 100755 --- a/www/analytics/core/Updates/1.9-b9.php +++ b/www/analytics/core/Updates/1.9-b9.php @@ -1,6 +1,6 @@ 1091, + "ALTER TABLE `$logVisit` $dropColumns" => 1091, + "ALTER TABLE `$logConversion` $dropColumns" => 1091, + + // add geoip columns to log_visit + "ALTER TABLE `$logVisit` $addColumns" => 1060, // add geoip columns to log_conversion - "ALTER TABLE `$logConversion` $addColumns" => 1091, + "ALTER TABLE `$logConversion` $addColumns" => 1060, ); } - static function update() + public function doUpdate(Updater $updater) { try { self::enableMaintenanceMode(); - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); self::disableMaintenanceMode(); } catch (\Exception $e) { self::disableMaintenanceMode(); @@ -54,4 +57,3 @@ class Updates_1_9_b9 extends Updates } } } - diff --git a/www/analytics/core/Updates/1.9.1-b2.php b/www/analytics/core/Updates/1.9.1-b2.php index d79dd992..794cd518 100644 --- a/www/analytics/core/Updates/1.9.1-b2.php +++ b/www/analytics/core/Updates/1.9.1-b2.php @@ -1,6 +1,6 @@ 1091 ); } - static function update() + public function doUpdate(Updater $updater) { // manually remove ExampleFeedburner column - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); // remove ExampleFeedburner plugin $pluginToDelete = 'ExampleFeedburner'; diff --git a/www/analytics/core/Updates/1.9.3-b10.php b/www/analytics/core/Updates/1.9.3-b10.php index 22583103..0a64fe6a 100755 --- a/www/analytics/core/Updates/1.9.3-b10.php +++ b/www/analytics/core/Updates/1.9.3-b10.php @@ -1,6 +1,6 @@ activatePlugin('Annotations'); diff --git a/www/analytics/core/Updates/1.9.3-b3.php b/www/analytics/core/Updates/1.9.3-b3.php index 0a511b14..77dcb2bb 100644 --- a/www/analytics/core/Updates/1.9.3-b3.php +++ b/www/analytics/core/Updates/1.9.3-b3.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/2.0-a12.php b/www/analytics/core/Updates/2.0-a12.php index cc202d4e..97a1087c 100644 --- a/www/analytics/core/Updates/2.0-a12.php +++ b/www/analytics/core/Updates/2.0-a12.php @@ -1,6 +1,6 @@ false @@ -40,9 +40,9 @@ class Updates_2_0_a12 extends Updates return $result; } - public static function update() + public function doUpdate(Updater $updater) { // change level column in logger_message table to string & remove other logging tables if empty - Updater::updateDatabase(__FILE__, self::getSql()); + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/2.0-a13.php b/www/analytics/core/Updates/2.0-a13.php index 70ca0590..43c292aa 100644 --- a/www/analytics/core/Updates/2.0-a13.php +++ b/www/analytics/core/Updates/2.0-a13.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); // old plugins deleted in 2.0-a17 update file @@ -61,6 +60,5 @@ class Updates_2_0_a13 extends Updates \Piwik\Plugin\Manager::getInstance()->activatePlugin('ScheduledReports'); } catch (\Exception $e) { } - } } diff --git a/www/analytics/core/Updates/2.0-a17.php b/www/analytics/core/Updates/2.0-a17.php index ec00c2ab..c4e2afe5 100644 --- a/www/analytics/core/Updates/2.0-a17.php +++ b/www/analytics/core/Updates/2.0-a17.php @@ -1,6 +1,6 @@ " . implode("
    ", $errors)); } - } + } } diff --git a/www/analytics/core/Updates/2.0-a7.php b/www/analytics/core/Updates/2.0-a7.php index cdbbc7e2..57544a07 100644 --- a/www/analytics/core/Updates/2.0-a7.php +++ b/www/analytics/core/Updates/2.0-a7.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/2.0-b10.php b/www/analytics/core/Updates/2.0-b10.php index 273c4d08..743c800c 100644 --- a/www/analytics/core/Updates/2.0-b10.php +++ b/www/analytics/core/Updates/2.0-b10.php @@ -1,6 +1,6 @@ " . implode("
    ", $errors)); } - } + } } diff --git a/www/analytics/core/Updates/2.0-b3.php b/www/analytics/core/Updates/2.0-b3.php index ae30df77..31e1e8f1 100644 --- a/www/analytics/core/Updates/2.0-b3.php +++ b/www/analytics/core/Updates/2.0-b3.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); try { \Piwik\Plugin\Manager::getInstance()->activatePlugin('Events'); diff --git a/www/analytics/core/Updates/2.0-b9.php b/www/analytics/core/Updates/2.0-b9.php index 612c79d1..13f887f9 100644 --- a/www/analytics/core/Updates/2.0-b9.php +++ b/www/analytics/core/Updates/2.0-b9.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } } diff --git a/www/analytics/core/Updates/2.0-rc1.php b/www/analytics/core/Updates/2.0-rc1.php index beda1cda..414f48e7 100644 --- a/www/analytics/core/Updates/2.0-rc1.php +++ b/www/analytics/core/Updates/2.0-rc1.php @@ -1,6 +1,6 @@ activatePlugin('Morpheus'); - } catch(\Exception $e) { + } catch (\Exception $e) { } } } diff --git a/www/analytics/core/Updates/2.0.3-b7.php b/www/analytics/core/Updates/2.0.3-b7.php index 2e9b593a..5771002b 100644 --- a/www/analytics/core/Updates/2.0.3-b7.php +++ b/www/analytics/core/Updates/2.0.3-b7.php @@ -1,6 +1,6 @@ isPluginActivated('DoNotTrack')) { - DoNotTrackHeaderChecker::activate(); + $checker->activate(); } // enable IP anonymization if AnonymizeIP plugin was enabled @@ -45,8 +44,7 @@ class Updates_2_0_3_b7 extends Updates foreach ($oldPlugins as $plugin) { try { \Piwik\Plugin\Manager::getInstance()->deactivatePlugin($plugin); - } catch(\Exception $e) { - + } catch (\Exception $e) { } $dir = PIWIK_INCLUDE_PATH . "/plugins/$plugin"; @@ -58,9 +56,8 @@ class Updates_2_0_3_b7 extends Updates if (file_exists($dir)) { $errors[] = "Please delete this directory manually (eg. using your FTP software): $dir \n"; } - } - if(!empty($errors)) { + if (!empty($errors)) { throw new \Exception("Warnings during the update:
    " . implode("
    ", $errors)); } } diff --git a/www/analytics/core/Updates/2.0.4-b5.php b/www/analytics/core/Updates/2.0.4-b5.php index 78c8ab2b..aadafa32 100644 --- a/www/analytics/core/Updates/2.0.4-b5.php +++ b/www/analytics/core/Updates/2.0.4-b5.php @@ -1,6 +1,6 @@ executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); try { self::migrateConfigSuperUserToDb(); @@ -56,7 +56,10 @@ class Updates_2_0_4_b5 extends Updates $superUser = null; } - if (!empty($superUser['bridge']) || empty($superUser)) { + if (!empty($superUser['bridge']) + || empty($superUser) + || empty($superUser['login']) + ) { // there is a super user which is not from the config but from the bridge, that means we already have // a super user in the database return; @@ -75,7 +78,7 @@ class Updates_2_0_4_b5 extends Updates 'superuser_access' => 1 ) ); - } catch(\Exception $e) { + } catch (\Exception $e) { echo "There was an issue, but we proceed: " . $e->getMessage(); } diff --git a/www/analytics/core/Updates/2.0.4-b7.php b/www/analytics/core/Updates/2.0.4-b7.php index 83b19903..61ab3ef7 100644 --- a/www/analytics/core/Updates/2.0.4-b7.php +++ b/www/analytics/core/Updates/2.0.4-b7.php @@ -1,6 +1,6 @@ forceSave(); - } catch (\Exception $e) { throw new UpdaterErrorException($e->getMessage()); } diff --git a/www/analytics/core/Updates/2.1.1-b11.php b/www/analytics/core/Updates/2.1.1-b11.php index 4eb924d3..543ae284 100644 --- a/www/analytics/core/Updates/2.1.1-b11.php +++ b/www/analytics/core/Updates/2.1.1-b11.php @@ -1,27 +1,27 @@ ", false, __FILE__); } diff --git a/www/analytics/core/Updates/2.10.0-b10.php b/www/analytics/core/Updates/2.10.0-b10.php new file mode 100644 index 00000000..1b0454ad --- /dev/null +++ b/www/analytics/core/Updates/2.10.0-b10.php @@ -0,0 +1,47 @@ +activatePlugin('DevicePlugins'); + } catch (\Exception $e) { + } + + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); + } +} diff --git a/www/analytics/core/Updates/2.10.0-b4.php b/www/analytics/core/Updates/2.10.0-b4.php new file mode 100644 index 00000000..1f7cee00 --- /dev/null +++ b/www/analytics/core/Updates/2.10.0-b4.php @@ -0,0 +1,30 @@ +activatePlugin('BulkTracking'); + } catch (\Exception $e) { + } + } +} diff --git a/www/analytics/core/Updates/2.10.0-b5.php b/www/analytics/core/Updates/2.10.0-b5.php new file mode 100644 index 00000000..fb77fc20 --- /dev/null +++ b/www/analytics/core/Updates/2.10.0-b5.php @@ -0,0 +1,208 @@ + false); + + // update scheduled reports to use new plugin + $reportsToReplace = array( + 'UserSettings_getBrowserVersion' => 'DevicesDetection_getBrowserVersions', + 'UserSettings_getBrowser' => 'DevicesDetection_getBrowsers', + 'UserSettings_getOSFamily' => 'DevicesDetection_getOsFamilies', + 'UserSettings_getOS' => 'DevicesDetection_getOsVersions', + 'UserSettings_getMobileVsDesktop' => 'DevicesDetection_getType', + 'UserSettings_getBrowserType' => 'DevicesDetection_getBrowserEngines', + 'UserSettings_getWideScreen' => 'UserSettings_getScreenType', + ); + + foreach ($reportsToReplace as $old => $new) { + $sqls["UPDATE " . Common::prefixTable('report') . " SET reports = REPLACE(reports, '".$old."', '".$new."')"] = false; + } + + // update dashboard to use new widgets + $oldWidgets = array( + array('module' => 'UserSettings', 'action' => 'getBrowserVersion', 'params' => array()), + array('module' => 'UserSettings', 'action' => 'getBrowser', 'params' => array()), + array('module' => 'UserSettings', 'action' => 'getOSFamily', 'params' => array()), + array('module' => 'UserSettings', 'action' => 'getOS', 'params' => array()), + array('module' => 'UserSettings', 'action' => 'getMobileVsDesktop', 'params' => array()), + array('module' => 'UserSettings', 'action' => 'getBrowserType', 'params' => array()), + array('module' => 'UserSettings', 'action' => 'getWideScreen', 'params' => array()), + ); + + $newWidgets = array( + array('module' => 'DevicesDetection', 'action' => 'getBrowserVersions', 'params' => array()), + array('module' => 'DevicesDetection', 'action' => 'getBrowsers', 'params' => array()), + array('module' => 'DevicesDetection', 'action' => 'getOsFamilies', 'params' => array()), + array('module' => 'DevicesDetection', 'action' => 'getOsVersions', 'params' => array()), + array('module' => 'DevicesDetection', 'action' => 'getType', 'params' => array()), + array('module' => 'DevicesDetection', 'action' => 'getBrowserEngines', 'params' => array()), + array('module' => 'UserSettings', 'action' => 'getScreenType', 'params' => array()), + ); + + $allDashboards = Db::get()->fetchAll(sprintf("SELECT * FROM %s", Common::prefixTable('user_dashboard'))); + + foreach ($allDashboards as $dashboard) { + $dashboardLayout = json_decode($dashboard['layout']); + + $dashboardLayout = DashboardModel::replaceDashboardWidgets($dashboardLayout, $oldWidgets, $newWidgets); + + $newLayout = json_encode($dashboardLayout); + if ($newLayout != $dashboard['layout']) { + $sqls["UPDATE " . Common::prefixTable('user_dashboard') . " SET layout = '".addslashes($newLayout)."' WHERE iddashboard = ".$dashboard['iddashboard']] = false; + } + } + + return $sqls; + } + + public function doUpdate(Updater $updater) + { + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); + + // DeviceDetection upgrade in beta1 timed out on demo #6750 + $archiveBlobTables = self::getAllArchiveBlobTables(); + + foreach ($archiveBlobTables as $table) { + self::updateBrowserArchives($table); + self::updateOsArchives($table); + } + } + + /** + * Returns all available archive blob tables + * + * @return array + */ + public static function getAllArchiveBlobTables() + { + if (empty(self::$archiveBlobTables)) { + $archiveTables = ArchiveTableCreator::getTablesArchivesInstalled(); + + self::$archiveBlobTables = array_filter($archiveTables, function ($name) { + return ArchiveTableCreator::getTypeFromTableName($name) == ArchiveTableCreator::BLOB_TABLE; + }); + + // sort tables so we have them in order of their date + rsort(self::$archiveBlobTables); + } + + return (array) self::$archiveBlobTables; + } + + /** + * Find the first day on which DevicesDetection archives were generated + * + * @return int Timestamp + */ + public static function getFirstDayOfArchivedDeviceDetectorData() + { + static $deviceDetectionBlobAvailableDate; + + if (empty($deviceDetectionBlobAvailableDate)) { + $archiveBlobTables = self::getAllArchiveBlobTables(); + + $deviceDetectionBlobAvailableDate = null; + foreach ($archiveBlobTables as $table) { + + // Look for all day archives and try to find that with the lowest date + $deviceDetectionBlobAvailableDate = Db::get()->fetchOne(sprintf("SELECT date1 FROM %s WHERE name = 'DevicesDetection_browserVersions' AND period = 1 ORDER BY date1 ASC LIMIT 1", $table)); + + if (!empty($deviceDetectionBlobAvailableDate)) { + break; + } + } + + $deviceDetectionBlobAvailableDate = strtotime($deviceDetectionBlobAvailableDate); + } + + return $deviceDetectionBlobAvailableDate; + } + + /** + * Updates all browser archives to new structure + * @param string $table + * @throws \Exception + */ + public static function updateBrowserArchives($table) + { + // rename old UserSettings archives where no DeviceDetection archives exists + Db::exec(sprintf("UPDATE IGNORE %s SET name='DevicesDetection_browserVersions' WHERE name = 'UserSettings_browser'", $table)); + + /* + * check dates of remaining (non-day) archives with calculated safe date + * archives before or within that week/month/year of that date will be replaced + */ + $oldBrowserBlobs = Db::get()->fetchAll(sprintf("SELECT * FROM %s WHERE name = 'UserSettings_browser' AND `period` > 1", $table)); + foreach ($oldBrowserBlobs as $blob) { + + // if start date of blob is before calculated date us old usersettings archive instead of already existing DevicesDetection archive + if (strtotime($blob['date1']) < self::getFirstDayOfArchivedDeviceDetectorData()) { + Db::get()->query(sprintf("DELETE FROM %s WHERE idarchive = ? AND name = ?", $table), array($blob['idarchive'], 'DevicesDetection_browserVersions')); + Db::get()->query(sprintf("UPDATE %s SET name = ? WHERE idarchive = ? AND name = ?", $table), array('DevicesDetection_browserVersions', $blob['idarchive'], 'UserSettings_browser')); + } + } + } + + public static function updateOsArchives($table) + { + Db::exec(sprintf("UPDATE IGNORE %s SET name='DevicesDetection_osVersions' WHERE name = 'UserSettings_os'", $table)); + + /* + * check dates of remaining (non-day) archives with calculated safe date + * archives before or within that week/month/year of that date will be replaced + */ + $oldOsBlobs = Db::get()->fetchAll(sprintf("SELECT * FROM %s WHERE name = 'UserSettings_os' AND `period` > 1", $table)); + foreach ($oldOsBlobs as $blob) { + + // if start date of blob is before calculated date us old usersettings archive instead of already existing DevicesDetection archive + if (strtotime($blob['date1']) < self::getFirstDayOfArchivedDeviceDetectorData()) { + Db::get()->query(sprintf("DELETE FROM %s WHERE idarchive = ? AND name = ?", $table), array($blob['idarchive'], 'DevicesDetection_osVersions')); + Db::get()->query(sprintf("UPDATE %s SET name = ? WHERE idarchive = ? AND name = ?", $table), array('DevicesDetection_osVersions', $blob['idarchive'], 'UserSettings_os')); + } + } + } +} diff --git a/www/analytics/core/Updates/2.10.0-b7.php b/www/analytics/core/Updates/2.10.0-b7.php new file mode 100644 index 00000000..50bc120a --- /dev/null +++ b/www/analytics/core/Updates/2.10.0-b7.php @@ -0,0 +1,41 @@ +executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); + } +} diff --git a/www/analytics/core/Updates/2.10.0-b8.php b/www/analytics/core/Updates/2.10.0-b8.php new file mode 100644 index 00000000..885d7482 --- /dev/null +++ b/www/analytics/core/Updates/2.10.0-b8.php @@ -0,0 +1,26 @@ +activatePlugin('Resolution'); + } catch (\Exception $e) { + } + } +} diff --git a/www/analytics/core/Updates/2.11.0-b2.php b/www/analytics/core/Updates/2.11.0-b2.php new file mode 100644 index 00000000..47430800 --- /dev/null +++ b/www/analytics/core/Updates/2.11.0-b2.php @@ -0,0 +1,65 @@ + 'Goals', 'action' => 'getEcommerceLog', 'params' => array()), + array('module' => 'Goals', 'action' => 'widgetGoalReport', 'params' => array('idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER)), + ); + + $newWidgets = array( + array('module' => 'Ecommerce', 'action' => 'getEcommerceLog', 'params' => array()), + array('module' => 'Ecommerce', 'action' => 'widgetGoalReport', 'params' => array('idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER)), + ); + + $allDashboards = Db::get()->fetchAll(sprintf("SELECT * FROM %s", Common::prefixTable('user_dashboard'))); + + foreach ($allDashboards as $dashboard) { + $dashboardLayout = json_decode($dashboard['layout']); + $dashboardLayout = DashboardModel::replaceDashboardWidgets($dashboardLayout, $oldWidgets, $newWidgets); + + $newLayout = json_encode($dashboardLayout); + if ($newLayout != $dashboard['layout']) { + $sqls["UPDATE " . Common::prefixTable('user_dashboard') . " SET layout = '".addslashes($newLayout)."' WHERE iddashboard = ".$dashboard['iddashboard']] = false; + } + } + + return $sqls; + } + + public function doUpdate(Updater $updater) + { + $pluginManager = \Piwik\Plugin\Manager::getInstance(); + + try { + $pluginManager->activatePlugin('Ecommerce'); + } catch (\Exception $e) { + } + + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); + } +} diff --git a/www/analytics/core/Updates/2.11.0-b4.php b/www/analytics/core/Updates/2.11.0-b4.php new file mode 100644 index 00000000..e35025f3 --- /dev/null +++ b/www/analytics/core/Updates/2.11.0-b4.php @@ -0,0 +1,47 @@ +activatePlugin('UserLanguage'); + } catch (\Exception $e) { + } + + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); + } +} diff --git a/www/analytics/core/Updates/2.11.0-b5.php b/www/analytics/core/Updates/2.11.0-b5.php new file mode 100644 index 00000000..37921e96 --- /dev/null +++ b/www/analytics/core/Updates/2.11.0-b5.php @@ -0,0 +1,24 @@ +activatePlugin('Monolog'); + } catch (\Exception $e) { + } + } +} diff --git a/www/analytics/core/Updates/2.11.1-b4.php b/www/analytics/core/Updates/2.11.1-b4.php new file mode 100644 index 00000000..3091445b --- /dev/null +++ b/www/analytics/core/Updates/2.11.1-b4.php @@ -0,0 +1,40 @@ +database_tests; + + if ($dbTests['username'] === '@USERNAME@') { + $dbTests['username'] = 'root'; + } + + $config->database_tests = $dbTests; + + $config->forceSave(); + } +} diff --git a/www/analytics/core/Updates/2.13.0-b3.php b/www/analytics/core/Updates/2.13.0-b3.php new file mode 100644 index 00000000..f0950c64 --- /dev/null +++ b/www/analytics/core/Updates/2.13.0-b3.php @@ -0,0 +1,26 @@ +activatePlugin('Diagnostics'); + } catch (\Exception $e) { + } + } +} diff --git a/www/analytics/core/Updates/2.13.1.php b/www/analytics/core/Updates/2.13.1.php new file mode 100644 index 00000000..73b01dc8 --- /dev/null +++ b/www/analytics/core/Updates/2.13.1.php @@ -0,0 +1,43 @@ + false + ); + } + + /** + * Here you can define any action that should be performed during the update. For instance executing SQL statements, + * renaming config entries, updating files, etc. + */ + public function doUpdate(Updater $updater) + { + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); + } +} diff --git a/www/analytics/core/Updates/2.14.0-b1.php b/www/analytics/core/Updates/2.14.0-b1.php new file mode 100644 index 00000000..e414bc3a --- /dev/null +++ b/www/analytics/core/Updates/2.14.0-b1.php @@ -0,0 +1,43 @@ +uninstallPlugin('UserSettings'); + } + + private function uninstallPlugin($plugin) + { + $pluginManager = Manager::getInstance(); + + if ($pluginManager->isPluginInstalled($plugin)) { + if ($pluginManager->isPluginActivated($plugin)) { + $pluginManager->deactivatePlugin($plugin); + } + + $pluginManager->unloadPlugin($plugin); + $pluginManager->uninstallPlugin($plugin); + } else { + $this->makeSurePluginIsRemovedFromFilesystem($plugin); + } + } + + private function makeSurePluginIsRemovedFromFilesystem($plugin) + { + Manager::deletePluginFromFilesystem($plugin); + } +} diff --git a/www/analytics/core/Updates/2.14.0-b2.php b/www/analytics/core/Updates/2.14.0-b2.php new file mode 100644 index 00000000..dfa8c3f5 --- /dev/null +++ b/www/analytics/core/Updates/2.14.0-b2.php @@ -0,0 +1,43 @@ +getEngine(); + + $table = Common::prefixTable('site_setting'); + + $sqlarray = array( + "DROP TABLE IF EXISTS `$table`" => false, + "CREATE TABLE `$table` ( + idsite INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `setting_name` VARCHAR(255) NOT NULL, + `setting_value` LONGTEXT NOT NULL, + PRIMARY KEY(idsite, setting_name) + ) ENGINE=$engine DEFAULT CHARSET=utf8" => 1050, + ); + + return $sqlarray; + } + + public function doUpdate(Updater $updater) + { + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); + } +} diff --git a/www/analytics/core/Updates/2.14.2.php b/www/analytics/core/Updates/2.14.2.php new file mode 100644 index 00000000..de416527 --- /dev/null +++ b/www/analytics/core/Updates/2.14.2.php @@ -0,0 +1,124 @@ +executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); + } +} diff --git a/www/analytics/core/Updates/2.15.0-b12.php b/www/analytics/core/Updates/2.15.0-b12.php new file mode 100644 index 00000000..f780e616 --- /dev/null +++ b/www/analytics/core/Updates/2.15.0-b12.php @@ -0,0 +1,44 @@ +migrateBetaUpgradesToReleaseChannel(); + } + + private function migrateBetaUpgradesToReleaseChannel() + { + $config = Config::getInstance(); + $debug = $config->Debug; + + if (array_key_exists('allow_upgrades_to_beta', $debug)) { + $allowUpgradesToBeta = 1 == $debug['allow_upgrades_to_beta']; + unset($debug['allow_upgrades_to_beta']); + + $general = $config->General; + if ($allowUpgradesToBeta) { + $general['release_channel'] = 'latest_beta'; + } else { + $general['release_channel'] = 'latest_stable'; + } + + $config->Debug = $debug; + $config->General = $general; + $config->forceSave(); + } + } +} diff --git a/www/analytics/core/Updates/2.15.0-b16.php b/www/analytics/core/Updates/2.15.0-b16.php new file mode 100644 index 00000000..a37e6211 --- /dev/null +++ b/www/analytics/core/Updates/2.15.0-b16.php @@ -0,0 +1,45 @@ +uninstallPlugin('LeftMenu'); + $this->uninstallPlugin('ZenMode'); + } + + private function uninstallPlugin($plugin) + { + $pluginManager = Manager::getInstance(); + + if ($pluginManager->isPluginInstalled($plugin)) { + if ($pluginManager->isPluginActivated($plugin)) { + $pluginManager->deactivatePlugin($plugin); + } + + $pluginManager->unloadPlugin($plugin); + $pluginManager->uninstallPlugin($plugin); + } else { + $this->makeSurePluginIsRemovedFromFilesystem($plugin); + } + } + + private function makeSurePluginIsRemovedFromFilesystem($plugin) + { + Manager::deletePluginFromFilesystem($plugin); + } +} diff --git a/www/analytics/core/Updates/2.15.0-b17.php b/www/analytics/core/Updates/2.15.0-b17.php new file mode 100644 index 00000000..86e3decf --- /dev/null +++ b/www/analytics/core/Updates/2.15.0-b17.php @@ -0,0 +1,44 @@ +removeDeprecatedDebugConfig('enable_measure_piwik_usage_in_idsite'); + } + + private function removeDeprecatedDebugConfig($name) + { + $config = Config::getInstance(); + $debug = $config->Debug; + unset($debug[$name]); + $config->Debug = $debug; + $config->forceSave(); + } +} diff --git a/www/analytics/core/Updates/2.15.0-b20.php b/www/analytics/core/Updates/2.15.0-b20.php new file mode 100644 index 00000000..79b4703f --- /dev/null +++ b/www/analytics/core/Updates/2.15.0-b20.php @@ -0,0 +1,42 @@ +makeSurePluginIsRemovedFromFilesystem('ZenMode'); + $this->makeSurePluginIsRemovedFromFilesystem('LeftMenu'); + } + + private function makeSurePluginIsRemovedFromFilesystem($plugin) + { + Plugin\Manager::deletePluginFromFilesystem($plugin); + } +} diff --git a/www/analytics/core/Updates/2.15.0-b3.php b/www/analytics/core/Updates/2.15.0-b3.php new file mode 100644 index 00000000..374563c6 --- /dev/null +++ b/www/analytics/core/Updates/2.15.0-b3.php @@ -0,0 +1,32 @@ + array(1060) + ); + return $updateSql; + } + + public function doUpdate(Updater $updater) + { + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); + } +} diff --git a/www/analytics/core/Updates/2.15.0-b4.php b/www/analytics/core/Updates/2.15.0-b4.php new file mode 100644 index 00000000..132c3319 --- /dev/null +++ b/www/analytics/core/Updates/2.15.0-b4.php @@ -0,0 +1,25 @@ +activatePlugin('Heartbeat'); + } catch (\Exception $e) { + } + } +} \ No newline at end of file diff --git a/www/analytics/core/Updates/2.16.0-rc2.php b/www/analytics/core/Updates/2.16.0-rc2.php new file mode 100644 index 00000000..2bddb6b5 --- /dev/null +++ b/www/analytics/core/Updates/2.16.0-rc2.php @@ -0,0 +1,28 @@ +isPluginActivated($pluginName)) { + $pluginManager->activatePlugin($pluginName); + } + } catch (\Exception $e) { + } + } +} \ No newline at end of file diff --git a/www/analytics/core/Updates/2.2.0-b15.php b/www/analytics/core/Updates/2.2.0-b15.php index 8437a83d..e161b8cb 100644 --- a/www/analytics/core/Updates/2.2.0-b15.php +++ b/www/analytics/core/Updates/2.2.0-b15.php @@ -1,21 +1,20 @@ activatePlugin('ZenMode'); + } catch (\Exception $e) { + } + } +} diff --git a/www/analytics/core/Updates/2.3.0-rc2.php b/www/analytics/core/Updates/2.3.0-rc2.php new file mode 100644 index 00000000..c24cb257 --- /dev/null +++ b/www/analytics/core/Updates/2.3.0-rc2.php @@ -0,0 +1,25 @@ +activatePlugin('Morpheus'); + } catch (\Exception $e) { + } + + try { + \Piwik\Plugin\Manager::getInstance()->deactivatePlugin('Zeitgeist'); + self::deletePluginFromConfigFile('Zeitgeist'); + } catch (\Exception $e) { + } + } +} diff --git a/www/analytics/core/Updates/2.4.0-b2.php b/www/analytics/core/Updates/2.4.0-b2.php new file mode 100644 index 00000000..16d09d03 --- /dev/null +++ b/www/analytics/core/Updates/2.4.0-b2.php @@ -0,0 +1,27 @@ +activatePlugin('LeftMenu'); + } catch (\Exception $e) { + } + + try { + $pluginManager->deactivatePlugin('Zeitgeist'); + } catch (\Exception $e) { + } + + try { + $pluginManager->uninstallPlugin('Zeitgeist'); + } catch (\Exception $e) { + } + } +} diff --git a/www/analytics/core/Updates/2.4.0-b4.php b/www/analytics/core/Updates/2.4.0-b4.php new file mode 100644 index 00000000..ec613ab0 --- /dev/null +++ b/www/analytics/core/Updates/2.4.0-b4.php @@ -0,0 +1,35 @@ +getAllPluginsNames(); + + if (!in_array('Zeitgeist', $pluginNames)) { + return; + } + + try { + $pluginManager->deactivatePlugin('Zeitgeist'); + } catch (\Exception $e) { + } + + try { + $pluginManager->uninstallPlugin('Zeitgeist'); + } catch (\Exception $e) { + } + } +} diff --git a/www/analytics/core/Updates/2.4.0-b6.php b/www/analytics/core/Updates/2.4.0-b6.php new file mode 100644 index 00000000..d8c4674b --- /dev/null +++ b/www/analytics/core/Updates/2.4.0-b6.php @@ -0,0 +1,25 @@ +activatePlugin('DevicesDetection'); + } catch (\Exception $e) { + } + } +} diff --git a/www/analytics/core/Updates/2.4.0-b8.php b/www/analytics/core/Updates/2.4.0-b8.php new file mode 100644 index 00000000..225a2aa9 --- /dev/null +++ b/www/analytics/core/Updates/2.4.0-b8.php @@ -0,0 +1,29 @@ + false, + ); + } + + public function doUpdate(Updater $updater) + { + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); + } +} diff --git a/www/analytics/core/Updates/2.5.0-b1.php b/www/analytics/core/Updates/2.5.0-b1.php new file mode 100644 index 00000000..bd1863e4 --- /dev/null +++ b/www/analytics/core/Updates/2.5.0-b1.php @@ -0,0 +1,37 @@ +Debug; + + if (array_key_exists('disable_merged_assets', $debug)) { + $development = $config->Development; + $development['disable_merged_assets'] = $debug['disable_merged_assets']; + unset($debug['disable_merged_assets']); + + $config->Debug = $debug; + $config->Development = $development; + $config->forceSave(); + } + } +} diff --git a/www/analytics/core/Updates/2.5.0-rc2.php b/www/analytics/core/Updates/2.5.0-rc2.php new file mode 100644 index 00000000..1cac2a92 --- /dev/null +++ b/www/analytics/core/Updates/2.5.0-rc2.php @@ -0,0 +1,69 @@ +Plugins_Tracker = array(); + $config->forceSave(); + } +} diff --git a/www/analytics/core/Updates/2.7.0-b2.php b/www/analytics/core/Updates/2.7.0-b2.php new file mode 100644 index 00000000..050de34e --- /dev/null +++ b/www/analytics/core/Updates/2.7.0-b2.php @@ -0,0 +1,28 @@ +activatePlugin('Contents'); + } catch (\Exception $e) { + } + } +} diff --git a/www/analytics/core/Updates/2.7.0-b4.php b/www/analytics/core/Updates/2.7.0-b4.php new file mode 100644 index 00000000..4ef52795 --- /dev/null +++ b/www/analytics/core/Updates/2.7.0-b4.php @@ -0,0 +1,33 @@ +activatePlugin('Contents'); + } catch (\Exception $e) { + } + } + + public static function isMajorUpdate() + { + return true; + } +} diff --git a/www/analytics/core/Updates/2.9.0-b1.php b/www/analytics/core/Updates/2.9.0-b1.php new file mode 100644 index 00000000..d22b9355 --- /dev/null +++ b/www/analytics/core/Updates/2.9.0-b1.php @@ -0,0 +1,89 @@ +executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); + + self::updateIPAnonymizationSettings(); + + try { + Manager::getInstance()->activatePlugin('TestRunner'); + } catch (\Exception $e) { + } + } + + private static function updateBrowserEngine($sql) + { + $sql[sprintf("ALTER TABLE `%s` ADD COLUMN `config_browser_engine` VARCHAR(10) NOT NULL", Common::prefixTable('log_visit'))] = 1060; + + $browserEngineMatch = array( + 'Trident' => array('IE'), + 'Gecko' => array('NS', 'PX', 'FF', 'FB', 'CA', 'GA', 'KM', 'MO', 'SM', 'CO', 'FE', 'KP', 'KZ', 'TB'), + 'KHTML' => array('KO'), + 'WebKit' => array('SF', 'CH', 'OW', 'AR', 'EP', 'FL', 'WO', 'AB', 'IR', 'CS', 'FD', 'HA', 'MI', 'GE', 'DF', 'BB', 'BP', 'TI', 'CF', 'RK', 'B2', 'NF'), + 'Presto' => array('OP'), + ); + + // Update visits, fill in now missing engine + $engineUpdate = "''"; + $ifFragment = "IF (`config_browser_name` IN ('%s'), '%s', %s)"; + + foreach ($browserEngineMatch as $engine => $browsers) { + $engineUpdate = sprintf($ifFragment, implode("','", $browsers), $engine, $engineUpdate); + } + + $engineUpdate = sprintf("UPDATE %s SET `config_browser_engine` = %s", Common::prefixTable('log_visit'), $engineUpdate); + $sql[$engineUpdate] = false; + + $archiveBlobTables = Db::get()->fetchCol("SHOW TABLES LIKE '%archive_blob%'"); + + // for each blob archive table, rename UserSettings_browserType to DevicesDetection_browserEngines + foreach ($archiveBlobTables as $table) { + + // try to rename old archives + $sql[sprintf("UPDATE IGNORE %s SET name='DevicesDetection_browserEngines' WHERE name = 'UserSettings_browserType'", $table)] = false; + } + + return $sql; + } + + private static function updateIPAnonymizationSettings() + { + $optionName = 'PrivacyManager.ipAnonymizerEnabled'; + + $value = Option::get($optionName); + + if ($value !== false) { + // If the config is defined, nothing to do + return; + } + + // We disable IP anonymization if it wasn't configured (because by default it has gone from disabled to enabled) + Option::set($optionName, '0'); + } +} diff --git a/www/analytics/core/Updates/2.9.0-b7.php b/www/analytics/core/Updates/2.9.0-b7.php new file mode 100644 index 00000000..bc0338ef --- /dev/null +++ b/www/analytics/core/Updates/2.9.0-b7.php @@ -0,0 +1,90 @@ +executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); + } + + private static function addArchivingIdMigrationQueries($sql) + { + $tables = ArchiveTableCreator::getTablesArchivesInstalled(); + + foreach ($tables as $table) { + $type = ArchiveTableCreator::getTypeFromTableName($table); + + if ($type === ArchiveTableCreator::NUMERIC_TABLE) { + $maxId = Db::fetchOne('SELECT MAX(idarchive) FROM ' . $table); + + if (!empty($maxId)) { + $maxId = (int) $maxId + 500; + } else { + $maxId = 1; + } + + $query = self::getQueryToCreateSequence($table, $maxId); + // refs #6696, ignores Integrity constraint violation: 1062 Duplicate entry 'piwik_archive_numeric_2010_01' for key 'PRIMARY' + $sql[$query] = '1062'; + } + } + + return $sql; + } + + private static function getQueryToCreateSequence($name, $initialValue) + { + $table = self::getSequenceTableName(); + $query = sprintf("INSERT INTO %s (name, value) VALUES ('%s', %d)", $table, $name, $initialValue); + + return $query; + } + + /** + * @return string + */ + private static function addCreateSequenceTableQuery($sql) + { + $dbSettings = new Db\Settings(); + $engine = $dbSettings->getEngine(); + $table = self::getSequenceTableName(); + + $query = "CREATE TABLE `$table` ( + `name` VARCHAR(120) NOT NULL, + `value` BIGINT(20) UNSIGNED NOT NULL, + PRIMARY KEY(`name`) + ) ENGINE=$engine DEFAULT CHARSET=utf8"; + + $sql[$query] = 1050; + + return $sql; + } + + private static function getSequenceTableName() + { + return Common::prefixTable('sequence'); + } +} diff --git a/www/analytics/core/Url.php b/www/analytics/core/Url.php index 1fe58a96..2a301a9b 100644 --- a/www/analytics/core/Url.php +++ b/www/analytics/core/Url.php @@ -1,6 +1,6 @@ 'UserSettings', + * 'module' => 'DevicesDetection', * 'action' => 'index' * )); * Url::redirectToUrl($url); * } - * + * * **Link to a different controller action in a template** - * + * * public function myControllerAction() * { * $url = Url::getCurrentQueryStringWithParametersModified(array( @@ -45,43 +46,38 @@ use Exception; * $view->realtimeMapUrl = $url; * return $view->render(); * } - * + * */ class Url { - /** - * List of hosts that are never checked for validity. - */ - private static $alwaysTrustedHosts = array('localhost', '127.0.0.1', '::1', '[::1]'); - /** * Returns the current URL. * * @return string eg, `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"` * @api */ - static public function getCurrentUrl() + public static function getCurrentUrl() { return self::getCurrentScheme() . '://' . self::getCurrentHost() - . self::getCurrentScriptName() + . self::getCurrentScriptName(false) . self::getCurrentQueryString(); } /** * Returns the current URL without the query string. - * + * * @param bool $checkTrustedHost Whether to do trusted host check. Should ALWAYS be true, * except in {@link Piwik\Plugin\Controller}. * @return string eg, `"http://example.org/dir1/dir2/index.php"` if the current URL is * `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"`. * @api */ - static public function getCurrentUrlWithoutQueryString($checkTrustedHost = true) + public static function getCurrentUrlWithoutQueryString($checkTrustedHost = true) { return self::getCurrentScheme() . '://' . self::getCurrentHost($default = 'unknown', $checkTrustedHost) - . self::getCurrentScriptName(); + . self::getCurrentScriptName(false); } /** @@ -92,7 +88,7 @@ class Url * `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"`. * @api */ - static public function getCurrentUrlWithoutFileName() + public static function getCurrentUrlWithoutFileName() { return self::getCurrentScheme() . '://' . self::getCurrentHost() @@ -106,7 +102,7 @@ class Url * `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"` * @api */ - static public function getCurrentScriptPath() + public static function getCurrentScriptPath() { $queryString = self::getCurrentScriptName(); @@ -123,11 +119,12 @@ class Url /** * Returns the path to the script being executed. Includes the script file name. * + * @param bool $removePathInfo If true (default value) then the PATH_INFO will be stripped. * @return string eg, `"/dir1/dir2/index.php"` if the current URL is * `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"` * @api */ - static public function getCurrentScriptName() + public static function getCurrentScriptName($removePathInfo = true) { $url = ''; @@ -145,7 +142,7 @@ class Url } // strip path_info - if (isset($_SERVER['PATH_INFO'])) { + if ($removePathInfo && isset($_SERVER['PATH_INFO'])) { $url = substr($url, 0, -strlen($_SERVER['PATH_INFO'])); } } @@ -177,21 +174,12 @@ class Url * @return string `'https'` or `'http'` * @api */ - static public function getCurrentScheme() + public static function getCurrentScheme() { - try { - $assume_secure_protocol = @Config::getInstance()->General['assume_secure_protocol']; - } catch (Exception $e) { - $assume_secure_protocol = false; - } - if ($assume_secure_protocol - || (isset($_SERVER['HTTPS']) - && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] === true)) - || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') - ) { + if (self::isPiwikConfiguredToAssumeSecureConnection()) { return 'https'; } - return 'http'; + return self::getCurrentSchemeFromRequestHeader(); } /** @@ -202,7 +190,7 @@ class Url * value from the request. * @return bool `true` if valid; `false` otherwise. */ - static public function isValidHost($host = false) + public static function isValidHost($host = false) { // only do trusted host check if it's enabled if (isset(Config::getInstance()->General['enable_trusted_host_check']) @@ -213,33 +201,38 @@ class Url if ($host === false) { $host = @$_SERVER['HTTP_HOST']; - if (empty($host)) // if no current host, assume valid - { + if (empty($host)) { + // if no current host, assume valid + return true; } } + // if host is in hardcoded whitelist, assume it's valid - if (in_array($host, self::$alwaysTrustedHosts)) { + if (in_array($host, self::getAlwaysTrustedHosts())) { return true; } $trustedHosts = self::getTrustedHosts(); + // Only punctuation we allow is '[', ']', ':', '.', '_' and '-' + $hostLength = strlen($host); + if ($hostLength !== strcspn($host, '`~!@#$%^&*()+={}\\|;"\'<>,?/ ')) { + return false; + } + // if no trusted hosts, just assume it's valid if (empty($trustedHosts)) { self::saveTrustedHostnameInConfig($host); return true; } - // Only punctuation we allow is '[', ']', ':', '.' and '-' - $hostLength = strlen($host); - if ($hostLength !== strcspn($host, '`~!@#$%^&*()_+={}\\|;"\'<>,?/ ')) { - return false; - } - + // Escape trusted hosts for preg_match call below foreach ($trustedHosts as &$trustedHost) { $trustedHost = preg_quote($trustedHost); } + $trustedHosts = str_replace("/", "\\/", $trustedHosts); + $untrustedHost = Common::mb_strtolower($host); $untrustedHost = rtrim($untrustedHost, '.'); @@ -258,11 +251,21 @@ class Url * @return bool */ public static function saveTrustedHostnameInConfig($host) + { + return self::saveHostsnameInConfig($host, 'General', 'trusted_hosts'); + } + + public static function saveCORSHostnameInConfig($host) + { + return self::saveHostsnameInConfig($host, 'General', 'cors_domains'); + } + + protected static function saveHostsnameInConfig($host, $domain, $key) { if (Piwik::hasUserSuperUserAccess() && file_exists(Config::getLocalConfigPath()) ) { - $general = Config::getInstance()->General; + $config = Config::getInstance()->$domain; if (!is_array($host)) { $host = array($host); } @@ -270,8 +273,8 @@ class Url if (empty($host)) { return false; } - $general['trusted_hosts'] = $host; - Config::getInstance()->General = $general; + $config[$key] = $host; + Config::getInstance()->$domain = $config; Config::getInstance()->forceSave(); return true; } @@ -285,7 +288,7 @@ class Url * except in Controller. * @return string|bool eg, `"demo.piwik.org"` or false if no host found. */ - static public function getHost($checkIfTrusted = true) + public static function getHost($checkIfTrusted = true) { // HTTP/1.1 request if (isset($_SERVER['HTTP_HOST']) @@ -305,11 +308,11 @@ class Url } /** - * Sets the host. Useful for CLI scripts, eg. archive.php - * + * Sets the host. Useful for CLI scripts, eg. core:archive command + * * @param $host string */ - static public function setHost($host) + public static function setHost($host) { $_SERVER['HTTP_HOST'] = $host; } @@ -324,12 +327,12 @@ class Url * `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"` * @api */ - static public function getCurrentHost($default = 'unknown', $checkTrustedHost = true) + public static function getCurrentHost($default = 'unknown', $checkTrustedHost = true) { $hostHeaders = array(); $config = Config::getInstance()->General; - if(isset($config['proxy_host_headers'])) { + if (isset($config['proxy_host_headers'])) { $hostHeaders = $config['proxy_host_headers']; } @@ -350,7 +353,7 @@ class Url * `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"` * @api */ - static public function getCurrentQueryString() + public static function getCurrentQueryString() { $url = ''; if (isset($_SERVER['QUERY_STRING']) @@ -367,14 +370,14 @@ class Url * * @return array If current URL is `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"` * this will return: - * + * * array( * 'param1' => string 'value1', * 'param2' => string 'value2' * ) * @api */ - static public function getArrayFromCurrentQueryString() + public static function getArrayFromCurrentQueryString() { $queryString = self::getCurrentQueryString(); $urlValues = UrlHelper::getArrayFromQueryString($queryString); @@ -392,7 +395,7 @@ class Url * @return string eg, `"?param2=value2¶m3=value3"` * @api */ - static function getCurrentQueryStringWithParametersModified($params) + public static function getCurrentQueryStringWithParametersModified($params) { $urlValues = self::getArrayFromCurrentQueryString(); foreach ($params as $key => $value) { @@ -407,13 +410,13 @@ class Url /** * Converts an array of parameters name => value mappings to a query - * string. - * + * string. Values must already be URL encoded before you call this function. + * * @param array $parameters eg. `array('param1' => 10, 'param2' => array(1,2))` * @return string eg. `"param1=10¶m2[]=1¶m2[]=2"` * @api */ - static public function getQueryStringFromParameters($parameters) + public static function getQueryStringFromParameters($parameters) { $query = ''; foreach ($parameters as $name => $value) { @@ -432,7 +435,7 @@ class Url return $query; } - static public function getQueryStringFromUrl($url) + public static function getQueryStringFromUrl($url) { return parse_url($url, PHP_URL_QUERY); } @@ -440,10 +443,10 @@ class Url /** * Redirects the user to the referrer. If no referrer exists, the user is redirected * to the current URL without query string. - * + * * @api */ - static public function redirectToReferrer() + public static function redirectToReferrer() { $referrer = self::getReferrer(); if ($referrer !== false) { @@ -452,34 +455,47 @@ class Url self::redirectToUrl(self::getCurrentUrlWithoutQueryString()); } - /** - * Redirects the user to the specified URL. - * - * @param string $url - * @api - */ - static public function redirectToUrl($url) + private static function redirectToUrlNoExit($url) { if (UrlHelper::isLookLikeUrl($url) || strpos($url, 'index.php') === 0 ) { - @header("Location: $url"); + Common::sendResponseCode(302); + Common::sendHeader("Location: $url"); } else { echo "Invalid URL to redirect to."; } - if(Common::isPhpCliMode()) { - die("If you were using a browser, Piwik would redirect you to this URL: $url \n\n"); + if (Common::isPhpCliMode()) { + throw new Exception("If you were using a browser, Piwik would redirect you to this URL: $url \n\n"); } + } + + /** + * Redirects the user to the specified URL. + * + * @param string $url + * @throws Exception + * @api + */ + public static function redirectToUrl($url) + { + // Close the session manually. + // We should not have to call this because it was registered via register_shutdown_function, + // but it is not always called fast enough + Session::close(); + + self::redirectToUrlNoExit($url); + exit; } /** * If the page is using HTTP, redirect to the same page over HTTPS */ - static public function redirectToHttps() + public static function redirectToHttps() { - if(ProxyHttp::isHttps()) { + if (ProxyHttp::isHttps()) { return; } $url = self::getCurrentUrl(); @@ -493,7 +509,7 @@ class Url * @return string|false * @api */ - static public function getReferrer() + public static function getReferrer() { if (!empty($_SERVER['HTTP_REFERER'])) { return $_SERVER['HTTP_REFERER']; @@ -508,7 +524,7 @@ class Url * @return bool True if local; false otherwise. * @api */ - static public function isLocalUrl($url) + public static function isLocalUrl($url) { if (empty($url)) { return true; @@ -523,40 +539,198 @@ class Url } // drop port numbers from hostnames and IP addresses - $hosts = array_map(array('Piwik\IP', 'sanitizeIp'), $hosts); + $hosts = array_map(array('self', 'getHostSanitized'), $hosts); $disableHostCheck = Config::getInstance()->General['enable_trusted_host_check'] == 0; // compare scheme and host $parsedUrl = @parse_url($url); - $host = IP::sanitizeIp(@$parsedUrl['host']); + $host = IPUtils::sanitizeIp(@$parsedUrl['host']); return !empty($host) - && ($disableHostCheck || in_array($host, $hosts)) - && !empty($parsedUrl['scheme']) - && in_array($parsedUrl['scheme'], array('http', 'https')); + && ($disableHostCheck || in_array($host, $hosts)) + && !empty($parsedUrl['scheme']) + && in_array($parsedUrl['scheme'], array('http', 'https')); + } + + /** + * Checks whether the given host is a local host like `127.0.0.1` or `localhost`. + * + * @param string $host + * @return bool + */ + public static function isLocalHost($host) + { + if (empty($host)) { + return false; + } + + return in_array($host, Url::getLocalHostnames(), true); } public static function getTrustedHostsFromConfig() { - $trustedHosts = @Config::getInstance()->General['trusted_hosts']; - if (!is_array($trustedHosts)) { - return array(); - } - foreach ($trustedHosts as &$trustedHost) { - // Case user wrote in the config, http://example.com/test instead of example.com - if (UrlHelper::isLookLikeUrl($trustedHost)) { - $trustedHost = parse_url($trustedHost, PHP_URL_HOST); + $hosts = self::getHostsFromConfig('General', 'trusted_hosts'); + + // Case user wrote in the config, http://example.com/test instead of example.com + foreach ($hosts as &$host) { + if (UrlHelper::isLookLikeUrl($host)) { + $host = parse_url($host, PHP_URL_HOST); } } - return $trustedHosts; + return $hosts; } public static function getTrustedHosts() { - $trustedHosts = self::getTrustedHostsFromConfig(); + return self::getTrustedHostsFromConfig(); + } - /* used by Piwik PRO */ - Piwik::postEvent('Url.filterTrustedHosts', array(&$trustedHosts)); + public static function getCorsHostsFromConfig() + { + return self::getHostsFromConfig('General', 'cors_domains'); + } - return $trustedHosts; + /** + * Returns hostname, without port numbers + * + * @param $host + * @return array + */ + public static function getHostSanitized($host) + { + if (!class_exists("Piwik\\Network\\IPUtils")) { + throw new Exception("Piwik\\Network\\IPUtils could not be found, maybe you are using Piwik from git and need to update Composer. $ php composer.phar update"); + } + return IPUtils::sanitizeIp($host); + } + + protected static function getHostsFromConfig($domain, $key) + { + $config = @Config::getInstance()->$domain; + + if (!isset($config[$key])) { + return array(); + } + + $hosts = $config[$key]; + if (!is_array($hosts)) { + return array(); + } + return $hosts; + } + + /** + * Returns the host part of any valid URL. + * + * @param string $url Any fully qualified URL + * @return string|null The actual host in lower case or null if $url is not a valid fully qualified URL. + */ + public static function getHostFromUrl($url) + { + $parsedUrl = parse_url($url); + + if (empty($parsedUrl['host'])) { + return; + } + + return Common::mb_strtolower($parsedUrl['host']); + } + + /** + * Checks whether any of the given URLs has the given host. If not, we will also check whether any URL uses a + * subdomain of the given host. For instance if host is "example.com" and a URL is "http://www.example.com" we + * consider this as valid and return true. The always trusted hosts such as "127.0.0.1" are considered valid as well. + * + * @param $host + * @param $urls + * @return bool + */ + public static function isHostInUrls($host, $urls) + { + if (empty($host)) { + return false; + } + + $host = Common::mb_strtolower($host); + + if (!empty($urls)) { + foreach ($urls as $url) { + if (Common::mb_strtolower($url) === $host) { + return true; + } + + $siteHost = self::getHostFromUrl($url); + + if ($siteHost === $host) { + return true; + } + + if (Common::stringEndsWith($siteHost, '.' . $host)) { + // allow subdomains + return true; + } + } + } + + return in_array($host, self::getAlwaysTrustedHosts()); + } + + /** + * List of hosts that are never checked for validity. + * + * @return array + */ + private static function getAlwaysTrustedHosts() + { + return self::getLocalHostnames(); + } + + /** + * @return array + */ + public static function getLocalHostnames() + { + return array('localhost', '127.0.0.1', '::1', '[::1]'); + } + + /** + * @return bool + */ + public static function isSecureConnectionAssumedByPiwikButNotForcedYet() + { + $isSecureConnectionLikelyNotUsed = Url::isSecureConnectionLikelyNotUsed(); + $hasSessionCookieSecureFlag = ProxyHttp::isHttps(); + $isSecureConnectionAssumedByPiwikButNotForcedYet = Url::isPiwikConfiguredToAssumeSecureConnection() && !SettingsPiwik::isHttpsForced(); + + return $isSecureConnectionLikelyNotUsed + && $hasSessionCookieSecureFlag + && $isSecureConnectionAssumedByPiwikButNotForcedYet; + } + + /** + * @return string + */ + protected static function getCurrentSchemeFromRequestHeader() + { + if ((isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] === true)) + || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') + ) { + + return 'https'; + } + return 'http'; + } + + protected static function isSecureConnectionLikelyNotUsed() + { + return Url::getCurrentSchemeFromRequestHeader() == 'http'; + } + + /** + * @return bool + */ + protected static function isPiwikConfiguredToAssumeSecureConnection() + { + $assume_secure_protocol = @Config::getInstance()->General['assume_secure_protocol']; + return (bool) $assume_secure_protocol; } } diff --git a/www/analytics/core/UrlHelper.php b/www/analytics/core/UrlHelper.php index 2ce26ba1..66a0e64e 100644 --- a/www/analytics/core/UrlHelper.php +++ b/www/analytics/core/UrlHelper.php @@ -1,6 +1,6 @@ getCountryList(true))); } return preg_replace( @@ -96,21 +101,22 @@ class UrlHelper * We don't need a precise test here because the value comes from the website * tracked source code and the URLs may look very strange. * + * @api * @param string $url * @return bool */ public static function isLookLikeUrl($url) { - return preg_match('~^(ftp|news|http|https)?://(.*)$~D', $url, $matches) !== 0 - && strlen($matches[2]) > 0; + return preg_match('~^(([[:alpha:]][[:alnum:]+.-]*)?:)?//(.*)$~D', $url, $matches) !== 0 + && strlen($matches[3]) > 0; } /** * Returns a URL created from the result of the [parse_url](http://php.net/manual/en/function.parse-url.php) * function. - * + * * Copied from the PHP comments at [http://php.net/parse_url](http://php.net/parse_url). - * + * * @param array $parsed Result of [parse_url](http://php.net/manual/en/function.parse-url.php). * @return false|string The URL or `false` if `$parsed` isn't an array. * @api @@ -149,6 +155,17 @@ class UrlHelper if (strlen($urlQuery) == 0) { return array(); } + + // TODO: this method should not use a cache. callers should instead have their own cache, configured through DI. + // one undesirable side effect of using a cache here, is that this method can now init the StaticContainer, which makes setting + // test environment for RequestCommand more complicated. + $cache = Cache::getTransientCache(); + $cacheKey = 'arrayFromQuery' . $urlQuery; + + if ($cache->contains($cacheKey)) { + return $cache->fetch($cacheKey); + } + if ($urlQuery[0] == '?') { $urlQuery = substr($urlQuery, 1); } @@ -190,10 +207,13 @@ class UrlHelper $nameToValue[$name] = array(); } array_push($nameToValue[$name], $value); - } else if (!empty($name)) { + } elseif (!empty($name)) { $nameToValue[$name] = $value; } } + + $cache->save($cacheKey, $nameToValue); + return $nameToValue; } @@ -208,6 +228,7 @@ class UrlHelper public static function getParameterFromQueryString($urlQuery, $parameter) { $nameToValue = self::getArrayFromQueryString($urlQuery); + if (isset($nameToValue[$parameter])) { return $nameToValue[$parameter]; } @@ -226,7 +247,10 @@ class UrlHelper $parsedUrl = parse_url($url); $result = ''; if (isset($parsedUrl['path'])) { - $result .= substr($parsedUrl['path'], 1); + if (substr($parsedUrl['path'], 0, 1) == '/') { + $parsedUrl['path'] = substr($parsedUrl['path'], 1); + } + $result .= $parsedUrl['path']; } if (isset($parsedUrl['query'])) { $result .= '?' . $parsedUrl['query']; @@ -234,229 +258,6 @@ class UrlHelper return $result; } - - /** - * Extracts a keyword from a raw not encoded URL. - * Will only extract keyword if a known search engine has been detected. - * Returns the keyword: - * - in UTF8: automatically converted from other charsets when applicable - * - strtolowered: "QUErY test!" will return "query test!" - * - trimmed: extra spaces before and after are removed - * - * Lists of supported search engines can be found in /core/DataFiles/SearchEngines.php - * The function returns false when a keyword couldn't be found. - * eg. if the url is "http://www.google.com/partners.html" this will return false, - * as the google keyword parameter couldn't be found. - * - * @see unit tests in /tests/core/Common.test.php - * @param string $referrerUrl URL referrer URL, eg. $_SERVER['HTTP_REFERER'] - * @return array|bool false if a keyword couldn't be extracted, - * or array( - * 'name' => 'Google', - * 'keywords' => 'my searched keywords') - */ - public static function extractSearchEngineInformationFromUrl($referrerUrl) - { - $referrerParsed = @parse_url($referrerUrl); - $referrerHost = ''; - if (isset($referrerParsed['host'])) { - $referrerHost = $referrerParsed['host']; - } - if (empty($referrerHost)) { - return false; - } - // some search engines (eg. Bing Images) use the same domain - // as an existing search engine (eg. Bing), we must also use the url path - $referrerPath = ''; - if (isset($referrerParsed['path'])) { - $referrerPath = $referrerParsed['path']; - } - - // no search query - if (!isset($referrerParsed['query'])) { - $referrerParsed['query'] = ''; - } - $query = $referrerParsed['query']; - - // Google Referrers URLs sometimes have the fragment which contains the keyword - if (!empty($referrerParsed['fragment'])) { - $query .= '&' . $referrerParsed['fragment']; - } - - $searchEngines = Common::getSearchEngineUrls(); - - $hostPattern = self::getLossyUrl($referrerHost); - if (array_key_exists($referrerHost . $referrerPath, $searchEngines)) { - $referrerHost = $referrerHost . $referrerPath; - } elseif (array_key_exists($hostPattern . $referrerPath, $searchEngines)) { - $referrerHost = $hostPattern . $referrerPath; - } elseif (array_key_exists($hostPattern, $searchEngines)) { - $referrerHost = $hostPattern; - } elseif (!array_key_exists($referrerHost, $searchEngines)) { - if (!strncmp($query, 'cx=partner-pub-', 15)) { - // Google custom search engine - $referrerHost = 'google.com/cse'; - } elseif (!strncmp($referrerPath, '/pemonitorhosted/ws/results/', 28)) { - // private-label search powered by InfoSpace Metasearch - $referrerHost = 'wsdsold.infospace.com'; - } elseif (strpos($referrerHost, '.images.search.yahoo.com') != false) { - // Yahoo! Images - $referrerHost = 'images.search.yahoo.com'; - } elseif (strpos($referrerHost, '.search.yahoo.com') != false) { - // Yahoo! - $referrerHost = 'search.yahoo.com'; - } else { - return false; - } - } - $searchEngineName = $searchEngines[$referrerHost][0]; - $variableNames = null; - if (isset($searchEngines[$referrerHost][1])) { - $variableNames = $searchEngines[$referrerHost][1]; - } - if (!$variableNames) { - $searchEngineNames = Common::getSearchEngineNames(); - $url = $searchEngineNames[$searchEngineName]; - $variableNames = $searchEngines[$url][1]; - } - if (!is_array($variableNames)) { - $variableNames = array($variableNames); - } - - $key = null; - if ($searchEngineName === 'Google Images' - || ($searchEngineName === 'Google' && strpos($referrerUrl, '/imgres') !== false) - ) { - if (strpos($query, '&prev') !== false) { - $query = urldecode(trim(self::getParameterFromQueryString($query, 'prev'))); - $query = str_replace('&', '&', strstr($query, '?')); - } - $searchEngineName = 'Google Images'; - } else if ($searchEngineName === 'Google' - && (strpos($query, '&as_') !== false || strpos($query, 'as_') === 0) - ) { - $keys = array(); - $key = self::getParameterFromQueryString($query, 'as_q'); - if (!empty($key)) { - array_push($keys, $key); - } - $key = self::getParameterFromQueryString($query, 'as_oq'); - if (!empty($key)) { - array_push($keys, str_replace('+', ' OR ', $key)); - } - $key = self::getParameterFromQueryString($query, 'as_epq'); - if (!empty($key)) { - array_push($keys, "\"$key\""); - } - $key = self::getParameterFromQueryString($query, 'as_eq'); - if (!empty($key)) { - array_push($keys, "-$key"); - } - $key = trim(urldecode(implode(' ', $keys))); - } - - if ($searchEngineName === 'Google') { - // top bar menu - $tbm = self::getParameterFromQueryString($query, 'tbm'); - switch ($tbm) { - case 'isch': - $searchEngineName = 'Google Images'; - break; - case 'vid': - $searchEngineName = 'Google Video'; - break; - case 'shop': - $searchEngineName = 'Google Shopping'; - break; - } - } - - if (empty($key)) { - foreach ($variableNames as $variableName) { - if ($variableName[0] == '/') { - // regular expression match - if (preg_match($variableName, $referrerUrl, $matches)) { - $key = trim(urldecode($matches[1])); - break; - } - } else { - // search for keywords now &vname=keyword - $key = self::getParameterFromQueryString($query, $variableName); - $key = trim(urldecode($key)); - - // Special case: Google & empty q parameter - if (empty($key) - && $variableName == 'q' - - && ( - // Google search with no keyword - ($searchEngineName == 'Google' - && ( // First, they started putting an empty q= parameter - strpos($query, '&q=') !== false - || strpos($query, '?q=') !== false - // then they started sending the full host only (no path/query string) - || (empty($query) && (empty($referrerPath) || $referrerPath == '/') && empty($referrerParsed['fragment'])) - ) - ) - // search engines with no keyword - || $searchEngineName == 'Google Images' - || $searchEngineName == 'DuckDuckGo') - ) { - $key = false; - } - if (!empty($key) - || $key === false - ) { - break; - } - } - } - } - - // $key === false is the special case "No keyword provided" which is a Search engine match - if ($key === null - || $key === '' - ) { - return false; - } - - if (!empty($key)) { - if (function_exists('iconv') - && isset($searchEngines[$referrerHost][3]) - ) { - // accepts string, array, or comma-separated list string in preferred order - $charsets = $searchEngines[$referrerHost][3]; - if (!is_array($charsets)) { - $charsets = explode(',', $charsets); - } - - if (!empty($charsets)) { - $charset = $charsets[0]; - if (count($charsets) > 1 - && function_exists('mb_detect_encoding') - ) { - $charset = mb_detect_encoding($key, $charsets); - if ($charset === false) { - $charset = $charsets[0]; - } - } - - $newkey = @iconv($charset, 'UTF-8//IGNORE', $key); - if (!empty($newkey)) { - $key = $newkey; - } - } - } - - $key = Common::mb_strtolower($key); - } - - return array( - 'name' => $searchEngineName, - 'keywords' => $key, - ); - } - /** * Returns the query part from any valid url and adds additional parameters to the query part if needed. * diff --git a/www/analytics/core/Version.php b/www/analytics/core/Version.php index d187b41b..7725230d 100644 --- a/www/analytics/core/Version.php +++ b/www/analytics/core/Version.php @@ -1,6 +1,6 @@ isStableVersion($version) || $this->isNonStableVersion($version); + } + + private function isNonStableVersion($version) + { + return (bool) preg_match('/^(\d+)\.(\d+)\.(\d+)-.{1,4}(\d+)$/', $version); + } } diff --git a/www/analytics/core/View.php b/www/analytics/core/View.php index 311b58e3..b57a7f55 100644 --- a/www/analytics/core/View.php +++ b/www/analytics/core/View.php @@ -1,6 +1,6 @@ property2 = "another view property"; * return $view->render(); * } - * + * * * @api */ @@ -118,7 +119,7 @@ class View implements ViewInterface /** * Constructor. - * + * * @param string $templateFile The template file to load. Must be in the following format: * `"@MyPlugin/templateFileName"`. Note the absence of .twig * from the end of the name. @@ -190,6 +191,17 @@ class View implements ViewInterface return $this->templateVars[$key]; } + /** + * Returns true if a template variable has been set or not. + * + * @param string $name The name of the template variable. + * @return bool + */ + public function __isset($name) + { + return isset($this->templateVars[$name]); + } + private function initializeTwig() { $piwikTwig = new Twig(); @@ -211,39 +223,54 @@ class View implements ViewInterface $this->url = Common::sanitizeInputValue(Url::getCurrentUrl()); $this->token_auth = Piwik::getCurrentUserTokenAuth(); $this->userHasSomeAdminAccess = Piwik::isUserHasSomeAdminAccess(); + $this->userIsAnonymous = Piwik::isUserIsAnonymous(); $this->userIsSuperUser = Piwik::hasUserSuperUserAccess(); $this->latest_version_available = UpdateCheck::isNewestVersionAvailable(); $this->disableLink = Common::getRequestVar('disableLink', 0, 'int'); $this->isWidget = Common::getRequestVar('widget', 0, 'int'); - $this->cacheBuster = UIAssetCacheBuster::getInstance()->piwikVersionBasedCacheBuster(); + $piwikProAds = StaticContainer::get('Piwik\PiwikPro\Advertising'); + $this->arePiwikProAdsEnabled = $piwikProAds->arePiwikProAdsEnabled(); + + if (Development::isEnabled()) { + $cacheBuster = rand(0, 10000); + } else { + $cacheBuster = UIAssetCacheBuster::getInstance()->piwikVersionBasedCacheBuster(); + } + + $this->cacheBuster = $cacheBuster; + $this->loginModule = Piwik::getLoginPluginName(); $user = APIUsersManager::getInstance()->getUser($this->userLogin); $this->userAlias = $user['alias']; } catch (Exception $e) { - // can fail, for example at installation (no plugin loaded yet) - } + Log::debug($e); - try { - $this->totalTimeGeneration = Registry::get('timer')->getTime(); - $this->totalNumberOfQueries = Profiler::getQueryCount(); - } catch (Exception $e) { - $this->totalNumberOfQueries = 0; + // can fail, for example at installation (no plugin loaded yet) } ProxyHttp::overrideCacheControlHeaders('no-store'); - @header('Content-Type: ' . $this->contentType); + Common::sendHeader('Content-Type: ' . $this->contentType); // always sending this header, sometimes empty, to ensure that Dashboard embed loads (which could call this header() multiple times, the last one will prevail) - @header('X-Frame-Options: ' . (string)$this->xFrameOptions); + Common::sendHeader('X-Frame-Options: ' . (string)$this->xFrameOptions); return $this->renderTwigTemplate(); } protected function renderTwigTemplate() { - $output = $this->twig->render($this->getTemplateFile(), $this->getTemplateVars()); + try { + $output = $this->twig->render($this->getTemplateFile(), $this->getTemplateVars()); + } catch (Exception $ex) { + // twig does not rethrow exceptions, it wraps them so we log the cause if we can find it + $cause = $ex->getPrevious(); + Log::debug($cause === null ? $ex : $cause); + + throw $ex; + } + $output = $this->applyFilter_cacheBuster($output); $helper = new Theme; @@ -253,8 +280,18 @@ class View implements ViewInterface protected function applyFilter_cacheBuster($output) { - $cacheBuster = UIAssetCacheBuster::getInstance()->piwikVersionBasedCacheBuster(); - $tag = 'cb=' . $cacheBuster; + $assetManager = AssetManager::getInstance(); + + $stylesheet = $assetManager->getMergedStylesheetAsset(); + if ($stylesheet->exists()) { + $content = $stylesheet->getContent(); + } else { + $content = $assetManager->getMergedStylesheet()->getContent(); + } + + $cacheBuster = UIAssetCacheBuster::getInstance(); + $tagJs = 'cb=' . $cacheBuster->piwikVersionBasedCacheBuster(); + $tagCss = 'cb=' . $cacheBuster->md5BasedCacheBuster($content); $pattern = array( '~ - - - */ -function copy(source, destination){ - if (isWindow(source) || isScope(source)) { - throw ngMinErr('cpws', - "Can't copy! Making copies of Window or Scope instances is not supported."); - } - - if (!destination) { - destination = source; - if (source) { - if (isArray(source)) { - destination = copy(source, []); - } else if (isDate(source)) { - destination = new Date(source.getTime()); - } else if (isRegExp(source)) { - destination = new RegExp(source.source); - } else if (isObject(source)) { - destination = copy(source, {}); - } - } - } else { - if (source === destination) throw ngMinErr('cpi', - "Can't copy! Source and destination are identical."); - if (isArray(source)) { - destination.length = 0; - for ( var i = 0; i < source.length; i++) { - destination.push(copy(source[i])); - } - } else { - var h = destination.$$hashKey; - forEach(destination, function(value, key){ - delete destination[key]; - }); - for ( var key in source) { - destination[key] = copy(source[key]); - } - setHashKey(destination,h); - } - } - return destination; -} - -/** - * Create a shallow copy of an object - */ -function shallowCopy(src, dst) { - dst = dst || {}; - - for(var key in src) { - // shallowCopy is only ever called by $compile nodeLinkFn, which has control over src - // so we don't need to worry about using our custom hasOwnProperty here - if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { - dst[key] = src[key]; - } - } - - return dst; -} - - -/** - * @ngdoc function - * @name angular.equals - * @function - * - * @description - * Determines if two objects or two values are equivalent. Supports value types, regular - * expressions, arrays and objects. - * - * Two objects or values are considered equivalent if at least one of the following is true: - * - * * Both objects or values pass `===` comparison. - * * Both objects or values are of the same type and all of their properties are equal by - * comparing them with `angular.equals`. - * * Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal) - * * Both values represent the same regular expression (In JavasScript, - * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual - * representation matches). - * - * During a property comparison, properties of `function` type and properties with names - * that begin with `$` are ignored. - * - * Scope and DOMWindow objects are being compared only by identify (`===`). - * - * @param {*} o1 Object or value to compare. - * @param {*} o2 Object or value to compare. - * @returns {boolean} True if arguments are equal. - */ -function equals(o1, o2) { - if (o1 === o2) return true; - if (o1 === null || o2 === null) return false; - if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN - var t1 = typeof o1, t2 = typeof o2, length, key, keySet; - if (t1 == t2) { - if (t1 == 'object') { - if (isArray(o1)) { - if (!isArray(o2)) return false; - if ((length = o1.length) == o2.length) { - for(key=0; key 2 ? sliceArgs(arguments, 2) : []; - if (isFunction(fn) && !(fn instanceof RegExp)) { - return curryArgs.length - ? function() { - return arguments.length - ? fn.apply(self, curryArgs.concat(slice.call(arguments, 0))) - : fn.apply(self, curryArgs); - } - : function() { - return arguments.length - ? fn.apply(self, arguments) - : fn.call(self); - }; - } else { - // in IE, native methods are not functions so they cannot be bound (note: they don't need to be) - return fn; - } -} - - -function toJsonReplacer(key, value) { - var val = value; - - if (typeof key === 'string' && key.charAt(0) === '$') { - val = undefined; - } else if (isWindow(value)) { - val = '$WINDOW'; - } else if (value && document === value) { - val = '$DOCUMENT'; - } else if (isScope(value)) { - val = '$SCOPE'; - } - - return val; -} - - -/** - * @ngdoc function - * @name angular.toJson - * @function - * - * @description - * Serializes input into a JSON-formatted string. Properties with leading $ characters will be - * stripped since angular uses this notation internally. - * - * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. - * @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace. - * @returns {string|undefined} JSON-ified string representing `obj`. - */ -function toJson(obj, pretty) { - if (typeof obj === 'undefined') return undefined; - return JSON.stringify(obj, toJsonReplacer, pretty ? ' ' : null); -} - - -/** - * @ngdoc function - * @name angular.fromJson - * @function - * - * @description - * Deserializes a JSON string. - * - * @param {string} json JSON string to deserialize. - * @returns {Object|Array|Date|string|number} Deserialized thingy. - */ -function fromJson(json) { - return isString(json) - ? JSON.parse(json) - : json; -} - - -function toBoolean(value) { - if (typeof value === 'function') { - value = true; - } else if (value && value.length !== 0) { - var v = lowercase("" + value); - value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); - } else { - value = false; - } - return value; -} - -/** - * @returns {string} Returns the string representation of the element. - */ -function startingTag(element) { - element = jqLite(element).clone(); - try { - // turns out IE does not let you set .html() on elements which - // are not allowed to have children. So we just ignore it. - element.empty(); - } catch(e) {} - // As Per DOM Standards - var TEXT_NODE = 3; - var elemHtml = jqLite('
    ').append(element).html(); - try { - return element[0].nodeType === TEXT_NODE ? lowercase(elemHtml) : - elemHtml. - match(/^(<[^>]+>)/)[1]. - replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); }); - } catch(e) { - return lowercase(elemHtml); - } - -} - - -///////////////////////////////////////////////// - -/** - * Tries to decode the URI component without throwing an exception. - * - * @private - * @param str value potential URI component to check. - * @returns {boolean} True if `value` can be decoded - * with the decodeURIComponent function. - */ -function tryDecodeURIComponent(value) { - try { - return decodeURIComponent(value); - } catch(e) { - // Ignore any invalid uri component - } -} - - -/** - * Parses an escaped url query string into key-value pairs. - * @returns Object.<(string|boolean)> - */ -function parseKeyValue(/**string*/keyValue) { - var obj = {}, key_value, key; - forEach((keyValue || "").split('&'), function(keyValue){ - if ( keyValue ) { - key_value = keyValue.split('='); - key = tryDecodeURIComponent(key_value[0]); - if ( isDefined(key) ) { - var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; - if (!obj[key]) { - obj[key] = val; - } else if(isArray(obj[key])) { - obj[key].push(val); - } else { - obj[key] = [obj[key],val]; - } - } - } - }); - return obj; -} - -function toKeyValue(obj) { - var parts = []; - forEach(obj, function(value, key) { - if (isArray(value)) { - forEach(value, function(arrayValue) { - parts.push(encodeUriQuery(key, true) + - (arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true))); - }); - } else { - parts.push(encodeUriQuery(key, true) + - (value === true ? '' : '=' + encodeUriQuery(value, true))); - } - }); - return parts.length ? parts.join('&') : ''; -} - - -/** - * We need our custom method because encodeURIComponent is too aggressive and doesn't follow - * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path - * segments: - * segment = *pchar - * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - * pct-encoded = "%" HEXDIG HEXDIG - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - * / "*" / "+" / "," / ";" / "=" - */ -function encodeUriSegment(val) { - return encodeUriQuery(val, true). - replace(/%26/gi, '&'). - replace(/%3D/gi, '='). - replace(/%2B/gi, '+'); -} - - -/** - * This method is intended for encoding *key* or *value* parts of query component. We need a custom - * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be - * encoded per http://tools.ietf.org/html/rfc3986: - * query = *( pchar / "/" / "?" ) - * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * pct-encoded = "%" HEXDIG HEXDIG - * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - * / "*" / "+" / "," / ";" / "=" - */ -function encodeUriQuery(val, pctEncodeSpaces) { - return encodeURIComponent(val). - replace(/%40/gi, '@'). - replace(/%3A/gi, ':'). - replace(/%24/g, '$'). - replace(/%2C/gi, ','). - replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); -} - - -/** - * @ngdoc directive - * @name ng.directive:ngApp - * - * @element ANY - * @param {angular.Module} ngApp an optional application - * {@link angular.module module} name to load. - * - * @description - * - * Use this directive to **auto-bootstrap** an AngularJS application. The `ngApp` directive - * designates the **root element** of the application and is typically placed near the root element - * of the page - e.g. on the `` or `` tags. - * - * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` - * found in the document will be used to define the root element to auto-bootstrap as an - * application. To run multiple applications in an HTML document you must manually bootstrap them using - * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other. - * - * You can specify an **AngularJS module** to be used as the root module for the application. This - * module will be loaded into the {@link AUTO.$injector} when the application is bootstrapped and - * should contain the application code needed or have dependencies on other modules that will - * contain the code. See {@link angular.module} for more information. - * - * In the example below if the `ngApp` directive were not placed on the `html` element then the - * document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}` - * would not be resolved to `3`. - * - * `ngApp` is the easiest, and most common, way to bootstrap an application. - * - - -
    - I can add: {{a}} + {{b}} = {{ a+b }} -
    -
    - - angular.module('ngAppDemo', []).controller('ngAppDemoController', function($scope) { - $scope.a = 1; - $scope.b = 2; - }); - -
    - * - */ -function angularInit(element, bootstrap) { - var elements = [element], - appElement, - module, - names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'], - NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/; - - function append(element) { - element && elements.push(element); - } - - forEach(names, function(name) { - names[name] = true; - append(document.getElementById(name)); - name = name.replace(':', '\\:'); - if (element.querySelectorAll) { - forEach(element.querySelectorAll('.' + name), append); - forEach(element.querySelectorAll('.' + name + '\\:'), append); - forEach(element.querySelectorAll('[' + name + ']'), append); - } - }); - - forEach(elements, function(element) { - if (!appElement) { - var className = ' ' + element.className + ' '; - var match = NG_APP_CLASS_REGEXP.exec(className); - if (match) { - appElement = element; - module = (match[2] || '').replace(/\s+/g, ','); - } else { - forEach(element.attributes, function(attr) { - if (!appElement && names[attr.name]) { - appElement = element; - module = attr.value; - } - }); - } - } - }); - if (appElement) { - bootstrap(appElement, module ? [module] : []); - } -} - -/** - * @ngdoc function - * @name angular.bootstrap - * @description - * Use this function to manually start up angular application. - * - * See: {@link guide/bootstrap Bootstrap} - * - * Note that ngScenario-based end-to-end tests cannot use this function to bootstrap manually. - * They must use {@link api/ng.directive:ngApp ngApp}. - * - * @param {Element} element DOM element which is the root of angular application. - * @param {Array=} modules an array of modules to load into the application. - * Each item in the array should be the name of a predefined module or a (DI annotated) - * function that will be invoked by the injector as a run block. - * See: {@link angular.module modules} - * @returns {AUTO.$injector} Returns the newly created injector for this app. - */ -function bootstrap(element, modules) { - var doBootstrap = function() { - element = jqLite(element); - - if (element.injector()) { - var tag = (element[0] === document) ? 'document' : startingTag(element); - throw ngMinErr('btstrpd', "App Already Bootstrapped with this Element '{0}'", tag); - } - - modules = modules || []; - modules.unshift(['$provide', function($provide) { - $provide.value('$rootElement', element); - }]); - modules.unshift('ng'); - var injector = createInjector(modules); - injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', - function(scope, element, compile, injector, animate) { - scope.$apply(function() { - element.data('$injector', injector); - compile(element)(scope); - }); - }] - ); - return injector; - }; - - var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; - - if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { - return doBootstrap(); - } - - window.name = window.name.replace(NG_DEFER_BOOTSTRAP, ''); - angular.resumeBootstrap = function(extraModules) { - forEach(extraModules, function(module) { - modules.push(module); - }); - doBootstrap(); - }; -} - -var SNAKE_CASE_REGEXP = /[A-Z]/g; -function snake_case(name, separator){ - separator = separator || '_'; - return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { - return (pos ? separator : '') + letter.toLowerCase(); - }); -} - -function bindJQuery() { - // bind to jQuery if present; - jQuery = window.jQuery; - // reset to jQuery or default to us. - if (jQuery) { - jqLite = jQuery; - extend(jQuery.fn, { - scope: JQLitePrototype.scope, - isolateScope: JQLitePrototype.isolateScope, - controller: JQLitePrototype.controller, - injector: JQLitePrototype.injector, - inheritedData: JQLitePrototype.inheritedData - }); - // Method signature: - // jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) - jqLitePatchJQueryRemove('remove', true, true, false); - jqLitePatchJQueryRemove('empty', false, false, false); - jqLitePatchJQueryRemove('html', false, false, true); - } else { - jqLite = JQLite; - } - angular.element = jqLite; -} - -/** - * throw error if the argument is falsy. - */ -function assertArg(arg, name, reason) { - if (!arg) { - throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); - } - return arg; -} - -function assertArgFn(arg, name, acceptArrayAnnotation) { - if (acceptArrayAnnotation && isArray(arg)) { - arg = arg[arg.length - 1]; - } - - assertArg(isFunction(arg), name, 'not a function, got ' + - (arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg)); - return arg; -} - -/** - * throw error if the name given is hasOwnProperty - * @param {String} name the name to test - * @param {String} context the context in which the name is used, such as module or directive - */ -function assertNotHasOwnProperty(name, context) { - if (name === 'hasOwnProperty') { - throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context); - } -} - -/** - * Return the value accessible from the object by path. Any undefined traversals are ignored - * @param {Object} obj starting object - * @param {string} path path to traverse - * @param {boolean=true} bindFnToScope - * @returns value as accessible by path - */ -//TODO(misko): this function needs to be removed -function getter(obj, path, bindFnToScope) { - if (!path) return obj; - var keys = path.split('.'); - var key; - var lastInstance = obj; - var len = keys.length; - - for (var i = 0; i < len; i++) { - key = keys[i]; - if (obj) { - obj = (lastInstance = obj)[key]; - } - } - if (!bindFnToScope && isFunction(obj)) { - return bind(lastInstance, obj); - } - return obj; -} - -/** - * Return the DOM siblings between the first and last node in the given array. - * @param {Array} array like object - * @returns jQlite object containing the elements - */ -function getBlockElements(nodes) { - var startNode = nodes[0], - endNode = nodes[nodes.length - 1]; - if (startNode === endNode) { - return jqLite(startNode); - } - - var element = startNode; - var elements = [element]; - - do { - element = element.nextSibling; - if (!element) break; - elements.push(element); - } while (element !== endNode); - - return jqLite(elements); -} - -/** - * @ngdoc interface - * @name angular.Module - * @description - * - * Interface for configuring angular {@link angular.module modules}. - */ - -function setupModuleLoader(window) { - - var $injectorMinErr = minErr('$injector'); - var ngMinErr = minErr('ng'); - - function ensure(obj, name, factory) { - return obj[name] || (obj[name] = factory()); - } - - var angular = ensure(window, 'angular', Object); - - // We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap - angular.$$minErr = angular.$$minErr || minErr; - - return ensure(angular, 'module', function() { - /** @type {Object.} */ - var modules = {}; - - /** - * @ngdoc function - * @name angular.module - * @description - * - * The `angular.module` is a global place for creating, registering and retrieving Angular - * modules. - * All modules (angular core or 3rd party) that should be available to an application must be - * registered using this mechanism. - * - * When passed two or more arguments, a new module is created. If passed only one argument, an - * existing module (the name passed as the first argument to `module`) is retrieved. - * - * - * # Module - * - * A module is a collection of services, directives, filters, and configuration information. - * `angular.module` is used to configure the {@link AUTO.$injector $injector}. - * - *
    -     * // Create a new module
    -     * var myModule = angular.module('myModule', []);
    -     *
    -     * // register a new service
    -     * myModule.value('appName', 'MyCoolApp');
    -     *
    -     * // configure existing services inside initialization blocks.
    -     * myModule.config(function($locationProvider) {
    -     *   // Configure existing providers
    -     *   $locationProvider.hashPrefix('!');
    -     * });
    -     * 
    - * - * Then you can create an injector and load your modules like this: - * - *
    -     * var injector = angular.injector(['ng', 'MyModule'])
    -     * 
    - * - * However it's more likely that you'll just use - * {@link ng.directive:ngApp ngApp} or - * {@link angular.bootstrap} to simplify this process for you. - * - * @param {!string} name The name of the module to create or retrieve. - * @param {Array.=} requires If specified then new module is being created. If - * unspecified then the the module is being retrieved for further configuration. - * @param {Function} configFn Optional configuration function for the module. Same as - * {@link angular.Module#methods_config Module#config()}. - * @returns {module} new module with the {@link angular.Module} api. - */ - return function module(name, requires, configFn) { - var assertNotHasOwnProperty = function(name, context) { - if (name === 'hasOwnProperty') { - throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); - } - }; - - assertNotHasOwnProperty(name, 'module'); - if (requires && modules.hasOwnProperty(name)) { - modules[name] = null; - } - return ensure(modules, name, function() { - if (!requires) { - throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " + - "the module name or forgot to load it. If registering a module ensure that you " + - "specify the dependencies as the second argument.", name); - } - - /** @type {!Array.>} */ - var invokeQueue = []; - - /** @type {!Array.} */ - var runBlocks = []; - - var config = invokeLater('$injector', 'invoke'); - - /** @type {angular.Module} */ - var moduleInstance = { - // Private state - _invokeQueue: invokeQueue, - _runBlocks: runBlocks, - - /** - * @ngdoc property - * @name angular.Module#requires - * @propertyOf angular.Module - * @returns {Array.} List of module names which must be loaded before this module. - * @description - * Holds the list of modules which the injector will load before the current module is - * loaded. - */ - requires: requires, - - /** - * @ngdoc property - * @name angular.Module#name - * @propertyOf angular.Module - * @returns {string} Name of the module. - * @description - */ - name: name, - - - /** - * @ngdoc method - * @name angular.Module#provider - * @methodOf angular.Module - * @param {string} name service name - * @param {Function} providerType Construction function for creating new instance of the - * service. - * @description - * See {@link AUTO.$provide#provider $provide.provider()}. - */ - provider: invokeLater('$provide', 'provider'), - - /** - * @ngdoc method - * @name angular.Module#factory - * @methodOf angular.Module - * @param {string} name service name - * @param {Function} providerFunction Function for creating new instance of the service. - * @description - * See {@link AUTO.$provide#factory $provide.factory()}. - */ - factory: invokeLater('$provide', 'factory'), - - /** - * @ngdoc method - * @name angular.Module#service - * @methodOf angular.Module - * @param {string} name service name - * @param {Function} constructor A constructor function that will be instantiated. - * @description - * See {@link AUTO.$provide#service $provide.service()}. - */ - service: invokeLater('$provide', 'service'), - - /** - * @ngdoc method - * @name angular.Module#value - * @methodOf angular.Module - * @param {string} name service name - * @param {*} object Service instance object. - * @description - * See {@link AUTO.$provide#value $provide.value()}. - */ - value: invokeLater('$provide', 'value'), - - /** - * @ngdoc method - * @name angular.Module#constant - * @methodOf angular.Module - * @param {string} name constant name - * @param {*} object Constant value. - * @description - * Because the constant are fixed, they get applied before other provide methods. - * See {@link AUTO.$provide#constant $provide.constant()}. - */ - constant: invokeLater('$provide', 'constant', 'unshift'), - - /** - * @ngdoc method - * @name angular.Module#animation - * @methodOf angular.Module - * @param {string} name animation name - * @param {Function} animationFactory Factory function for creating new instance of an - * animation. - * @description - * - * **NOTE**: animations take effect only if the **ngAnimate** module is loaded. - * - * - * Defines an animation hook that can be later used with - * {@link ngAnimate.$animate $animate} service and directives that use this service. - * - *
    -           * module.animation('.animation-name', function($inject1, $inject2) {
    -           *   return {
    -           *     eventName : function(element, done) {
    -           *       //code to run the animation
    -           *       //once complete, then run done()
    -           *       return function cancellationFunction(element) {
    -           *         //code to cancel the animation
    -           *       }
    -           *     }
    -           *   }
    -           * })
    -           * 
    - * - * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and - * {@link ngAnimate ngAnimate module} for more information. - */ - animation: invokeLater('$animateProvider', 'register'), - - /** - * @ngdoc method - * @name angular.Module#filter - * @methodOf angular.Module - * @param {string} name Filter name. - * @param {Function} filterFactory Factory function for creating new instance of filter. - * @description - * See {@link ng.$filterProvider#register $filterProvider.register()}. - */ - filter: invokeLater('$filterProvider', 'register'), - - /** - * @ngdoc method - * @name angular.Module#controller - * @methodOf angular.Module - * @param {string|Object} name Controller name, or an object map of controllers where the - * keys are the names and the values are the constructors. - * @param {Function} constructor Controller constructor function. - * @description - * See {@link ng.$controllerProvider#register $controllerProvider.register()}. - */ - controller: invokeLater('$controllerProvider', 'register'), - - /** - * @ngdoc method - * @name angular.Module#directive - * @methodOf angular.Module - * @param {string|Object} name Directive name, or an object map of directives where the - * keys are the names and the values are the factories. - * @param {Function} directiveFactory Factory function for creating new instance of - * directives. - * @description - * See {@link ng.$compileProvider#methods_directive $compileProvider.directive()}. - */ - directive: invokeLater('$compileProvider', 'directive'), - - /** - * @ngdoc method - * @name angular.Module#config - * @methodOf angular.Module - * @param {Function} configFn Execute this function on module load. Useful for service - * configuration. - * @description - * Use this method to register work which needs to be performed on module loading. - */ - config: config, - - /** - * @ngdoc method - * @name angular.Module#run - * @methodOf angular.Module - * @param {Function} initializationFn Execute this function after injector creation. - * Useful for application initialization. - * @description - * Use this method to register work which should be performed when the injector is done - * loading all modules. - */ - run: function(block) { - runBlocks.push(block); - return this; - } - }; - - if (configFn) { - config(configFn); - } - - return moduleInstance; - - /** - * @param {string} provider - * @param {string} method - * @param {String=} insertMethod - * @returns {angular.Module} - */ - function invokeLater(provider, method, insertMethod) { - return function() { - invokeQueue[insertMethod || 'push']([provider, method, arguments]); - return moduleInstance; - }; - } - }); - }; - }); - -} - -/* global - angularModule: true, - version: true, - - $LocaleProvider, - $CompileProvider, - - htmlAnchorDirective, - inputDirective, - inputDirective, - formDirective, - scriptDirective, - selectDirective, - styleDirective, - optionDirective, - ngBindDirective, - ngBindHtmlDirective, - ngBindTemplateDirective, - ngClassDirective, - ngClassEvenDirective, - ngClassOddDirective, - ngCspDirective, - ngCloakDirective, - ngControllerDirective, - ngFormDirective, - ngHideDirective, - ngIfDirective, - ngIncludeDirective, - ngIncludeFillContentDirective, - ngInitDirective, - ngNonBindableDirective, - ngPluralizeDirective, - ngRepeatDirective, - ngShowDirective, - ngStyleDirective, - ngSwitchDirective, - ngSwitchWhenDirective, - ngSwitchDefaultDirective, - ngOptionsDirective, - ngTranscludeDirective, - ngModelDirective, - ngListDirective, - ngChangeDirective, - requiredDirective, - requiredDirective, - ngValueDirective, - ngAttributeAliasDirectives, - ngEventDirectives, - - $AnchorScrollProvider, - $AnimateProvider, - $BrowserProvider, - $CacheFactoryProvider, - $ControllerProvider, - $DocumentProvider, - $ExceptionHandlerProvider, - $FilterProvider, - $InterpolateProvider, - $IntervalProvider, - $HttpProvider, - $HttpBackendProvider, - $LocationProvider, - $LogProvider, - $ParseProvider, - $RootScopeProvider, - $QProvider, - $$SanitizeUriProvider, - $SceProvider, - $SceDelegateProvider, - $SnifferProvider, - $TemplateCacheProvider, - $TimeoutProvider, - $WindowProvider -*/ - - -/** - * @ngdoc property - * @name angular.version - * @description - * An object that contains information about the current AngularJS version. This object has the - * following properties: - * - * - `full` – `{string}` – Full version string, such as "0.9.18". - * - `major` – `{number}` – Major version number, such as "0". - * - `minor` – `{number}` – Minor version number, such as "9". - * - `dot` – `{number}` – Dot version number, such as "18". - * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". - */ -var version = { - full: '1.2.13', // all of these placeholder strings will be replaced by grunt's - major: 1, // package task - minor: 2, - dot: 13, - codeName: 'romantic-transclusion' -}; - - -function publishExternalAPI(angular){ - extend(angular, { - 'bootstrap': bootstrap, - 'copy': copy, - 'extend': extend, - 'equals': equals, - 'element': jqLite, - 'forEach': forEach, - 'injector': createInjector, - 'noop':noop, - 'bind':bind, - 'toJson': toJson, - 'fromJson': fromJson, - 'identity':identity, - 'isUndefined': isUndefined, - 'isDefined': isDefined, - 'isString': isString, - 'isFunction': isFunction, - 'isObject': isObject, - 'isNumber': isNumber, - 'isElement': isElement, - 'isArray': isArray, - 'version': version, - 'isDate': isDate, - 'lowercase': lowercase, - 'uppercase': uppercase, - 'callbacks': {counter: 0}, - '$$minErr': minErr, - '$$csp': csp - }); - - angularModule = setupModuleLoader(window); - try { - angularModule('ngLocale'); - } catch (e) { - angularModule('ngLocale', []).provider('$locale', $LocaleProvider); - } - - angularModule('ng', ['ngLocale'], ['$provide', - function ngModule($provide) { - // $$sanitizeUriProvider needs to be before $compileProvider as it is used by it. - $provide.provider({ - $$sanitizeUri: $$SanitizeUriProvider - }); - $provide.provider('$compile', $CompileProvider). - directive({ - a: htmlAnchorDirective, - input: inputDirective, - textarea: inputDirective, - form: formDirective, - script: scriptDirective, - select: selectDirective, - style: styleDirective, - option: optionDirective, - ngBind: ngBindDirective, - ngBindHtml: ngBindHtmlDirective, - ngBindTemplate: ngBindTemplateDirective, - ngClass: ngClassDirective, - ngClassEven: ngClassEvenDirective, - ngClassOdd: ngClassOddDirective, - ngCloak: ngCloakDirective, - ngController: ngControllerDirective, - ngForm: ngFormDirective, - ngHide: ngHideDirective, - ngIf: ngIfDirective, - ngInclude: ngIncludeDirective, - ngInit: ngInitDirective, - ngNonBindable: ngNonBindableDirective, - ngPluralize: ngPluralizeDirective, - ngRepeat: ngRepeatDirective, - ngShow: ngShowDirective, - ngStyle: ngStyleDirective, - ngSwitch: ngSwitchDirective, - ngSwitchWhen: ngSwitchWhenDirective, - ngSwitchDefault: ngSwitchDefaultDirective, - ngOptions: ngOptionsDirective, - ngTransclude: ngTranscludeDirective, - ngModel: ngModelDirective, - ngList: ngListDirective, - ngChange: ngChangeDirective, - required: requiredDirective, - ngRequired: requiredDirective, - ngValue: ngValueDirective - }). - directive({ - ngInclude: ngIncludeFillContentDirective - }). - directive(ngAttributeAliasDirectives). - directive(ngEventDirectives); - $provide.provider({ - $anchorScroll: $AnchorScrollProvider, - $animate: $AnimateProvider, - $browser: $BrowserProvider, - $cacheFactory: $CacheFactoryProvider, - $controller: $ControllerProvider, - $document: $DocumentProvider, - $exceptionHandler: $ExceptionHandlerProvider, - $filter: $FilterProvider, - $interpolate: $InterpolateProvider, - $interval: $IntervalProvider, - $http: $HttpProvider, - $httpBackend: $HttpBackendProvider, - $location: $LocationProvider, - $log: $LogProvider, - $parse: $ParseProvider, - $rootScope: $RootScopeProvider, - $q: $QProvider, - $sce: $SceProvider, - $sceDelegate: $SceDelegateProvider, - $sniffer: $SnifferProvider, - $templateCache: $TemplateCacheProvider, - $timeout: $TimeoutProvider, - $window: $WindowProvider - }); - } - ]); -} - -/* global - - -JQLitePrototype, - -addEventListenerFn, - -removeEventListenerFn, - -BOOLEAN_ATTR -*/ - -////////////////////////////////// -//JQLite -////////////////////////////////// - -/** - * @ngdoc function - * @name angular.element - * @function - * - * @description - * Wraps a raw DOM element or HTML string as a [jQuery](http://jquery.com) element. - * - * If jQuery is available, `angular.element` is an alias for the - * [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element` - * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite." - * - *
    jqLite is a tiny, API-compatible subset of jQuery that allows - * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most - * commonly needed functionality with the goal of having a very small footprint.
    - * - * To use jQuery, simply load it before `DOMContentLoaded` event fired. - * - *
    **Note:** all element references in Angular are always wrapped with jQuery or - * jqLite; they are never raw DOM references.
    - * - * ## Angular's jqLite - * jqLite provides only the following jQuery methods: - * - * - [`addClass()`](http://api.jquery.com/addClass/) - * - [`after()`](http://api.jquery.com/after/) - * - [`append()`](http://api.jquery.com/append/) - * - [`attr()`](http://api.jquery.com/attr/) - * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData - * - [`children()`](http://api.jquery.com/children/) - Does not support selectors - * - [`clone()`](http://api.jquery.com/clone/) - * - [`contents()`](http://api.jquery.com/contents/) - * - [`css()`](http://api.jquery.com/css/) - * - [`data()`](http://api.jquery.com/data/) - * - [`empty()`](http://api.jquery.com/empty/) - * - [`eq()`](http://api.jquery.com/eq/) - * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name - * - [`hasClass()`](http://api.jquery.com/hasClass/) - * - [`html()`](http://api.jquery.com/html/) - * - [`next()`](http://api.jquery.com/next/) - Does not support selectors - * - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData - * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors - * - [`one()`](http://api.jquery.com/one/) - Does not support namespaces or selectors - * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors - * - [`prepend()`](http://api.jquery.com/prepend/) - * - [`prop()`](http://api.jquery.com/prop/) - * - [`ready()`](http://api.jquery.com/ready/) - * - [`remove()`](http://api.jquery.com/remove/) - * - [`removeAttr()`](http://api.jquery.com/removeAttr/) - * - [`removeClass()`](http://api.jquery.com/removeClass/) - * - [`removeData()`](http://api.jquery.com/removeData/) - * - [`replaceWith()`](http://api.jquery.com/replaceWith/) - * - [`text()`](http://api.jquery.com/text/) - * - [`toggleClass()`](http://api.jquery.com/toggleClass/) - * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. - * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces - * - [`val()`](http://api.jquery.com/val/) - * - [`wrap()`](http://api.jquery.com/wrap/) - * - * ## jQuery/jqLite Extras - * Angular also provides the following additional methods and events to both jQuery and jqLite: - * - * ### Events - * - `$destroy` - AngularJS intercepts all jqLite/jQuery's DOM destruction apis and fires this event - * on all DOM nodes being removed. This can be used to clean up any 3rd party bindings to the DOM - * element before it is removed. - * - * ### Methods - * - `controller(name)` - retrieves the controller of the current element or its parent. By default - * retrieves controller associated with the `ngController` directive. If `name` is provided as - * camelCase directive name, then the controller for this directive will be retrieved (e.g. - * `'ngModel'`). - * - `injector()` - retrieves the injector of the current element or its parent. - * - `scope()` - retrieves the {@link api/ng.$rootScope.Scope scope} of the current - * element or its parent. - * - `isolateScope()` - retrieves an isolate {@link api/ng.$rootScope.Scope scope} if one is attached directly to the - * current element. This getter should be used only on elements that contain a directive which starts a new isolate - * scope. Calling `scope()` on this element always returns the original non-isolate scope. - * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top - * parent element is reached. - * - * @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery. - * @returns {Object} jQuery object. - */ - -var jqCache = JQLite.cache = {}, - jqName = JQLite.expando = 'ng-' + new Date().getTime(), - jqId = 1, - addEventListenerFn = (window.document.addEventListener - ? function(element, type, fn) {element.addEventListener(type, fn, false);} - : function(element, type, fn) {element.attachEvent('on' + type, fn);}), - removeEventListenerFn = (window.document.removeEventListener - ? function(element, type, fn) {element.removeEventListener(type, fn, false); } - : function(element, type, fn) {element.detachEvent('on' + type, fn); }); - -/* - * !!! This is an undocumented "private" function !!! - */ -var jqData = JQLite._data = function(node) { - //jQuery always returns an object on cache miss - return this.cache[node[this.expando]] || {}; -}; - -function jqNextId() { return ++jqId; } - - -var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; -var MOZ_HACK_REGEXP = /^moz([A-Z])/; -var jqLiteMinErr = minErr('jqLite'); - -/** - * Converts snake_case to camelCase. - * Also there is special case for Moz prefix starting with upper case letter. - * @param name Name to normalize - */ -function camelCase(name) { - return name. - replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { - return offset ? letter.toUpperCase() : letter; - }). - replace(MOZ_HACK_REGEXP, 'Moz$1'); -} - -///////////////////////////////////////////// -// jQuery mutation patch -// -// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a -// $destroy event on all DOM nodes being removed. -// -///////////////////////////////////////////// - -function jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) { - var originalJqFn = jQuery.fn[name]; - originalJqFn = originalJqFn.$original || originalJqFn; - removePatch.$original = originalJqFn; - jQuery.fn[name] = removePatch; - - function removePatch(param) { - // jshint -W040 - var list = filterElems && param ? [this.filter(param)] : [this], - fireEvent = dispatchThis, - set, setIndex, setLength, - element, childIndex, childLength, children; - - if (!getterIfNoArguments || param != null) { - while(list.length) { - set = list.shift(); - for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) { - element = jqLite(set[setIndex]); - if (fireEvent) { - element.triggerHandler('$destroy'); - } else { - fireEvent = !fireEvent; - } - for(childIndex = 0, childLength = (children = element.children()).length; - childIndex < childLength; - childIndex++) { - list.push(jQuery(children[childIndex])); - } - } - } - } - return originalJqFn.apply(this, arguments); - } -} - -///////////////////////////////////////////// -function JQLite(element) { - if (element instanceof JQLite) { - return element; - } - if (isString(element)) { - element = trim(element); - } - if (!(this instanceof JQLite)) { - if (isString(element) && element.charAt(0) != '<') { - throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element'); - } - return new JQLite(element); - } - - if (isString(element)) { - var div = document.createElement('div'); - // Read about the NoScope elements here: - // http://msdn.microsoft.com/en-us/library/ms533897(VS.85).aspx - div.innerHTML = '
     
    ' + element; // IE insanity to make NoScope elements work! - div.removeChild(div.firstChild); // remove the superfluous div - jqLiteAddNodes(this, div.childNodes); - var fragment = jqLite(document.createDocumentFragment()); - fragment.append(this); // detach the elements from the temporary DOM div. - } else { - jqLiteAddNodes(this, element); - } -} - -function jqLiteClone(element) { - return element.cloneNode(true); -} - -function jqLiteDealoc(element){ - jqLiteRemoveData(element); - for ( var i = 0, children = element.childNodes || []; i < children.length; i++) { - jqLiteDealoc(children[i]); - } -} - -function jqLiteOff(element, type, fn, unsupported) { - if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument'); - - var events = jqLiteExpandoStore(element, 'events'), - handle = jqLiteExpandoStore(element, 'handle'); - - if (!handle) return; //no listeners registered - - if (isUndefined(type)) { - forEach(events, function(eventHandler, type) { - removeEventListenerFn(element, type, eventHandler); - delete events[type]; - }); - } else { - forEach(type.split(' '), function(type) { - if (isUndefined(fn)) { - removeEventListenerFn(element, type, events[type]); - delete events[type]; - } else { - arrayRemove(events[type] || [], fn); - } - }); - } -} - -function jqLiteRemoveData(element, name) { - var expandoId = element[jqName], - expandoStore = jqCache[expandoId]; - - if (expandoStore) { - if (name) { - delete jqCache[expandoId].data[name]; - return; - } - - if (expandoStore.handle) { - expandoStore.events.$destroy && expandoStore.handle({}, '$destroy'); - jqLiteOff(element); - } - delete jqCache[expandoId]; - element[jqName] = undefined; // ie does not allow deletion of attributes on elements. - } -} - -function jqLiteExpandoStore(element, key, value) { - var expandoId = element[jqName], - expandoStore = jqCache[expandoId || -1]; - - if (isDefined(value)) { - if (!expandoStore) { - element[jqName] = expandoId = jqNextId(); - expandoStore = jqCache[expandoId] = {}; - } - expandoStore[key] = value; - } else { - return expandoStore && expandoStore[key]; - } -} - -function jqLiteData(element, key, value) { - var data = jqLiteExpandoStore(element, 'data'), - isSetter = isDefined(value), - keyDefined = !isSetter && isDefined(key), - isSimpleGetter = keyDefined && !isObject(key); - - if (!data && !isSimpleGetter) { - jqLiteExpandoStore(element, 'data', data = {}); - } - - if (isSetter) { - data[key] = value; - } else { - if (keyDefined) { - if (isSimpleGetter) { - // don't create data in this case. - return data && data[key]; - } else { - extend(data, key); - } - } else { - return data; - } - } -} - -function jqLiteHasClass(element, selector) { - if (!element.getAttribute) return false; - return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " "). - indexOf( " " + selector + " " ) > -1); -} - -function jqLiteRemoveClass(element, cssClasses) { - if (cssClasses && element.setAttribute) { - forEach(cssClasses.split(' '), function(cssClass) { - element.setAttribute('class', trim( - (" " + (element.getAttribute('class') || '') + " ") - .replace(/[\n\t]/g, " ") - .replace(" " + trim(cssClass) + " ", " ")) - ); - }); - } -} - -function jqLiteAddClass(element, cssClasses) { - if (cssClasses && element.setAttribute) { - var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') - .replace(/[\n\t]/g, " "); - - forEach(cssClasses.split(' '), function(cssClass) { - cssClass = trim(cssClass); - if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { - existingClasses += cssClass + ' '; - } - }); - - element.setAttribute('class', trim(existingClasses)); - } -} - -function jqLiteAddNodes(root, elements) { - if (elements) { - elements = (!elements.nodeName && isDefined(elements.length) && !isWindow(elements)) - ? elements - : [ elements ]; - for(var i=0; i < elements.length; i++) { - root.push(elements[i]); - } - } -} - -function jqLiteController(element, name) { - return jqLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller'); -} - -function jqLiteInheritedData(element, name, value) { - element = jqLite(element); - - // if element is the document object work with the html element instead - // this makes $(document).scope() possible - if(element[0].nodeType == 9) { - element = element.find('html'); - } - var names = isArray(name) ? name : [name]; - - while (element.length) { - - for (var i = 0, ii = names.length; i < ii; i++) { - if ((value = element.data(names[i])) !== undefined) return value; - } - element = element.parent(); - } -} - -function jqLiteEmpty(element) { - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - jqLiteDealoc(childNodes[i]); - } - while (element.firstChild) { - element.removeChild(element.firstChild); - } -} - -////////////////////////////////////////// -// Functions which are declared directly. -////////////////////////////////////////// -var JQLitePrototype = JQLite.prototype = { - ready: function(fn) { - var fired = false; - - function trigger() { - if (fired) return; - fired = true; - fn(); - } - - // check if document already is loaded - if (document.readyState === 'complete'){ - setTimeout(trigger); - } else { - this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 - // we can not use jqLite since we are not done loading and jQuery could be loaded later. - // jshint -W064 - JQLite(window).on('load', trigger); // fallback to window.onload for others - // jshint +W064 - } - }, - toString: function() { - var value = []; - forEach(this, function(e){ value.push('' + e);}); - return '[' + value.join(', ') + ']'; - }, - - eq: function(index) { - return (index >= 0) ? jqLite(this[index]) : jqLite(this[this.length + index]); - }, - - length: 0, - push: push, - sort: [].sort, - splice: [].splice -}; - -////////////////////////////////////////// -// Functions iterating getter/setters. -// these functions return self on setter and -// value on get. -////////////////////////////////////////// -var BOOLEAN_ATTR = {}; -forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), function(value) { - BOOLEAN_ATTR[lowercase(value)] = value; -}); -var BOOLEAN_ELEMENTS = {}; -forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { - BOOLEAN_ELEMENTS[uppercase(value)] = true; -}); - -function getBooleanAttrName(element, name) { - // check dom last since we will most likely fail on name - var booleanAttr = BOOLEAN_ATTR[name.toLowerCase()]; - - // booleanAttr is here twice to minimize DOM access - return booleanAttr && BOOLEAN_ELEMENTS[element.nodeName] && booleanAttr; -} - -forEach({ - data: jqLiteData, - inheritedData: jqLiteInheritedData, - - scope: function(element) { - // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite(element).data('$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); - }, - - isolateScope: function(element) { - // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite(element).data('$isolateScope') || jqLite(element).data('$isolateScopeNoTemplate'); - }, - - controller: jqLiteController , - - injector: function(element) { - return jqLiteInheritedData(element, '$injector'); - }, - - removeAttr: function(element,name) { - element.removeAttribute(name); - }, - - hasClass: jqLiteHasClass, - - css: function(element, name, value) { - name = camelCase(name); - - if (isDefined(value)) { - element.style[name] = value; - } else { - var val; - - if (msie <= 8) { - // this is some IE specific weirdness that jQuery 1.6.4 does not sure why - val = element.currentStyle && element.currentStyle[name]; - if (val === '') val = 'auto'; - } - - val = val || element.style[name]; - - if (msie <= 8) { - // jquery weirdness :-/ - val = (val === '') ? undefined : val; - } - - return val; - } - }, - - attr: function(element, name, value){ - var lowercasedName = lowercase(name); - if (BOOLEAN_ATTR[lowercasedName]) { - if (isDefined(value)) { - if (!!value) { - element[name] = true; - element.setAttribute(name, lowercasedName); - } else { - element[name] = false; - element.removeAttribute(lowercasedName); - } - } else { - return (element[name] || - (element.attributes.getNamedItem(name)|| noop).specified) - ? lowercasedName - : undefined; - } - } else if (isDefined(value)) { - element.setAttribute(name, value); - } else if (element.getAttribute) { - // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code - // some elements (e.g. Document) don't have get attribute, so return undefined - var ret = element.getAttribute(name, 2); - // normalize non-existing attributes to undefined (as jQuery) - return ret === null ? undefined : ret; - } - }, - - prop: function(element, name, value) { - if (isDefined(value)) { - element[name] = value; - } else { - return element[name]; - } - }, - - text: (function() { - var NODE_TYPE_TEXT_PROPERTY = []; - if (msie < 9) { - NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/ - NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/ - } else { - NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/ - NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/ - } - getText.$dv = ''; - return getText; - - function getText(element, value) { - var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType]; - if (isUndefined(value)) { - return textProp ? element[textProp] : ''; - } - element[textProp] = value; - } - })(), - - val: function(element, value) { - if (isUndefined(value)) { - if (nodeName_(element) === 'SELECT' && element.multiple) { - var result = []; - forEach(element.options, function (option) { - if (option.selected) { - result.push(option.value || option.text); - } - }); - return result.length === 0 ? null : result; - } - return element.value; - } - element.value = value; - }, - - html: function(element, value) { - if (isUndefined(value)) { - return element.innerHTML; - } - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - jqLiteDealoc(childNodes[i]); - } - element.innerHTML = value; - }, - - empty: jqLiteEmpty -}, function(fn, name){ - /** - * Properties: writes return selection, reads return first value - */ - JQLite.prototype[name] = function(arg1, arg2) { - var i, key; - - // jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it - // in a way that survives minification. - // jqLiteEmpty takes no arguments but is a setter. - if (fn !== jqLiteEmpty && - (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) { - if (isObject(arg1)) { - - // we are a write, but the object properties are the key/values - for (i = 0; i < this.length; i++) { - if (fn === jqLiteData) { - // data() takes the whole object in jQuery - fn(this[i], arg1); - } else { - for (key in arg1) { - fn(this[i], key, arg1[key]); - } - } - } - // return self for chaining - return this; - } else { - // we are a read, so read the first child. - var value = fn.$dv; - // Only if we have $dv do we iterate over all, otherwise it is just the first element. - var jj = (value === undefined) ? Math.min(this.length, 1) : this.length; - for (var j = 0; j < jj; j++) { - var nodeValue = fn(this[j], arg1, arg2); - value = value ? value + nodeValue : nodeValue; - } - return value; - } - } else { - // we are a write, so apply to all children - for (i = 0; i < this.length; i++) { - fn(this[i], arg1, arg2); - } - // return self for chaining - return this; - } - }; -}); - -function createEventHandler(element, events) { - var eventHandler = function (event, type) { - if (!event.preventDefault) { - event.preventDefault = function() { - event.returnValue = false; //ie - }; - } - - if (!event.stopPropagation) { - event.stopPropagation = function() { - event.cancelBubble = true; //ie - }; - } - - if (!event.target) { - event.target = event.srcElement || document; - } - - if (isUndefined(event.defaultPrevented)) { - var prevent = event.preventDefault; - event.preventDefault = function() { - event.defaultPrevented = true; - prevent.call(event); - }; - event.defaultPrevented = false; - } - - event.isDefaultPrevented = function() { - return event.defaultPrevented || event.returnValue === false; - }; - - // Copy event handlers in case event handlers array is modified during execution. - var eventHandlersCopy = shallowCopy(events[type || event.type] || []); - - forEach(eventHandlersCopy, function(fn) { - fn.call(element, event); - }); - - // Remove monkey-patched methods (IE), - // as they would cause memory leaks in IE8. - if (msie <= 8) { - // IE7/8 does not allow to delete property on native object - event.preventDefault = null; - event.stopPropagation = null; - event.isDefaultPrevented = null; - } else { - // It shouldn't affect normal browsers (native methods are defined on prototype). - delete event.preventDefault; - delete event.stopPropagation; - delete event.isDefaultPrevented; - } - }; - eventHandler.elem = element; - return eventHandler; -} - -////////////////////////////////////////// -// Functions iterating traversal. -// These functions chain results into a single -// selector. -////////////////////////////////////////// -forEach({ - removeData: jqLiteRemoveData, - - dealoc: jqLiteDealoc, - - on: function onFn(element, type, fn, unsupported){ - if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); - - var events = jqLiteExpandoStore(element, 'events'), - handle = jqLiteExpandoStore(element, 'handle'); - - if (!events) jqLiteExpandoStore(element, 'events', events = {}); - if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); - - forEach(type.split(' '), function(type){ - var eventFns = events[type]; - - if (!eventFns) { - if (type == 'mouseenter' || type == 'mouseleave') { - var contains = document.body.contains || document.body.compareDocumentPosition ? - function( a, b ) { - // jshint bitwise: false - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - events[type] = []; - - // Refer to jQuery's implementation of mouseenter & mouseleave - // Read about mouseenter and mouseleave: - // http://www.quirksmode.org/js/events_mouse.html#link8 - var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"}; - - onFn(element, eventmap[type], function(event) { - var target = this, related = event.relatedTarget; - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !contains(target, related)) ){ - handle(event, type); - } - }); - - } else { - addEventListenerFn(element, type, handle); - events[type] = []; - } - eventFns = events[type]; - } - eventFns.push(fn); - }); - }, - - off: jqLiteOff, - - one: function(element, type, fn) { - element = jqLite(element); - - //add the listener twice so that when it is called - //you can remove the original function and still be - //able to call element.off(ev, fn) normally - element.on(type, function onFn() { - element.off(type, fn); - element.off(type, onFn); - }); - element.on(type, fn); - }, - - replaceWith: function(element, replaceNode) { - var index, parent = element.parentNode; - jqLiteDealoc(element); - forEach(new JQLite(replaceNode), function(node){ - if (index) { - parent.insertBefore(node, index.nextSibling); - } else { - parent.replaceChild(node, element); - } - index = node; - }); - }, - - children: function(element) { - var children = []; - forEach(element.childNodes, function(element){ - if (element.nodeType === 1) - children.push(element); - }); - return children; - }, - - contents: function(element) { - return element.childNodes || []; - }, - - append: function(element, node) { - forEach(new JQLite(node), function(child){ - if (element.nodeType === 1 || element.nodeType === 11) { - element.appendChild(child); - } - }); - }, - - prepend: function(element, node) { - if (element.nodeType === 1) { - var index = element.firstChild; - forEach(new JQLite(node), function(child){ - element.insertBefore(child, index); - }); - } - }, - - wrap: function(element, wrapNode) { - wrapNode = jqLite(wrapNode)[0]; - var parent = element.parentNode; - if (parent) { - parent.replaceChild(wrapNode, element); - } - wrapNode.appendChild(element); - }, - - remove: function(element) { - jqLiteDealoc(element); - var parent = element.parentNode; - if (parent) parent.removeChild(element); - }, - - after: function(element, newElement) { - var index = element, parent = element.parentNode; - forEach(new JQLite(newElement), function(node){ - parent.insertBefore(node, index.nextSibling); - index = node; - }); - }, - - addClass: jqLiteAddClass, - removeClass: jqLiteRemoveClass, - - toggleClass: function(element, selector, condition) { - if (isUndefined(condition)) { - condition = !jqLiteHasClass(element, selector); - } - (condition ? jqLiteAddClass : jqLiteRemoveClass)(element, selector); - }, - - parent: function(element) { - var parent = element.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - - next: function(element) { - if (element.nextElementSibling) { - return element.nextElementSibling; - } - - // IE8 doesn't have nextElementSibling - var elm = element.nextSibling; - while (elm != null && elm.nodeType !== 1) { - elm = elm.nextSibling; - } - return elm; - }, - - find: function(element, selector) { - if (element.getElementsByTagName) { - return element.getElementsByTagName(selector); - } else { - return []; - } - }, - - clone: jqLiteClone, - - triggerHandler: function(element, eventName, eventData) { - var eventFns = (jqLiteExpandoStore(element, 'events') || {})[eventName]; - - eventData = eventData || []; - - var event = [{ - preventDefault: noop, - stopPropagation: noop - }]; - - forEach(eventFns, function(fn) { - fn.apply(element, event.concat(eventData)); - }); - } -}, function(fn, name){ - /** - * chaining functions - */ - JQLite.prototype[name] = function(arg1, arg2, arg3) { - var value; - for(var i=0; i < this.length; i++) { - if (isUndefined(value)) { - value = fn(this[i], arg1, arg2, arg3); - if (isDefined(value)) { - // any function which returns a value needs to be wrapped - value = jqLite(value); - } - } else { - jqLiteAddNodes(value, fn(this[i], arg1, arg2, arg3)); - } - } - return isDefined(value) ? value : this; - }; - - // bind legacy bind/unbind to on/off - JQLite.prototype.bind = JQLite.prototype.on; - JQLite.prototype.unbind = JQLite.prototype.off; -}); - -/** - * Computes a hash of an 'obj'. - * Hash of a: - * string is string - * number is number as string - * object is either result of calling $$hashKey function on the object or uniquely generated id, - * that is also assigned to the $$hashKey property of the object. - * - * @param obj - * @returns {string} hash string such that the same input will have the same hash string. - * The resulting string key is in 'type:hashKey' format. - */ -function hashKey(obj) { - var objType = typeof obj, - key; - - if (objType == 'object' && obj !== null) { - if (typeof (key = obj.$$hashKey) == 'function') { - // must invoke on object to keep the right this - key = obj.$$hashKey(); - } else if (key === undefined) { - key = obj.$$hashKey = nextUid(); - } - } else { - key = obj; - } - - return objType + ':' + key; -} - -/** - * HashMap which can use objects as keys - */ -function HashMap(array){ - forEach(array, this.put, this); -} -HashMap.prototype = { - /** - * Store key value pair - * @param key key to store can be any type - * @param value value to store can be any type - */ - put: function(key, value) { - this[hashKey(key)] = value; - }, - - /** - * @param key - * @returns the value for the key - */ - get: function(key) { - return this[hashKey(key)]; - }, - - /** - * Remove the key/value pair - * @param key - */ - remove: function(key) { - var value = this[key = hashKey(key)]; - delete this[key]; - return value; - } -}; - -/** - * @ngdoc function - * @name angular.injector - * @function - * - * @description - * Creates an injector function that can be used for retrieving services as well as for - * dependency injection (see {@link guide/di dependency injection}). - * - - * @param {Array.} modules A list of module functions or their aliases. See - * {@link angular.module}. The `ng` module must be explicitly added. - * @returns {function()} Injector function. See {@link AUTO.$injector $injector}. - * - * @example - * Typical usage - *
    - *   // create an injector
    - *   var $injector = angular.injector(['ng']);
    - *
    - *   // use the injector to kick off your application
    - *   // use the type inference to auto inject arguments, or use implicit injection
    - *   $injector.invoke(function($rootScope, $compile, $document){
    - *     $compile($document)($rootScope);
    - *     $rootScope.$digest();
    - *   });
    - * 
    - * - * Sometimes you want to get access to the injector of a currently running Angular app - * from outside Angular. Perhaps, you want to inject and compile some markup after the - * application has been bootstrapped. You can do this using extra `injector()` added - * to JQuery/jqLite elements. See {@link angular.element}. - * - * *This is fairly rare but could be the case if a third party library is injecting the - * markup.* - * - * In the following example a new block of HTML containing a `ng-controller` - * directive is added to the end of the document body by JQuery. We then compile and link - * it into the current AngularJS scope. - * - *
    - * var $div = $('
    {{content.label}}
    '); - * $(document.body).append($div); - * - * angular.element(document).injector().invoke(function($compile) { - * var scope = angular.element($div).scope(); - * $compile($div)(scope); - * }); - *
    - */ - - -/** - * @ngdoc overview - * @name AUTO - * @description - * - * Implicit module which gets automatically added to each {@link AUTO.$injector $injector}. - */ - -var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; -var FN_ARG_SPLIT = /,/; -var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; -var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; -var $injectorMinErr = minErr('$injector'); -function annotate(fn) { - var $inject, - fnText, - argDecl, - last; - - if (typeof fn == 'function') { - if (!($inject = fn.$inject)) { - $inject = []; - if (fn.length) { - fnText = fn.toString().replace(STRIP_COMMENTS, ''); - argDecl = fnText.match(FN_ARGS); - forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ - arg.replace(FN_ARG, function(all, underscore, name){ - $inject.push(name); - }); - }); - } - fn.$inject = $inject; - } - } else if (isArray(fn)) { - last = fn.length - 1; - assertArgFn(fn[last], 'fn'); - $inject = fn.slice(0, last); - } else { - assertArgFn(fn, 'fn', true); - } - return $inject; -} - -/////////////////////////////////////// - -/** - * @ngdoc object - * @name AUTO.$injector - * @function - * - * @description - * - * `$injector` is used to retrieve object instances as defined by - * {@link AUTO.$provide provider}, instantiate types, invoke methods, - * and load modules. - * - * The following always holds true: - * - *
    - *   var $injector = angular.injector();
    - *   expect($injector.get('$injector')).toBe($injector);
    - *   expect($injector.invoke(function($injector){
    - *     return $injector;
    - *   }).toBe($injector);
    - * 
    - * - * # Injection Function Annotation - * - * JavaScript does not have annotations, and annotations are needed for dependency injection. The - * following are all valid ways of annotating function with injection arguments and are equivalent. - * - *
    - *   // inferred (only works if code not minified/obfuscated)
    - *   $injector.invoke(function(serviceA){});
    - *
    - *   // annotated
    - *   function explicit(serviceA) {};
    - *   explicit.$inject = ['serviceA'];
    - *   $injector.invoke(explicit);
    - *
    - *   // inline
    - *   $injector.invoke(['serviceA', function(serviceA){}]);
    - * 
    - * - * ## Inference - * - * In JavaScript calling `toString()` on a function returns the function definition. The definition - * can then be parsed and the function arguments can be extracted. *NOTE:* This does not work with - * minification, and obfuscation tools since these tools change the argument names. - * - * ## `$inject` Annotation - * By adding a `$inject` property onto a function the injection parameters can be specified. - * - * ## Inline - * As an array of injection names, where the last item in the array is the function to call. - */ - -/** - * @ngdoc method - * @name AUTO.$injector#get - * @methodOf AUTO.$injector - * - * @description - * Return an instance of the service. - * - * @param {string} name The name of the instance to retrieve. - * @return {*} The instance. - */ - -/** - * @ngdoc method - * @name AUTO.$injector#invoke - * @methodOf AUTO.$injector - * - * @description - * Invoke the method and supply the method arguments from the `$injector`. - * - * @param {!function} fn The function to invoke. Function parameters are injected according to the - * {@link guide/di $inject Annotation} rules. - * @param {Object=} self The `this` for the invoked method. - * @param {Object=} locals Optional object. If preset then any argument names are read from this - * object first, before the `$injector` is consulted. - * @returns {*} the value returned by the invoked `fn` function. - */ - -/** - * @ngdoc method - * @name AUTO.$injector#has - * @methodOf AUTO.$injector - * - * @description - * Allows the user to query if the particular service exist. - * - * @param {string} Name of the service to query. - * @returns {boolean} returns true if injector has given service. - */ - -/** - * @ngdoc method - * @name AUTO.$injector#instantiate - * @methodOf AUTO.$injector - * @description - * Create a new instance of JS type. The method takes a constructor function invokes the new - * operator and supplies all of the arguments to the constructor function as specified by the - * constructor annotation. - * - * @param {function} Type Annotated constructor function. - * @param {Object=} locals Optional object. If preset then any argument names are read from this - * object first, before the `$injector` is consulted. - * @returns {Object} new instance of `Type`. - */ - -/** - * @ngdoc method - * @name AUTO.$injector#annotate - * @methodOf AUTO.$injector - * - * @description - * Returns an array of service names which the function is requesting for injection. This API is - * used by the injector to determine which services need to be injected into the function when the - * function is invoked. There are three ways in which the function can be annotated with the needed - * dependencies. - * - * # Argument names - * - * The simplest form is to extract the dependencies from the arguments of the function. This is done - * by converting the function into a string using `toString()` method and extracting the argument - * names. - *
    - *   // Given
    - *   function MyController($scope, $route) {
    - *     // ...
    - *   }
    - *
    - *   // Then
    - *   expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
    - * 
    - * - * This method does not work with code minification / obfuscation. For this reason the following - * annotation strategies are supported. - * - * # The `$inject` property - * - * If a function has an `$inject` property and its value is an array of strings, then the strings - * represent names of services to be injected into the function. - *
    - *   // Given
    - *   var MyController = function(obfuscatedScope, obfuscatedRoute) {
    - *     // ...
    - *   }
    - *   // Define function dependencies
    - *   MyController['$inject'] = ['$scope', '$route'];
    - *
    - *   // Then
    - *   expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
    - * 
    - * - * # The array notation - * - * It is often desirable to inline Injected functions and that's when setting the `$inject` property - * is very inconvenient. In these situations using the array notation to specify the dependencies in - * a way that survives minification is a better choice: - * - *
    - *   // We wish to write this (not minification / obfuscation safe)
    - *   injector.invoke(function($compile, $rootScope) {
    - *     // ...
    - *   });
    - *
    - *   // We are forced to write break inlining
    - *   var tmpFn = function(obfuscatedCompile, obfuscatedRootScope) {
    - *     // ...
    - *   };
    - *   tmpFn.$inject = ['$compile', '$rootScope'];
    - *   injector.invoke(tmpFn);
    - *
    - *   // To better support inline function the inline annotation is supported
    - *   injector.invoke(['$compile', '$rootScope', function(obfCompile, obfRootScope) {
    - *     // ...
    - *   }]);
    - *
    - *   // Therefore
    - *   expect(injector.annotate(
    - *      ['$compile', '$rootScope', function(obfus_$compile, obfus_$rootScope) {}])
    - *    ).toEqual(['$compile', '$rootScope']);
    - * 
    - * - * @param {function|Array.} fn Function for which dependent service names need to - * be retrieved as described above. - * - * @returns {Array.} The names of the services which the function requires. - */ - - - - -/** - * @ngdoc object - * @name AUTO.$provide - * - * @description - * - * The {@link AUTO.$provide $provide} service has a number of methods for registering components - * with the {@link AUTO.$injector $injector}. Many of these functions are also exposed on - * {@link angular.Module}. - * - * An Angular **service** is a singleton object created by a **service factory**. These **service - * factories** are functions which, in turn, are created by a **service provider**. - * The **service providers** are constructor functions. When instantiated they must contain a - * property called `$get`, which holds the **service factory** function. - * - * When you request a service, the {@link AUTO.$injector $injector} is responsible for finding the - * correct **service provider**, instantiating it and then calling its `$get` **service factory** - * function to get the instance of the **service**. - * - * Often services have no configuration options and there is no need to add methods to the service - * provider. The provider will be no more than a constructor function with a `$get` property. For - * these cases the {@link AUTO.$provide $provide} service has additional helper methods to register - * services without specifying a provider. - * - * * {@link AUTO.$provide#methods_provider provider(provider)} - registers a **service provider** with the - * {@link AUTO.$injector $injector} - * * {@link AUTO.$provide#methods_constant constant(obj)} - registers a value/object that can be accessed by - * providers and services. - * * {@link AUTO.$provide#methods_value value(obj)} - registers a value/object that can only be accessed by - * services, not providers. - * * {@link AUTO.$provide#methods_factory factory(fn)} - registers a service **factory function**, `fn`, - * that will be wrapped in a **service provider** object, whose `$get` property will contain the - * given factory function. - * * {@link AUTO.$provide#methods_service service(class)} - registers a **constructor function**, `class` that - * that will be wrapped in a **service provider** object, whose `$get` property will instantiate - * a new object using the given constructor function. - * - * See the individual methods for more information and examples. - */ - -/** - * @ngdoc method - * @name AUTO.$provide#provider - * @methodOf AUTO.$provide - * @description - * - * Register a **provider function** with the {@link AUTO.$injector $injector}. Provider functions - * are constructor functions, whose instances are responsible for "providing" a factory for a - * service. - * - * Service provider names start with the name of the service they provide followed by `Provider`. - * For example, the {@link ng.$log $log} service has a provider called - * {@link ng.$logProvider $logProvider}. - * - * Service provider objects can have additional methods which allow configuration of the provider - * and its service. Importantly, you can configure what kind of service is created by the `$get` - * method, or how that service will act. For example, the {@link ng.$logProvider $logProvider} has a - * method {@link ng.$logProvider#debugEnabled debugEnabled} - * which lets you specify whether the {@link ng.$log $log} service will log debug messages to the - * console or not. - * - * @param {string} name The name of the instance. NOTE: the provider will be available under `name + - 'Provider'` key. - * @param {(Object|function())} provider If the provider is: - * - * - `Object`: then it should have a `$get` method. The `$get` method will be invoked using - * {@link AUTO.$injector#invoke $injector.invoke()} when an instance needs to be created. - * - `Constructor`: a new instance of the provider will be created using - * {@link AUTO.$injector#instantiate $injector.instantiate()}, then treated as `object`. - * - * @returns {Object} registered provider instance - - * @example - * - * The following example shows how to create a simple event tracking service and register it using - * {@link AUTO.$provide#methods_provider $provide.provider()}. - * - *
    - *  // Define the eventTracker provider
    - *  function EventTrackerProvider() {
    - *    var trackingUrl = '/track';
    - *
    - *    // A provider method for configuring where the tracked events should been saved
    - *    this.setTrackingUrl = function(url) {
    - *      trackingUrl = url;
    - *    };
    - *
    - *    // The service factory function
    - *    this.$get = ['$http', function($http) {
    - *      var trackedEvents = {};
    - *      return {
    - *        // Call this to track an event
    - *        event: function(event) {
    - *          var count = trackedEvents[event] || 0;
    - *          count += 1;
    - *          trackedEvents[event] = count;
    - *          return count;
    - *        },
    - *        // Call this to save the tracked events to the trackingUrl
    - *        save: function() {
    - *          $http.post(trackingUrl, trackedEvents);
    - *        }
    - *      };
    - *    }];
    - *  }
    - *
    - *  describe('eventTracker', function() {
    - *    var postSpy;
    - *
    - *    beforeEach(module(function($provide) {
    - *      // Register the eventTracker provider
    - *      $provide.provider('eventTracker', EventTrackerProvider);
    - *    }));
    - *
    - *    beforeEach(module(function(eventTrackerProvider) {
    - *      // Configure eventTracker provider
    - *      eventTrackerProvider.setTrackingUrl('/custom-track');
    - *    }));
    - *
    - *    it('tracks events', inject(function(eventTracker) {
    - *      expect(eventTracker.event('login')).toEqual(1);
    - *      expect(eventTracker.event('login')).toEqual(2);
    - *    }));
    - *
    - *    it('saves to the tracking url', inject(function(eventTracker, $http) {
    - *      postSpy = spyOn($http, 'post');
    - *      eventTracker.event('login');
    - *      eventTracker.save();
    - *      expect(postSpy).toHaveBeenCalled();
    - *      expect(postSpy.mostRecentCall.args[0]).not.toEqual('/track');
    - *      expect(postSpy.mostRecentCall.args[0]).toEqual('/custom-track');
    - *      expect(postSpy.mostRecentCall.args[1]).toEqual({ 'login': 1 });
    - *    }));
    - *  });
    - * 
    - */ - -/** - * @ngdoc method - * @name AUTO.$provide#factory - * @methodOf AUTO.$provide - * @description - * - * Register a **service factory**, which will be called to return the service instance. - * This is short for registering a service where its provider consists of only a `$get` property, - * which is the given service factory function. - * You should use {@link AUTO.$provide#factory $provide.factory(getFn)} if you do not need to - * configure your service in a provider. - * - * @param {string} name The name of the instance. - * @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand - * for `$provide.provider(name, {$get: $getFn})`. - * @returns {Object} registered provider instance - * - * @example - * Here is an example of registering a service - *
    - *   $provide.factory('ping', ['$http', function($http) {
    - *     return function ping() {
    - *       return $http.send('/ping');
    - *     };
    - *   }]);
    - * 
    - * You would then inject and use this service like this: - *
    - *   someModule.controller('Ctrl', ['ping', function(ping) {
    - *     ping();
    - *   }]);
    - * 
    - */ - - -/** - * @ngdoc method - * @name AUTO.$provide#service - * @methodOf AUTO.$provide - * @description - * - * Register a **service constructor**, which will be invoked with `new` to create the service - * instance. - * This is short for registering a service where its provider's `$get` property is the service - * constructor function that will be used to instantiate the service instance. - * - * You should use {@link AUTO.$provide#methods_service $provide.service(class)} if you define your service - * as a type/class. - * - * @param {string} name The name of the instance. - * @param {Function} constructor A class (constructor function) that will be instantiated. - * @returns {Object} registered provider instance - * - * @example - * Here is an example of registering a service using - * {@link AUTO.$provide#methods_service $provide.service(class)}. - *
    - *   var Ping = function($http) {
    - *     this.$http = $http;
    - *   };
    - * 
    - *   Ping.$inject = ['$http'];
    - *   
    - *   Ping.prototype.send = function() {
    - *     return this.$http.get('/ping');
    - *   };
    - *   $provide.service('ping', Ping);
    - * 
    - * You would then inject and use this service like this: - *
    - *   someModule.controller('Ctrl', ['ping', function(ping) {
    - *     ping.send();
    - *   }]);
    - * 
    - */ - - -/** - * @ngdoc method - * @name AUTO.$provide#value - * @methodOf AUTO.$provide - * @description - * - * Register a **value service** with the {@link AUTO.$injector $injector}, such as a string, a - * number, an array, an object or a function. This is short for registering a service where its - * provider's `$get` property is a factory function that takes no arguments and returns the **value - * service**. - * - * Value services are similar to constant services, except that they cannot be injected into a - * module configuration function (see {@link angular.Module#config}) but they can be overridden by - * an Angular - * {@link AUTO.$provide#decorator decorator}. - * - * @param {string} name The name of the instance. - * @param {*} value The value. - * @returns {Object} registered provider instance - * - * @example - * Here are some examples of creating value services. - *
    - *   $provide.value('ADMIN_USER', 'admin');
    - *
    - *   $provide.value('RoleLookup', { admin: 0, writer: 1, reader: 2 });
    - *
    - *   $provide.value('halfOf', function(value) {
    - *     return value / 2;
    - *   });
    - * 
    - */ - - -/** - * @ngdoc method - * @name AUTO.$provide#constant - * @methodOf AUTO.$provide - * @description - * - * Register a **constant service**, such as a string, a number, an array, an object or a function, - * with the {@link AUTO.$injector $injector}. Unlike {@link AUTO.$provide#value value} it can be - * injected into a module configuration function (see {@link angular.Module#config}) and it cannot - * be overridden by an Angular {@link AUTO.$provide#decorator decorator}. - * - * @param {string} name The name of the constant. - * @param {*} value The constant value. - * @returns {Object} registered instance - * - * @example - * Here a some examples of creating constants: - *
    - *   $provide.constant('SHARD_HEIGHT', 306);
    - *
    - *   $provide.constant('MY_COLOURS', ['red', 'blue', 'grey']);
    - *
    - *   $provide.constant('double', function(value) {
    - *     return value * 2;
    - *   });
    - * 
    - */ - - -/** - * @ngdoc method - * @name AUTO.$provide#decorator - * @methodOf AUTO.$provide - * @description - * - * Register a **service decorator** with the {@link AUTO.$injector $injector}. A service decorator - * intercepts the creation of a service, allowing it to override or modify the behaviour of the - * service. The object returned by the decorator may be the original service, or a new service - * object which replaces or wraps and delegates to the original service. - * - * @param {string} name The name of the service to decorate. - * @param {function()} decorator This function will be invoked when the service needs to be - * instantiated and should return the decorated service instance. The function is called using - * the {@link AUTO.$injector#invoke injector.invoke} method and is therefore fully injectable. - * Local injection arguments: - * - * * `$delegate` - The original service instance, which can be monkey patched, configured, - * decorated or delegated to. - * - * @example - * Here we decorate the {@link ng.$log $log} service to convert warnings to errors by intercepting - * calls to {@link ng.$log#error $log.warn()}. - *
    - *   $provide.decorator('$log', ['$delegate', function($delegate) {
    - *     $delegate.warn = $delegate.error;
    - *     return $delegate;
    - *   }]);
    - * 
    - */ - - -function createInjector(modulesToLoad) { - var INSTANTIATING = {}, - providerSuffix = 'Provider', - path = [], - loadedModules = new HashMap(), - providerCache = { - $provide: { - provider: supportObject(provider), - factory: supportObject(factory), - service: supportObject(service), - value: supportObject(value), - constant: supportObject(constant), - decorator: decorator - } - }, - providerInjector = (providerCache.$injector = - createInternalInjector(providerCache, function() { - throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); - })), - instanceCache = {}, - instanceInjector = (instanceCache.$injector = - createInternalInjector(instanceCache, function(servicename) { - var provider = providerInjector.get(servicename + providerSuffix); - return instanceInjector.invoke(provider.$get, provider); - })); - - - forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); }); - - return instanceInjector; - - //////////////////////////////////// - // $provider - //////////////////////////////////// - - function supportObject(delegate) { - return function(key, value) { - if (isObject(key)) { - forEach(key, reverseParams(delegate)); - } else { - return delegate(key, value); - } - }; - } - - function provider(name, provider_) { - assertNotHasOwnProperty(name, 'service'); - if (isFunction(provider_) || isArray(provider_)) { - provider_ = providerInjector.instantiate(provider_); - } - if (!provider_.$get) { - throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); - } - return providerCache[name + providerSuffix] = provider_; - } - - function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); } - - function service(name, constructor) { - return factory(name, ['$injector', function($injector) { - return $injector.instantiate(constructor); - }]); - } - - function value(name, val) { return factory(name, valueFn(val)); } - - function constant(name, value) { - assertNotHasOwnProperty(name, 'constant'); - providerCache[name] = value; - instanceCache[name] = value; - } - - function decorator(serviceName, decorFn) { - var origProvider = providerInjector.get(serviceName + providerSuffix), - orig$get = origProvider.$get; - - origProvider.$get = function() { - var origInstance = instanceInjector.invoke(orig$get, origProvider); - return instanceInjector.invoke(decorFn, null, {$delegate: origInstance}); - }; - } - - //////////////////////////////////// - // Module Loading - //////////////////////////////////// - function loadModules(modulesToLoad){ - var runBlocks = [], moduleFn, invokeQueue, i, ii; - forEach(modulesToLoad, function(module) { - if (loadedModules.get(module)) return; - loadedModules.put(module, true); - - try { - if (isString(module)) { - moduleFn = angularModule(module); - runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); - - for(invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { - var invokeArgs = invokeQueue[i], - provider = providerInjector.get(invokeArgs[0]); - - provider[invokeArgs[1]].apply(provider, invokeArgs[2]); - } - } else if (isFunction(module)) { - runBlocks.push(providerInjector.invoke(module)); - } else if (isArray(module)) { - runBlocks.push(providerInjector.invoke(module)); - } else { - assertArgFn(module, 'module'); - } - } catch (e) { - if (isArray(module)) { - module = module[module.length - 1]; - } - if (e.message && e.stack && e.stack.indexOf(e.message) == -1) { - // Safari & FF's stack traces don't contain error.message content - // unlike those of Chrome and IE - // So if stack doesn't contain message, we create a new string that contains both. - // Since error.stack is read-only in Safari, I'm overriding e and not e.stack here. - /* jshint -W022 */ - e = e.message + '\n' + e.stack; - } - throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}", - module, e.stack || e.message || e); - } - }); - return runBlocks; - } - - //////////////////////////////////// - // internal Injector - //////////////////////////////////// - - function createInternalInjector(cache, factory) { - - function getService(serviceName) { - if (cache.hasOwnProperty(serviceName)) { - if (cache[serviceName] === INSTANTIATING) { - throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- ')); - } - return cache[serviceName]; - } else { - try { - path.unshift(serviceName); - cache[serviceName] = INSTANTIATING; - return cache[serviceName] = factory(serviceName); - } catch (err) { - if (cache[serviceName] === INSTANTIATING) { - delete cache[serviceName]; - } - throw err; - } finally { - path.shift(); - } - } - } - - function invoke(fn, self, locals){ - var args = [], - $inject = annotate(fn), - length, i, - key; - - for(i = 0, length = $inject.length; i < length; i++) { - key = $inject[i]; - if (typeof key !== 'string') { - throw $injectorMinErr('itkn', - 'Incorrect injection token! Expected service name as string, got {0}', key); - } - args.push( - locals && locals.hasOwnProperty(key) - ? locals[key] - : getService(key) - ); - } - if (!fn.$inject) { - // this means that we must be an array. - fn = fn[length]; - } - - // http://jsperf.com/angularjs-invoke-apply-vs-switch - // #5388 - return fn.apply(self, args); - } - - function instantiate(Type, locals) { - var Constructor = function() {}, - instance, returnedValue; - - // Check if Type is annotated and use just the given function at n-1 as parameter - // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]); - Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype; - instance = new Constructor(); - returnedValue = invoke(Type, instance, locals); - - return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; - } - - return { - invoke: invoke, - instantiate: instantiate, - get: getService, - annotate: annotate, - has: function(name) { - return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); - } - }; - } -} - -/** - * @ngdoc function - * @name ng.$anchorScroll - * @requires $window - * @requires $location - * @requires $rootScope - * - * @description - * When called, it checks current value of `$location.hash()` and scroll to related element, - * according to rules specified in - * {@link http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document Html5 spec}. - * - * It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor. - * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. - * - * @example - - -
    - Go to bottom - You're at the bottom! -
    -
    - - function ScrollCtrl($scope, $location, $anchorScroll) { - $scope.gotoBottom = function (){ - // set the location.hash to the id of - // the element you wish to scroll to. - $location.hash('bottom'); - - // call $anchorScroll() - $anchorScroll(); - } - } - - - #scrollArea { - height: 350px; - overflow: auto; - } - - #bottom { - display: block; - margin-top: 2000px; - } - -
    - */ -function $AnchorScrollProvider() { - - var autoScrollingEnabled = true; - - this.disableAutoScrolling = function() { - autoScrollingEnabled = false; - }; - - this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) { - var document = $window.document; - - // helper function to get first anchor from a NodeList - // can't use filter.filter, as it accepts only instances of Array - // and IE can't convert NodeList to an array using [].slice - // TODO(vojta): use filter if we change it to accept lists as well - function getFirstAnchor(list) { - var result = null; - forEach(list, function(element) { - if (!result && lowercase(element.nodeName) === 'a') result = element; - }); - return result; - } - - function scroll() { - var hash = $location.hash(), elm; - - // empty hash, scroll to the top of the page - if (!hash) $window.scrollTo(0, 0); - - // element with given id - else if ((elm = document.getElementById(hash))) elm.scrollIntoView(); - - // first anchor with given name :-D - else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView(); - - // no element and hash == 'top', scroll to the top of the page - else if (hash === 'top') $window.scrollTo(0, 0); - } - - // does not scroll when user clicks on anchor link that is currently on - // (no url change, no $location.hash() change), browser native does scroll - if (autoScrollingEnabled) { - $rootScope.$watch(function autoScrollWatch() {return $location.hash();}, - function autoScrollWatchAction() { - $rootScope.$evalAsync(scroll); - }); - } - - return scroll; - }]; -} - -var $animateMinErr = minErr('$animate'); - -/** - * @ngdoc object - * @name ng.$animateProvider - * - * @description - * Default implementation of $animate that doesn't perform any animations, instead just - * synchronously performs DOM - * updates and calls done() callbacks. - * - * In order to enable animations the ngAnimate module has to be loaded. - * - * To see the functional implementation check out src/ngAnimate/animate.js - */ -var $AnimateProvider = ['$provide', function($provide) { - - - this.$$selectors = {}; - - - /** - * @ngdoc function - * @name ng.$animateProvider#register - * @methodOf ng.$animateProvider - * - * @description - * Registers a new injectable animation factory function. The factory function produces the - * animation object which contains callback functions for each event that is expected to be - * animated. - * - * * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction` - * must be called once the element animation is complete. If a function is returned then the - * animation service will use this function to cancel the animation whenever a cancel event is - * triggered. - * - * - *
    -   *   return {
    -     *     eventFn : function(element, done) {
    -     *       //code to run the animation
    -     *       //once complete, then run done()
    -     *       return function cancellationFunction() {
    -     *         //code to cancel the animation
    -     *       }
    -     *     }
    -     *   }
    -   *
    - * - * @param {string} name The name of the animation. - * @param {function} factory The factory function that will be executed to return the animation - * object. - */ - this.register = function(name, factory) { - var key = name + '-animation'; - if (name && name.charAt(0) != '.') throw $animateMinErr('notcsel', - "Expecting class selector starting with '.' got '{0}'.", name); - this.$$selectors[name.substr(1)] = key; - $provide.factory(key, factory); - }; - - /** - * @ngdoc function - * @name ng.$animateProvider#classNameFilter - * @methodOf ng.$animateProvider - * - * @description - * Sets and/or returns the CSS class regular expression that is checked when performing - * an animation. Upon bootstrap the classNameFilter value is not set at all and will - * therefore enable $animate to attempt to perform an animation on any element. - * When setting the classNameFilter value, animations will only be performed on elements - * that successfully match the filter expression. This in turn can boost performance - * for low-powered devices as well as applications containing a lot of structural operations. - * @param {RegExp=} expression The className expression which will be checked against all animations - * @return {RegExp} The current CSS className expression value. If null then there is no expression value - */ - this.classNameFilter = function(expression) { - if(arguments.length === 1) { - this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; - } - return this.$$classNameFilter; - }; - - this.$get = ['$timeout', function($timeout) { - - /** - * - * @ngdoc object - * @name ng.$animate - * @description The $animate service provides rudimentary DOM manipulation functions to - * insert, remove and move elements within the DOM, as well as adding and removing classes. - * This service is the core service used by the ngAnimate $animator service which provides - * high-level animation hooks for CSS and JavaScript. - * - * $animate is available in the AngularJS core, however, the ngAnimate module must be included - * to enable full out animation support. Otherwise, $animate will only perform simple DOM - * manipulation operations. - * - * To learn more about enabling animation support, click here to visit the {@link ngAnimate - * ngAnimate module page} as well as the {@link ngAnimate.$animate ngAnimate $animate service - * page}. - */ - return { - - /** - * - * @ngdoc function - * @name ng.$animate#enter - * @methodOf ng.$animate - * @function - * @description Inserts the element into the DOM either after the `after` element or within - * the `parent` element. Once complete, the done() callback will be fired (if provided). - * @param {jQuery/jqLite element} element the element which will be inserted into the DOM - * @param {jQuery/jqLite element} parent the parent element which will append the element as - * a child (if the after element is not present) - * @param {jQuery/jqLite element} after the sibling element which will append the element - * after itself - * @param {function=} done callback function that will be called after the element has been - * inserted into the DOM - */ - enter : function(element, parent, after, done) { - if (after) { - after.after(element); - } else { - if (!parent || !parent[0]) { - parent = after.parent(); - } - parent.append(element); - } - done && $timeout(done, 0, false); - }, - - /** - * - * @ngdoc function - * @name ng.$animate#leave - * @methodOf ng.$animate - * @function - * @description Removes the element from the DOM. Once complete, the done() callback will be - * fired (if provided). - * @param {jQuery/jqLite element} element the element which will be removed from the DOM - * @param {function=} done callback function that will be called after the element has been - * removed from the DOM - */ - leave : function(element, done) { - element.remove(); - done && $timeout(done, 0, false); - }, - - /** - * - * @ngdoc function - * @name ng.$animate#move - * @methodOf ng.$animate - * @function - * @description Moves the position of the provided element within the DOM to be placed - * either after the `after` element or inside of the `parent` element. Once complete, the - * done() callback will be fired (if provided). - * - * @param {jQuery/jqLite element} element the element which will be moved around within the - * DOM - * @param {jQuery/jqLite element} parent the parent element where the element will be - * inserted into (if the after element is not present) - * @param {jQuery/jqLite element} after the sibling element where the element will be - * positioned next to - * @param {function=} done the callback function (if provided) that will be fired after the - * element has been moved to its new position - */ - move : function(element, parent, after, done) { - // Do not remove element before insert. Removing will cause data associated with the - // element to be dropped. Insert will implicitly do the remove. - this.enter(element, parent, after, done); - }, - - /** - * - * @ngdoc function - * @name ng.$animate#addClass - * @methodOf ng.$animate - * @function - * @description Adds the provided className CSS class value to the provided element. Once - * complete, the done() callback will be fired (if provided). - * @param {jQuery/jqLite element} element the element which will have the className value - * added to it - * @param {string} className the CSS class which will be added to the element - * @param {function=} done the callback function (if provided) that will be fired after the - * className value has been added to the element - */ - addClass : function(element, className, done) { - className = isString(className) ? - className : - isArray(className) ? className.join(' ') : ''; - forEach(element, function (element) { - jqLiteAddClass(element, className); - }); - done && $timeout(done, 0, false); - }, - - /** - * - * @ngdoc function - * @name ng.$animate#removeClass - * @methodOf ng.$animate - * @function - * @description Removes the provided className CSS class value from the provided element. - * Once complete, the done() callback will be fired (if provided). - * @param {jQuery/jqLite element} element the element which will have the className value - * removed from it - * @param {string} className the CSS class which will be removed from the element - * @param {function=} done the callback function (if provided) that will be fired after the - * className value has been removed from the element - */ - removeClass : function(element, className, done) { - className = isString(className) ? - className : - isArray(className) ? className.join(' ') : ''; - forEach(element, function (element) { - jqLiteRemoveClass(element, className); - }); - done && $timeout(done, 0, false); - }, - - /** - * - * @ngdoc function - * @name ng.$animate#setClass - * @methodOf ng.$animate - * @function - * @description Adds and/or removes the given CSS classes to and from the element. - * Once complete, the done() callback will be fired (if provided). - * @param {jQuery/jqLite element} element the element which will it's CSS classes changed - * removed from it - * @param {string} add the CSS classes which will be added to the element - * @param {string} remove the CSS class which will be removed from the element - * @param {function=} done the callback function (if provided) that will be fired after the - * CSS classes have been set on the element - */ - setClass : function(element, add, remove, done) { - forEach(element, function (element) { - jqLiteAddClass(element, add); - jqLiteRemoveClass(element, remove); - }); - done && $timeout(done, 0, false); - }, - - enabled : noop - }; - }]; -}]; - -/** - * ! This is a private undocumented service ! - * - * @name ng.$browser - * @requires $log - * @description - * This object has two goals: - * - * - hide all the global state in the browser caused by the window object - * - abstract away all the browser specific features and inconsistencies - * - * For tests we provide {@link ngMock.$browser mock implementation} of the `$browser` - * service, which can be used for convenient testing of the application without the interaction with - * the real browser apis. - */ -/** - * @param {object} window The global window object. - * @param {object} document jQuery wrapped document. - * @param {function()} XHR XMLHttpRequest constructor. - * @param {object} $log console.log or an object with the same interface. - * @param {object} $sniffer $sniffer service - */ -function Browser(window, document, $log, $sniffer) { - var self = this, - rawDocument = document[0], - location = window.location, - history = window.history, - setTimeout = window.setTimeout, - clearTimeout = window.clearTimeout, - pendingDeferIds = {}; - - self.isMock = false; - - var outstandingRequestCount = 0; - var outstandingRequestCallbacks = []; - - // TODO(vojta): remove this temporary api - self.$$completeOutstandingRequest = completeOutstandingRequest; - self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; }; - - /** - * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks` - * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. - */ - function completeOutstandingRequest(fn) { - try { - fn.apply(null, sliceArgs(arguments, 1)); - } finally { - outstandingRequestCount--; - if (outstandingRequestCount === 0) { - while(outstandingRequestCallbacks.length) { - try { - outstandingRequestCallbacks.pop()(); - } catch (e) { - $log.error(e); - } - } - } - } - } - - /** - * @private - * Note: this method is used only by scenario runner - * TODO(vojta): prefix this method with $$ ? - * @param {function()} callback Function that will be called when no outstanding request - */ - self.notifyWhenNoOutstandingRequests = function(callback) { - // force browser to execute all pollFns - this is needed so that cookies and other pollers fire - // at some deterministic time in respect to the test runner's actions. Leaving things up to the - // regular poller would result in flaky tests. - forEach(pollFns, function(pollFn){ pollFn(); }); - - if (outstandingRequestCount === 0) { - callback(); - } else { - outstandingRequestCallbacks.push(callback); - } - }; - - ////////////////////////////////////////////////////////////// - // Poll Watcher API - ////////////////////////////////////////////////////////////// - var pollFns = [], - pollTimeout; - - /** - * @name ng.$browser#addPollFn - * @methodOf ng.$browser - * - * @param {function()} fn Poll function to add - * - * @description - * Adds a function to the list of functions that poller periodically executes, - * and starts polling if not started yet. - * - * @returns {function()} the added function - */ - self.addPollFn = function(fn) { - if (isUndefined(pollTimeout)) startPoller(100, setTimeout); - pollFns.push(fn); - return fn; - }; - - /** - * @param {number} interval How often should browser call poll functions (ms) - * @param {function()} setTimeout Reference to a real or fake `setTimeout` function. - * - * @description - * Configures the poller to run in the specified intervals, using the specified - * setTimeout fn and kicks it off. - */ - function startPoller(interval, setTimeout) { - (function check() { - forEach(pollFns, function(pollFn){ pollFn(); }); - pollTimeout = setTimeout(check, interval); - })(); - } - - ////////////////////////////////////////////////////////////// - // URL API - ////////////////////////////////////////////////////////////// - - var lastBrowserUrl = location.href, - baseElement = document.find('base'), - newLocation = null; - - /** - * @name ng.$browser#url - * @methodOf ng.$browser - * - * @description - * GETTER: - * Without any argument, this method just returns current value of location.href. - * - * SETTER: - * With at least one argument, this method sets url to new value. - * If html5 history api supported, pushState/replaceState is used, otherwise - * location.href/location.replace is used. - * Returns its own instance to allow chaining - * - * NOTE: this api is intended for use only by the $location service. Please use the - * {@link ng.$location $location service} to change url. - * - * @param {string} url New url (when used as setter) - * @param {boolean=} replace Should new url replace current history record ? - */ - self.url = function(url, replace) { - // Android Browser BFCache causes location, history reference to become stale. - if (location !== window.location) location = window.location; - if (history !== window.history) history = window.history; - - // setter - if (url) { - if (lastBrowserUrl == url) return; - lastBrowserUrl = url; - if ($sniffer.history) { - if (replace) history.replaceState(null, '', url); - else { - history.pushState(null, '', url); - // Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462 - baseElement.attr('href', baseElement.attr('href')); - } - } else { - newLocation = url; - if (replace) { - location.replace(url); - } else { - location.href = url; - } - } - return self; - // getter - } else { - // - newLocation is a workaround for an IE7-9 issue with location.replace and location.href - // methods not updating location.href synchronously. - // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 - return newLocation || location.href.replace(/%27/g,"'"); - } - }; - - var urlChangeListeners = [], - urlChangeInit = false; - - function fireUrlChange() { - newLocation = null; - if (lastBrowserUrl == self.url()) return; - - lastBrowserUrl = self.url(); - forEach(urlChangeListeners, function(listener) { - listener(self.url()); - }); - } - - /** - * @name ng.$browser#onUrlChange - * @methodOf ng.$browser - * @TODO(vojta): refactor to use node's syntax for events - * - * @description - * Register callback function that will be called, when url changes. - * - * It's only called when the url is changed from outside of angular: - * - user types different url into address bar - * - user clicks on history (forward/back) button - * - user clicks on a link - * - * It's not called when url is changed by $browser.url() method - * - * The listener gets called with new url as parameter. - * - * NOTE: this api is intended for use only by the $location service. Please use the - * {@link ng.$location $location service} to monitor url changes in angular apps. - * - * @param {function(string)} listener Listener function to be called when url changes. - * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. - */ - self.onUrlChange = function(callback) { - if (!urlChangeInit) { - // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) - // don't fire popstate when user change the address bar and don't fire hashchange when url - // changed by push/replaceState - - // html5 history api - popstate event - if ($sniffer.history) jqLite(window).on('popstate', fireUrlChange); - // hashchange event - if ($sniffer.hashchange) jqLite(window).on('hashchange', fireUrlChange); - // polling - else self.addPollFn(fireUrlChange); - - urlChangeInit = true; - } - - urlChangeListeners.push(callback); - return callback; - }; - - ////////////////////////////////////////////////////////////// - // Misc API - ////////////////////////////////////////////////////////////// - - /** - * @name ng.$browser#baseHref - * @methodOf ng.$browser - * - * @description - * Returns current - * (always relative - without domain) - * - * @returns {string=} current - */ - self.baseHref = function() { - var href = baseElement.attr('href'); - return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : ''; - }; - - ////////////////////////////////////////////////////////////// - // Cookies API - ////////////////////////////////////////////////////////////// - var lastCookies = {}; - var lastCookieString = ''; - var cookiePath = self.baseHref(); - - /** - * @name ng.$browser#cookies - * @methodOf ng.$browser - * - * @param {string=} name Cookie name - * @param {string=} value Cookie value - * - * @description - * The cookies method provides a 'private' low level access to browser cookies. - * It is not meant to be used directly, use the $cookie service instead. - * - * The return values vary depending on the arguments that the method was called with as follows: - * - * - cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify - * it - * - cookies(name, value) -> set name to value, if value is undefined delete the cookie - * - cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that - * way) - * - * @returns {Object} Hash of all cookies (if called without any parameter) - */ - self.cookies = function(name, value) { - /* global escape: false, unescape: false */ - var cookieLength, cookieArray, cookie, i, index; - - if (name) { - if (value === undefined) { - rawDocument.cookie = escape(name) + "=;path=" + cookiePath + - ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; - } else { - if (isString(value)) { - cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(value) + - ';path=' + cookiePath).length + 1; - - // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: - // - 300 cookies - // - 20 cookies per unique domain - // - 4096 bytes per cookie - if (cookieLength > 4096) { - $log.warn("Cookie '"+ name + - "' possibly not set or overflowed because it was too large ("+ - cookieLength + " > 4096 bytes)!"); - } - } - } - } else { - if (rawDocument.cookie !== lastCookieString) { - lastCookieString = rawDocument.cookie; - cookieArray = lastCookieString.split("; "); - lastCookies = {}; - - for (i = 0; i < cookieArray.length; i++) { - cookie = cookieArray[i]; - index = cookie.indexOf('='); - if (index > 0) { //ignore nameless cookies - name = unescape(cookie.substring(0, index)); - // the first value that is seen for a cookie is the most - // specific one. values for the same cookie name that - // follow are for less specific paths. - if (lastCookies[name] === undefined) { - lastCookies[name] = unescape(cookie.substring(index + 1)); - } - } - } - } - return lastCookies; - } - }; - - - /** - * @name ng.$browser#defer - * @methodOf ng.$browser - * @param {function()} fn A function, who's execution should be deferred. - * @param {number=} [delay=0] of milliseconds to defer the function execution. - * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. - * - * @description - * Executes a fn asynchronously via `setTimeout(fn, delay)`. - * - * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using - * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed - * via `$browser.defer.flush()`. - * - */ - self.defer = function(fn, delay) { - var timeoutId; - outstandingRequestCount++; - timeoutId = setTimeout(function() { - delete pendingDeferIds[timeoutId]; - completeOutstandingRequest(fn); - }, delay || 0); - pendingDeferIds[timeoutId] = true; - return timeoutId; - }; - - - /** - * @name ng.$browser#defer.cancel - * @methodOf ng.$browser.defer - * - * @description - * Cancels a deferred task identified with `deferId`. - * - * @param {*} deferId Token returned by the `$browser.defer` function. - * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully - * canceled. - */ - self.defer.cancel = function(deferId) { - if (pendingDeferIds[deferId]) { - delete pendingDeferIds[deferId]; - clearTimeout(deferId); - completeOutstandingRequest(noop); - return true; - } - return false; - }; - -} - -function $BrowserProvider(){ - this.$get = ['$window', '$log', '$sniffer', '$document', - function( $window, $log, $sniffer, $document){ - return new Browser($window, $document, $log, $sniffer); - }]; -} - -/** - * @ngdoc object - * @name ng.$cacheFactory - * - * @description - * Factory that constructs cache objects and gives access to them. - * - *
    - * 
    - *  var cache = $cacheFactory('cacheId');
    - *  expect($cacheFactory.get('cacheId')).toBe(cache);
    - *  expect($cacheFactory.get('noSuchCacheId')).not.toBeDefined();
    - *
    - *  cache.put("key", "value");
    - *  cache.put("another key", "another value");
    - *
    - *  // We've specified no options on creation
    - *  expect(cache.info()).toEqual({id: 'cacheId', size: 2}); 
    - * 
    - * 
    - * - * - * @param {string} cacheId Name or id of the newly created cache. - * @param {object=} options Options object that specifies the cache behavior. Properties: - * - * - `{number=}` `capacity` — turns the cache into LRU cache. - * - * @returns {object} Newly created cache object with the following set of methods: - * - * - `{object}` `info()` — Returns id, size, and options of cache. - * - `{{*}}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache and returns - * it. - * - `{{*}}` `get({string} key)` — Returns cached value for `key` or undefined for cache miss. - * - `{void}` `remove({string} key)` — Removes a key-value pair from the cache. - * - `{void}` `removeAll()` — Removes all cached values. - * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory. - * - */ -function $CacheFactoryProvider() { - - this.$get = function() { - var caches = {}; - - function cacheFactory(cacheId, options) { - if (cacheId in caches) { - throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId); - } - - var size = 0, - stats = extend({}, options, {id: cacheId}), - data = {}, - capacity = (options && options.capacity) || Number.MAX_VALUE, - lruHash = {}, - freshEnd = null, - staleEnd = null; - - return caches[cacheId] = { - - put: function(key, value) { - var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); - - refresh(lruEntry); - - if (isUndefined(value)) return; - if (!(key in data)) size++; - data[key] = value; - - if (size > capacity) { - this.remove(staleEnd.key); - } - - return value; - }, - - - get: function(key) { - var lruEntry = lruHash[key]; - - if (!lruEntry) return; - - refresh(lruEntry); - - return data[key]; - }, - - - remove: function(key) { - var lruEntry = lruHash[key]; - - if (!lruEntry) return; - - if (lruEntry == freshEnd) freshEnd = lruEntry.p; - if (lruEntry == staleEnd) staleEnd = lruEntry.n; - link(lruEntry.n,lruEntry.p); - - delete lruHash[key]; - delete data[key]; - size--; - }, - - - removeAll: function() { - data = {}; - size = 0; - lruHash = {}; - freshEnd = staleEnd = null; - }, - - - destroy: function() { - data = null; - stats = null; - lruHash = null; - delete caches[cacheId]; - }, - - - info: function() { - return extend({}, stats, {size: size}); - } - }; - - - /** - * makes the `entry` the freshEnd of the LRU linked list - */ - function refresh(entry) { - if (entry != freshEnd) { - if (!staleEnd) { - staleEnd = entry; - } else if (staleEnd == entry) { - staleEnd = entry.n; - } - - link(entry.n, entry.p); - link(entry, freshEnd); - freshEnd = entry; - freshEnd.n = null; - } - } - - - /** - * bidirectionally links two entries of the LRU linked list - */ - function link(nextEntry, prevEntry) { - if (nextEntry != prevEntry) { - if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify - if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify - } - } - } - - - /** - * @ngdoc method - * @name ng.$cacheFactory#info - * @methodOf ng.$cacheFactory - * - * @description - * Get information about all the of the caches that have been created - * - * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info` - */ - cacheFactory.info = function() { - var info = {}; - forEach(caches, function(cache, cacheId) { - info[cacheId] = cache.info(); - }); - return info; - }; - - - /** - * @ngdoc method - * @name ng.$cacheFactory#get - * @methodOf ng.$cacheFactory - * - * @description - * Get access to a cache object by the `cacheId` used when it was created. - * - * @param {string} cacheId Name or id of a cache to access. - * @returns {object} Cache object identified by the cacheId or undefined if no such cache. - */ - cacheFactory.get = function(cacheId) { - return caches[cacheId]; - }; - - - return cacheFactory; - }; -} - -/** - * @ngdoc object - * @name ng.$templateCache - * - * @description - * The first time a template is used, it is loaded in the template cache for quick retrieval. You - * can load templates directly into the cache in a `script` tag, or by consuming the - * `$templateCache` service directly. - * - * Adding via the `script` tag: - *
    - * 
    - * 
    - * 
    - * 
    - *   ...
    - * 
    - * 
    - * - * **Note:** the `script` tag containing the template does not need to be included in the `head` of - * the document, but it must be below the `ng-app` definition. - * - * Adding via the $templateCache service: - * - *
    - * var myApp = angular.module('myApp', []);
    - * myApp.run(function($templateCache) {
    - *   $templateCache.put('templateId.html', 'This is the content of the template');
    - * });
    - * 
    - * - * To retrieve the template later, simply use it in your HTML: - *
    - * 
    - *
    - * - * or get it via Javascript: - *
    - * $templateCache.get('templateId.html')
    - * 
    - * - * See {@link ng.$cacheFactory $cacheFactory}. - * - */ -function $TemplateCacheProvider() { - this.$get = ['$cacheFactory', function($cacheFactory) { - return $cacheFactory('templates'); - }]; -} - -/* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE! - * - * DOM-related variables: - * - * - "node" - DOM Node - * - "element" - DOM Element or Node - * - "$node" or "$element" - jqLite-wrapped node or element - * - * - * Compiler related stuff: - * - * - "linkFn" - linking fn of a single directive - * - "nodeLinkFn" - function that aggregates all linking fns for a particular node - * - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node - * - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList) - */ - - -/** - * @ngdoc function - * @name ng.$compile - * @function - * - * @description - * Compiles an HTML string or DOM into a template and produces a template function, which - * can then be used to link {@link ng.$rootScope.Scope `scope`} and the template together. - * - * The compilation is a process of walking the DOM tree and matching DOM elements to - * {@link ng.$compileProvider#methods_directive directives}. - * - *
    - * **Note:** This document is an in-depth reference of all directive options. - * For a gentle introduction to directives with examples of common use cases, - * see the {@link guide/directive directive guide}. - *
    - * - * ## Comprehensive Directive API - * - * There are many different options for a directive. - * - * The difference resides in the return value of the factory function. - * You can either return a "Directive Definition Object" (see below) that defines the directive properties, - * or just the `postLink` function (all other properties will have the default values). - * - *
    - * **Best Practice:** It's recommended to use the "directive definition object" form. - *
    - * - * Here's an example directive declared with a Directive Definition Object: - * - *
    - *   var myModule = angular.module(...);
    - *
    - *   myModule.directive('directiveName', function factory(injectables) {
    - *     var directiveDefinitionObject = {
    - *       priority: 0,
    - *       template: '
    ', // or // function(tElement, tAttrs) { ... }, - * // or - * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... }, - * replace: false, - * transclude: false, - * restrict: 'A', - * scope: false, - * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, - * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], - * compile: function compile(tElement, tAttrs, transclude) { - * return { - * pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * post: function postLink(scope, iElement, iAttrs, controller) { ... } - * } - * // or - * // return function postLink( ... ) { ... } - * }, - * // or - * // link: { - * // pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * // post: function postLink(scope, iElement, iAttrs, controller) { ... } - * // } - * // or - * // link: function postLink( ... ) { ... } - * }; - * return directiveDefinitionObject; - * }); - *
    - * - *
    - * **Note:** Any unspecified options will use the default value. You can see the default values below. - *
    - * - * Therefore the above can be simplified as: - * - *
    - *   var myModule = angular.module(...);
    - *
    - *   myModule.directive('directiveName', function factory(injectables) {
    - *     var directiveDefinitionObject = {
    - *       link: function postLink(scope, iElement, iAttrs) { ... }
    - *     };
    - *     return directiveDefinitionObject;
    - *     // or
    - *     // return function postLink(scope, iElement, iAttrs) { ... }
    - *   });
    - * 
    - * - * - * - * ### Directive Definition Object - * - * The directive definition object provides instructions to the {@link api/ng.$compile - * compiler}. The attributes are: - * - * #### `priority` - * When there are multiple directives defined on a single DOM element, sometimes it - * is necessary to specify the order in which the directives are applied. The `priority` is used - * to sort the directives before their `compile` functions get called. Priority is defined as a - * number. Directives with greater numerical `priority` are compiled first. Pre-link functions - * are also run in priority order, but post-link functions are run in reverse order. The order - * of directives with the same priority is undefined. The default priority is `0`. - * - * #### `terminal` - * If set to true then the current `priority` will be the last set of directives - * which will execute (any directives at the current priority will still execute - * as the order of execution on same `priority` is undefined). - * - * #### `scope` - * **If set to `true`,** then a new scope will be created for this directive. If multiple directives on the - * same element request a new scope, only one new scope is created. The new scope rule does not - * apply for the root of the template since the root of the template always gets a new scope. - * - * **If set to `{}` (object hash),** then a new "isolate" scope is created. The 'isolate' scope differs from - * normal scope in that it does not prototypically inherit from the parent scope. This is useful - * when creating reusable components, which should not accidentally read or modify data in the - * parent scope. - * - * The 'isolate' scope takes an object hash which defines a set of local scope properties - * derived from the parent scope. These local properties are useful for aliasing values for - * templates. Locals definition is a hash of local scope property to its source: - * - * * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is - * always a string since DOM attributes are strings. If no `attr` name is specified then the - * attribute name is assumed to be the same as the local name. - * Given `` and widget definition - * of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect - * the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the - * `localName` property on the widget scope. The `name` is read from the parent scope (not - * component scope). - * - * * `=` or `=attr` - set up bi-directional binding between a local scope property and the - * parent scope property of name defined via the value of the `attr` attribute. If no `attr` - * name is specified then the attribute name is assumed to be the same as the local name. - * Given `` and widget definition of - * `scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the - * value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected - * in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent - * scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You - * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional. - * - * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. - * If no `attr` name is specified then the attribute name is assumed to be the same as the - * local name. Given `` and widget definition of - * `scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to - * a function wrapper for the `count = count + value` expression. Often it's desirable to - * pass data from the isolated scope via an expression and to the parent scope, this can be - * done by passing a map of local variable names and values into the expression wrapper fn. - * For example, if the expression is `increment(amount)` then we can specify the amount value - * by calling the `localFn` as `localFn({amount: 22})`. - * - * - * - * #### `controller` - * Controller constructor function. The controller is instantiated before the - * pre-linking phase and it is shared with other directives (see - * `require` attribute). This allows the directives to communicate with each other and augment - * each other's behavior. The controller is injectable (and supports bracket notation) with the following locals: - * - * * `$scope` - Current scope associated with the element - * * `$element` - Current element - * * `$attrs` - Current attributes object for the element - * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope. - * The scope can be overridden by an optional first argument. - * `function([scope], cloneLinkingFn)`. - * - * - * #### `require` - * Require another directive and inject its controller as the fourth argument to the linking function. The - * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the - * injected argument will be an array in corresponding order. If no such directive can be - * found, or if the directive does not have a controller, then an error is raised. The name can be prefixed with: - * - * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. - * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. - * * `^` - Locate the required controller by searching the element's parents. Throw an error if not found. - * * `?^` - Attempt to locate the required controller by searching the element's parents or pass `null` to the - * `link` fn if not found. - * - * - * #### `controllerAs` - * Controller alias at the directive scope. An alias for the controller so it - * can be referenced at the directive template. The directive needs to define a scope for this - * configuration to be used. Useful in the case when directive is used as component. - * - * - * #### `restrict` - * String of subset of `EACM` which restricts the directive to a specific directive - * declaration style. If omitted, the default (attributes only) is used. - * - * * `E` - Element name: `` - * * `A` - Attribute (default): `
    ` - * * `C` - Class: `
    ` - * * `M` - Comment: `` - * - * - * #### `template` - * replace the current element with the contents of the HTML. The replacement process - * migrates all of the attributes / classes from the old element to the new one. See the - * {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive - * Directives Guide} for an example. - * - * You can specify `template` as a string representing the template or as a function which takes - * two arguments `tElement` and `tAttrs` (described in the `compile` function api below) and - * returns a string value representing the template. - * - * - * #### `templateUrl` - * Same as `template` but the template is loaded from the specified URL. Because - * the template loading is asynchronous the compilation/linking is suspended until the template - * is loaded. - * - * You can specify `templateUrl` as a string representing the URL or as a function which takes two - * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns - * a string value representing the url. In either case, the template URL is passed through {@link - * api/ng.$sce#methods_getTrustedResourceUrl $sce.getTrustedResourceUrl}. - * - * - * #### `replace` - * specify where the template should be inserted. Defaults to `false`. - * - * * `true` - the template will replace the current element. - * * `false` - the template will replace the contents of the current element. - * - * - * #### `transclude` - * compile the content of the element and make it available to the directive. - * Typically used with {@link api/ng.directive:ngTransclude - * ngTransclude}. The advantage of transclusion is that the linking function receives a - * transclusion function which is pre-bound to the correct scope. In a typical setup the widget - * creates an `isolate` scope, but the transclusion is not a child, but a sibling of the `isolate` - * scope. This makes it possible for the widget to have private state, and the transclusion to - * be bound to the parent (pre-`isolate`) scope. - * - * * `true` - transclude the content of the directive. - * * `'element'` - transclude the whole element including any directives defined at lower priority. - * - * - * #### `compile` - * - *
    - *   function compile(tElement, tAttrs, transclude) { ... }
    - * 
    - * - * The compile function deals with transforming the template DOM. Since most directives do not do - * template transformation, it is not used often. Examples that require compile functions are - * directives that transform template DOM, such as {@link - * api/ng.directive:ngRepeat ngRepeat}, or load the contents - * asynchronously, such as {@link api/ngRoute.directive:ngView ngView}. The - * compile function takes the following arguments. - * - * * `tElement` - template element - The element where the directive has been declared. It is - * safe to do template transformation on the element and child elements only. - * - * * `tAttrs` - template attributes - Normalized list of attributes declared on this element shared - * between all directive compile functions. - * - * * `transclude` - [*DEPRECATED*!] A transclude linking function: `function(scope, cloneLinkingFn)` - * - *
    - * **Note:** The template instance and the link instance may be different objects if the template has - * been cloned. For this reason it is **not** safe to do anything other than DOM transformations that - * apply to all cloned DOM nodes within the compile function. Specifically, DOM listener registration - * should be done in a linking function rather than in a compile function. - *
    - * - *
    - * **Note:** The `transclude` function that is passed to the compile function is deprecated, as it - * e.g. does not know about the right outer scope. Please use the transclude function that is passed - * to the link function instead. - *
    - - * A compile function can have a return value which can be either a function or an object. - * - * * returning a (post-link) function - is equivalent to registering the linking function via the - * `link` property of the config object when the compile function is empty. - * - * * returning an object with function(s) registered via `pre` and `post` properties - allows you to - * control when a linking function should be called during the linking phase. See info about - * pre-linking and post-linking functions below. - * - * - * #### `link` - * This property is used only if the `compile` property is not defined. - * - *
    - *   function link(scope, iElement, iAttrs, controller, transcludeFn) { ... }
    - * 
    - * - * The link function is responsible for registering DOM listeners as well as updating the DOM. It is - * executed after the template has been cloned. This is where most of the directive logic will be - * put. - * - * * `scope` - {@link api/ng.$rootScope.Scope Scope} - The scope to be used by the - * directive for registering {@link api/ng.$rootScope.Scope#methods_$watch watches}. - * - * * `iElement` - instance element - The element where the directive is to be used. It is safe to - * manipulate the children of the element only in `postLink` function since the children have - * already been linked. - * - * * `iAttrs` - instance attributes - Normalized list of attributes declared on this element shared - * between all directive linking functions. - * - * * `controller` - a controller instance - A controller instance if at least one directive on the - * element defines a controller. The controller is shared among all the directives, which allows - * the directives to use the controllers as a communication channel. - * - * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. - * The scope can be overridden by an optional first argument. This is the same as the `$transclude` - * parameter of directive controllers. - * `function([scope], cloneLinkingFn)`. - * - * - * #### Pre-linking function - * - * Executed before the child elements are linked. Not safe to do DOM transformation since the - * compiler linking function will fail to locate the correct elements for linking. - * - * #### Post-linking function - * - * Executed after the child elements are linked. It is safe to do DOM transformation in the post-linking function. - * - * - * ### Attributes - * - * The {@link api/ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the - * `link()` or `compile()` functions. It has a variety of uses. - * - * accessing *Normalized attribute names:* - * Directives like 'ngBind' can be expressed in many ways: 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. - * the attributes object allows for normalized access to - * the attributes. - * - * * *Directive inter-communication:* All directives share the same instance of the attributes - * object which allows the directives to use the attributes object as inter directive - * communication. - * - * * *Supports interpolation:* Interpolation attributes are assigned to the attribute object - * allowing other directives to read the interpolated value. - * - * * *Observing interpolated attributes:* Use `$observe` to observe the value changes of attributes - * that contain interpolation (e.g. `src="{{bar}}"`). Not only is this very efficient but it's also - * the only way to easily get the actual value because during the linking phase the interpolation - * hasn't been evaluated yet and so the value is at this time set to `undefined`. - * - *
    - * function linkingFn(scope, elm, attrs, ctrl) {
    - *   // get the attribute value
    - *   console.log(attrs.ngModel);
    - *
    - *   // change the attribute
    - *   attrs.$set('ngModel', 'new value');
    - *
    - *   // observe changes to interpolated attribute
    - *   attrs.$observe('ngModel', function(value) {
    - *     console.log('ngModel has changed value to ' + value);
    - *   });
    - * }
    - * 
    - * - * Below is an example using `$compileProvider`. - * - *
    - * **Note**: Typically directives are registered with `module.directive`. The example below is - * to illustrate how `$compile` works. - *
    - * - - - -
    -
    -
    -
    -
    -
    - - it('should auto compile', function() { - var textarea = $('textarea'); - var output = $('div[compile]'); - // The initial state reads 'Hello Angular'. - expect(output.getText()).toBe('Hello Angular'); - textarea.clear(); - textarea.sendKeys('{{name}}!'); - expect(output.getText()).toBe('Angular!'); - }); - -
    - - * - * - * @param {string|DOMElement} element Element or HTML string to compile into a template function. - * @param {function(angular.Scope[, cloneAttachFn]} transclude function available to directives. - * @param {number} maxPriority only apply directives lower then given priority (Only effects the - * root element(s), not their children) - * @returns {function(scope[, cloneAttachFn])} a link function which is used to bind template - * (a DOM element/tree) to a scope. Where: - * - * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to. - * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the - * `template` and call the `cloneAttachFn` function allowing the caller to attach the - * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is - * called as:
    `cloneAttachFn(clonedElement, scope)` where: - * - * * `clonedElement` - is a clone of the original `element` passed into the compiler. - * * `scope` - is the current scope with which the linking function is working with. - * - * Calling the linking function returns the element of the template. It is either the original - * element passed in, or the clone of the element if the `cloneAttachFn` is provided. - * - * After linking the view is not updated until after a call to $digest which typically is done by - * Angular automatically. - * - * If you need access to the bound view, there are two ways to do it: - * - * - If you are not asking the linking function to clone the template, create the DOM element(s) - * before you send them to the compiler and keep this reference around. - *
    - *     var element = $compile('

    {{total}}

    ')(scope); - *
    - * - * - if on the other hand, you need the element to be cloned, the view reference from the original - * example would not point to the clone, but rather to the original template that was cloned. In - * this case, you can access the clone via the cloneAttachFn: - *
    - *     var templateElement = angular.element('

    {{total}}

    '), - * scope = ....; - * - * var clonedElement = $compile(templateElement)(scope, function(clonedElement, scope) { - * //attach the clone to DOM document at the right place - * }); - * - * //now we have reference to the cloned DOM via `clonedElement` - *
    - * - * - * For information on how the compiler works, see the - * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. - */ - -var $compileMinErr = minErr('$compile'); - -/** - * @ngdoc service - * @name ng.$compileProvider - * @function - * - * @description - */ -$CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; -function $CompileProvider($provide, $$sanitizeUriProvider) { - var hasDirectives = {}, - Suffix = 'Directive', - COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, - CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/, - TABLE_CONTENT_REGEXP = /^<\s*(tr|th|td|tbody)(\s+[^>]*)?>/i; - - // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes - // The assumption is that future DOM event attribute names will begin with - // 'on' and be composed of only English letters. - var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; - - /** - * @ngdoc function - * @name ng.$compileProvider#directive - * @methodOf ng.$compileProvider - * @function - * - * @description - * Register a new directive with the compiler. - * - * @param {string|Object} name Name of the directive in camel-case (i.e. ngBind which - * will match as ng-bind), or an object map of directives where the keys are the - * names and the values are the factories. - * @param {function|Array} directiveFactory An injectable directive factory function. See - * {@link guide/directive} for more info. - * @returns {ng.$compileProvider} Self for chaining. - */ - this.directive = function registerDirective(name, directiveFactory) { - assertNotHasOwnProperty(name, 'directive'); - if (isString(name)) { - assertArg(directiveFactory, 'directiveFactory'); - if (!hasDirectives.hasOwnProperty(name)) { - hasDirectives[name] = []; - $provide.factory(name + Suffix, ['$injector', '$exceptionHandler', - function($injector, $exceptionHandler) { - var directives = []; - forEach(hasDirectives[name], function(directiveFactory, index) { - try { - var directive = $injector.invoke(directiveFactory); - if (isFunction(directive)) { - directive = { compile: valueFn(directive) }; - } else if (!directive.compile && directive.link) { - directive.compile = valueFn(directive.link); - } - directive.priority = directive.priority || 0; - directive.index = index; - directive.name = directive.name || name; - directive.require = directive.require || (directive.controller && directive.name); - directive.restrict = directive.restrict || 'A'; - directives.push(directive); - } catch (e) { - $exceptionHandler(e); - } - }); - return directives; - }]); - } - hasDirectives[name].push(directiveFactory); - } else { - forEach(name, reverseParams(registerDirective)); - } - return this; - }; - - - /** - * @ngdoc function - * @name ng.$compileProvider#aHrefSanitizationWhitelist - * @methodOf ng.$compileProvider - * @function - * - * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe - * urls during a[href] sanitization. - * - * The sanitization is a security measure aimed at prevent XSS attacks via html links. - * - * Any url about to be assigned to a[href] via data-binding is first normalized and turned into - * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` - * regular expression. If a match is found, the original url is written into the dom. Otherwise, - * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. - * - * @param {RegExp=} regexp New regexp to whitelist urls with. - * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for - * chaining otherwise. - */ - this.aHrefSanitizationWhitelist = function(regexp) { - if (isDefined(regexp)) { - $$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp); - return this; - } else { - return $$sanitizeUriProvider.aHrefSanitizationWhitelist(); - } - }; - - - /** - * @ngdoc function - * @name ng.$compileProvider#imgSrcSanitizationWhitelist - * @methodOf ng.$compileProvider - * @function - * - * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe - * urls during img[src] sanitization. - * - * The sanitization is a security measure aimed at prevent XSS attacks via html links. - * - * Any url about to be assigned to img[src] via data-binding is first normalized and turned into - * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` - * regular expression. If a match is found, the original url is written into the dom. Otherwise, - * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. - * - * @param {RegExp=} regexp New regexp to whitelist urls with. - * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for - * chaining otherwise. - */ - this.imgSrcSanitizationWhitelist = function(regexp) { - if (isDefined(regexp)) { - $$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp); - return this; - } else { - return $$sanitizeUriProvider.imgSrcSanitizationWhitelist(); - } - }; - - this.$get = [ - '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', - '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', - function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, - $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { - - var Attributes = function(element, attr) { - this.$$element = element; - this.$attr = attr || {}; - }; - - Attributes.prototype = { - $normalize: directiveNormalize, - - - /** - * @ngdoc function - * @name ng.$compile.directive.Attributes#$addClass - * @methodOf ng.$compile.directive.Attributes - * @function - * - * @description - * Adds the CSS class value specified by the classVal parameter to the element. If animations - * are enabled then an animation will be triggered for the class addition. - * - * @param {string} classVal The className value that will be added to the element - */ - $addClass : function(classVal) { - if(classVal && classVal.length > 0) { - $animate.addClass(this.$$element, classVal); - } - }, - - /** - * @ngdoc function - * @name ng.$compile.directive.Attributes#$removeClass - * @methodOf ng.$compile.directive.Attributes - * @function - * - * @description - * Removes the CSS class value specified by the classVal parameter from the element. If - * animations are enabled then an animation will be triggered for the class removal. - * - * @param {string} classVal The className value that will be removed from the element - */ - $removeClass : function(classVal) { - if(classVal && classVal.length > 0) { - $animate.removeClass(this.$$element, classVal); - } - }, - - /** - * @ngdoc function - * @name ng.$compile.directive.Attributes#$updateClass - * @methodOf ng.$compile.directive.Attributes - * @function - * - * @description - * Adds and removes the appropriate CSS class values to the element based on the difference - * between the new and old CSS class values (specified as newClasses and oldClasses). - * - * @param {string} newClasses The current CSS className value - * @param {string} oldClasses The former CSS className value - */ - $updateClass : function(newClasses, oldClasses) { - var toAdd = tokenDifference(newClasses, oldClasses); - var toRemove = tokenDifference(oldClasses, newClasses); - - if(toAdd.length === 0) { - $animate.removeClass(this.$$element, toRemove); - } else if(toRemove.length === 0) { - $animate.addClass(this.$$element, toAdd); - } else { - $animate.setClass(this.$$element, toAdd, toRemove); - } - }, - - /** - * Set a normalized attribute on the element in a way such that all directives - * can share the attribute. This function properly handles boolean attributes. - * @param {string} key Normalized key. (ie ngAttribute) - * @param {string|boolean} value The value to set. If `null` attribute will be deleted. - * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute. - * Defaults to true. - * @param {string=} attrName Optional none normalized name. Defaults to key. - */ - $set: function(key, value, writeAttr, attrName) { - // TODO: decide whether or not to throw an error if "class" - //is set through this function since it may cause $updateClass to - //become unstable. - - var booleanKey = getBooleanAttrName(this.$$element[0], key), - normalizedVal, - nodeName; - - if (booleanKey) { - this.$$element.prop(key, value); - attrName = booleanKey; - } - - this[key] = value; - - // translate normalized key to actual key - if (attrName) { - this.$attr[key] = attrName; - } else { - attrName = this.$attr[key]; - if (!attrName) { - this.$attr[key] = attrName = snake_case(key, '-'); - } - } - - nodeName = nodeName_(this.$$element); - - // sanitize a[href] and img[src] values - if ((nodeName === 'A' && key === 'href') || - (nodeName === 'IMG' && key === 'src')) { - this[key] = value = $$sanitizeUri(value, key === 'src'); - } - - if (writeAttr !== false) { - if (value === null || value === undefined) { - this.$$element.removeAttr(attrName); - } else { - this.$$element.attr(attrName, value); - } - } - - // fire observers - var $$observers = this.$$observers; - $$observers && forEach($$observers[key], function(fn) { - try { - fn(value); - } catch (e) { - $exceptionHandler(e); - } - }); - }, - - - /** - * @ngdoc function - * @name ng.$compile.directive.Attributes#$observe - * @methodOf ng.$compile.directive.Attributes - * @function - * - * @description - * Observes an interpolated attribute. - * - * The observer function will be invoked once during the next `$digest` following - * compilation. The observer is then invoked whenever the interpolated value - * changes. - * - * @param {string} key Normalized key. (ie ngAttribute) . - * @param {function(interpolatedValue)} fn Function that will be called whenever - the interpolated value of the attribute changes. - * See the {@link guide/directive#Attributes Directives} guide for more info. - * @returns {function()} the `fn` parameter. - */ - $observe: function(key, fn) { - var attrs = this, - $$observers = (attrs.$$observers || (attrs.$$observers = {})), - listeners = ($$observers[key] || ($$observers[key] = [])); - - listeners.push(fn); - $rootScope.$evalAsync(function() { - if (!listeners.$$inter) { - // no one registered attribute interpolation function, so lets call it manually - fn(attrs[key]); - } - }); - return fn; - } - }; - - var startSymbol = $interpolate.startSymbol(), - endSymbol = $interpolate.endSymbol(), - denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') - ? identity - : function denormalizeTemplate(template) { - return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); - }, - NG_ATTR_BINDING = /^ngAttr[A-Z]/; - - - return compile; - - //================================ - - function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, - previousCompileContext) { - if (!($compileNodes instanceof jqLite)) { - // jquery always rewraps, whereas we need to preserve the original selector so that we can - // modify it. - $compileNodes = jqLite($compileNodes); - } - // We can not compile top level text elements since text nodes can be merged and we will - // not be able to attach scope data to them, so we will wrap them in - forEach($compileNodes, function(node, index){ - if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { - $compileNodes[index] = node = jqLite(node).wrap('').parent()[0]; - } - }); - var compositeLinkFn = - compileNodes($compileNodes, transcludeFn, $compileNodes, - maxPriority, ignoreDirective, previousCompileContext); - safeAddClass($compileNodes, 'ng-scope'); - return function publicLinkFn(scope, cloneConnectFn, transcludeControllers){ - assertArg(scope, 'scope'); - // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart - // and sometimes changes the structure of the DOM. - var $linkNode = cloneConnectFn - ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!! - : $compileNodes; - - forEach(transcludeControllers, function(instance, name) { - $linkNode.data('$' + name + 'Controller', instance); - }); - - // Attach scope only to non-text nodes. - for(var i = 0, ii = $linkNode.length; i - addDirective(directives, - directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority, ignoreDirective); - - // iterate over the attributes - for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes, - j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { - var attrStartName = false; - var attrEndName = false; - - attr = nAttrs[j]; - if (!msie || msie >= 8 || attr.specified) { - name = attr.name; - // support ngAttr attribute binding - ngAttrName = directiveNormalize(name); - if (NG_ATTR_BINDING.test(ngAttrName)) { - name = snake_case(ngAttrName.substr(6), '-'); - } - - var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); - if (ngAttrName === directiveNName + 'Start') { - attrStartName = name; - attrEndName = name.substr(0, name.length - 5) + 'end'; - name = name.substr(0, name.length - 6); - } - - nName = directiveNormalize(name.toLowerCase()); - attrsMap[nName] = name; - attrs[nName] = value = trim(attr.value); - if (getBooleanAttrName(node, nName)) { - attrs[nName] = true; // presence means true - } - addAttrInterpolateDirective(node, directives, value, nName); - addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, - attrEndName); - } - } - - // use class as directive - className = node.className; - if (isString(className) && className !== '') { - while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { - nName = directiveNormalize(match[2]); - if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) { - attrs[nName] = trim(match[3]); - } - className = className.substr(match.index + match[0].length); - } - } - break; - case 3: /* Text Node */ - addTextInterpolateDirective(directives, node.nodeValue); - break; - case 8: /* Comment */ - try { - match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); - if (match) { - nName = directiveNormalize(match[1]); - if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { - attrs[nName] = trim(match[2]); - } - } - } catch (e) { - // turns out that under some circumstances IE9 throws errors when one attempts to read - // comment's node value. - // Just ignore it and continue. (Can't seem to reproduce in test case.) - } - break; - } - - directives.sort(byPriority); - return directives; - } - - /** - * Given a node with an directive-start it collects all of the siblings until it finds - * directive-end. - * @param node - * @param attrStart - * @param attrEnd - * @returns {*} - */ - function groupScan(node, attrStart, attrEnd) { - var nodes = []; - var depth = 0; - if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { - var startNode = node; - do { - if (!node) { - throw $compileMinErr('uterdir', - "Unterminated attribute, found '{0}' but no matching '{1}' found.", - attrStart, attrEnd); - } - if (node.nodeType == 1 /** Element **/) { - if (node.hasAttribute(attrStart)) depth++; - if (node.hasAttribute(attrEnd)) depth--; - } - nodes.push(node); - node = node.nextSibling; - } while (depth > 0); - } else { - nodes.push(node); - } - - return jqLite(nodes); - } - - /** - * Wrapper for linking function which converts normal linking function into a grouped - * linking function. - * @param linkFn - * @param attrStart - * @param attrEnd - * @returns {Function} - */ - function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { - return function(scope, element, attrs, controllers, transcludeFn) { - element = groupScan(element[0], attrStart, attrEnd); - return linkFn(scope, element, attrs, controllers, transcludeFn); - }; - } - - /** - * Once the directives have been collected, their compile functions are executed. This method - * is responsible for inlining directive templates as well as terminating the application - * of the directives if the terminal directive has been reached. - * - * @param {Array} directives Array of collected directives to execute their compile function. - * this needs to be pre-sorted by priority order. - * @param {Node} compileNode The raw DOM node to apply the compile functions to - * @param {Object} templateAttrs The shared attribute function - * @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the - * scope argument is auto-generated to the new - * child of the transcluded parent scope. - * @param {JQLite} jqCollection If we are working on the root of the compile tree then this - * argument has the root jqLite array so that we can replace nodes - * on it. - * @param {Object=} originalReplaceDirective An optional directive that will be ignored when - * compiling the transclusion. - * @param {Array.} preLinkFns - * @param {Array.} postLinkFns - * @param {Object} previousCompileContext Context used for previous compilation of the current - * node - * @returns linkFn - */ - function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, - jqCollection, originalReplaceDirective, preLinkFns, postLinkFns, - previousCompileContext) { - previousCompileContext = previousCompileContext || {}; - - var terminalPriority = -Number.MAX_VALUE, - newScopeDirective, - controllerDirectives = previousCompileContext.controllerDirectives, - newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, - templateDirective = previousCompileContext.templateDirective, - nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective, - hasTranscludeDirective = false, - hasElementTranscludeDirective = previousCompileContext.hasElementTranscludeDirective, - $compileNode = templateAttrs.$$element = jqLite(compileNode), - directive, - directiveName, - $template, - replaceDirective = originalReplaceDirective, - childTranscludeFn = transcludeFn, - linkFn, - directiveValue; - - // executes all directives on the current element - for(var i = 0, ii = directives.length; i < ii; i++) { - directive = directives[i]; - var attrStart = directive.$$start; - var attrEnd = directive.$$end; - - // collect multiblock sections - if (attrStart) { - $compileNode = groupScan(compileNode, attrStart, attrEnd); - } - $template = undefined; - - if (terminalPriority > directive.priority) { - break; // prevent further processing of directives - } - - if (directiveValue = directive.scope) { - newScopeDirective = newScopeDirective || directive; - - // skip the check for directives with async templates, we'll check the derived sync - // directive when the template arrives - if (!directive.templateUrl) { - assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, - $compileNode); - if (isObject(directiveValue)) { - newIsolateScopeDirective = directive; - } - } - } - - directiveName = directive.name; - - if (!directive.templateUrl && directive.controller) { - directiveValue = directive.controller; - controllerDirectives = controllerDirectives || {}; - assertNoDuplicate("'" + directiveName + "' controller", - controllerDirectives[directiveName], directive, $compileNode); - controllerDirectives[directiveName] = directive; - } - - if (directiveValue = directive.transclude) { - hasTranscludeDirective = true; - - // Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion. - // This option should only be used by directives that know how to safely handle element transclusion, - // where the transcluded nodes are added or replaced after linking. - if (!directive.$$tlb) { - assertNoDuplicate('transclusion', nonTlbTranscludeDirective, directive, $compileNode); - nonTlbTranscludeDirective = directive; - } - - if (directiveValue == 'element') { - hasElementTranscludeDirective = true; - terminalPriority = directive.priority; - $template = groupScan(compileNode, attrStart, attrEnd); - $compileNode = templateAttrs.$$element = - jqLite(document.createComment(' ' + directiveName + ': ' + - templateAttrs[directiveName] + ' ')); - compileNode = $compileNode[0]; - replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode); - - childTranscludeFn = compile($template, transcludeFn, terminalPriority, - replaceDirective && replaceDirective.name, { - // Don't pass in: - // - controllerDirectives - otherwise we'll create duplicates controllers - // - newIsolateScopeDirective or templateDirective - combining templates with - // element transclusion doesn't make sense. - // - // We need only nonTlbTranscludeDirective so that we prevent putting transclusion - // on the same element more than once. - nonTlbTranscludeDirective: nonTlbTranscludeDirective - }); - } else { - $template = jqLite(jqLiteClone(compileNode)).contents(); - $compileNode.empty(); // clear contents - childTranscludeFn = compile($template, transcludeFn); - } - } - - if (directive.template) { - assertNoDuplicate('template', templateDirective, directive, $compileNode); - templateDirective = directive; - - directiveValue = (isFunction(directive.template)) - ? directive.template($compileNode, templateAttrs) - : directive.template; - - directiveValue = denormalizeTemplate(directiveValue); - - if (directive.replace) { - replaceDirective = directive; - $template = directiveTemplateContents(directiveValue); - compileNode = $template[0]; - - if ($template.length != 1 || compileNode.nodeType !== 1) { - throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", - directiveName, ''); - } - - replaceWith(jqCollection, $compileNode, compileNode); - - var newTemplateAttrs = {$attr: {}}; - - // combine directives from the original node and from the template: - // - take the array of directives for this element - // - split it into two parts, those that already applied (processed) and those that weren't (unprocessed) - // - collect directives from the template and sort them by priority - // - combine directives as: processed + template + unprocessed - var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs); - var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1)); - - if (newIsolateScopeDirective) { - markDirectivesAsIsolate(templateDirectives); - } - directives = directives.concat(templateDirectives).concat(unprocessedDirectives); - mergeTemplateAttributes(templateAttrs, newTemplateAttrs); - - ii = directives.length; - } else { - $compileNode.html(directiveValue); - } - } - - if (directive.templateUrl) { - assertNoDuplicate('template', templateDirective, directive, $compileNode); - templateDirective = directive; - - if (directive.replace) { - replaceDirective = directive; - } - - nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, - templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns, { - controllerDirectives: controllerDirectives, - newIsolateScopeDirective: newIsolateScopeDirective, - templateDirective: templateDirective, - nonTlbTranscludeDirective: nonTlbTranscludeDirective - }); - ii = directives.length; - } else if (directive.compile) { - try { - linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn); - if (isFunction(linkFn)) { - addLinkFns(null, linkFn, attrStart, attrEnd); - } else if (linkFn) { - addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); - } - } catch (e) { - $exceptionHandler(e, startingTag($compileNode)); - } - } - - if (directive.terminal) { - nodeLinkFn.terminal = true; - terminalPriority = Math.max(terminalPriority, directive.priority); - } - - } - - nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; - nodeLinkFn.transclude = hasTranscludeDirective && childTranscludeFn; - previousCompileContext.hasElementTranscludeDirective = hasElementTranscludeDirective; - - // might be normal or delayed nodeLinkFn depending on if templateUrl is present - return nodeLinkFn; - - //////////////////// - - function addLinkFns(pre, post, attrStart, attrEnd) { - if (pre) { - if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd); - pre.require = directive.require; - if (newIsolateScopeDirective === directive || directive.$$isolateScope) { - pre = cloneAndAnnotateFn(pre, {isolateScope: true}); - } - preLinkFns.push(pre); - } - if (post) { - if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd); - post.require = directive.require; - if (newIsolateScopeDirective === directive || directive.$$isolateScope) { - post = cloneAndAnnotateFn(post, {isolateScope: true}); - } - postLinkFns.push(post); - } - } - - - function getControllers(require, $element, elementControllers) { - var value, retrievalMethod = 'data', optional = false; - if (isString(require)) { - while((value = require.charAt(0)) == '^' || value == '?') { - require = require.substr(1); - if (value == '^') { - retrievalMethod = 'inheritedData'; - } - optional = optional || value == '?'; - } - value = null; - - if (elementControllers && retrievalMethod === 'data') { - value = elementControllers[require]; - } - value = value || $element[retrievalMethod]('$' + require + 'Controller'); - - if (!value && !optional) { - throw $compileMinErr('ctreq', - "Controller '{0}', required by directive '{1}', can't be found!", - require, directiveName); - } - return value; - } else if (isArray(require)) { - value = []; - forEach(require, function(require) { - value.push(getControllers(require, $element, elementControllers)); - }); - } - return value; - } - - - function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { - var attrs, $element, i, ii, linkFn, controller, isolateScope, elementControllers = {}, transcludeFn; - - if (compileNode === linkNode) { - attrs = templateAttrs; - } else { - attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); - } - $element = attrs.$$element; - - if (newIsolateScopeDirective) { - var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; - var $linkNode = jqLite(linkNode); - - isolateScope = scope.$new(true); - - if (templateDirective && (templateDirective === newIsolateScopeDirective.$$originalDirective)) { - $linkNode.data('$isolateScope', isolateScope) ; - } else { - $linkNode.data('$isolateScopeNoTemplate', isolateScope); - } - - - - safeAddClass($linkNode, 'ng-isolate-scope'); - - forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { - var match = definition.match(LOCAL_REGEXP) || [], - attrName = match[3] || scopeName, - optional = (match[2] == '?'), - mode = match[1], // @, =, or & - lastValue, - parentGet, parentSet, compare; - - isolateScope.$$isolateBindings[scopeName] = mode + attrName; - - switch (mode) { - - case '@': - attrs.$observe(attrName, function(value) { - isolateScope[scopeName] = value; - }); - attrs.$$observers[attrName].$$scope = scope; - if( attrs[attrName] ) { - // If the attribute has been provided then we trigger an interpolation to ensure - // the value is there for use in the link fn - isolateScope[scopeName] = $interpolate(attrs[attrName])(scope); - } - break; - - case '=': - if (optional && !attrs[attrName]) { - return; - } - parentGet = $parse(attrs[attrName]); - if (parentGet.literal) { - compare = equals; - } else { - compare = function(a,b) { return a === b; }; - } - parentSet = parentGet.assign || function() { - // reset the change, or we will throw this exception on every $digest - lastValue = isolateScope[scopeName] = parentGet(scope); - throw $compileMinErr('nonassign', - "Expression '{0}' used with directive '{1}' is non-assignable!", - attrs[attrName], newIsolateScopeDirective.name); - }; - lastValue = isolateScope[scopeName] = parentGet(scope); - isolateScope.$watch(function parentValueWatch() { - var parentValue = parentGet(scope); - if (!compare(parentValue, isolateScope[scopeName])) { - // we are out of sync and need to copy - if (!compare(parentValue, lastValue)) { - // parent changed and it has precedence - isolateScope[scopeName] = parentValue; - } else { - // if the parent can be assigned then do so - parentSet(scope, parentValue = isolateScope[scopeName]); - } - } - return lastValue = parentValue; - }, null, parentGet.literal); - break; - - case '&': - parentGet = $parse(attrs[attrName]); - isolateScope[scopeName] = function(locals) { - return parentGet(scope, locals); - }; - break; - - default: - throw $compileMinErr('iscp', - "Invalid isolate scope definition for directive '{0}'." + - " Definition: {... {1}: '{2}' ...}", - newIsolateScopeDirective.name, scopeName, definition); - } - }); - } - transcludeFn = boundTranscludeFn && controllersBoundTransclude; - if (controllerDirectives) { - forEach(controllerDirectives, function(directive) { - var locals = { - $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, - $element: $element, - $attrs: attrs, - $transclude: transcludeFn - }, controllerInstance; - - controller = directive.controller; - if (controller == '@') { - controller = attrs[directive.name]; - } - - controllerInstance = $controller(controller, locals); - // For directives with element transclusion the element is a comment, - // but jQuery .data doesn't support attaching data to comment nodes as it's hard to - // clean up (http://bugs.jquery.com/ticket/8335). - // Instead, we save the controllers for the element in a local hash and attach to .data - // later, once we have the actual element. - elementControllers[directive.name] = controllerInstance; - if (!hasElementTranscludeDirective) { - $element.data('$' + directive.name + 'Controller', controllerInstance); - } - - if (directive.controllerAs) { - locals.$scope[directive.controllerAs] = controllerInstance; - } - }); - } - - // PRELINKING - for(i = 0, ii = preLinkFns.length; i < ii; i++) { - try { - linkFn = preLinkFns[i]; - linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, - linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } - } - - // RECURSION - // We only pass the isolate scope, if the isolate directive has a template, - // otherwise the child elements do not belong to the isolate directive. - var scopeToChild = scope; - if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) { - scopeToChild = isolateScope; - } - childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); - - // POSTLINKING - for(i = postLinkFns.length - 1; i >= 0; i--) { - try { - linkFn = postLinkFns[i]; - linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, - linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } - } - - // This is the function that is injected as `$transclude`. - function controllersBoundTransclude(scope, cloneAttachFn) { - var transcludeControllers; - - // no scope passed - if (arguments.length < 2) { - cloneAttachFn = scope; - scope = undefined; - } - - if (hasElementTranscludeDirective) { - transcludeControllers = elementControllers; - } - - return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers); - } - } - } - - function markDirectivesAsIsolate(directives) { - // mark all directives as needing isolate scope. - for (var j = 0, jj = directives.length; j < jj; j++) { - directives[j] = inherit(directives[j], {$$isolateScope: true}); - } - } - - /** - * looks up the directive and decorates it with exception handling and proper parameters. We - * call this the boundDirective. - * - * @param {string} name name of the directive to look up. - * @param {string} location The directive must be found in specific format. - * String containing any of theses characters: - * - * * `E`: element name - * * `A': attribute - * * `C`: class - * * `M`: comment - * @returns true if directive was added. - */ - function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName, - endAttrName) { - if (name === ignoreDirective) return null; - var match = null; - if (hasDirectives.hasOwnProperty(name)) { - for(var directive, directives = $injector.get(name + Suffix), - i = 0, ii = directives.length; i directive.priority) && - directive.restrict.indexOf(location) != -1) { - if (startAttrName) { - directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); - } - tDirectives.push(directive); - match = directive; - } - } catch(e) { $exceptionHandler(e); } - } - } - return match; - } - - - /** - * When the element is replaced with HTML template then the new attributes - * on the template need to be merged with the existing attributes in the DOM. - * The desired effect is to have both of the attributes present. - * - * @param {object} dst destination attributes (original DOM) - * @param {object} src source attributes (from the directive template) - */ - function mergeTemplateAttributes(dst, src) { - var srcAttr = src.$attr, - dstAttr = dst.$attr, - $element = dst.$$element; - - // reapply the old attributes to the new element - forEach(dst, function(value, key) { - if (key.charAt(0) != '$') { - if (src[key]) { - value += (key === 'style' ? ';' : ' ') + src[key]; - } - dst.$set(key, value, true, srcAttr[key]); - } - }); - - // copy the new attributes on the old attrs object - forEach(src, function(value, key) { - if (key == 'class') { - safeAddClass($element, value); - dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value; - } else if (key == 'style') { - $element.attr('style', $element.attr('style') + ';' + value); - dst['style'] = (dst['style'] ? dst['style'] + ';' : '') + value; - // `dst` will never contain hasOwnProperty as DOM parser won't let it. - // You will get an "InvalidCharacterError: DOM Exception 5" error if you - // have an attribute like "has-own-property" or "data-has-own-property", etc. - } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { - dst[key] = value; - dstAttr[key] = srcAttr[key]; - } - }); - } - - - function directiveTemplateContents(template) { - var type; - template = trim(template); - if ((type = TABLE_CONTENT_REGEXP.exec(template))) { - type = type[1].toLowerCase(); - var table = jqLite('' + template + '
    '), - tbody = table.children('tbody'), - leaf = /(td|th)/.test(type) && table.find('tr'); - if (tbody.length && type !== 'tbody') { - table = tbody; - } - if (leaf && leaf.length) { - table = leaf; - } - return table.contents(); - } - return jqLite('
    ' + - template + - '
    ').contents(); - } - - - function compileTemplateUrl(directives, $compileNode, tAttrs, - $rootElement, childTranscludeFn, preLinkFns, postLinkFns, previousCompileContext) { - var linkQueue = [], - afterTemplateNodeLinkFn, - afterTemplateChildLinkFn, - beforeTemplateCompileNode = $compileNode[0], - origAsyncDirective = directives.shift(), - // The fact that we have to copy and patch the directive seems wrong! - derivedSyncDirective = extend({}, origAsyncDirective, { - templateUrl: null, transclude: null, replace: null, $$originalDirective: origAsyncDirective - }), - templateUrl = (isFunction(origAsyncDirective.templateUrl)) - ? origAsyncDirective.templateUrl($compileNode, tAttrs) - : origAsyncDirective.templateUrl; - - $compileNode.empty(); - - $http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}). - success(function(content) { - var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn; - - content = denormalizeTemplate(content); - - if (origAsyncDirective.replace) { - $template = directiveTemplateContents(content); - compileNode = $template[0]; - - if ($template.length != 1 || compileNode.nodeType !== 1) { - throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", - origAsyncDirective.name, templateUrl); - } - - tempTemplateAttrs = {$attr: {}}; - replaceWith($rootElement, $compileNode, compileNode); - var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs); - - if (isObject(origAsyncDirective.scope)) { - markDirectivesAsIsolate(templateDirectives); - } - directives = templateDirectives.concat(directives); - mergeTemplateAttributes(tAttrs, tempTemplateAttrs); - } else { - compileNode = beforeTemplateCompileNode; - $compileNode.html(content); - } - - directives.unshift(derivedSyncDirective); - - afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, - childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, - previousCompileContext); - forEach($rootElement, function(node, i) { - if (node == compileNode) { - $rootElement[i] = $compileNode[0]; - } - }); - afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn); - - - while(linkQueue.length) { - var scope = linkQueue.shift(), - beforeTemplateLinkNode = linkQueue.shift(), - linkRootElement = linkQueue.shift(), - boundTranscludeFn = linkQueue.shift(), - linkNode = $compileNode[0]; - - if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { - var oldClasses = beforeTemplateLinkNode.className; - - if (!(previousCompileContext.hasElementTranscludeDirective && - origAsyncDirective.replace)) { - // it was cloned therefore we have to clone as well. - linkNode = jqLiteClone(compileNode); - } - - replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); - - // Copy in CSS classes from original node - safeAddClass(jqLite(linkNode), oldClasses); - } - if (afterTemplateNodeLinkFn.transclude) { - childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude); - } else { - childBoundTranscludeFn = boundTranscludeFn; - } - afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, - childBoundTranscludeFn); - } - linkQueue = null; - }). - error(function(response, code, headers, config) { - throw $compileMinErr('tpload', 'Failed to load template: {0}', config.url); - }); - - return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { - if (linkQueue) { - linkQueue.push(scope); - linkQueue.push(node); - linkQueue.push(rootElement); - linkQueue.push(boundTranscludeFn); - } else { - afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, boundTranscludeFn); - } - }; - } - - - /** - * Sorting function for bound directives. - */ - function byPriority(a, b) { - var diff = b.priority - a.priority; - if (diff !== 0) return diff; - if (a.name !== b.name) return (a.name < b.name) ? -1 : 1; - return a.index - b.index; - } - - - function assertNoDuplicate(what, previousDirective, directive, element) { - if (previousDirective) { - throw $compileMinErr('multidir', 'Multiple directives [{0}, {1}] asking for {2} on: {3}', - previousDirective.name, directive.name, what, startingTag(element)); - } - } - - - function addTextInterpolateDirective(directives, text) { - var interpolateFn = $interpolate(text, true); - if (interpolateFn) { - directives.push({ - priority: 0, - compile: valueFn(function textInterpolateLinkFn(scope, node) { - var parent = node.parent(), - bindings = parent.data('$binding') || []; - bindings.push(interpolateFn); - safeAddClass(parent.data('$binding', bindings), 'ng-binding'); - scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { - node[0].nodeValue = value; - }); - }) - }); - } - } - - - function getTrustedContext(node, attrNormalizedName) { - if (attrNormalizedName == "srcdoc") { - return $sce.HTML; - } - var tag = nodeName_(node); - // maction[xlink:href] can source SVG. It's not limited to . - if (attrNormalizedName == "xlinkHref" || - (tag == "FORM" && attrNormalizedName == "action") || - (tag != "IMG" && (attrNormalizedName == "src" || - attrNormalizedName == "ngSrc"))) { - return $sce.RESOURCE_URL; - } - } - - - function addAttrInterpolateDirective(node, directives, value, name) { - var interpolateFn = $interpolate(value, true); - - // no interpolation found -> ignore - if (!interpolateFn) return; - - - if (name === "multiple" && nodeName_(node) === "SELECT") { - throw $compileMinErr("selmulti", - "Binding to the 'multiple' attribute is not supported. Element: {0}", - startingTag(node)); - } - - directives.push({ - priority: 100, - compile: function() { - return { - pre: function attrInterpolatePreLinkFn(scope, element, attr) { - var $$observers = (attr.$$observers || (attr.$$observers = {})); - - if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { - throw $compileMinErr('nodomevents', - "Interpolations for HTML DOM event attributes are disallowed. Please use the " + - "ng- versions (such as ng-click instead of onclick) instead."); - } - - // we need to interpolate again, in case the attribute value has been updated - // (e.g. by another directive's compile function) - interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name)); - - // if attribute was updated so that there is no interpolation going on we don't want to - // register any observers - if (!interpolateFn) return; - - // TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the - // actual attr value - attr[name] = interpolateFn(scope); - ($$observers[name] || ($$observers[name] = [])).$$inter = true; - (attr.$$observers && attr.$$observers[name].$$scope || scope). - $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { - //special case for class attribute addition + removal - //so that class changes can tap into the animation - //hooks provided by the $animate service. Be sure to - //skip animations when the first digest occurs (when - //both the new and the old values are the same) since - //the CSS classes are the non-interpolated values - if(name === 'class' && newValue != oldValue) { - attr.$updateClass(newValue, oldValue); - } else { - attr.$set(name, newValue); - } - }); - } - }; - } - }); - } - - - /** - * This is a special jqLite.replaceWith, which can replace items which - * have no parents, provided that the containing jqLite collection is provided. - * - * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes - * in the root of the tree. - * @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep - * the shell, but replace its DOM node reference. - * @param {Node} newNode The new DOM node. - */ - function replaceWith($rootElement, elementsToRemove, newNode) { - var firstElementToRemove = elementsToRemove[0], - removeCount = elementsToRemove.length, - parent = firstElementToRemove.parentNode, - i, ii; - - if ($rootElement) { - for(i = 0, ii = $rootElement.length; i < ii; i++) { - if ($rootElement[i] == firstElementToRemove) { - $rootElement[i++] = newNode; - for (var j = i, j2 = j + removeCount - 1, - jj = $rootElement.length; - j < jj; j++, j2++) { - if (j2 < jj) { - $rootElement[j] = $rootElement[j2]; - } else { - delete $rootElement[j]; - } - } - $rootElement.length -= removeCount - 1; - break; - } - } - } - - if (parent) { - parent.replaceChild(newNode, firstElementToRemove); - } - var fragment = document.createDocumentFragment(); - fragment.appendChild(firstElementToRemove); - newNode[jqLite.expando] = firstElementToRemove[jqLite.expando]; - for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { - var element = elementsToRemove[k]; - jqLite(element).remove(); // must do this way to clean up expando - fragment.appendChild(element); - delete elementsToRemove[k]; - } - - elementsToRemove[0] = newNode; - elementsToRemove.length = 1; - } - - - function cloneAndAnnotateFn(fn, annotation) { - return extend(function() { return fn.apply(null, arguments); }, fn, annotation); - } - }]; -} - -var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i; -/** - * Converts all accepted directives format into proper directive name. - * All of these will become 'myDirective': - * my:Directive - * my-directive - * x-my-directive - * data-my:directive - * - * Also there is special case for Moz prefix starting with upper case letter. - * @param name Name to normalize - */ -function directiveNormalize(name) { - return camelCase(name.replace(PREFIX_REGEXP, '')); -} - -/** - * @ngdoc object - * @name ng.$compile.directive.Attributes - * - * @description - * A shared object between directive compile / linking functions which contains normalized DOM - * element attributes. The values reflect current binding state `{{ }}`. The normalization is - * needed since all of these are treated as equivalent in Angular: - * - * - */ - -/** - * @ngdoc property - * @name ng.$compile.directive.Attributes#$attr - * @propertyOf ng.$compile.directive.Attributes - * @returns {object} A map of DOM element attribute names to the normalized name. This is - * needed to do reverse lookup from normalized name back to actual name. - */ - - -/** - * @ngdoc function - * @name ng.$compile.directive.Attributes#$set - * @methodOf ng.$compile.directive.Attributes - * @function - * - * @description - * Set DOM element attribute value. - * - * - * @param {string} name Normalized element attribute name of the property to modify. The name is - * reverse-translated using the {@link ng.$compile.directive.Attributes#$attr $attr} - * property to the original name. - * @param {string} value Value to set the attribute to. The value can be an interpolated string. - */ - - - -/** - * Closure compiler type information - */ - -function nodesetLinkingFn( - /* angular.Scope */ scope, - /* NodeList */ nodeList, - /* Element */ rootElement, - /* function(Function) */ boundTranscludeFn -){} - -function directiveLinkingFn( - /* nodesetLinkingFn */ nodesetLinkingFn, - /* angular.Scope */ scope, - /* Node */ node, - /* Element */ rootElement, - /* function(Function) */ boundTranscludeFn -){} - -function tokenDifference(str1, str2) { - var values = '', - tokens1 = str1.split(/\s+/), - tokens2 = str2.split(/\s+/); - - outer: - for(var i = 0; i < tokens1.length; i++) { - var token = tokens1[i]; - for(var j = 0; j < tokens2.length; j++) { - if(token == tokens2[j]) continue outer; - } - values += (values.length > 0 ? ' ' : '') + token; - } - return values; -} - -/** - * @ngdoc object - * @name ng.$controllerProvider - * @description - * The {@link ng.$controller $controller service} is used by Angular to create new - * controllers. - * - * This provider allows controller registration via the - * {@link ng.$controllerProvider#methods_register register} method. - */ -function $ControllerProvider() { - var controllers = {}, - CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; - - - /** - * @ngdoc function - * @name ng.$controllerProvider#register - * @methodOf ng.$controllerProvider - * @param {string|Object} name Controller name, or an object map of controllers where the keys are - * the names and the values are the constructors. - * @param {Function|Array} constructor Controller constructor fn (optionally decorated with DI - * annotations in the array notation). - */ - this.register = function(name, constructor) { - assertNotHasOwnProperty(name, 'controller'); - if (isObject(name)) { - extend(controllers, name); - } else { - controllers[name] = constructor; - } - }; - - - this.$get = ['$injector', '$window', function($injector, $window) { - - /** - * @ngdoc function - * @name ng.$controller - * @requires $injector - * - * @param {Function|string} constructor If called with a function then it's considered to be the - * controller constructor function. Otherwise it's considered to be a string which is used - * to retrieve the controller constructor using the following steps: - * - * * check if a controller with given name is registered via `$controllerProvider` - * * check if evaluating the string on the current scope returns a constructor - * * check `window[constructor]` on the global `window` object - * - * @param {Object} locals Injection locals for Controller. - * @return {Object} Instance of given controller. - * - * @description - * `$controller` service is responsible for instantiating controllers. - * - * It's just a simple call to {@link AUTO.$injector $injector}, but extracted into - * a service, so that one can override this service with {@link https://gist.github.com/1649788 - * BC version}. - */ - return function(expression, locals) { - var instance, match, constructor, identifier; - - if(isString(expression)) { - match = expression.match(CNTRL_REG), - constructor = match[1], - identifier = match[3]; - expression = controllers.hasOwnProperty(constructor) - ? controllers[constructor] - : getter(locals.$scope, constructor, true) || getter($window, constructor, true); - - assertArgFn(expression, constructor, true); - } - - instance = $injector.instantiate(expression, locals); - - if (identifier) { - if (!(locals && typeof locals.$scope == 'object')) { - throw minErr('$controller')('noscp', - "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", - constructor || expression.name, identifier); - } - - locals.$scope[identifier] = instance; - } - - return instance; - }; - }]; -} - -/** - * @ngdoc object - * @name ng.$document - * @requires $window - * - * @description - * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. - */ -function $DocumentProvider(){ - this.$get = ['$window', function(window){ - return jqLite(window.document); - }]; -} - -/** - * @ngdoc function - * @name ng.$exceptionHandler - * @requires $log - * - * @description - * Any uncaught exception in angular expressions is delegated to this service. - * The default implementation simply delegates to `$log.error` which logs it into - * the browser console. - * - * In unit tests, if `angular-mocks.js` is loaded, this service is overridden by - * {@link ngMock.$exceptionHandler mock $exceptionHandler} which aids in testing. - * - * ## Example: - * - *
    - *   angular.module('exceptionOverride', []).factory('$exceptionHandler', function () {
    - *     return function (exception, cause) {
    - *       exception.message += ' (caused by "' + cause + '")';
    - *       throw exception;
    - *     };
    - *   });
    - * 
    - * - * This example will override the normal action of `$exceptionHandler`, to make angular - * exceptions fail hard when they happen, instead of just logging to the console. - * - * @param {Error} exception Exception associated with the error. - * @param {string=} cause optional information about the context in which - * the error was thrown. - * - */ -function $ExceptionHandlerProvider() { - this.$get = ['$log', function($log) { - return function(exception, cause) { - $log.error.apply($log, arguments); - }; - }]; -} - -/** - * Parse headers into key value object - * - * @param {string} headers Raw headers as a string - * @returns {Object} Parsed headers as key value object - */ -function parseHeaders(headers) { - var parsed = {}, key, val, i; - - if (!headers) return parsed; - - forEach(headers.split('\n'), function(line) { - i = line.indexOf(':'); - key = lowercase(trim(line.substr(0, i))); - val = trim(line.substr(i + 1)); - - if (key) { - if (parsed[key]) { - parsed[key] += ', ' + val; - } else { - parsed[key] = val; - } - } - }); - - return parsed; -} - - -/** - * Returns a function that provides access to parsed headers. - * - * Headers are lazy parsed when first requested. - * @see parseHeaders - * - * @param {(string|Object)} headers Headers to provide access to. - * @returns {function(string=)} Returns a getter function which if called with: - * - * - if called with single an argument returns a single header value or null - * - if called with no arguments returns an object containing all headers. - */ -function headersGetter(headers) { - var headersObj = isObject(headers) ? headers : undefined; - - return function(name) { - if (!headersObj) headersObj = parseHeaders(headers); - - if (name) { - return headersObj[lowercase(name)] || null; - } - - return headersObj; - }; -} - - -/** - * Chain all given functions - * - * This function is used for both request and response transforming - * - * @param {*} data Data to transform. - * @param {function(string=)} headers Http headers getter fn. - * @param {(function|Array.)} fns Function or an array of functions. - * @returns {*} Transformed data. - */ -function transformData(data, headers, fns) { - if (isFunction(fns)) - return fns(data, headers); - - forEach(fns, function(fn) { - data = fn(data, headers); - }); - - return data; -} - - -function isSuccess(status) { - return 200 <= status && status < 300; -} - - -function $HttpProvider() { - var JSON_START = /^\s*(\[|\{[^\{])/, - JSON_END = /[\}\]]\s*$/, - PROTECTION_PREFIX = /^\)\]\}',?\n/, - CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': 'application/json;charset=utf-8'}; - - var defaults = this.defaults = { - // transform incoming response data - transformResponse: [function(data) { - if (isString(data)) { - // strip json vulnerability protection prefix - data = data.replace(PROTECTION_PREFIX, ''); - if (JSON_START.test(data) && JSON_END.test(data)) - data = fromJson(data); - } - return data; - }], - - // transform outgoing request data - transformRequest: [function(d) { - return isObject(d) && !isFile(d) ? toJson(d) : d; - }], - - // default headers - headers: { - common: { - 'Accept': 'application/json, text/plain, */*' - }, - post: copy(CONTENT_TYPE_APPLICATION_JSON), - put: copy(CONTENT_TYPE_APPLICATION_JSON), - patch: copy(CONTENT_TYPE_APPLICATION_JSON) - }, - - xsrfCookieName: 'XSRF-TOKEN', - xsrfHeaderName: 'X-XSRF-TOKEN' - }; - - /** - * Are ordered by request, i.e. they are applied in the same order as the - * array, on request, but reverse order, on response. - */ - var interceptorFactories = this.interceptors = []; - - /** - * For historical reasons, response interceptors are ordered by the order in which - * they are applied to the response. (This is the opposite of interceptorFactories) - */ - var responseInterceptorFactories = this.responseInterceptors = []; - - this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { - - var defaultCache = $cacheFactory('$http'); - - /** - * Interceptors stored in reverse order. Inner interceptors before outer interceptors. - * The reversal is needed so that we can build up the interception chain around the - * server request. - */ - var reversedInterceptors = []; - - forEach(interceptorFactories, function(interceptorFactory) { - reversedInterceptors.unshift(isString(interceptorFactory) - ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); - }); - - forEach(responseInterceptorFactories, function(interceptorFactory, index) { - var responseFn = isString(interceptorFactory) - ? $injector.get(interceptorFactory) - : $injector.invoke(interceptorFactory); - - /** - * Response interceptors go before "around" interceptors (no real reason, just - * had to pick one.) But they are already reversed, so we can't use unshift, hence - * the splice. - */ - reversedInterceptors.splice(index, 0, { - response: function(response) { - return responseFn($q.when(response)); - }, - responseError: function(response) { - return responseFn($q.reject(response)); - } - }); - }); - - - /** - * @ngdoc function - * @name ng.$http - * @requires $httpBackend - * @requires $browser - * @requires $cacheFactory - * @requires $rootScope - * @requires $q - * @requires $injector - * - * @description - * The `$http` service is a core Angular service that facilitates communication with the remote - * HTTP servers via the browser's {@link https://developer.mozilla.org/en/xmlhttprequest - * XMLHttpRequest} object or via {@link http://en.wikipedia.org/wiki/JSONP JSONP}. - * - * For unit testing applications that use `$http` service, see - * {@link ngMock.$httpBackend $httpBackend mock}. - * - * For a higher level of abstraction, please check out the {@link ngResource.$resource - * $resource} service. - * - * The $http API is based on the {@link ng.$q deferred/promise APIs} exposed by - * the $q service. While for simple usage patterns this doesn't matter much, for advanced usage - * it is important to familiarize yourself with these APIs and the guarantees they provide. - * - * - * # General usage - * The `$http` service is a function which takes a single argument — a configuration object — - * that is used to generate an HTTP request and returns a {@link ng.$q promise} - * with two $http specific methods: `success` and `error`. - * - *
    -     *   $http({method: 'GET', url: '/someUrl'}).
    -     *     success(function(data, status, headers, config) {
    -     *       // this callback will be called asynchronously
    -     *       // when the response is available
    -     *     }).
    -     *     error(function(data, status, headers, config) {
    -     *       // called asynchronously if an error occurs
    -     *       // or server returns response with an error status.
    -     *     });
    -     * 
    - * - * Since the returned value of calling the $http function is a `promise`, you can also use - * the `then` method to register callbacks, and these callbacks will receive a single argument – - * an object representing the response. See the API signature and type info below for more - * details. - * - * A response status code between 200 and 299 is considered a success status and - * will result in the success callback being called. Note that if the response is a redirect, - * XMLHttpRequest will transparently follow it, meaning that the error callback will not be - * called for such responses. - * - * # Writing Unit Tests that use $http - * When unit testing (using {@link api/ngMock ngMock}), it is necessary to call - * {@link api/ngMock.$httpBackend#methods_flush $httpBackend.flush()} to flush each pending - * request using trained responses. - * - * ``` - * $httpBackend.expectGET(...); - * $http.get(...); - * $httpBackend.flush(); - * ``` - * - * # Shortcut methods - * - * Since all invocations of the $http service require passing in an HTTP method and URL, and - * POST/PUT requests require request data to be provided as well, shortcut methods - * were created: - * - *
    -     *   $http.get('/someUrl').success(successCallback);
    -     *   $http.post('/someUrl', data).success(successCallback);
    -     * 
    - * - * Complete list of shortcut methods: - * - * - {@link ng.$http#methods_get $http.get} - * - {@link ng.$http#methods_head $http.head} - * - {@link ng.$http#methods_post $http.post} - * - {@link ng.$http#methods_put $http.put} - * - {@link ng.$http#methods_delete $http.delete} - * - {@link ng.$http#methods_jsonp $http.jsonp} - * - * - * # Setting HTTP Headers - * - * The $http service will automatically add certain HTTP headers to all requests. These defaults - * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration - * object, which currently contains this default configuration: - * - * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): - * - `Accept: application/json, text/plain, * / *` - * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) - * - `Content-Type: application/json` - * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) - * - `Content-Type: application/json` - * - * To add or overwrite these defaults, simply add or remove a property from these configuration - * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object - * with the lowercased HTTP method name as the key, e.g. - * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }. - * - * The defaults can also be set at runtime via the `$http.defaults` object in the same - * fashion. For example: - * - * ``` - * module.run(function($http) { - * $http.defaults.headers.common.Authentication = 'Basic YmVlcDpib29w' - * }); - * ``` - * - * In addition, you can supply a `headers` property in the config object passed when - * calling `$http(config)`, which overrides the defaults without changing them globally. - * - * - * # Transforming Requests and Responses - * - * Both requests and responses can be transformed using transform functions. By default, Angular - * applies these transformations: - * - * Request transformations: - * - * - If the `data` property of the request configuration object contains an object, serialize it - * into JSON format. - * - * Response transformations: - * - * - If XSRF prefix is detected, strip it (see Security Considerations section below). - * - If JSON response is detected, deserialize it using a JSON parser. - * - * To globally augment or override the default transforms, modify the - * `$httpProvider.defaults.transformRequest` and `$httpProvider.defaults.transformResponse` - * properties. These properties are by default an array of transform functions, which allows you - * to `push` or `unshift` a new transformation function into the transformation chain. You can - * also decide to completely override any default transformations by assigning your - * transformation functions to these properties directly without the array wrapper. These defaults - * are again available on the $http factory at run-time, which may be useful if you have run-time - * services you wish to be involved in your transformations. - * - * Similarly, to locally override the request/response transforms, augment the - * `transformRequest` and/or `transformResponse` properties of the configuration object passed - * into `$http`. - * - * - * # Caching - * - * To enable caching, set the request configuration `cache` property to `true` (to use default - * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}). - * When the cache is enabled, `$http` stores the response from the server in the specified - * cache. The next time the same request is made, the response is served from the cache without - * sending a request to the server. - * - * Note that even if the response is served from cache, delivery of the data is asynchronous in - * the same way that real requests are. - * - * If there are multiple GET requests for the same URL that should be cached using the same - * cache, but the cache is not populated yet, only one request to the server will be made and - * the remaining requests will be fulfilled using the response from the first request. - * - * You can change the default cache to a new object (built with - * {@link ng.$cacheFactory `$cacheFactory`}) by updating the - * {@link ng.$http#properties_defaults `$http.defaults.cache`} property. All requests who set - * their `cache` property to `true` will now use this cache object. - * - * If you set the default cache to `false` then only requests that specify their own custom - * cache object will be cached. - * - * # Interceptors - * - * Before you start creating interceptors, be sure to understand the - * {@link ng.$q $q and deferred/promise APIs}. - * - * For purposes of global error handling, authentication, or any kind of synchronous or - * asynchronous pre-processing of request or postprocessing of responses, it is desirable to be - * able to intercept requests before they are handed to the server and - * responses before they are handed over to the application code that - * initiated these requests. The interceptors leverage the {@link ng.$q - * promise APIs} to fulfill this need for both synchronous and asynchronous pre-processing. - * - * The interceptors are service factories that are registered with the `$httpProvider` by - * adding them to the `$httpProvider.interceptors` array. The factory is called and - * injected with dependencies (if specified) and returns the interceptor. - * - * There are two kinds of interceptors (and two kinds of rejection interceptors): - * - * * `request`: interceptors get called with http `config` object. The function is free to - * modify the `config` or create a new one. The function needs to return the `config` - * directly or as a promise. - * * `requestError`: interceptor gets called when a previous interceptor threw an error or - * resolved with a rejection. - * * `response`: interceptors get called with http `response` object. The function is free to - * modify the `response` or create a new one. The function needs to return the `response` - * directly or as a promise. - * * `responseError`: interceptor gets called when a previous interceptor threw an error or - * resolved with a rejection. - * - * - *
    -     *   // register the interceptor as a service
    -     *   $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
    -     *     return {
    -     *       // optional method
    -     *       'request': function(config) {
    -     *         // do something on success
    -     *         return config || $q.when(config);
    -     *       },
    -     *
    -     *       // optional method
    -     *      'requestError': function(rejection) {
    -     *         // do something on error
    -     *         if (canRecover(rejection)) {
    -     *           return responseOrNewPromise
    -     *         }
    -     *         return $q.reject(rejection);
    -     *       },
    -     *
    -     *
    -     *
    -     *       // optional method
    -     *       'response': function(response) {
    -     *         // do something on success
    -     *         return response || $q.when(response);
    -     *       },
    -     *
    -     *       // optional method
    -     *      'responseError': function(rejection) {
    -     *         // do something on error
    -     *         if (canRecover(rejection)) {
    -     *           return responseOrNewPromise
    -     *         }
    -     *         return $q.reject(rejection);
    -     *       }
    -     *     };
    -     *   });
    -     *
    -     *   $httpProvider.interceptors.push('myHttpInterceptor');
    -     *
    -     *
    -     *   // alternatively, register the interceptor via an anonymous factory
    -     *   $httpProvider.interceptors.push(function($q, dependency1, dependency2) {
    -     *     return {
    -     *      'request': function(config) {
    -     *          // same as above
    -     *       },
    -     *
    -     *       'response': function(response) {
    -     *          // same as above
    -     *       }
    -     *     };
    -     *   });
    -     * 
    - * - * # Response interceptors (DEPRECATED) - * - * Before you start creating interceptors, be sure to understand the - * {@link ng.$q $q and deferred/promise APIs}. - * - * For purposes of global error handling, authentication or any kind of synchronous or - * asynchronous preprocessing of received responses, it is desirable to be able to intercept - * responses for http requests before they are handed over to the application code that - * initiated these requests. The response interceptors leverage the {@link ng.$q - * promise apis} to fulfil this need for both synchronous and asynchronous preprocessing. - * - * The interceptors are service factories that are registered with the $httpProvider by - * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and - * injected with dependencies (if specified) and returns the interceptor — a function that - * takes a {@link ng.$q promise} and returns the original or a new promise. - * - *
    -     *   // register the interceptor as a service
    -     *   $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
    -     *     return function(promise) {
    -     *       return promise.then(function(response) {
    -     *         // do something on success
    -     *         return response;
    -     *       }, function(response) {
    -     *         // do something on error
    -     *         if (canRecover(response)) {
    -     *           return responseOrNewPromise
    -     *         }
    -     *         return $q.reject(response);
    -     *       });
    -     *     }
    -     *   });
    -     *
    -     *   $httpProvider.responseInterceptors.push('myHttpInterceptor');
    -     *
    -     *
    -     *   // register the interceptor via an anonymous factory
    -     *   $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) {
    -     *     return function(promise) {
    -     *       // same as above
    -     *     }
    -     *   });
    -     * 
    - * - * - * # Security Considerations - * - * When designing web applications, consider security threats from: - * - * - {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx - * JSON vulnerability} - * - {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} - * - * Both server and the client must cooperate in order to eliminate these threats. Angular comes - * pre-configured with strategies that address these issues, but for this to work backend server - * cooperation is required. - * - * ## JSON Vulnerability Protection - * - * A {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx - * JSON vulnerability} allows third party website to turn your JSON resource URL into - * {@link http://en.wikipedia.org/wiki/JSONP JSONP} request under some conditions. To - * counter this your server can prefix all JSON requests with following string `")]}',\n"`. - * Angular will automatically strip the prefix before processing it as JSON. - * - * For example if your server needs to return: - *
    -     * ['one','two']
    -     * 
    - * - * which is vulnerable to attack, your server can return: - *
    -     * )]}',
    -     * ['one','two']
    -     * 
    - * - * Angular will strip the prefix, before processing the JSON. - * - * - * ## Cross Site Request Forgery (XSRF) Protection - * - * {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} is a technique by which - * an unauthorized site can gain your user's private data. Angular provides a mechanism - * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie - * (by default, `XSRF-TOKEN`) and sets it as an HTTP header (`X-XSRF-TOKEN`). Since only - * JavaScript that runs on your domain could read the cookie, your server can be assured that - * the XHR came from JavaScript running on your domain. The header will not be set for - * cross-domain requests. - * - * To take advantage of this, your server needs to set a token in a JavaScript readable session - * cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the - * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure - * that only JavaScript running on your domain could have sent the request. The token must be - * unique for each user and must be verifiable by the server (to prevent the JavaScript from - * making up its own tokens). We recommend that the token is a digest of your site's - * authentication cookie with a {@link https://en.wikipedia.org/wiki/Salt_(cryptography) salt} - * for added security. - * - * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName - * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time, - * or the per-request config object. - * - * - * @param {object} config Object describing the request to be made and how it should be - * processed. The object has following properties: - * - * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) - * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. - * - **params** – `{Object.}` – Map of strings or objects which will be turned - * to `?key1=value1&key2=value2` after the url. If the value is not a string, it will be - * JSONified. - * - **data** – `{string|Object}` – Data to be sent as the request message data. - * - **headers** – `{Object}` – Map of strings or functions which return strings representing - * HTTP headers to send to the server. If the return value of a function is null, the - * header will not be sent. - * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token. - * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token. - * - **transformRequest** – - * `{function(data, headersGetter)|Array.}` – - * transform function or an array of such functions. The transform function takes the http - * request body and headers and returns its transformed (typically serialized) version. - * - **transformResponse** – - * `{function(data, headersGetter)|Array.}` – - * transform function or an array of such functions. The transform function takes the http - * response body and headers and returns its transformed (typically deserialized) version. - * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the - * GET request, otherwise if a cache instance built with - * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for - * caching. - * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} - * that should abort the request when resolved. - * - **withCredentials** - `{boolean}` - whether to to set the `withCredentials` flag on the - * XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5 - * requests with credentials} for more information. - * - **responseType** - `{string}` - see {@link - * https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType requestType}. - * - * @returns {HttpPromise} Returns a {@link ng.$q promise} object with the - * standard `then` method and two http specific methods: `success` and `error`. The `then` - * method takes two arguments a success and an error callback which will be called with a - * response object. The `success` and `error` methods take a single argument - a function that - * will be called when the request succeeds or fails respectively. The arguments passed into - * these functions are destructured representation of the response object passed into the - * `then` method. The response object has these properties: - * - * - **data** – `{string|Object}` – The response body transformed with the transform - * functions. - * - **status** – `{number}` – HTTP status code of the response. - * - **headers** – `{function([headerName])}` – Header getter function. - * - **config** – `{Object}` – The configuration object that was used to generate the request. - * - * @property {Array.} pendingRequests Array of config objects for currently pending - * requests. This is primarily meant to be used for debugging purposes. - * - * - * @example - - -
    - - -
    - - - -
    http status code: {{status}}
    -
    http response data: {{data}}
    -
    -
    - - function FetchCtrl($scope, $http, $templateCache) { - $scope.method = 'GET'; - $scope.url = 'http-hello.html'; - - $scope.fetch = function() { - $scope.code = null; - $scope.response = null; - - $http({method: $scope.method, url: $scope.url, cache: $templateCache}). - success(function(data, status) { - $scope.status = status; - $scope.data = data; - }). - error(function(data, status) { - $scope.data = data || "Request failed"; - $scope.status = status; - }); - }; - - $scope.updateModel = function(method, url) { - $scope.method = method; - $scope.url = url; - }; - } - - - Hello, $http! - - - var status = element(by.binding('status')); - var data = element(by.binding('data')); - var fetchBtn = element(by.id('fetchbtn')); - var sampleGetBtn = element(by.id('samplegetbtn')); - var sampleJsonpBtn = element(by.id('samplejsonpbtn')); - var invalidJsonpBtn = element(by.id('invalidjsonpbtn')); - - it('should make an xhr GET request', function() { - sampleGetBtn.click(); - fetchBtn.click(); - expect(status.getText()).toMatch('200'); - expect(data.getText()).toMatch(/Hello, \$http!/) - }); - - it('should make a JSONP request to angularjs.org', function() { - sampleJsonpBtn.click(); - fetchBtn.click(); - expect(status.getText()).toMatch('200'); - expect(data.getText()).toMatch(/Super Hero!/); - }); - - it('should make JSONP request to invalid URL and invoke the error handler', - function() { - invalidJsonpBtn.click(); - fetchBtn.click(); - expect(status.getText()).toMatch('0'); - expect(data.getText()).toMatch('Request failed'); - }); - -
    - */ - function $http(requestConfig) { - var config = { - transformRequest: defaults.transformRequest, - transformResponse: defaults.transformResponse - }; - var headers = mergeHeaders(requestConfig); - - extend(config, requestConfig); - config.headers = headers; - config.method = uppercase(config.method); - - var xsrfValue = urlIsSameOrigin(config.url) - ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] - : undefined; - if (xsrfValue) { - headers[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; - } - - - var serverRequest = function(config) { - headers = config.headers; - var reqData = transformData(config.data, headersGetter(headers), config.transformRequest); - - // strip content-type if data is undefined - if (isUndefined(config.data)) { - forEach(headers, function(value, header) { - if (lowercase(header) === 'content-type') { - delete headers[header]; - } - }); - } - - if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) { - config.withCredentials = defaults.withCredentials; - } - - // send request - return sendReq(config, reqData, headers).then(transformResponse, transformResponse); - }; - - var chain = [serverRequest, undefined]; - var promise = $q.when(config); - - // apply interceptors - forEach(reversedInterceptors, function(interceptor) { - if (interceptor.request || interceptor.requestError) { - chain.unshift(interceptor.request, interceptor.requestError); - } - if (interceptor.response || interceptor.responseError) { - chain.push(interceptor.response, interceptor.responseError); - } - }); - - while(chain.length) { - var thenFn = chain.shift(); - var rejectFn = chain.shift(); - - promise = promise.then(thenFn, rejectFn); - } - - promise.success = function(fn) { - promise.then(function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; - - promise.error = function(fn) { - promise.then(null, function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; - - return promise; - - function transformResponse(response) { - // make a copy since the response must be cacheable - var resp = extend({}, response, { - data: transformData(response.data, response.headers, config.transformResponse) - }); - return (isSuccess(response.status)) - ? resp - : $q.reject(resp); - } - - function mergeHeaders(config) { - var defHeaders = defaults.headers, - reqHeaders = extend({}, config.headers), - defHeaderName, lowercaseDefHeaderName, reqHeaderName; - - defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); - - // execute if header value is function - execHeaders(defHeaders); - execHeaders(reqHeaders); - - // using for-in instead of forEach to avoid unecessary iteration after header has been found - defaultHeadersIteration: - for (defHeaderName in defHeaders) { - lowercaseDefHeaderName = lowercase(defHeaderName); - - for (reqHeaderName in reqHeaders) { - if (lowercase(reqHeaderName) === lowercaseDefHeaderName) { - continue defaultHeadersIteration; - } - } - - reqHeaders[defHeaderName] = defHeaders[defHeaderName]; - } - - return reqHeaders; - - function execHeaders(headers) { - var headerContent; - - forEach(headers, function(headerFn, header) { - if (isFunction(headerFn)) { - headerContent = headerFn(); - if (headerContent != null) { - headers[header] = headerContent; - } else { - delete headers[header]; - } - } - }); - } - } - } - - $http.pendingRequests = []; - - /** - * @ngdoc method - * @name ng.$http#get - * @methodOf ng.$http - * - * @description - * Shortcut method to perform `GET` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - - /** - * @ngdoc method - * @name ng.$http#delete - * @methodOf ng.$http - * - * @description - * Shortcut method to perform `DELETE` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - - /** - * @ngdoc method - * @name ng.$http#head - * @methodOf ng.$http - * - * @description - * Shortcut method to perform `HEAD` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - - /** - * @ngdoc method - * @name ng.$http#jsonp - * @methodOf ng.$http - * - * @description - * Shortcut method to perform `JSONP` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request. - * Should contain `JSON_CALLBACK` string. - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - createShortMethods('get', 'delete', 'head', 'jsonp'); - - /** - * @ngdoc method - * @name ng.$http#post - * @methodOf ng.$http - * - * @description - * Shortcut method to perform `POST` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {*} data Request content - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - - /** - * @ngdoc method - * @name ng.$http#put - * @methodOf ng.$http - * - * @description - * Shortcut method to perform `PUT` request. - * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {*} data Request content - * @param {Object=} config Optional configuration object - * @returns {HttpPromise} Future object - */ - createShortMethodsWithData('post', 'put'); - - /** - * @ngdoc property - * @name ng.$http#defaults - * @propertyOf ng.$http - * - * @description - * Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of - * default headers, withCredentials as well as request and response transformations. - * - * See "Setting HTTP Headers" and "Transforming Requests and Responses" sections above. - */ - $http.defaults = defaults; - - - return $http; - - - function createShortMethods(names) { - forEach(arguments, function(name) { - $http[name] = function(url, config) { - return $http(extend(config || {}, { - method: name, - url: url - })); - }; - }); - } - - - function createShortMethodsWithData(name) { - forEach(arguments, function(name) { - $http[name] = function(url, data, config) { - return $http(extend(config || {}, { - method: name, - url: url, - data: data - })); - }; - }); - } - - - /** - * Makes the request. - * - * !!! ACCESSES CLOSURE VARS: - * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests - */ - function sendReq(config, reqData, reqHeaders) { - var deferred = $q.defer(), - promise = deferred.promise, - cache, - cachedResp, - url = buildUrl(config.url, config.params); - - $http.pendingRequests.push(config); - promise.then(removePendingReq, removePendingReq); - - - if ((config.cache || defaults.cache) && config.cache !== false && config.method == 'GET') { - cache = isObject(config.cache) ? config.cache - : isObject(defaults.cache) ? defaults.cache - : defaultCache; - } - - if (cache) { - cachedResp = cache.get(url); - if (isDefined(cachedResp)) { - if (cachedResp.then) { - // cached request has already been sent, but there is no response yet - cachedResp.then(removePendingReq, removePendingReq); - return cachedResp; - } else { - // serving from cache - if (isArray(cachedResp)) { - resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2])); - } else { - resolvePromise(cachedResp, 200, {}); - } - } - } else { - // put the promise for the non-transformed response into cache as a placeholder - cache.put(url, promise); - } - } - - // if we won't have the response in cache, send the request to the backend - if (isUndefined(cachedResp)) { - $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, - config.withCredentials, config.responseType); - } - - return promise; - - - /** - * Callback registered to $httpBackend(): - * - caches the response if desired - * - resolves the raw $http promise - * - calls $apply - */ - function done(status, response, headersString) { - if (cache) { - if (isSuccess(status)) { - cache.put(url, [status, response, parseHeaders(headersString)]); - } else { - // remove promise from the cache - cache.remove(url); - } - } - - resolvePromise(response, status, headersString); - if (!$rootScope.$$phase) $rootScope.$apply(); - } - - - /** - * Resolves the raw $http promise. - */ - function resolvePromise(response, status, headers) { - // normalize internal statuses to 0 - status = Math.max(status, 0); - - (isSuccess(status) ? deferred.resolve : deferred.reject)({ - data: response, - status: status, - headers: headersGetter(headers), - config: config - }); - } - - - function removePendingReq() { - var idx = indexOf($http.pendingRequests, config); - if (idx !== -1) $http.pendingRequests.splice(idx, 1); - } - } - - - function buildUrl(url, params) { - if (!params) return url; - var parts = []; - forEachSorted(params, function(value, key) { - if (value === null || isUndefined(value)) return; - if (!isArray(value)) value = [value]; - - forEach(value, function(v) { - if (isObject(v)) { - v = toJson(v); - } - parts.push(encodeUriQuery(key) + '=' + - encodeUriQuery(v)); - }); - }); - return url + ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); - } - - - }]; -} - -function createXhr(method) { - //if IE and the method is not RFC2616 compliant, or if XMLHttpRequest - //is not available, try getting an ActiveXObject. Otherwise, use XMLHttpRequest - //if it is available - if (msie <= 8 && (!method.match(/^(get|post|head|put|delete|options)$/i) || - !window.XMLHttpRequest)) { - return new window.ActiveXObject("Microsoft.XMLHTTP"); - } else if (window.XMLHttpRequest) { - return new window.XMLHttpRequest(); - } - - throw minErr('$httpBackend')('noxhr', "This browser does not support XMLHttpRequest."); -} - -/** - * @ngdoc object - * @name ng.$httpBackend - * @requires $browser - * @requires $window - * @requires $document - * - * @description - * HTTP backend used by the {@link ng.$http service} that delegates to - * XMLHttpRequest object or JSONP and deals with browser incompatibilities. - * - * You should never need to use this service directly, instead use the higher-level abstractions: - * {@link ng.$http $http} or {@link ngResource.$resource $resource}. - * - * During testing this implementation is swapped with {@link ngMock.$httpBackend mock - * $httpBackend} which can be trained with responses. - */ -function $HttpBackendProvider() { - this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { - return createHttpBackend($browser, createXhr, $browser.defer, $window.angular.callbacks, $document[0]); - }]; -} - -function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { - var ABORTED = -1; - - // TODO(vojta): fix the signature - return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { - var status; - $browser.$$incOutstandingRequestCount(); - url = url || $browser.url(); - - if (lowercase(method) == 'jsonp') { - var callbackId = '_' + (callbacks.counter++).toString(36); - callbacks[callbackId] = function(data) { - callbacks[callbackId].data = data; - }; - - var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), - function() { - if (callbacks[callbackId].data) { - completeRequest(callback, 200, callbacks[callbackId].data); - } else { - completeRequest(callback, status || -2); - } - callbacks[callbackId] = angular.noop; - }); - } else { - - var xhr = createXhr(method); - - xhr.open(method, url, true); - forEach(headers, function(value, key) { - if (isDefined(value)) { - xhr.setRequestHeader(key, value); - } - }); - - // In IE6 and 7, this might be called synchronously when xhr.send below is called and the - // response is in the cache. the promise api will ensure that to the app code the api is - // always async - xhr.onreadystatechange = function() { - // onreadystatechange might get called multiple times with readyState === 4 on mobile webkit caused by - // xhrs that are resolved while the app is in the background (see #5426). - // since calling completeRequest sets the `xhr` variable to null, we just check if it's not null before - // continuing - // - // we can't set xhr.onreadystatechange to undefined or delete it because that breaks IE8 (method=PATCH) and - // Safari respectively. - if (xhr && xhr.readyState == 4) { - var responseHeaders = null, - response = null; - - if(status !== ABORTED) { - responseHeaders = xhr.getAllResponseHeaders(); - - // responseText is the old-school way of retrieving response (supported by IE8 & 9) - // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) - response = ('response' in xhr) ? xhr.response : xhr.responseText; - } - - completeRequest(callback, - status || xhr.status, - response, - responseHeaders); - } - }; - - if (withCredentials) { - xhr.withCredentials = true; - } - - if (responseType) { - try { - xhr.responseType = responseType; - } catch (e) { - // WebKit added support for the json responseType value on 09/03/2013 - // https://bugs.webkit.org/show_bug.cgi?id=73648. Versions of Safari prior to 7 are - // known to throw when setting the value "json" as the response type. Other older - // browsers implementing the responseType - // - // The json response type can be ignored if not supported, because JSON payloads are - // parsed on the client-side regardless. - if (responseType !== 'json') { - throw e; - } - } - } - - xhr.send(post || null); - } - - if (timeout > 0) { - var timeoutId = $browserDefer(timeoutRequest, timeout); - } else if (timeout && timeout.then) { - timeout.then(timeoutRequest); - } - - - function timeoutRequest() { - status = ABORTED; - jsonpDone && jsonpDone(); - xhr && xhr.abort(); - } - - function completeRequest(callback, status, response, headersString) { - // cancel timeout and subsequent timeout promise resolution - timeoutId && $browserDefer.cancel(timeoutId); - jsonpDone = xhr = null; - - // fix status code when it is 0 (0 status is undocumented). - // Occurs when accessing file resources. - // On Android 4.1 stock browser it occurs while retrieving files from application cache. - status = (status === 0) ? (response ? 200 : 404) : status; - - // normalize IE bug (http://bugs.jquery.com/ticket/1450) - status = status == 1223 ? 204 : status; - - callback(status, response, headersString); - $browser.$$completeOutstandingRequest(noop); - } - }; - - function jsonpReq(url, done) { - // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: - // - fetches local scripts via XHR and evals them - // - adds and immediately removes script elements from the document - var script = rawDocument.createElement('script'), - doneWrapper = function() { - script.onreadystatechange = script.onload = script.onerror = null; - rawDocument.body.removeChild(script); - if (done) done(); - }; - - script.type = 'text/javascript'; - script.src = url; - - if (msie && msie <= 8) { - script.onreadystatechange = function() { - if (/loaded|complete/.test(script.readyState)) { - doneWrapper(); - } - }; - } else { - script.onload = script.onerror = function() { - doneWrapper(); - }; - } - - rawDocument.body.appendChild(script); - return doneWrapper; - } -} - -var $interpolateMinErr = minErr('$interpolate'); - -/** - * @ngdoc object - * @name ng.$interpolateProvider - * @function - * - * @description - * - * Used for configuring the interpolation markup. Defaults to `{{` and `}}`. - * - * @example - - - -
    - //demo.label// -
    -
    - - it('should interpolate binding with custom symbols', function() { - expect(element(by.binding('demo.label')).getText()).toBe('This binding is brought you by // interpolation symbols.'); - }); - -
    - */ -function $InterpolateProvider() { - var startSymbol = '{{'; - var endSymbol = '}}'; - - /** - * @ngdoc method - * @name ng.$interpolateProvider#startSymbol - * @methodOf ng.$interpolateProvider - * @description - * Symbol to denote start of expression in the interpolated string. Defaults to `{{`. - * - * @param {string=} value new value to set the starting symbol to. - * @returns {string|self} Returns the symbol when used as getter and self if used as setter. - */ - this.startSymbol = function(value){ - if (value) { - startSymbol = value; - return this; - } else { - return startSymbol; - } - }; - - /** - * @ngdoc method - * @name ng.$interpolateProvider#endSymbol - * @methodOf ng.$interpolateProvider - * @description - * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. - * - * @param {string=} value new value to set the ending symbol to. - * @returns {string|self} Returns the symbol when used as getter and self if used as setter. - */ - this.endSymbol = function(value){ - if (value) { - endSymbol = value; - return this; - } else { - return endSymbol; - } - }; - - - this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { - var startSymbolLength = startSymbol.length, - endSymbolLength = endSymbol.length; - - /** - * @ngdoc function - * @name ng.$interpolate - * @function - * - * @requires $parse - * @requires $sce - * - * @description - * - * Compiles a string with markup into an interpolation function. This service is used by the - * HTML {@link ng.$compile $compile} service for data binding. See - * {@link ng.$interpolateProvider $interpolateProvider} for configuring the - * interpolation markup. - * - * -
    -         var $interpolate = ...; // injected
    -         var exp = $interpolate('Hello {{name | uppercase}}!');
    -         expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!');
    -       
    - * - * - * @param {string} text The text with markup to interpolate. - * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have - * embedded expression in order to return an interpolation function. Strings with no - * embedded expression will return null for the interpolation function. - * @param {string=} trustedContext when provided, the returned function passes the interpolated - * result through {@link ng.$sce#methods_getTrusted $sce.getTrusted(interpolatedResult, - * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that - * provides Strict Contextual Escaping for details. - * @returns {function(context)} an interpolation function which is used to compute the - * interpolated string. The function has these parameters: - * - * * `context`: an object against which any expressions embedded in the strings are evaluated - * against. - * - */ - function $interpolate(text, mustHaveExpression, trustedContext) { - var startIndex, - endIndex, - index = 0, - parts = [], - length = text.length, - hasInterpolation = false, - fn, - exp, - concat = []; - - while(index < length) { - if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && - ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { - (index != startIndex) && parts.push(text.substring(index, startIndex)); - parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex))); - fn.exp = exp; - index = endIndex + endSymbolLength; - hasInterpolation = true; - } else { - // we did not find anything, so we have to add the remainder to the parts array - (index != length) && parts.push(text.substring(index)); - index = length; - } - } - - if (!(length = parts.length)) { - // we added, nothing, must have been an empty string. - parts.push(''); - length = 1; - } - - // Concatenating expressions makes it hard to reason about whether some combination of - // concatenated values are unsafe to use and could easily lead to XSS. By requiring that a - // single expression be used for iframe[src], object[src], etc., we ensure that the value - // that's used is assigned or constructed by some JS code somewhere that is more testable or - // make it obvious that you bound the value to some user controlled value. This helps reduce - // the load when auditing for XSS issues. - if (trustedContext && parts.length > 1) { - throw $interpolateMinErr('noconcat', - "Error while interpolating: {0}\nStrict Contextual Escaping disallows " + - "interpolations that concatenate multiple expressions when a trusted value is " + - "required. See http://docs.angularjs.org/api/ng.$sce", text); - } - - if (!mustHaveExpression || hasInterpolation) { - concat.length = length; - fn = function(context) { - try { - for(var i = 0, ii = length, part; i - * **Note**: Intervals created by this service must be explicitly destroyed when you are finished - * with them. In particular they are not automatically destroyed when a controller's scope or a - * directive's element are destroyed. - * You should take this into consideration and make sure to always cancel the interval at the - * appropriate moment. See the example below for more details on how and when to do this. - * - * - * @param {function()} fn A function that should be called repeatedly. - * @param {number} delay Number of milliseconds between each function call. - * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat - * indefinitely. - * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#methods_$apply $apply} block. - * @returns {promise} A promise which will be notified on each iteration. - * - * @example - - - - -
    -
    - Date format:
    - Current time is: -
    - Blood 1 : {{blood_1}} - Blood 2 : {{blood_2}} - - - -
    -
    - -
    -
    - */ - function interval(fn, delay, count, invokeApply) { - var setInterval = $window.setInterval, - clearInterval = $window.clearInterval, - deferred = $q.defer(), - promise = deferred.promise, - iteration = 0, - skipApply = (isDefined(invokeApply) && !invokeApply); - - count = isDefined(count) ? count : 0; - - promise.then(null, null, fn); - - promise.$$intervalId = setInterval(function tick() { - deferred.notify(iteration++); - - if (count > 0 && iteration >= count) { - deferred.resolve(iteration); - clearInterval(promise.$$intervalId); - delete intervals[promise.$$intervalId]; - } - - if (!skipApply) $rootScope.$apply(); - - }, delay); - - intervals[promise.$$intervalId] = deferred; - - return promise; - } - - - /** - * @ngdoc function - * @name ng.$interval#cancel - * @methodOf ng.$interval - * - * @description - * Cancels a task associated with the `promise`. - * - * @param {number} promise Promise returned by the `$interval` function. - * @returns {boolean} Returns `true` if the task was successfully canceled. - */ - interval.cancel = function(promise) { - if (promise && promise.$$intervalId in intervals) { - intervals[promise.$$intervalId].reject('canceled'); - clearInterval(promise.$$intervalId); - delete intervals[promise.$$intervalId]; - return true; - } - return false; - }; - - return interval; - }]; -} - -/** - * @ngdoc object - * @name ng.$locale - * - * @description - * $locale service provides localization rules for various Angular components. As of right now the - * only public api is: - * - * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) - */ -function $LocaleProvider(){ - this.$get = function() { - return { - id: 'en-us', - - NUMBER_FORMATS: { - DECIMAL_SEP: '.', - GROUP_SEP: ',', - PATTERNS: [ - { // Decimal Pattern - minInt: 1, - minFrac: 0, - maxFrac: 3, - posPre: '', - posSuf: '', - negPre: '-', - negSuf: '', - gSize: 3, - lgSize: 3 - },{ //Currency Pattern - minInt: 1, - minFrac: 2, - maxFrac: 2, - posPre: '\u00A4', - posSuf: '', - negPre: '(\u00A4', - negSuf: ')', - gSize: 3, - lgSize: 3 - } - ], - CURRENCY_SYM: '$' - }, - - DATETIME_FORMATS: { - MONTH: - 'January,February,March,April,May,June,July,August,September,October,November,December' - .split(','), - SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','), - DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','), - SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','), - AMPMS: ['AM','PM'], - medium: 'MMM d, y h:mm:ss a', - short: 'M/d/yy h:mm a', - fullDate: 'EEEE, MMMM d, y', - longDate: 'MMMM d, y', - mediumDate: 'MMM d, y', - shortDate: 'M/d/yy', - mediumTime: 'h:mm:ss a', - shortTime: 'h:mm a' - }, - - pluralCat: function(num) { - if (num === 1) { - return 'one'; - } - return 'other'; - } - }; - }; -} - -var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, - DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; -var $locationMinErr = minErr('$location'); - - -/** - * Encode path using encodeUriSegment, ignoring forward slashes - * - * @param {string} path Path to encode - * @returns {string} - */ -function encodePath(path) { - var segments = path.split('/'), - i = segments.length; - - while (i--) { - segments[i] = encodeUriSegment(segments[i]); - } - - return segments.join('/'); -} - -function parseAbsoluteUrl(absoluteUrl, locationObj, appBase) { - var parsedUrl = urlResolve(absoluteUrl, appBase); - - locationObj.$$protocol = parsedUrl.protocol; - locationObj.$$host = parsedUrl.hostname; - locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; -} - - -function parseAppUrl(relativeUrl, locationObj, appBase) { - var prefixed = (relativeUrl.charAt(0) !== '/'); - if (prefixed) { - relativeUrl = '/' + relativeUrl; - } - var match = urlResolve(relativeUrl, appBase); - locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? - match.pathname.substring(1) : match.pathname); - locationObj.$$search = parseKeyValue(match.search); - locationObj.$$hash = decodeURIComponent(match.hash); - - // make sure path starts with '/'; - if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') { - locationObj.$$path = '/' + locationObj.$$path; - } -} - - -/** - * - * @param {string} begin - * @param {string} whole - * @returns {string} returns text from whole after begin or undefined if it does not begin with - * expected string. - */ -function beginsWith(begin, whole) { - if (whole.indexOf(begin) === 0) { - return whole.substr(begin.length); - } -} - - -function stripHash(url) { - var index = url.indexOf('#'); - return index == -1 ? url : url.substr(0, index); -} - - -function stripFile(url) { - return url.substr(0, stripHash(url).lastIndexOf('/') + 1); -} - -/* return the server only (scheme://host:port) */ -function serverBase(url) { - return url.substring(0, url.indexOf('/', url.indexOf('//') + 2)); -} - - -/** - * LocationHtml5Url represents an url - * This object is exposed as $location service when HTML5 mode is enabled and supported - * - * @constructor - * @param {string} appBase application base URL - * @param {string} basePrefix url path prefix - */ -function LocationHtml5Url(appBase, basePrefix) { - this.$$html5 = true; - basePrefix = basePrefix || ''; - var appBaseNoFile = stripFile(appBase); - parseAbsoluteUrl(appBase, this, appBase); - - - /** - * Parse given html5 (regular) url string into properties - * @param {string} newAbsoluteUrl HTML5 url - * @private - */ - this.$$parse = function(url) { - var pathUrl = beginsWith(appBaseNoFile, url); - if (!isString(pathUrl)) { - throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, - appBaseNoFile); - } - - parseAppUrl(pathUrl, this, appBase); - - if (!this.$$path) { - this.$$path = '/'; - } - - this.$$compose(); - }; - - /** - * Compose url and update `absUrl` property - * @private - */ - this.$$compose = function() { - var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; - - this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' - }; - - this.$$rewrite = function(url) { - var appUrl, prevAppUrl; - - if ( (appUrl = beginsWith(appBase, url)) !== undefined ) { - prevAppUrl = appUrl; - if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) { - return appBaseNoFile + (beginsWith('/', appUrl) || appUrl); - } else { - return appBase + prevAppUrl; - } - } else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) { - return appBaseNoFile + appUrl; - } else if (appBaseNoFile == url + '/') { - return appBaseNoFile; - } - }; -} - - -/** - * LocationHashbangUrl represents url - * This object is exposed as $location service when developer doesn't opt into html5 mode. - * It also serves as the base class for html5 mode fallback on legacy browsers. - * - * @constructor - * @param {string} appBase application base URL - * @param {string} hashPrefix hashbang prefix - */ -function LocationHashbangUrl(appBase, hashPrefix) { - var appBaseNoFile = stripFile(appBase); - - parseAbsoluteUrl(appBase, this, appBase); - - - /** - * Parse given hashbang url into properties - * @param {string} url Hashbang url - * @private - */ - this.$$parse = function(url) { - var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); - var withoutHashUrl = withoutBaseUrl.charAt(0) == '#' - ? beginsWith(hashPrefix, withoutBaseUrl) - : (this.$$html5) - ? withoutBaseUrl - : ''; - - if (!isString(withoutHashUrl)) { - throw $locationMinErr('ihshprfx', 'Invalid url "{0}", missing hash prefix "{1}".', url, - hashPrefix); - } - parseAppUrl(withoutHashUrl, this, appBase); - - this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase); - - this.$$compose(); - - /* - * In Windows, on an anchor node on documents loaded from - * the filesystem, the browser will return a pathname - * prefixed with the drive name ('/C:/path') when a - * pathname without a drive is set: - * * a.setAttribute('href', '/foo') - * * a.pathname === '/C:/foo' //true - * - * Inside of Angular, we're always using pathnames that - * do not include drive names for routing. - */ - function removeWindowsDriveName (path, url, base) { - /* - Matches paths for file protocol on windows, - such as /C:/foo/bar, and captures only /foo/bar. - */ - var windowsFilePathExp = /^\/?.*?:(\/.*)/; - - var firstPathSegmentMatch; - - //Get the relative path from the input URL. - if (url.indexOf(base) === 0) { - url = url.replace(base, ''); - } - - /* - * The input URL intentionally contains a - * first path segment that ends with a colon. - */ - if (windowsFilePathExp.exec(url)) { - return path; - } - - firstPathSegmentMatch = windowsFilePathExp.exec(path); - return firstPathSegmentMatch ? firstPathSegmentMatch[1] : path; - } - }; - - /** - * Compose hashbang url and update `absUrl` property - * @private - */ - this.$$compose = function() { - var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; - - this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); - }; - - this.$$rewrite = function(url) { - if(stripHash(appBase) == stripHash(url)) { - return url; - } - }; -} - - -/** - * LocationHashbangUrl represents url - * This object is exposed as $location service when html5 history api is enabled but the browser - * does not support it. - * - * @constructor - * @param {string} appBase application base URL - * @param {string} hashPrefix hashbang prefix - */ -function LocationHashbangInHtml5Url(appBase, hashPrefix) { - this.$$html5 = true; - LocationHashbangUrl.apply(this, arguments); - - var appBaseNoFile = stripFile(appBase); - - this.$$rewrite = function(url) { - var appUrl; - - if ( appBase == stripHash(url) ) { - return url; - } else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) { - return appBase + hashPrefix + appUrl; - } else if ( appBaseNoFile === url + '/') { - return appBaseNoFile; - } - }; -} - - -LocationHashbangInHtml5Url.prototype = - LocationHashbangUrl.prototype = - LocationHtml5Url.prototype = { - - /** - * Are we in html5 mode? - * @private - */ - $$html5: false, - - /** - * Has any change been replacing ? - * @private - */ - $$replace: false, - - /** - * @ngdoc method - * @name ng.$location#absUrl - * @methodOf ng.$location - * - * @description - * This method is getter only. - * - * Return full url representation with all segments encoded according to rules specified in - * {@link http://www.ietf.org/rfc/rfc3986.txt RFC 3986}. - * - * @return {string} full url - */ - absUrl: locationGetter('$$absUrl'), - - /** - * @ngdoc method - * @name ng.$location#url - * @methodOf ng.$location - * - * @description - * This method is getter / setter. - * - * Return url (e.g. `/path?a=b#hash`) when called without any parameter. - * - * Change path, search and hash, when called with parameter and return `$location`. - * - * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) - * @param {string=} replace The path that will be changed - * @return {string} url - */ - url: function(url, replace) { - if (isUndefined(url)) - return this.$$url; - - var match = PATH_MATCH.exec(url); - if (match[1]) this.path(decodeURIComponent(match[1])); - if (match[2] || match[1]) this.search(match[3] || ''); - this.hash(match[5] || '', replace); - - return this; - }, - - /** - * @ngdoc method - * @name ng.$location#protocol - * @methodOf ng.$location - * - * @description - * This method is getter only. - * - * Return protocol of current url. - * - * @return {string} protocol of current url - */ - protocol: locationGetter('$$protocol'), - - /** - * @ngdoc method - * @name ng.$location#host - * @methodOf ng.$location - * - * @description - * This method is getter only. - * - * Return host of current url. - * - * @return {string} host of current url. - */ - host: locationGetter('$$host'), - - /** - * @ngdoc method - * @name ng.$location#port - * @methodOf ng.$location - * - * @description - * This method is getter only. - * - * Return port of current url. - * - * @return {Number} port - */ - port: locationGetter('$$port'), - - /** - * @ngdoc method - * @name ng.$location#path - * @methodOf ng.$location - * - * @description - * This method is getter / setter. - * - * Return path of current url when called without any parameter. - * - * Change path when called with parameter and return `$location`. - * - * Note: Path should always begin with forward slash (/), this method will add the forward slash - * if it is missing. - * - * @param {string=} path New path - * @return {string} path - */ - path: locationGetterSetter('$$path', function(path) { - return path.charAt(0) == '/' ? path : '/' + path; - }), - - /** - * @ngdoc method - * @name ng.$location#search - * @methodOf ng.$location - * - * @description - * This method is getter / setter. - * - * Return search part (as object) of current url when called without any parameter. - * - * Change search part when called with parameter and return `$location`. - * - * @param {string|Object.|Object.>} search New search params - string or - * hash object. Hash object may contain an array of values, which will be decoded as duplicates in - * the url. - * - * @param {(string|Array)=} paramValue If `search` is a string, then `paramValue` will override only a - * single search parameter. If `paramValue` is an array, it will set the parameter as a - * comma-separated value. If `paramValue` is `null`, the parameter will be deleted. - * - * @return {string} search - */ - search: function(search, paramValue) { - switch (arguments.length) { - case 0: - return this.$$search; - case 1: - if (isString(search)) { - this.$$search = parseKeyValue(search); - } else if (isObject(search)) { - this.$$search = search; - } else { - throw $locationMinErr('isrcharg', - 'The first argument of the `$location#search()` call must be a string or an object.'); - } - break; - default: - if (isUndefined(paramValue) || paramValue === null) { - delete this.$$search[search]; - } else { - this.$$search[search] = paramValue; - } - } - - this.$$compose(); - return this; - }, - - /** - * @ngdoc method - * @name ng.$location#hash - * @methodOf ng.$location - * - * @description - * This method is getter / setter. - * - * Return hash fragment when called without any parameter. - * - * Change hash fragment when called with parameter and return `$location`. - * - * @param {string=} hash New hash fragment - * @return {string} hash - */ - hash: locationGetterSetter('$$hash', identity), - - /** - * @ngdoc method - * @name ng.$location#replace - * @methodOf ng.$location - * - * @description - * If called, all changes to $location during current `$digest` will be replacing current history - * record, instead of adding new one. - */ - replace: function() { - this.$$replace = true; - return this; - } -}; - -function locationGetter(property) { - return function() { - return this[property]; - }; -} - - -function locationGetterSetter(property, preprocess) { - return function(value) { - if (isUndefined(value)) - return this[property]; - - this[property] = preprocess(value); - this.$$compose(); - - return this; - }; -} - - -/** - * @ngdoc object - * @name ng.$location - * - * @requires $browser - * @requires $sniffer - * @requires $rootElement - * - * @description - * The $location service parses the URL in the browser address bar (based on the - * {@link https://developer.mozilla.org/en/window.location window.location}) and makes the URL - * available to your application. Changes to the URL in the address bar are reflected into - * $location service and changes to $location are reflected into the browser address bar. - * - * **The $location service:** - * - * - Exposes the current URL in the browser address bar, so you can - * - Watch and observe the URL. - * - Change the URL. - * - Synchronizes the URL with the browser when the user - * - Changes the address bar. - * - Clicks the back or forward button (or clicks a History link). - * - Clicks on a link. - * - Represents the URL object as a set of methods (protocol, host, port, path, search, hash). - * - * For more information see {@link guide/dev_guide.services.$location Developer Guide: Angular - * Services: Using $location} - */ - -/** - * @ngdoc object - * @name ng.$locationProvider - * @description - * Use the `$locationProvider` to configure how the application deep linking paths are stored. - */ -function $LocationProvider(){ - var hashPrefix = '', - html5Mode = false; - - /** - * @ngdoc property - * @name ng.$locationProvider#hashPrefix - * @methodOf ng.$locationProvider - * @description - * @param {string=} prefix Prefix for hash part (containing path and search) - * @returns {*} current value if used as getter or itself (chaining) if used as setter - */ - this.hashPrefix = function(prefix) { - if (isDefined(prefix)) { - hashPrefix = prefix; - return this; - } else { - return hashPrefix; - } - }; - - /** - * @ngdoc property - * @name ng.$locationProvider#html5Mode - * @methodOf ng.$locationProvider - * @description - * @param {boolean=} mode Use HTML5 strategy if available. - * @returns {*} current value if used as getter or itself (chaining) if used as setter - */ - this.html5Mode = function(mode) { - if (isDefined(mode)) { - html5Mode = mode; - return this; - } else { - return html5Mode; - } - }; - - /** - * @ngdoc event - * @name ng.$location#$locationChangeStart - * @eventOf ng.$location - * @eventType broadcast on root scope - * @description - * Broadcasted before a URL will change. This change can be prevented by calling - * `preventDefault` method of the event. See {@link ng.$rootScope.Scope#methods_$on} for more - * details about event object. Upon successful change - * {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired. - * - * @param {Object} angularEvent Synthetic event object. - * @param {string} newUrl New URL - * @param {string=} oldUrl URL that was before it was changed. - */ - - /** - * @ngdoc event - * @name ng.$location#$locationChangeSuccess - * @eventOf ng.$location - * @eventType broadcast on root scope - * @description - * Broadcasted after a URL was changed. - * - * @param {Object} angularEvent Synthetic event object. - * @param {string} newUrl New URL - * @param {string=} oldUrl URL that was before it was changed. - */ - - this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', - function( $rootScope, $browser, $sniffer, $rootElement) { - var $location, - LocationMode, - baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to '' - initialUrl = $browser.url(), - appBase; - - if (html5Mode) { - appBase = serverBase(initialUrl) + (baseHref || '/'); - LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; - } else { - appBase = stripHash(initialUrl); - LocationMode = LocationHashbangUrl; - } - $location = new LocationMode(appBase, '#' + hashPrefix); - $location.$$parse($location.$$rewrite(initialUrl)); - - $rootElement.on('click', function(event) { - // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) - // currently we open nice url link and redirect then - - if (event.ctrlKey || event.metaKey || event.which == 2) return; - - var elm = jqLite(event.target); - - // traverse the DOM up to find first A tag - while (lowercase(elm[0].nodeName) !== 'a') { - // ignore rewriting if no A tag (reached root element, or no parent - removed from document) - if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; - } - - var absHref = elm.prop('href'); - - if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') { - // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during - // an animation. - absHref = urlResolve(absHref.animVal).href; - } - - var rewrittenUrl = $location.$$rewrite(absHref); - - if (absHref && !elm.attr('target') && rewrittenUrl && !event.isDefaultPrevented()) { - event.preventDefault(); - if (rewrittenUrl != $browser.url()) { - // update location manually - $location.$$parse(rewrittenUrl); - $rootScope.$apply(); - // hack to work around FF6 bug 684208 when scenario runner clicks on links - window.angular['ff-684208-preventDefault'] = true; - } - } - }); - - - // rewrite hashbang url <> html5 url - if ($location.absUrl() != initialUrl) { - $browser.url($location.absUrl(), true); - } - - // update $location when $browser url changes - $browser.onUrlChange(function(newUrl) { - if ($location.absUrl() != newUrl) { - $rootScope.$evalAsync(function() { - var oldUrl = $location.absUrl(); - - $location.$$parse(newUrl); - if ($rootScope.$broadcast('$locationChangeStart', newUrl, - oldUrl).defaultPrevented) { - $location.$$parse(oldUrl); - $browser.url(oldUrl); - } else { - afterLocationChange(oldUrl); - } - }); - if (!$rootScope.$$phase) $rootScope.$digest(); - } - }); - - // update browser - var changeCounter = 0; - $rootScope.$watch(function $locationWatch() { - var oldUrl = $browser.url(); - var currentReplace = $location.$$replace; - - if (!changeCounter || oldUrl != $location.absUrl()) { - changeCounter++; - $rootScope.$evalAsync(function() { - if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl). - defaultPrevented) { - $location.$$parse(oldUrl); - } else { - $browser.url($location.absUrl(), currentReplace); - afterLocationChange(oldUrl); - } - }); - } - $location.$$replace = false; - - return changeCounter; - }); - - return $location; - - function afterLocationChange(oldUrl) { - $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl); - } -}]; -} - -/** - * @ngdoc object - * @name ng.$log - * @requires $window - * - * @description - * Simple service for logging. Default implementation safely writes the message - * into the browser's console (if present). - * - * The main purpose of this service is to simplify debugging and troubleshooting. - * - * The default is to log `debug` messages. You can use - * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. - * - * @example - - - function LogCtrl($scope, $log) { - $scope.$log = $log; - $scope.message = 'Hello World!'; - } - - -
    -

    Reload this page with open console, enter text and hit the log button...

    - Message: - - - - - -
    -
    -
    - */ - -/** - * @ngdoc object - * @name ng.$logProvider - * @description - * Use the `$logProvider` to configure how the application logs messages - */ -function $LogProvider(){ - var debug = true, - self = this; - - /** - * @ngdoc property - * @name ng.$logProvider#debugEnabled - * @methodOf ng.$logProvider - * @description - * @param {boolean=} flag enable or disable debug level messages - * @returns {*} current value if used as getter or itself (chaining) if used as setter - */ - this.debugEnabled = function(flag) { - if (isDefined(flag)) { - debug = flag; - return this; - } else { - return debug; - } - }; - - this.$get = ['$window', function($window){ - return { - /** - * @ngdoc method - * @name ng.$log#log - * @methodOf ng.$log - * - * @description - * Write a log message - */ - log: consoleLog('log'), - - /** - * @ngdoc method - * @name ng.$log#info - * @methodOf ng.$log - * - * @description - * Write an information message - */ - info: consoleLog('info'), - - /** - * @ngdoc method - * @name ng.$log#warn - * @methodOf ng.$log - * - * @description - * Write a warning message - */ - warn: consoleLog('warn'), - - /** - * @ngdoc method - * @name ng.$log#error - * @methodOf ng.$log - * - * @description - * Write an error message - */ - error: consoleLog('error'), - - /** - * @ngdoc method - * @name ng.$log#debug - * @methodOf ng.$log - * - * @description - * Write a debug message - */ - debug: (function () { - var fn = consoleLog('debug'); - - return function() { - if (debug) { - fn.apply(self, arguments); - } - }; - }()) - }; - - function formatError(arg) { - if (arg instanceof Error) { - if (arg.stack) { - arg = (arg.message && arg.stack.indexOf(arg.message) === -1) - ? 'Error: ' + arg.message + '\n' + arg.stack - : arg.stack; - } else if (arg.sourceURL) { - arg = arg.message + '\n' + arg.sourceURL + ':' + arg.line; - } - } - return arg; - } - - function consoleLog(type) { - var console = $window.console || {}, - logFn = console[type] || console.log || noop, - hasApply = false; - - // Note: reading logFn.apply throws an error in IE11 in IE8 document mode. - // The reason behind this is that console.log has type "object" in IE8... - try { - hasApply = !! logFn.apply; - } catch (e) {} - - if (hasApply) { - return function() { - var args = []; - forEach(arguments, function(arg) { - args.push(formatError(arg)); - }); - return logFn.apply(console, args); - }; - } - - // we are IE which either doesn't have window.console => this is noop and we do nothing, - // or we are IE where console.log doesn't have apply so we log at least first 2 args - return function(arg1, arg2) { - logFn(arg1, arg2 == null ? '' : arg2); - }; - } - }]; -} - -var $parseMinErr = minErr('$parse'); -var promiseWarningCache = {}; -var promiseWarning; - -// Sandboxing Angular Expressions -// ------------------------------ -// Angular expressions are generally considered safe because these expressions only have direct -// access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by -// obtaining a reference to native JS functions such as the Function constructor. -// -// As an example, consider the following Angular expression: -// -// {}.toString.constructor(alert("evil JS code")) -// -// We want to prevent this type of access. For the sake of performance, during the lexing phase we -// disallow any "dotted" access to any member named "constructor". -// -// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor -// while evaluating the expression, which is a stronger but more expensive test. Since reflective -// calls are expensive anyway, this is not such a big deal compared to static dereferencing. -// -// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits -// against the expression language, but not to prevent exploits that were enabled by exposing -// sensitive JavaScript or browser apis on Scope. Exposing such objects on a Scope is never a good -// practice and therefore we are not even trying to protect against interaction with an object -// explicitly exposed in this way. -// -// A developer could foil the name check by aliasing the Function constructor under a different -// name on the scope. -// -// In general, it is not possible to access a Window object from an angular expression unless a -// window or some DOM object that has a reference to window is published onto a Scope. - -function ensureSafeMemberName(name, fullExpression) { - if (name === "constructor") { - throw $parseMinErr('isecfld', - 'Referencing "constructor" field in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - return name; -} - -function ensureSafeObject(obj, fullExpression) { - // nifty check if obj is Function that is fast and works across iframes and other contexts - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isWindow(obj) - obj.document && obj.location && obj.alert && obj.setInterval) { - throw $parseMinErr('isecwindow', - 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isElement(obj) - obj.children && (obj.nodeName || (obj.on && obj.find))) { - throw $parseMinErr('isecdom', - 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } - return obj; -} - -var OPERATORS = { - /* jshint bitwise : false */ - 'null':function(){return null;}, - 'true':function(){return true;}, - 'false':function(){return false;}, - undefined:noop, - '+':function(self, locals, a,b){ - a=a(self, locals); b=b(self, locals); - if (isDefined(a)) { - if (isDefined(b)) { - return a + b; - } - return a; - } - return isDefined(b)?b:undefined;}, - '-':function(self, locals, a,b){ - a=a(self, locals); b=b(self, locals); - return (isDefined(a)?a:0)-(isDefined(b)?b:0); - }, - '*':function(self, locals, a,b){return a(self, locals)*b(self, locals);}, - '/':function(self, locals, a,b){return a(self, locals)/b(self, locals);}, - '%':function(self, locals, a,b){return a(self, locals)%b(self, locals);}, - '^':function(self, locals, a,b){return a(self, locals)^b(self, locals);}, - '=':noop, - '===':function(self, locals, a, b){return a(self, locals)===b(self, locals);}, - '!==':function(self, locals, a, b){return a(self, locals)!==b(self, locals);}, - '==':function(self, locals, a,b){return a(self, locals)==b(self, locals);}, - '!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);}, - '<':function(self, locals, a,b){return a(self, locals)':function(self, locals, a,b){return a(self, locals)>b(self, locals);}, - '<=':function(self, locals, a,b){return a(self, locals)<=b(self, locals);}, - '>=':function(self, locals, a,b){return a(self, locals)>=b(self, locals);}, - '&&':function(self, locals, a,b){return a(self, locals)&&b(self, locals);}, - '||':function(self, locals, a,b){return a(self, locals)||b(self, locals);}, - '&':function(self, locals, a,b){return a(self, locals)&b(self, locals);}, -// '|':function(self, locals, a,b){return a|b;}, - '|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));}, - '!':function(self, locals, a){return !a(self, locals);} -}; -/* jshint bitwise: true */ -var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; - - -///////////////////////////////////////// - - -/** - * @constructor - */ -var Lexer = function (options) { - this.options = options; -}; - -Lexer.prototype = { - constructor: Lexer, - - lex: function (text) { - this.text = text; - - this.index = 0; - this.ch = undefined; - this.lastCh = ':'; // can start regexp - - this.tokens = []; - - var token; - var json = []; - - while (this.index < this.text.length) { - this.ch = this.text.charAt(this.index); - if (this.is('"\'')) { - this.readString(this.ch); - } else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) { - this.readNumber(); - } else if (this.isIdent(this.ch)) { - this.readIdent(); - // identifiers can only be if the preceding char was a { or , - if (this.was('{,') && json[0] === '{' && - (token = this.tokens[this.tokens.length - 1])) { - token.json = token.text.indexOf('.') === -1; - } - } else if (this.is('(){}[].,;:?')) { - this.tokens.push({ - index: this.index, - text: this.ch, - json: (this.was(':[,') && this.is('{[')) || this.is('}]:,') - }); - if (this.is('{[')) json.unshift(this.ch); - if (this.is('}]')) json.shift(); - this.index++; - } else if (this.isWhitespace(this.ch)) { - this.index++; - continue; - } else { - var ch2 = this.ch + this.peek(); - var ch3 = ch2 + this.peek(2); - var fn = OPERATORS[this.ch]; - var fn2 = OPERATORS[ch2]; - var fn3 = OPERATORS[ch3]; - if (fn3) { - this.tokens.push({index: this.index, text: ch3, fn: fn3}); - this.index += 3; - } else if (fn2) { - this.tokens.push({index: this.index, text: ch2, fn: fn2}); - this.index += 2; - } else if (fn) { - this.tokens.push({ - index: this.index, - text: this.ch, - fn: fn, - json: (this.was('[,:') && this.is('+-')) - }); - this.index += 1; - } else { - this.throwError('Unexpected next character ', this.index, this.index + 1); - } - } - this.lastCh = this.ch; - } - return this.tokens; - }, - - is: function(chars) { - return chars.indexOf(this.ch) !== -1; - }, - - was: function(chars) { - return chars.indexOf(this.lastCh) !== -1; - }, - - peek: function(i) { - var num = i || 1; - return (this.index + num < this.text.length) ? this.text.charAt(this.index + num) : false; - }, - - isNumber: function(ch) { - return ('0' <= ch && ch <= '9'); - }, - - isWhitespace: function(ch) { - // IE treats non-breaking space as \u00A0 - return (ch === ' ' || ch === '\r' || ch === '\t' || - ch === '\n' || ch === '\v' || ch === '\u00A0'); - }, - - isIdent: function(ch) { - return ('a' <= ch && ch <= 'z' || - 'A' <= ch && ch <= 'Z' || - '_' === ch || ch === '$'); - }, - - isExpOperator: function(ch) { - return (ch === '-' || ch === '+' || this.isNumber(ch)); - }, - - throwError: function(error, start, end) { - end = end || this.index; - var colStr = (isDefined(start) - ? 's ' + start + '-' + this.index + ' [' + this.text.substring(start, end) + ']' - : ' ' + end); - throw $parseMinErr('lexerr', 'Lexer Error: {0} at column{1} in expression [{2}].', - error, colStr, this.text); - }, - - readNumber: function() { - var number = ''; - var start = this.index; - while (this.index < this.text.length) { - var ch = lowercase(this.text.charAt(this.index)); - if (ch == '.' || this.isNumber(ch)) { - number += ch; - } else { - var peekCh = this.peek(); - if (ch == 'e' && this.isExpOperator(peekCh)) { - number += ch; - } else if (this.isExpOperator(ch) && - peekCh && this.isNumber(peekCh) && - number.charAt(number.length - 1) == 'e') { - number += ch; - } else if (this.isExpOperator(ch) && - (!peekCh || !this.isNumber(peekCh)) && - number.charAt(number.length - 1) == 'e') { - this.throwError('Invalid exponent'); - } else { - break; - } - } - this.index++; - } - number = 1 * number; - this.tokens.push({ - index: start, - text: number, - json: true, - fn: function() { return number; } - }); - }, - - readIdent: function() { - var parser = this; - - var ident = ''; - var start = this.index; - - var lastDot, peekIndex, methodName, ch; - - while (this.index < this.text.length) { - ch = this.text.charAt(this.index); - if (ch === '.' || this.isIdent(ch) || this.isNumber(ch)) { - if (ch === '.') lastDot = this.index; - ident += ch; - } else { - break; - } - this.index++; - } - - //check if this is not a method invocation and if it is back out to last dot - if (lastDot) { - peekIndex = this.index; - while (peekIndex < this.text.length) { - ch = this.text.charAt(peekIndex); - if (ch === '(') { - methodName = ident.substr(lastDot - start + 1); - ident = ident.substr(0, lastDot - start); - this.index = peekIndex; - break; - } - if (this.isWhitespace(ch)) { - peekIndex++; - } else { - break; - } - } - } - - - var token = { - index: start, - text: ident - }; - - // OPERATORS is our own object so we don't need to use special hasOwnPropertyFn - if (OPERATORS.hasOwnProperty(ident)) { - token.fn = OPERATORS[ident]; - token.json = OPERATORS[ident]; - } else { - var getter = getterFn(ident, this.options, this.text); - token.fn = extend(function(self, locals) { - return (getter(self, locals)); - }, { - assign: function(self, value) { - return setter(self, ident, value, parser.text, parser.options); - } - }); - } - - this.tokens.push(token); - - if (methodName) { - this.tokens.push({ - index:lastDot, - text: '.', - json: false - }); - this.tokens.push({ - index: lastDot + 1, - text: methodName, - json: false - }); - } - }, - - readString: function(quote) { - var start = this.index; - this.index++; - var string = ''; - var rawString = quote; - var escape = false; - while (this.index < this.text.length) { - var ch = this.text.charAt(this.index); - rawString += ch; - if (escape) { - if (ch === 'u') { - var hex = this.text.substring(this.index + 1, this.index + 5); - if (!hex.match(/[\da-f]{4}/i)) - this.throwError('Invalid unicode escape [\\u' + hex + ']'); - this.index += 4; - string += String.fromCharCode(parseInt(hex, 16)); - } else { - var rep = ESCAPE[ch]; - if (rep) { - string += rep; - } else { - string += ch; - } - } - escape = false; - } else if (ch === '\\') { - escape = true; - } else if (ch === quote) { - this.index++; - this.tokens.push({ - index: start, - text: rawString, - string: string, - json: true, - fn: function() { return string; } - }); - return; - } else { - string += ch; - } - this.index++; - } - this.throwError('Unterminated quote', start); - } -}; - - -/** - * @constructor - */ -var Parser = function (lexer, $filter, options) { - this.lexer = lexer; - this.$filter = $filter; - this.options = options; -}; - -Parser.ZERO = function () { return 0; }; - -Parser.prototype = { - constructor: Parser, - - parse: function (text, json) { - this.text = text; - - //TODO(i): strip all the obsolte json stuff from this file - this.json = json; - - this.tokens = this.lexer.lex(text); - - if (json) { - // The extra level of aliasing is here, just in case the lexer misses something, so that - // we prevent any accidental execution in JSON. - this.assignment = this.logicalOR; - - this.functionCall = - this.fieldAccess = - this.objectIndex = - this.filterChain = function() { - this.throwError('is not valid json', {text: text, index: 0}); - }; - } - - var value = json ? this.primary() : this.statements(); - - if (this.tokens.length !== 0) { - this.throwError('is an unexpected token', this.tokens[0]); - } - - value.literal = !!value.literal; - value.constant = !!value.constant; - - return value; - }, - - primary: function () { - var primary; - if (this.expect('(')) { - primary = this.filterChain(); - this.consume(')'); - } else if (this.expect('[')) { - primary = this.arrayDeclaration(); - } else if (this.expect('{')) { - primary = this.object(); - } else { - var token = this.expect(); - primary = token.fn; - if (!primary) { - this.throwError('not a primary expression', token); - } - if (token.json) { - primary.constant = true; - primary.literal = true; - } - } - - var next, context; - while ((next = this.expect('(', '[', '.'))) { - if (next.text === '(') { - primary = this.functionCall(primary, context); - context = null; - } else if (next.text === '[') { - context = primary; - primary = this.objectIndex(primary); - } else if (next.text === '.') { - context = primary; - primary = this.fieldAccess(primary); - } else { - this.throwError('IMPOSSIBLE'); - } - } - return primary; - }, - - throwError: function(msg, token) { - throw $parseMinErr('syntax', - 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', - token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); - }, - - peekToken: function() { - if (this.tokens.length === 0) - throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); - return this.tokens[0]; - }, - - peek: function(e1, e2, e3, e4) { - if (this.tokens.length > 0) { - var token = this.tokens[0]; - var t = token.text; - if (t === e1 || t === e2 || t === e3 || t === e4 || - (!e1 && !e2 && !e3 && !e4)) { - return token; - } - } - return false; - }, - - expect: function(e1, e2, e3, e4){ - var token = this.peek(e1, e2, e3, e4); - if (token) { - if (this.json && !token.json) { - this.throwError('is not valid json', token); - } - this.tokens.shift(); - return token; - } - return false; - }, - - consume: function(e1){ - if (!this.expect(e1)) { - this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); - } - }, - - unaryFn: function(fn, right) { - return extend(function(self, locals) { - return fn(self, locals, right); - }, { - constant:right.constant - }); - }, - - ternaryFn: function(left, middle, right){ - return extend(function(self, locals){ - return left(self, locals) ? middle(self, locals) : right(self, locals); - }, { - constant: left.constant && middle.constant && right.constant - }); - }, - - binaryFn: function(left, fn, right) { - return extend(function(self, locals) { - return fn(self, locals, left, right); - }, { - constant:left.constant && right.constant - }); - }, - - statements: function() { - var statements = []; - while (true) { - if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) - statements.push(this.filterChain()); - if (!this.expect(';')) { - // optimize for the common case where there is only one statement. - // TODO(size): maybe we should not support multiple statements? - return (statements.length === 1) - ? statements[0] - : function(self, locals) { - var value; - for (var i = 0; i < statements.length; i++) { - var statement = statements[i]; - if (statement) { - value = statement(self, locals); - } - } - return value; - }; - } - } - }, - - filterChain: function() { - var left = this.expression(); - var token; - while (true) { - if ((token = this.expect('|'))) { - left = this.binaryFn(left, token.fn, this.filter()); - } else { - return left; - } - } - }, - - filter: function() { - var token = this.expect(); - var fn = this.$filter(token.text); - var argsFn = []; - while (true) { - if ((token = this.expect(':'))) { - argsFn.push(this.expression()); - } else { - var fnInvoke = function(self, locals, input) { - var args = [input]; - for (var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](self, locals)); - } - return fn.apply(self, args); - }; - return function() { - return fnInvoke; - }; - } - } - }, - - expression: function() { - return this.assignment(); - }, - - assignment: function() { - var left = this.ternary(); - var right; - var token; - if ((token = this.expect('='))) { - if (!left.assign) { - this.throwError('implies assignment but [' + - this.text.substring(0, token.index) + '] can not be assigned to', token); - } - right = this.ternary(); - return function(scope, locals) { - return left.assign(scope, right(scope, locals), locals); - }; - } - return left; - }, - - ternary: function() { - var left = this.logicalOR(); - var middle; - var token; - if ((token = this.expect('?'))) { - middle = this.ternary(); - if ((token = this.expect(':'))) { - return this.ternaryFn(left, middle, this.ternary()); - } else { - this.throwError('expected :', token); - } - } else { - return left; - } - }, - - logicalOR: function() { - var left = this.logicalAND(); - var token; - while (true) { - if ((token = this.expect('||'))) { - left = this.binaryFn(left, token.fn, this.logicalAND()); - } else { - return left; - } - } - }, - - logicalAND: function() { - var left = this.equality(); - var token; - if ((token = this.expect('&&'))) { - left = this.binaryFn(left, token.fn, this.logicalAND()); - } - return left; - }, - - equality: function() { - var left = this.relational(); - var token; - if ((token = this.expect('==','!=','===','!=='))) { - left = this.binaryFn(left, token.fn, this.equality()); - } - return left; - }, - - relational: function() { - var left = this.additive(); - var token; - if ((token = this.expect('<', '>', '<=', '>='))) { - left = this.binaryFn(left, token.fn, this.relational()); - } - return left; - }, - - additive: function() { - var left = this.multiplicative(); - var token; - while ((token = this.expect('+','-'))) { - left = this.binaryFn(left, token.fn, this.multiplicative()); - } - return left; - }, - - multiplicative: function() { - var left = this.unary(); - var token; - while ((token = this.expect('*','/','%'))) { - left = this.binaryFn(left, token.fn, this.unary()); - } - return left; - }, - - unary: function() { - var token; - if (this.expect('+')) { - return this.primary(); - } else if ((token = this.expect('-'))) { - return this.binaryFn(Parser.ZERO, token.fn, this.unary()); - } else if ((token = this.expect('!'))) { - return this.unaryFn(token.fn, this.unary()); - } else { - return this.primary(); - } - }, - - fieldAccess: function(object) { - var parser = this; - var field = this.expect().text; - var getter = getterFn(field, this.options, this.text); - - return extend(function(scope, locals, self) { - return getter(self || object(scope, locals)); - }, { - assign: function(scope, value, locals) { - return setter(object(scope, locals), field, value, parser.text, parser.options); - } - }); - }, - - objectIndex: function(obj) { - var parser = this; - - var indexFn = this.expression(); - this.consume(']'); - - return extend(function(self, locals) { - var o = obj(self, locals), - i = indexFn(self, locals), - v, p; - - if (!o) return undefined; - v = ensureSafeObject(o[i], parser.text); - if (v && v.then && parser.options.unwrapPromises) { - p = v; - if (!('$$v' in v)) { - p.$$v = undefined; - p.then(function(val) { p.$$v = val; }); - } - v = v.$$v; - } - return v; - }, { - assign: function(self, value, locals) { - var key = indexFn(self, locals); - // prevent overwriting of Function.constructor which would break ensureSafeObject check - var safe = ensureSafeObject(obj(self, locals), parser.text); - return safe[key] = value; - } - }); - }, - - functionCall: function(fn, contextGetter) { - var argsFn = []; - if (this.peekToken().text !== ')') { - do { - argsFn.push(this.expression()); - } while (this.expect(',')); - } - this.consume(')'); - - var parser = this; - - return function(scope, locals) { - var args = []; - var context = contextGetter ? contextGetter(scope, locals) : scope; - - for (var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](scope, locals)); - } - var fnPtr = fn(scope, locals, context) || noop; - - ensureSafeObject(context, parser.text); - ensureSafeObject(fnPtr, parser.text); - - // IE stupidity! (IE doesn't have apply for some native functions) - var v = fnPtr.apply - ? fnPtr.apply(context, args) - : fnPtr(args[0], args[1], args[2], args[3], args[4]); - - return ensureSafeObject(v, parser.text); - }; - }, - - // This is used with json array declaration - arrayDeclaration: function () { - var elementFns = []; - var allConstant = true; - if (this.peekToken().text !== ']') { - do { - var elementFn = this.expression(); - elementFns.push(elementFn); - if (!elementFn.constant) { - allConstant = false; - } - } while (this.expect(',')); - } - this.consume(']'); - - return extend(function(self, locals) { - var array = []; - for (var i = 0; i < elementFns.length; i++) { - array.push(elementFns[i](self, locals)); - } - return array; - }, { - literal: true, - constant: allConstant - }); - }, - - object: function () { - var keyValues = []; - var allConstant = true; - if (this.peekToken().text !== '}') { - do { - var token = this.expect(), - key = token.string || token.text; - this.consume(':'); - var value = this.expression(); - keyValues.push({key: key, value: value}); - if (!value.constant) { - allConstant = false; - } - } while (this.expect(',')); - } - this.consume('}'); - - return extend(function(self, locals) { - var object = {}; - for (var i = 0; i < keyValues.length; i++) { - var keyValue = keyValues[i]; - object[keyValue.key] = keyValue.value(self, locals); - } - return object; - }, { - literal: true, - constant: allConstant - }); - } -}; - - -////////////////////////////////////////////////// -// Parser helper functions -////////////////////////////////////////////////// - -function setter(obj, path, setValue, fullExp, options) { - //needed? - options = options || {}; - - var element = path.split('.'), key; - for (var i = 0; element.length > 1; i++) { - key = ensureSafeMemberName(element.shift(), fullExp); - var propertyObj = obj[key]; - if (!propertyObj) { - propertyObj = {}; - obj[key] = propertyObj; - } - obj = propertyObj; - if (obj.then && options.unwrapPromises) { - promiseWarning(fullExp); - if (!("$$v" in obj)) { - (function(promise) { - promise.then(function(val) { promise.$$v = val; }); } - )(obj); - } - if (obj.$$v === undefined) { - obj.$$v = {}; - } - obj = obj.$$v; - } - } - key = ensureSafeMemberName(element.shift(), fullExp); - obj[key] = setValue; - return setValue; -} - -var getterFnCache = {}; - -/** - * Implementation of the "Black Hole" variant from: - * - http://jsperf.com/angularjs-parse-getter/4 - * - http://jsperf.com/path-evaluation-simplified/7 - */ -function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, options) { - ensureSafeMemberName(key0, fullExp); - ensureSafeMemberName(key1, fullExp); - ensureSafeMemberName(key2, fullExp); - ensureSafeMemberName(key3, fullExp); - ensureSafeMemberName(key4, fullExp); - - return !options.unwrapPromises - ? function cspSafeGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; - - if (pathVal == null) return pathVal; - pathVal = pathVal[key0]; - - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key1]; - - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key2]; - - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key3]; - - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key4]; - - return pathVal; - } - : function cspSafePromiseEnabledGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, - promise; - - if (pathVal == null) return pathVal; - - pathVal = pathVal[key0]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key1]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key2]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key3]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key4]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - return pathVal; - }; -} - -function simpleGetterFn1(key0, fullExp) { - ensureSafeMemberName(key0, fullExp); - - return function simpleGetterFn1(scope, locals) { - if (scope == null) return undefined; - return ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; - }; -} - -function simpleGetterFn2(key0, key1, fullExp) { - ensureSafeMemberName(key0, fullExp); - ensureSafeMemberName(key1, fullExp); - - return function simpleGetterFn2(scope, locals) { - if (scope == null) return undefined; - scope = ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; - return scope == null ? undefined : scope[key1]; - }; -} - -function getterFn(path, options, fullExp) { - // Check whether the cache has this getter already. - // We can use hasOwnProperty directly on the cache because we ensure, - // see below, that the cache never stores a path called 'hasOwnProperty' - if (getterFnCache.hasOwnProperty(path)) { - return getterFnCache[path]; - } - - var pathKeys = path.split('.'), - pathKeysLength = pathKeys.length, - fn; - - // When we have only 1 or 2 tokens, use optimized special case closures. - // http://jsperf.com/angularjs-parse-getter/6 - if (!options.unwrapPromises && pathKeysLength === 1) { - fn = simpleGetterFn1(pathKeys[0], fullExp); - } else if (!options.unwrapPromises && pathKeysLength === 2) { - fn = simpleGetterFn2(pathKeys[0], pathKeys[1], fullExp); - } else if (options.csp) { - if (pathKeysLength < 6) { - fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, - options); - } else { - fn = function(scope, locals) { - var i = 0, val; - do { - val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], - pathKeys[i++], fullExp, options)(scope, locals); - - locals = undefined; // clear after first iteration - scope = val; - } while (i < pathKeysLength); - return val; - }; - } - } else { - var code = 'var p;\n'; - forEach(pathKeys, function(key, index) { - ensureSafeMemberName(key, fullExp); - code += 'if(s == null) return undefined;\n' + - 's='+ (index - // we simply dereference 's' on any .dot notation - ? 's' - // but if we are first then we check locals first, and if so read it first - : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + - (options.unwrapPromises - ? 'if (s && s.then) {\n' + - ' pw("' + fullExp.replace(/(["\r\n])/g, '\\$1') + '");\n' + - ' if (!("$$v" in s)) {\n' + - ' p=s;\n' + - ' p.$$v = undefined;\n' + - ' p.then(function(v) {p.$$v=v;});\n' + - '}\n' + - ' s=s.$$v\n' + - '}\n' - : ''); - }); - code += 'return s;'; - - /* jshint -W054 */ - var evaledFnGetter = new Function('s', 'k', 'pw', code); // s=scope, k=locals, pw=promiseWarning - /* jshint +W054 */ - evaledFnGetter.toString = valueFn(code); - fn = options.unwrapPromises ? function(scope, locals) { - return evaledFnGetter(scope, locals, promiseWarning); - } : evaledFnGetter; - } - - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - if (path !== 'hasOwnProperty') { - getterFnCache[path] = fn; - } - return fn; -} - -/////////////////////////////////// - -/** - * @ngdoc function - * @name ng.$parse - * @function - * - * @description - * - * Converts Angular {@link guide/expression expression} into a function. - * - *
    - *   var getter = $parse('user.name');
    - *   var setter = getter.assign;
    - *   var context = {user:{name:'angular'}};
    - *   var locals = {user:{name:'local'}};
    - *
    - *   expect(getter(context)).toEqual('angular');
    - *   setter(context, 'newValue');
    - *   expect(context.user.name).toEqual('newValue');
    - *   expect(getter(context, locals)).toEqual('local');
    - * 
    - * - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - * - * The returned function also has the following properties: - * * `literal` – `{boolean}` – whether the expression's top-level node is a JavaScript - * literal. - * * `constant` – `{boolean}` – whether the expression is made entirely of JavaScript - * constant literals. - * * `assign` – `{?function(context, value)}` – if the expression is assignable, this will be - * set to a function to change its value on the given context. - * - */ - - -/** - * @ngdoc object - * @name ng.$parseProvider - * @function - * - * @description - * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} - * service. - */ -function $ParseProvider() { - var cache = {}; - - var $parseOptions = { - csp: false, - unwrapPromises: false, - logPromiseWarnings: true - }; - - - /** - * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. - * - * @ngdoc method - * @name ng.$parseProvider#unwrapPromises - * @methodOf ng.$parseProvider - * @description - * - * **This feature is deprecated, see deprecation notes below for more info** - * - * If set to true (default is false), $parse will unwrap promises automatically when a promise is - * found at any part of the expression. In other words, if set to true, the expression will always - * result in a non-promise value. - * - * While the promise is unresolved, it's treated as undefined, but once resolved and fulfilled, - * the fulfillment value is used in place of the promise while evaluating the expression. - * - * **Deprecation notice** - * - * This is a feature that didn't prove to be wildly useful or popular, primarily because of the - * dichotomy between data access in templates (accessed as raw values) and controller code - * (accessed as promises). - * - * In most code we ended up resolving promises manually in controllers anyway and thus unifying - * the model access there. - * - * Other downsides of automatic promise unwrapping: - * - * - when building components it's often desirable to receive the raw promises - * - adds complexity and slows down expression evaluation - * - makes expression code pre-generation unattractive due to the amount of code that needs to be - * generated - * - makes IDE auto-completion and tool support hard - * - * **Warning Logs** - * - * If the unwrapping is enabled, Angular will log a warning about each expression that unwraps a - * promise (to reduce the noise, each expression is logged only once). To disable this logging use - * `$parseProvider.logPromiseWarnings(false)` api. - * - * - * @param {boolean=} value New value. - * @returns {boolean|self} Returns the current setting when used as getter and self if used as - * setter. - */ - this.unwrapPromises = function(value) { - if (isDefined(value)) { - $parseOptions.unwrapPromises = !!value; - return this; - } else { - return $parseOptions.unwrapPromises; - } - }; - - - /** - * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. - * - * @ngdoc method - * @name ng.$parseProvider#logPromiseWarnings - * @methodOf ng.$parseProvider - * @description - * - * Controls whether Angular should log a warning on any encounter of a promise in an expression. - * - * The default is set to `true`. - * - * This setting applies only if `$parseProvider.unwrapPromises` setting is set to true as well. - * - * @param {boolean=} value New value. - * @returns {boolean|self} Returns the current setting when used as getter and self if used as - * setter. - */ - this.logPromiseWarnings = function(value) { - if (isDefined(value)) { - $parseOptions.logPromiseWarnings = value; - return this; - } else { - return $parseOptions.logPromiseWarnings; - } - }; - - - this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) { - $parseOptions.csp = $sniffer.csp; - - promiseWarning = function promiseWarningFn(fullExp) { - if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return; - promiseWarningCache[fullExp] = true; - $log.warn('[$parse] Promise found in the expression `' + fullExp + '`. ' + - 'Automatic unwrapping of promises in Angular expressions is deprecated.'); - }; - - return function(exp) { - var parsedExpression; - - switch (typeof exp) { - case 'string': - - if (cache.hasOwnProperty(exp)) { - return cache[exp]; - } - - var lexer = new Lexer($parseOptions); - var parser = new Parser(lexer, $filter, $parseOptions); - parsedExpression = parser.parse(exp, false); - - if (exp !== 'hasOwnProperty') { - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - cache[exp] = parsedExpression; - } - - return parsedExpression; - - case 'function': - return exp; - - default: - return noop; - } - }; - }]; -} - -/** - * @ngdoc service - * @name ng.$q - * @requires $rootScope - * - * @description - * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). - * - * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an - * interface for interacting with an object that represents the result of an action that is - * performed asynchronously, and may or may not be finished at any given point in time. - * - * From the perspective of dealing with error handling, deferred and promise APIs are to - * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. - * - *
    - *   // for the purpose of this example let's assume that variables `$q`, `scope` and `okToGreet`
    - *   // are available in the current lexical scope (they could have been injected or passed in).
    - * 
    - *   function asyncGreet(name) {
    - *     var deferred = $q.defer();
    - *
    - *     setTimeout(function() {
    - *       // since this fn executes async in a future turn of the event loop, we need to wrap
    - *       // our code into an $apply call so that the model changes are properly observed.
    - *       scope.$apply(function() {
    - *         deferred.notify('About to greet ' + name + '.');
    - *
    - *         if (okToGreet(name)) {
    - *           deferred.resolve('Hello, ' + name + '!');
    - *         } else {
    - *           deferred.reject('Greeting ' + name + ' is not allowed.');
    - *         }
    - *       });
    - *     }, 1000);
    - *
    - *     return deferred.promise;
    - *   }
    - *
    - *   var promise = asyncGreet('Robin Hood');
    - *   promise.then(function(greeting) {
    - *     alert('Success: ' + greeting);
    - *   }, function(reason) {
    - *     alert('Failed: ' + reason);
    - *   }, function(update) {
    - *     alert('Got notification: ' + update);
    - *   });
    - * 
    - * - * At first it might not be obvious why this extra complexity is worth the trouble. The payoff - * comes in the way of guarantees that promise and deferred APIs make, see - * https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md. - * - * Additionally the promise api allows for composition that is very hard to do with the - * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. - * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the - * section on serial or parallel joining of promises. - * - * - * # The Deferred API - * - * A new instance of deferred is constructed by calling `$q.defer()`. - * - * The purpose of the deferred object is to expose the associated Promise instance as well as APIs - * that can be used for signaling the successful or unsuccessful completion, as well as the status - * of the task. - * - * **Methods** - * - * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection - * constructed via `$q.reject`, the promise will be rejected instead. - * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to - * resolving it with a rejection constructed via `$q.reject`. - * - `notify(value)` - provides updates on the status of the promise's execution. This may be called - * multiple times before the promise is either resolved or rejected. - * - * **Properties** - * - * - promise – `{Promise}` – promise object associated with this deferred. - * - * - * # The Promise API - * - * A new promise instance is created when a deferred instance is created and can be retrieved by - * calling `deferred.promise`. - * - * The purpose of the promise object is to allow for interested parties to get access to the result - * of the deferred task when it completes. - * - * **Methods** - * - * - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or - * will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously - * as soon as the result is available. The callbacks are called with a single argument: the result - * or rejection reason. Additionally, the notify callback may be called zero or more times to - * provide a progress indication, before the promise is resolved or rejected. - * - * This method *returns a new promise* which is resolved or rejected via the return value of the - * `successCallback`, `errorCallback`. It also notifies via the return value of the - * `notifyCallback` method. The promise can not be resolved or rejected from the notifyCallback - * method. - * - * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` - * - * - `finally(callback)` – allows you to observe either the fulfillment or rejection of a promise, - * but to do so without modifying the final value. This is useful to release resources or do some - * clean-up that needs to be done whether the promise was rejected or resolved. See the [full - * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for - * more information. - * - * Because `finally` is a reserved word in JavaScript and reserved keywords are not supported as - * property names by ES3, you'll need to invoke the method like `promise['finally'](callback)` to - * make your code IE8 compatible. - * - * # Chaining promises - * - * Because calling the `then` method of a promise returns a new derived promise, it is easily - * possible to create a chain of promises: - * - *
    - *   promiseB = promiseA.then(function(result) {
    - *     return result + 1;
    - *   });
    - *
    - *   // promiseB will be resolved immediately after promiseA is resolved and its value
    - *   // will be the result of promiseA incremented by 1
    - * 
    - * - * It is possible to create chains of any length and since a promise can be resolved with another - * promise (which will defer its resolution further), it is possible to pause/defer resolution of - * the promises at any point in the chain. This makes it possible to implement powerful APIs like - * $http's response interceptors. - * - * - * # Differences between Kris Kowal's Q and $q - * - * There are two main differences: - * - * - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation - * mechanism in angular, which means faster propagation of resolution or rejection into your - * models and avoiding unnecessary browser repaints, which would result in flickering UI. - * - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains - * all the important functionality needed for common async tasks. - * - * # Testing - * - *
    - *    it('should simulate promise', inject(function($q, $rootScope) {
    - *      var deferred = $q.defer();
    - *      var promise = deferred.promise;
    - *      var resolvedValue;
    - *
    - *      promise.then(function(value) { resolvedValue = value; });
    - *      expect(resolvedValue).toBeUndefined();
    - *
    - *      // Simulate resolving of promise
    - *      deferred.resolve(123);
    - *      // Note that the 'then' function does not get called synchronously.
    - *      // This is because we want the promise API to always be async, whether or not
    - *      // it got called synchronously or asynchronously.
    - *      expect(resolvedValue).toBeUndefined();
    - *
    - *      // Propagate promise resolution to 'then' functions using $apply().
    - *      $rootScope.$apply();
    - *      expect(resolvedValue).toEqual(123);
    - *    }));
    - *  
    - */ -function $QProvider() { - - this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { - return qFactory(function(callback) { - $rootScope.$evalAsync(callback); - }, $exceptionHandler); - }]; -} - - -/** - * Constructs a promise manager. - * - * @param {function(function)} nextTick Function for executing functions in the next turn. - * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for - * debugging purposes. - * @returns {object} Promise manager. - */ -function qFactory(nextTick, exceptionHandler) { - - /** - * @ngdoc - * @name ng.$q#defer - * @methodOf ng.$q - * @description - * Creates a `Deferred` object which represents a task which will finish in the future. - * - * @returns {Deferred} Returns a new instance of deferred. - */ - var defer = function() { - var pending = [], - value, deferred; - - deferred = { - - resolve: function(val) { - if (pending) { - var callbacks = pending; - pending = undefined; - value = ref(val); - - if (callbacks.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - value.then(callback[0], callback[1], callback[2]); - } - }); - } - } - }, - - - reject: function(reason) { - deferred.resolve(createInternalRejectedPromise(reason)); - }, - - - notify: function(progress) { - if (pending) { - var callbacks = pending; - - if (pending.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - callback[2](progress); - } - }); - } - } - }, - - - promise: { - then: function(callback, errback, progressback) { - var result = defer(); - - var wrappedCallback = function(value) { - try { - result.resolve((isFunction(callback) ? callback : defaultCallback)(value)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }; - - var wrappedErrback = function(reason) { - try { - result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }; - - var wrappedProgressback = function(progress) { - try { - result.notify((isFunction(progressback) ? progressback : defaultCallback)(progress)); - } catch(e) { - exceptionHandler(e); - } - }; - - if (pending) { - pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]); - } else { - value.then(wrappedCallback, wrappedErrback, wrappedProgressback); - } - - return result.promise; - }, - - "catch": function(callback) { - return this.then(null, callback); - }, - - "finally": function(callback) { - - function makePromise(value, resolved) { - var result = defer(); - if (resolved) { - result.resolve(value); - } else { - result.reject(value); - } - return result.promise; - } - - function handleCallback(value, isResolved) { - var callbackOutput = null; - try { - callbackOutput = (callback ||defaultCallback)(); - } catch(e) { - return makePromise(e, false); - } - if (callbackOutput && isFunction(callbackOutput.then)) { - return callbackOutput.then(function() { - return makePromise(value, isResolved); - }, function(error) { - return makePromise(error, false); - }); - } else { - return makePromise(value, isResolved); - } - } - - return this.then(function(value) { - return handleCallback(value, true); - }, function(error) { - return handleCallback(error, false); - }); - } - } - }; - - return deferred; - }; - - - var ref = function(value) { - if (value && isFunction(value.then)) return value; - return { - then: function(callback) { - var result = defer(); - nextTick(function() { - result.resolve(callback(value)); - }); - return result.promise; - } - }; - }; - - - /** - * @ngdoc - * @name ng.$q#reject - * @methodOf ng.$q - * @description - * Creates a promise that is resolved as rejected with the specified `reason`. This api should be - * used to forward rejection in a chain of promises. If you are dealing with the last promise in - * a promise chain, you don't need to worry about it. - * - * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of - * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via - * a promise error callback and you want to forward the error to the promise derived from the - * current promise, you have to "rethrow" the error by returning a rejection constructed via - * `reject`. - * - *
    -   *   promiseB = promiseA.then(function(result) {
    -   *     // success: do something and resolve promiseB
    -   *     //          with the old or a new result
    -   *     return result;
    -   *   }, function(reason) {
    -   *     // error: handle the error if possible and
    -   *     //        resolve promiseB with newPromiseOrValue,
    -   *     //        otherwise forward the rejection to promiseB
    -   *     if (canHandle(reason)) {
    -   *      // handle the error and recover
    -   *      return newPromiseOrValue;
    -   *     }
    -   *     return $q.reject(reason);
    -   *   });
    -   * 
    - * - * @param {*} reason Constant, message, exception or an object representing the rejection reason. - * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. - */ - var reject = function(reason) { - var result = defer(); - result.reject(reason); - return result.promise; - }; - - var createInternalRejectedPromise = function(reason) { - return { - then: function(callback, errback) { - var result = defer(); - nextTick(function() { - try { - result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }); - return result.promise; - } - }; - }; - - - /** - * @ngdoc - * @name ng.$q#when - * @methodOf ng.$q - * @description - * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. - * This is useful when you are dealing with an object that might or might not be a promise, or if - * the promise comes from a source that can't be trusted. - * - * @param {*} value Value or a promise - * @returns {Promise} Returns a promise of the passed value or promise - */ - var when = function(value, callback, errback, progressback) { - var result = defer(), - done; - - var wrappedCallback = function(value) { - try { - return (isFunction(callback) ? callback : defaultCallback)(value); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - - var wrappedErrback = function(reason) { - try { - return (isFunction(errback) ? errback : defaultErrback)(reason); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - - var wrappedProgressback = function(progress) { - try { - return (isFunction(progressback) ? progressback : defaultCallback)(progress); - } catch (e) { - exceptionHandler(e); - } - }; - - nextTick(function() { - ref(value).then(function(value) { - if (done) return; - done = true; - result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback)); - }, function(reason) { - if (done) return; - done = true; - result.resolve(wrappedErrback(reason)); - }, function(progress) { - if (done) return; - result.notify(wrappedProgressback(progress)); - }); - }); - - return result.promise; - }; - - - function defaultCallback(value) { - return value; - } - - - function defaultErrback(reason) { - return reject(reason); - } - - - /** - * @ngdoc - * @name ng.$q#all - * @methodOf ng.$q - * @description - * Combines multiple promises into a single promise that is resolved when all of the input - * promises are resolved. - * - * @param {Array.|Object.} promises An array or hash of promises. - * @returns {Promise} Returns a single promise that will be resolved with an array/hash of values, - * each value corresponding to the promise at the same index/key in the `promises` array/hash. - * If any of the promises is resolved with a rejection, this resulting promise will be rejected - * with the same rejection value. - */ - function all(promises) { - var deferred = defer(), - counter = 0, - results = isArray(promises) ? [] : {}; - - forEach(promises, function(promise, key) { - counter++; - ref(promise).then(function(value) { - if (results.hasOwnProperty(key)) return; - results[key] = value; - if (!(--counter)) deferred.resolve(results); - }, function(reason) { - if (results.hasOwnProperty(key)) return; - deferred.reject(reason); - }); - }); - - if (counter === 0) { - deferred.resolve(results); - } - - return deferred.promise; - } - - return { - defer: defer, - reject: reject, - when: when, - all: all - }; -} - -/** - * DESIGN NOTES - * - * The design decisions behind the scope are heavily favored for speed and memory consumption. - * - * The typical use of scope is to watch the expressions, which most of the time return the same - * value as last time so we optimize the operation. - * - * Closures construction is expensive in terms of speed as well as memory: - * - No closures, instead use prototypical inheritance for API - * - Internal state needs to be stored on scope directly, which means that private state is - * exposed as $$____ properties - * - * Loop operations are optimized by using while(count--) { ... } - * - this means that in order to keep the same order of execution as addition we have to add - * items to the array at the beginning (shift) instead of at the end (push) - * - * Child scopes are created and removed often - * - Using an array would be slow since inserts in middle are expensive so we use linked list - * - * There are few watches then a lot of observers. This is why you don't want the observer to be - * implemented in the same way as watch. Watch requires return of initialization function which - * are expensive to construct. - */ - - -/** - * @ngdoc object - * @name ng.$rootScopeProvider - * @description - * - * Provider for the $rootScope service. - */ - -/** - * @ngdoc function - * @name ng.$rootScopeProvider#digestTtl - * @methodOf ng.$rootScopeProvider - * @description - * - * Sets the number of `$digest` iterations the scope should attempt to execute before giving up and - * assuming that the model is unstable. - * - * The current default is 10 iterations. - * - * In complex applications it's possible that the dependencies between `$watch`s will result in - * several digest iterations. However if an application needs more than the default 10 digest - * iterations for its model to stabilize then you should investigate what is causing the model to - * continuously change during the digest. - * - * Increasing the TTL could have performance implications, so you should not change it without - * proper justification. - * - * @param {number} limit The number of digest iterations. - */ - - -/** - * @ngdoc object - * @name ng.$rootScope - * @description - * - * Every application has a single root {@link ng.$rootScope.Scope scope}. - * All other scopes are descendant scopes of the root scope. Scopes provide separation - * between the model and the view, via a mechanism for watching the model for changes. - * They also provide an event emission/broadcast and subscription facility. See the - * {@link guide/scope developer guide on scopes}. - */ -function $RootScopeProvider(){ - var TTL = 10; - var $rootScopeMinErr = minErr('$rootScope'); - var lastDirtyWatch = null; - - this.digestTtl = function(value) { - if (arguments.length) { - TTL = value; - } - return TTL; - }; - - this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', - function( $injector, $exceptionHandler, $parse, $browser) { - - /** - * @ngdoc function - * @name ng.$rootScope.Scope - * - * @description - * A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the - * {@link AUTO.$injector $injector}. Child scopes are created using the - * {@link ng.$rootScope.Scope#methods_$new $new()} method. (Most scopes are created automatically when - * compiled HTML template is executed.) - * - * Here is a simple scope snippet to show how you can interact with the scope. - *
    -     * 
    -     * 
    - * - * # Inheritance - * A scope can inherit from a parent scope, as in this example: - *
    -         var parent = $rootScope;
    -         var child = parent.$new();
    -
    -         parent.salutation = "Hello";
    -         child.name = "World";
    -         expect(child.salutation).toEqual('Hello');
    -
    -         child.salutation = "Welcome";
    -         expect(child.salutation).toEqual('Welcome');
    -         expect(parent.salutation).toEqual('Hello');
    -     * 
    - * - * - * @param {Object.=} providers Map of service factory which need to be - * provided for the current scope. Defaults to {@link ng}. - * @param {Object.=} instanceCache Provides pre-instantiated services which should - * append/override services provided by `providers`. This is handy - * when unit-testing and having the need to override a default - * service. - * @returns {Object} Newly created scope. - * - */ - function Scope() { - this.$id = nextUid(); - this.$$phase = this.$parent = this.$$watchers = - this.$$nextSibling = this.$$prevSibling = - this.$$childHead = this.$$childTail = null; - this['this'] = this.$root = this; - this.$$destroyed = false; - this.$$asyncQueue = []; - this.$$postDigestQueue = []; - this.$$listeners = {}; - this.$$listenerCount = {}; - this.$$isolateBindings = {}; - } - - /** - * @ngdoc property - * @name ng.$rootScope.Scope#$id - * @propertyOf ng.$rootScope.Scope - * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for - * debugging. - */ - - - Scope.prototype = { - constructor: Scope, - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$new - * @methodOf ng.$rootScope.Scope - * @function - * - * @description - * Creates a new child {@link ng.$rootScope.Scope scope}. - * - * The parent scope will propagate the {@link ng.$rootScope.Scope#methods_$digest $digest()} and - * {@link ng.$rootScope.Scope#methods_$digest $digest()} events. The scope can be removed from the - * scope hierarchy using {@link ng.$rootScope.Scope#methods_$destroy $destroy()}. - * - * {@link ng.$rootScope.Scope#methods_$destroy $destroy()} must be called on a scope when it is - * desired for the scope and its child scopes to be permanently detached from the parent and - * thus stop participating in model change detection and listener notification by invoking. - * - * @param {boolean} isolate If true, then the scope does not prototypically inherit from the - * parent scope. The scope is isolated, as it can not see parent scope properties. - * When creating widgets, it is useful for the widget to not accidentally read parent - * state. - * - * @returns {Object} The newly created child scope. - * - */ - $new: function(isolate) { - var ChildScope, - child; - - if (isolate) { - child = new Scope(); - child.$root = this.$root; - // ensure that there is just one async queue per $rootScope and its children - child.$$asyncQueue = this.$$asyncQueue; - child.$$postDigestQueue = this.$$postDigestQueue; - } else { - ChildScope = function() {}; // should be anonymous; This is so that when the minifier munges - // the name it does not become random set of chars. This will then show up as class - // name in the web inspector. - ChildScope.prototype = this; - child = new ChildScope(); - child.$id = nextUid(); - } - child['this'] = child; - child.$$listeners = {}; - child.$$listenerCount = {}; - child.$parent = this; - child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null; - child.$$prevSibling = this.$$childTail; - if (this.$$childHead) { - this.$$childTail.$$nextSibling = child; - this.$$childTail = child; - } else { - this.$$childHead = this.$$childTail = child; - } - return child; - }, - - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$watch - * @methodOf ng.$rootScope.Scope - * @function - * - * @description - * Registers a `listener` callback to be executed whenever the `watchExpression` changes. - * - * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#methods_$digest - * $digest()} and should return the value that will be watched. (Since - * {@link ng.$rootScope.Scope#methods_$digest $digest()} reruns when it detects changes the - * `watchExpression` can execute multiple times per - * {@link ng.$rootScope.Scope#methods_$digest $digest()} and should be idempotent.) - * - The `listener` is called only when the value from the current `watchExpression` and the - * previous call to `watchExpression` are not equal (with the exception of the initial run, - * see below). The inequality is determined according to - * {@link angular.equals} function. To save the value of the object for later comparison, - * the {@link angular.copy} function is used. It also means that watching complex options - * will have adverse memory and performance implications. - * - The watch `listener` may change the model, which may trigger other `listener`s to fire. - * This is achieved by rerunning the watchers until no changes are detected. The rerun - * iteration limit is 10 to prevent an infinite loop deadlock. - * - * - * If you want to be notified whenever {@link ng.$rootScope.Scope#methods_$digest $digest} is called, - * you can register a `watchExpression` function with no `listener`. (Since `watchExpression` - * can execute multiple times per {@link ng.$rootScope.Scope#methods_$digest $digest} cycle when a - * change is detected, be prepared for multiple calls to your listener.) - * - * After a watcher is registered with the scope, the `listener` fn is called asynchronously - * (via {@link ng.$rootScope.Scope#methods_$evalAsync $evalAsync}) to initialize the - * watcher. In rare cases, this is undesirable because the listener is called when the result - * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you - * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the - * listener was called due to initialization. - * - * The example below contains an illustration of using a function as your $watch listener - * - * - * # Example - *
    -           // let's assume that scope was dependency injected as the $rootScope
    -           var scope = $rootScope;
    -           scope.name = 'misko';
    -           scope.counter = 0;
    -
    -           expect(scope.counter).toEqual(0);
    -           scope.$watch('name', function(newValue, oldValue) {
    -             scope.counter = scope.counter + 1;
    -           });
    -           expect(scope.counter).toEqual(0);
    -
    -           scope.$digest();
    -           // no variable change
    -           expect(scope.counter).toEqual(0);
    -
    -           scope.name = 'adam';
    -           scope.$digest();
    -           expect(scope.counter).toEqual(1);
    -
    -
    -
    -           // Using a listener function
    -           var food;
    -           scope.foodCounter = 0;
    -           expect(scope.foodCounter).toEqual(0);
    -           scope.$watch(
    -             // This is the listener function
    -             function() { return food; },
    -             // This is the change handler
    -             function(newValue, oldValue) {
    -               if ( newValue !== oldValue ) {
    -                 // Only increment the counter if the value changed
    -                 scope.foodCounter = scope.foodCounter + 1;
    -               }
    -             }
    -           );
    -           // No digest has been run so the counter will be zero
    -           expect(scope.foodCounter).toEqual(0);
    -
    -           // Run the digest but since food has not changed count will still be zero
    -           scope.$digest();
    -           expect(scope.foodCounter).toEqual(0);
    -
    -           // Update food and run digest.  Now the counter will increment
    -           food = 'cheeseburger';
    -           scope.$digest();
    -           expect(scope.foodCounter).toEqual(1);
    -
    -       * 
    - * - * - * - * @param {(function()|string)} watchExpression Expression that is evaluated on each - * {@link ng.$rootScope.Scope#methods_$digest $digest} cycle. A change in the return value triggers - * a call to the `listener`. - * - * - `string`: Evaluated as {@link guide/expression expression} - * - `function(scope)`: called with current `scope` as a parameter. - * @param {(function()|string)=} listener Callback called whenever the return value of - * the `watchExpression` changes. - * - * - `string`: Evaluated as {@link guide/expression expression} - * - `function(newValue, oldValue, scope)`: called with current and previous values as - * parameters. - * - * @param {boolean=} objectEquality Compare object for equality rather than for reference. - * @returns {function()} Returns a deregistration function for this listener. - */ - $watch: function(watchExp, listener, objectEquality) { - var scope = this, - get = compileToFn(watchExp, 'watch'), - array = scope.$$watchers, - watcher = { - fn: listener, - last: initWatchVal, - get: get, - exp: watchExp, - eq: !!objectEquality - }; - - lastDirtyWatch = null; - - // in the case user pass string, we need to compile it, do we really need this ? - if (!isFunction(listener)) { - var listenFn = compileToFn(listener || noop, 'listener'); - watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; - } - - if (typeof watchExp == 'string' && get.constant) { - var originalFn = watcher.fn; - watcher.fn = function(newVal, oldVal, scope) { - originalFn.call(this, newVal, oldVal, scope); - arrayRemove(array, watcher); - }; - } - - if (!array) { - array = scope.$$watchers = []; - } - // we use unshift since we use a while loop in $digest for speed. - // the while loop reads in reverse order. - array.unshift(watcher); - - return function() { - arrayRemove(array, watcher); - lastDirtyWatch = null; - }; - }, - - - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$watchCollection - * @methodOf ng.$rootScope.Scope - * @function - * - * @description - * Shallow watches the properties of an object and fires whenever any of the properties change - * (for arrays, this implies watching the array items; for object maps, this implies watching - * the properties). If a change is detected, the `listener` callback is fired. - * - * - The `obj` collection is observed via standard $watch operation and is examined on every - * call to $digest() to see if any items have been added, removed, or moved. - * - The `listener` is called whenever anything within the `obj` has changed. Examples include - * adding, removing, and moving items belonging to an object or array. - * - * - * # Example - *
    -          $scope.names = ['igor', 'matias', 'misko', 'james'];
    -          $scope.dataCount = 4;
    -
    -          $scope.$watchCollection('names', function(newNames, oldNames) {
    -            $scope.dataCount = newNames.length;
    -          });
    -
    -          expect($scope.dataCount).toEqual(4);
    -          $scope.$digest();
    -
    -          //still at 4 ... no changes
    -          expect($scope.dataCount).toEqual(4);
    -
    -          $scope.names.pop();
    -          $scope.$digest();
    -
    -          //now there's been a change
    -          expect($scope.dataCount).toEqual(3);
    -       * 
    - * - * - * @param {string|Function(scope)} obj Evaluated as {@link guide/expression expression}. The - * expression value should evaluate to an object or an array which is observed on each - * {@link ng.$rootScope.Scope#methods_$digest $digest} cycle. Any shallow change within the - * collection will trigger a call to the `listener`. - * - * @param {function(newCollection, oldCollection, scope)} listener a callback function that is - * fired with both the `newCollection` and `oldCollection` as parameters. - * The `newCollection` object is the newly modified data obtained from the `obj` expression - * and the `oldCollection` object is a copy of the former collection data. - * The `scope` refers to the current scope. - * - * @returns {function()} Returns a de-registration function for this listener. When the - * de-registration function is executed, the internal watch operation is terminated. - */ - $watchCollection: function(obj, listener) { - var self = this; - var oldValue; - var newValue; - var changeDetected = 0; - var objGetter = $parse(obj); - var internalArray = []; - var internalObject = {}; - var oldLength = 0; - - function $watchCollectionWatch() { - newValue = objGetter(self); - var newLength, key; - - if (!isObject(newValue)) { - if (oldValue !== newValue) { - oldValue = newValue; - changeDetected++; - } - } else if (isArrayLike(newValue)) { - if (oldValue !== internalArray) { - // we are transitioning from something which was not an array into array. - oldValue = internalArray; - oldLength = oldValue.length = 0; - changeDetected++; - } - - newLength = newValue.length; - - if (oldLength !== newLength) { - // if lengths do not match we need to trigger change notification - changeDetected++; - oldValue.length = oldLength = newLength; - } - // copy the items to oldValue and look for changes. - for (var i = 0; i < newLength; i++) { - if (oldValue[i] !== newValue[i]) { - changeDetected++; - oldValue[i] = newValue[i]; - } - } - } else { - if (oldValue !== internalObject) { - // we are transitioning from something which was not an object into object. - oldValue = internalObject = {}; - oldLength = 0; - changeDetected++; - } - // copy the items to oldValue and look for changes. - newLength = 0; - for (key in newValue) { - if (newValue.hasOwnProperty(key)) { - newLength++; - if (oldValue.hasOwnProperty(key)) { - if (oldValue[key] !== newValue[key]) { - changeDetected++; - oldValue[key] = newValue[key]; - } - } else { - oldLength++; - oldValue[key] = newValue[key]; - changeDetected++; - } - } - } - if (oldLength > newLength) { - // we used to have more keys, need to find them and destroy them. - changeDetected++; - for(key in oldValue) { - if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) { - oldLength--; - delete oldValue[key]; - } - } - } - } - return changeDetected; - } - - function $watchCollectionAction() { - listener(newValue, oldValue, self); - } - - return this.$watch($watchCollectionWatch, $watchCollectionAction); - }, - - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$digest - * @methodOf ng.$rootScope.Scope - * @function - * - * @description - * Processes all of the {@link ng.$rootScope.Scope#methods_$watch watchers} of the current scope and - * its children. Because a {@link ng.$rootScope.Scope#methods_$watch watcher}'s listener can change - * the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#methods_$watch watchers} - * until no more listeners are firing. This means that it is possible to get into an infinite - * loop. This function will throw `'Maximum iteration limit exceeded.'` if the number of - * iterations exceeds 10. - * - * Usually, you don't call `$digest()` directly in - * {@link ng.directive:ngController controllers} or in - * {@link ng.$compileProvider#methods_directive directives}. - * Instead, you should call {@link ng.$rootScope.Scope#methods_$apply $apply()} (typically from within - * a {@link ng.$compileProvider#methods_directive directives}), which will force a `$digest()`. - * - * If you want to be notified whenever `$digest()` is called, - * you can register a `watchExpression` function with - * {@link ng.$rootScope.Scope#methods_$watch $watch()} with no `listener`. - * - * In unit tests, you may need to call `$digest()` to simulate the scope life cycle. - * - * # Example - *
    -           var scope = ...;
    -           scope.name = 'misko';
    -           scope.counter = 0;
    -
    -           expect(scope.counter).toEqual(0);
    -           scope.$watch('name', function(newValue, oldValue) {
    -             scope.counter = scope.counter + 1;
    -           });
    -           expect(scope.counter).toEqual(0);
    -
    -           scope.$digest();
    -           // no variable change
    -           expect(scope.counter).toEqual(0);
    -
    -           scope.name = 'adam';
    -           scope.$digest();
    -           expect(scope.counter).toEqual(1);
    -       * 
    - * - */ - $digest: function() { - var watch, value, last, - watchers, - asyncQueue = this.$$asyncQueue, - postDigestQueue = this.$$postDigestQueue, - length, - dirty, ttl = TTL, - next, current, target = this, - watchLog = [], - logIdx, logMsg, asyncTask; - - beginPhase('$digest'); - - lastDirtyWatch = null; - - do { // "while dirty" loop - dirty = false; - current = target; - - while(asyncQueue.length) { - try { - asyncTask = asyncQueue.shift(); - asyncTask.scope.$eval(asyncTask.expression); - } catch (e) { - clearPhase(); - $exceptionHandler(e); - } - lastDirtyWatch = null; - } - - traverseScopesLoop: - do { // "traverse the scopes" loop - if ((watchers = current.$$watchers)) { - // process our watches - length = watchers.length; - while (length--) { - try { - watch = watchers[length]; - // Most common watches are on primitives, in which case we can short - // circuit it with === operator, only when === fails do we use .equals - if (watch) { - if ((value = watch.get(current)) !== (last = watch.last) && - !(watch.eq - ? equals(value, last) - : (typeof value == 'number' && typeof last == 'number' - && isNaN(value) && isNaN(last)))) { - dirty = true; - lastDirtyWatch = watch; - watch.last = watch.eq ? copy(value) : value; - watch.fn(value, ((last === initWatchVal) ? value : last), current); - if (ttl < 5) { - logIdx = 4 - ttl; - if (!watchLog[logIdx]) watchLog[logIdx] = []; - logMsg = (isFunction(watch.exp)) - ? 'fn: ' + (watch.exp.name || watch.exp.toString()) - : watch.exp; - logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); - watchLog[logIdx].push(logMsg); - } - } else if (watch === lastDirtyWatch) { - // If the most recently dirty watcher is now clean, short circuit since the remaining watchers - // have already been tested. - dirty = false; - break traverseScopesLoop; - } - } - } catch (e) { - clearPhase(); - $exceptionHandler(e); - } - } - } - - // Insanity Warning: scope depth-first traversal - // yes, this code is a bit crazy, but it works and we have tests to prove it! - // this piece should be kept in sync with the traversal in $broadcast - if (!(next = (current.$$childHead || - (current !== target && current.$$nextSibling)))) { - while(current !== target && !(next = current.$$nextSibling)) { - current = current.$parent; - } - } - } while ((current = next)); - - // `break traverseScopesLoop;` takes us to here - - if((dirty || asyncQueue.length) && !(ttl--)) { - clearPhase(); - throw $rootScopeMinErr('infdig', - '{0} $digest() iterations reached. Aborting!\n' + - 'Watchers fired in the last 5 iterations: {1}', - TTL, toJson(watchLog)); - } - - } while (dirty || asyncQueue.length); - - clearPhase(); - - while(postDigestQueue.length) { - try { - postDigestQueue.shift()(); - } catch (e) { - $exceptionHandler(e); - } - } - }, - - - /** - * @ngdoc event - * @name ng.$rootScope.Scope#$destroy - * @eventOf ng.$rootScope.Scope - * @eventType broadcast on scope being destroyed - * - * @description - * Broadcasted when a scope and its children are being destroyed. - * - * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to - * clean up DOM bindings before an element is removed from the DOM. - */ - - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$destroy - * @methodOf ng.$rootScope.Scope - * @function - * - * @description - * Removes the current scope (and all of its children) from the parent scope. Removal implies - * that calls to {@link ng.$rootScope.Scope#methods_$digest $digest()} will no longer - * propagate to the current scope and its children. Removal also implies that the current - * scope is eligible for garbage collection. - * - * The `$destroy()` is usually used by directives such as - * {@link ng.directive:ngRepeat ngRepeat} for managing the - * unrolling of the loop. - * - * Just before a scope is destroyed, a `$destroy` event is broadcasted on this scope. - * Application code can register a `$destroy` event handler that will give it a chance to - * perform any necessary cleanup. - * - * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to - * clean up DOM bindings before an element is removed from the DOM. - */ - $destroy: function() { - // we can't destroy the root scope or a scope that has been already destroyed - if (this.$$destroyed) return; - var parent = this.$parent; - - this.$broadcast('$destroy'); - this.$$destroyed = true; - if (this === $rootScope) return; - - forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); - - if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; - if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; - if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; - if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; - - // This is bogus code that works around Chrome's GC leak - // see: https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 - this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = - this.$$childTail = null; - }, - - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$eval - * @methodOf ng.$rootScope.Scope - * @function - * - * @description - * Executes the `expression` on the current scope and returns the result. Any exceptions in - * the expression are propagated (uncaught). This is useful when evaluating Angular - * expressions. - * - * # Example - *
    -           var scope = ng.$rootScope.Scope();
    -           scope.a = 1;
    -           scope.b = 2;
    -
    -           expect(scope.$eval('a+b')).toEqual(3);
    -           expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3);
    -       * 
    - * - * @param {(string|function())=} expression An angular expression to be executed. - * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. - * - `function(scope)`: execute the function with the current `scope` parameter. - * - * @param {(object)=} locals Local variables object, useful for overriding values in scope. - * @returns {*} The result of evaluating the expression. - */ - $eval: function(expr, locals) { - return $parse(expr)(this, locals); - }, - - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$evalAsync - * @methodOf ng.$rootScope.Scope - * @function - * - * @description - * Executes the expression on the current scope at a later point in time. - * - * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only - * that: - * - * - it will execute after the function that scheduled the evaluation (preferably before DOM - * rendering). - * - at least one {@link ng.$rootScope.Scope#methods_$digest $digest cycle} will be performed after - * `expression` execution. - * - * Any exceptions from the execution of the expression are forwarded to the - * {@link ng.$exceptionHandler $exceptionHandler} service. - * - * __Note:__ if this function is called outside of a `$digest` cycle, a new `$digest` cycle - * will be scheduled. However, it is encouraged to always call code that changes the model - * from within an `$apply` call. That includes code evaluated via `$evalAsync`. - * - * @param {(string|function())=} expression An angular expression to be executed. - * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. - * - `function(scope)`: execute the function with the current `scope` parameter. - * - */ - $evalAsync: function(expr) { - // if we are outside of an $digest loop and this is the first time we are scheduling async - // task also schedule async auto-flush - if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { - $browser.defer(function() { - if ($rootScope.$$asyncQueue.length) { - $rootScope.$digest(); - } - }); - } - - this.$$asyncQueue.push({scope: this, expression: expr}); - }, - - $$postDigest : function(fn) { - this.$$postDigestQueue.push(fn); - }, - - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$apply - * @methodOf ng.$rootScope.Scope - * @function - * - * @description - * `$apply()` is used to execute an expression in angular from outside of the angular - * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). - * Because we are calling into the angular framework we need to perform proper scope life - * cycle of {@link ng.$exceptionHandler exception handling}, - * {@link ng.$rootScope.Scope#methods_$digest executing watches}. - * - * ## Life cycle - * - * # Pseudo-Code of `$apply()` - *
    -           function $apply(expr) {
    -             try {
    -               return $eval(expr);
    -             } catch (e) {
    -               $exceptionHandler(e);
    -             } finally {
    -               $root.$digest();
    -             }
    -           }
    -       * 
    - * - * - * Scope's `$apply()` method transitions through the following stages: - * - * 1. The {@link guide/expression expression} is executed using the - * {@link ng.$rootScope.Scope#methods_$eval $eval()} method. - * 2. Any exceptions from the execution of the expression are forwarded to the - * {@link ng.$exceptionHandler $exceptionHandler} service. - * 3. The {@link ng.$rootScope.Scope#methods_$watch watch} listeners are fired immediately after the - * expression was executed using the {@link ng.$rootScope.Scope#methods_$digest $digest()} method. - * - * - * @param {(string|function())=} exp An angular expression to be executed. - * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. - * - `function(scope)`: execute the function with current `scope` parameter. - * - * @returns {*} The result of evaluating the expression. - */ - $apply: function(expr) { - try { - beginPhase('$apply'); - return this.$eval(expr); - } catch (e) { - $exceptionHandler(e); - } finally { - clearPhase(); - try { - $rootScope.$digest(); - } catch (e) { - $exceptionHandler(e); - throw e; - } - } - }, - - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$on - * @methodOf ng.$rootScope.Scope - * @function - * - * @description - * Listens on events of a given type. See {@link ng.$rootScope.Scope#methods_$emit $emit} for - * discussion of event life cycle. - * - * The event listener function format is: `function(event, args...)`. The `event` object - * passed into the listener has the following attributes: - * - * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or - * `$broadcast`-ed. - * - `currentScope` - `{Scope}`: the current scope which is handling the event. - * - `name` - `{string}`: name of the event. - * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel - * further event propagation (available only for events that were `$emit`-ed). - * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag - * to true. - * - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called. - * - * @param {string} name Event name to listen on. - * @param {function(event, args...)} listener Function to call when the event is emitted. - * @returns {function()} Returns a deregistration function for this listener. - */ - $on: function(name, listener) { - var namedListeners = this.$$listeners[name]; - if (!namedListeners) { - this.$$listeners[name] = namedListeners = []; - } - namedListeners.push(listener); - - var current = this; - do { - if (!current.$$listenerCount[name]) { - current.$$listenerCount[name] = 0; - } - current.$$listenerCount[name]++; - } while ((current = current.$parent)); - - var self = this; - return function() { - namedListeners[indexOf(namedListeners, listener)] = null; - decrementListenerCount(self, 1, name); - }; - }, - - - /** - * @ngdoc function - * @name ng.$rootScope.Scope#$emit - * @methodOf ng.$rootScope.Scope - * @function - * - * @description - * Dispatches an event `name` upwards through the scope hierarchy notifying the - * registered {@link ng.$rootScope.Scope#methods_$on} listeners. - * - * The event life cycle starts at the scope on which `$emit` was called. All - * {@link ng.$rootScope.Scope#methods_$on listeners} listening for `name` event on this scope get - * notified. Afterwards, the event traverses upwards toward the root scope and calls all - * registered listeners along the way. The event will stop propagating if one of the listeners - * cancels it. - * - * Any exception emitted from the {@link ng.$rootScope.Scope#methods_$on listeners} will be passed - * onto the {@link ng.$exceptionHandler $exceptionHandler} service. - * - * @param {string} name Event name to emit. - * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. - * @return {Object} Event object (see {@link ng.$rootScope.Scope#methods_$on}). - */ - $emit: function(name, args) { - var empty = [], - namedListeners, - scope = this, - stopPropagation = false, - event = { - name: name, - targetScope: scope, - stopPropagation: function() {stopPropagation = true;}, - preventDefault: function() { - event.defaultPrevented = true; - }, - defaultPrevented: false - }, - listenerArgs = concat([event], arguments, 1), - i, length; - - do { - namedListeners = scope.$$listeners[name] || empty; - event.currentScope = scope; - for (i=0, length=namedListeners.length; i= 8 ) { - normalizedVal = urlResolve(uri).href; - if (normalizedVal !== '' && !normalizedVal.match(regex)) { - return 'unsafe:'+normalizedVal; - } - } - return uri; - }; - }; -} - -var $sceMinErr = minErr('$sce'); - -var SCE_CONTEXTS = { - HTML: 'html', - CSS: 'css', - URL: 'url', - // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a - // url. (e.g. ng-include, script src, templateUrl) - RESOURCE_URL: 'resourceUrl', - JS: 'js' -}; - -// Helper functions follow. - -// Copied from: -// http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962 -// Prereq: s is a string. -function escapeForRegexp(s) { - return s.replace(/([-()\[\]{}+?*.$\^|,:# -1) { - throw $sceMinErr('iwcard', - 'Illegal sequence *** in string matcher. String: {0}', matcher); - } - matcher = escapeForRegexp(matcher). - replace('\\*\\*', '.*'). - replace('\\*', '[^:/.?&;]*'); - return new RegExp('^' + matcher + '$'); - } else if (isRegExp(matcher)) { - // The only other type of matcher allowed is a Regexp. - // Match entire URL / disallow partial matches. - // Flags are reset (i.e. no global, ignoreCase or multiline) - return new RegExp('^' + matcher.source + '$'); - } else { - throw $sceMinErr('imatcher', - 'Matchers may only be "self", string patterns or RegExp objects'); - } -} - - -function adjustMatchers(matchers) { - var adjustedMatchers = []; - if (isDefined(matchers)) { - forEach(matchers, function(matcher) { - adjustedMatchers.push(adjustMatcher(matcher)); - }); - } - return adjustedMatchers; -} - - -/** - * @ngdoc service - * @name ng.$sceDelegate - * @function - * - * @description - * - * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict - * Contextual Escaping (SCE)} services to AngularJS. - * - * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of - * the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is - * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to - * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things - * work because `$sce` delegates to `$sceDelegate` for these operations. - * - * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service. - * - * The default instance of `$sceDelegate` should work out of the box with little pain. While you - * can override it completely to change the behavior of `$sce`, the common case would - * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting - * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as - * templates. Refer {@link ng.$sceDelegateProvider#methods_resourceUrlWhitelist - * $sceDelegateProvider.resourceUrlWhitelist} and {@link - * ng.$sceDelegateProvider#methods_resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} - */ - -/** - * @ngdoc object - * @name ng.$sceDelegateProvider - * @description - * - * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate - * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure - * that the URLs used for sourcing Angular templates are safe. Refer {@link - * ng.$sceDelegateProvider#methods_resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and - * {@link ng.$sceDelegateProvider#methods_resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} - * - * For the general details about this service in Angular, read the main page for {@link ng.$sce - * Strict Contextual Escaping (SCE)}. - * - * **Example**: Consider the following case. - * - * - your app is hosted at url `http://myapp.example.com/` - * - but some of your templates are hosted on other domains you control such as - * `http://srv01.assets.example.com/`,  `http://srv02.assets.example.com/`, etc. - * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. - * - * Here is what a secure configuration for this scenario might look like: - * - *
    - *    angular.module('myApp', []).config(function($sceDelegateProvider) {
    - *      $sceDelegateProvider.resourceUrlWhitelist([
    - *        // Allow same origin resource loads.
    - *        'self',
    - *        // Allow loading from our assets domain.  Notice the difference between * and **.
    - *        'http://srv*.assets.example.com/**']);
    - *
    - *      // The blacklist overrides the whitelist so the open redirect here is blocked.
    - *      $sceDelegateProvider.resourceUrlBlacklist([
    - *        'http://myapp.example.com/clickThru**']);
    - *      });
    - * 
    - */ - -function $SceDelegateProvider() { - this.SCE_CONTEXTS = SCE_CONTEXTS; - - // Resource URLs can also be trusted by policy. - var resourceUrlWhitelist = ['self'], - resourceUrlBlacklist = []; - - /** - * @ngdoc function - * @name ng.sceDelegateProvider#resourceUrlWhitelist - * @methodOf ng.$sceDelegateProvider - * @function - * - * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value - * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. - * - * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items - * allowed in this array. - * - * Note: **an empty whitelist array will block all URLs**! - * - * @return {Array} the currently set whitelist array. - * - * The **default value** when no whitelist has been explicitly set is `['self']` allowing only - * same origin resource requests. - * - * @description - * Sets/Gets the whitelist of trusted resource URLs. - */ - this.resourceUrlWhitelist = function (value) { - if (arguments.length) { - resourceUrlWhitelist = adjustMatchers(value); - } - return resourceUrlWhitelist; - }; - - /** - * @ngdoc function - * @name ng.sceDelegateProvider#resourceUrlBlacklist - * @methodOf ng.$sceDelegateProvider - * @function - * - * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value - * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. - * - * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items - * allowed in this array. - * - * The typical usage for the blacklist is to **block - * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as - * these would otherwise be trusted but actually return content from the redirected domain. - * - * Finally, **the blacklist overrides the whitelist** and has the final say. - * - * @return {Array} the currently set blacklist array. - * - * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there - * is no blacklist.) - * - * @description - * Sets/Gets the blacklist of trusted resource URLs. - */ - - this.resourceUrlBlacklist = function (value) { - if (arguments.length) { - resourceUrlBlacklist = adjustMatchers(value); - } - return resourceUrlBlacklist; - }; - - this.$get = ['$injector', function($injector) { - - var htmlSanitizer = function htmlSanitizer(html) { - throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); - }; - - if ($injector.has('$sanitize')) { - htmlSanitizer = $injector.get('$sanitize'); - } - - - function matchUrl(matcher, parsedUrl) { - if (matcher === 'self') { - return urlIsSameOrigin(parsedUrl); - } else { - // definitely a regex. See adjustMatchers() - return !!matcher.exec(parsedUrl.href); - } - } - - function isResourceUrlAllowedByPolicy(url) { - var parsedUrl = urlResolve(url.toString()); - var i, n, allowed = false; - // Ensure that at least one item from the whitelist allows this url. - for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) { - if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) { - allowed = true; - break; - } - } - if (allowed) { - // Ensure that no item from the blacklist blocked this url. - for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) { - if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) { - allowed = false; - break; - } - } - } - return allowed; - } - - function generateHolderType(Base) { - var holderType = function TrustedValueHolderType(trustedValue) { - this.$$unwrapTrustedValue = function() { - return trustedValue; - }; - }; - if (Base) { - holderType.prototype = new Base(); - } - holderType.prototype.valueOf = function sceValueOf() { - return this.$$unwrapTrustedValue(); - }; - holderType.prototype.toString = function sceToString() { - return this.$$unwrapTrustedValue().toString(); - }; - return holderType; - } - - var trustedValueHolderBase = generateHolderType(), - byType = {}; - - byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase); - byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase); - byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase); - byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase); - byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]); - - /** - * @ngdoc method - * @name ng.$sceDelegate#trustAs - * @methodOf ng.$sceDelegate - * - * @description - * Returns an object that is trusted by angular for use in specified strict - * contextual escaping contexts (such as ng-bind-html, ng-include, any src - * attribute interpolation, any dom event binding attribute interpolation - * such as for onclick, etc.) that uses the provided value. - * See {@link ng.$sce $sce} for enabling strict contextual escaping. - * - * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resourceUrl, html, js and css. - * @param {*} value The value that that should be considered trusted/safe. - * @returns {*} A value that can be used to stand in for the provided `value` in places - * where Angular expects a $sce.trustAs() return value. - */ - function trustAs(type, trustedValue) { - var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); - if (!Constructor) { - throw $sceMinErr('icontext', - 'Attempted to trust a value in invalid context. Context: {0}; Value: {1}', - type, trustedValue); - } - if (trustedValue === null || trustedValue === undefined || trustedValue === '') { - return trustedValue; - } - // All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting - // mutable objects, we ensure here that the value passed in is actually a string. - if (typeof trustedValue !== 'string') { - throw $sceMinErr('itype', - 'Attempted to trust a non-string value in a content requiring a string: Context: {0}', - type); - } - return new Constructor(trustedValue); - } - - /** - * @ngdoc method - * @name ng.$sceDelegate#valueOf - * @methodOf ng.$sceDelegate - * - * @description - * If the passed parameter had been returned by a prior call to {@link ng.$sceDelegate#methods_trustAs - * `$sceDelegate.trustAs`}, returns the value that had been passed to {@link - * ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`}. - * - * If the passed parameter is not a value that had been returned by {@link - * ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`}, returns it as-is. - * - * @param {*} value The result of a prior {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`} - * call or anything else. - * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#methods_trustAs - * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns - * `value` unchanged. - */ - function valueOf(maybeTrusted) { - if (maybeTrusted instanceof trustedValueHolderBase) { - return maybeTrusted.$$unwrapTrustedValue(); - } else { - return maybeTrusted; - } - } - - /** - * @ngdoc method - * @name ng.$sceDelegate#getTrusted - * @methodOf ng.$sceDelegate - * - * @description - * Takes the result of a {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`} call and - * returns the originally supplied value if the queried context type is a supertype of the - * created type. If this condition isn't satisfied, throws an exception. - * - * @param {string} type The kind of context in which this value is to be used. - * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#methods_trustAs - * `$sceDelegate.trustAs`} call. - * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#methods_trustAs - * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. - */ - function getTrusted(type, maybeTrusted) { - if (maybeTrusted === null || maybeTrusted === undefined || maybeTrusted === '') { - return maybeTrusted; - } - var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); - if (constructor && maybeTrusted instanceof constructor) { - return maybeTrusted.$$unwrapTrustedValue(); - } - // If we get here, then we may only take one of two actions. - // 1. sanitize the value for the requested type, or - // 2. throw an exception. - if (type === SCE_CONTEXTS.RESOURCE_URL) { - if (isResourceUrlAllowedByPolicy(maybeTrusted)) { - return maybeTrusted; - } else { - throw $sceMinErr('insecurl', - 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}', - maybeTrusted.toString()); - } - } else if (type === SCE_CONTEXTS.HTML) { - return htmlSanitizer(maybeTrusted); - } - throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); - } - - return { trustAs: trustAs, - getTrusted: getTrusted, - valueOf: valueOf }; - }]; -} - - -/** - * @ngdoc object - * @name ng.$sceProvider - * @description - * - * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. - * - enable/disable Strict Contextual Escaping (SCE) in a module - * - override the default implementation with a custom delegate - * - * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. - */ - -/* jshint maxlen: false*/ - -/** - * @ngdoc service - * @name ng.$sce - * @function - * - * @description - * - * `$sce` is a service that provides Strict Contextual Escaping services to AngularJS. - * - * # Strict Contextual Escaping - * - * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain - * contexts to result in a value that is marked as safe to use for that context. One example of - * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer - * to these contexts as privileged or SCE contexts. - * - * As of version 1.2, Angular ships with SCE enabled by default. - * - * Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows - * one to execute arbitrary javascript by the use of the expression() syntax. Refer - * to learn more about them. - * You can ensure your document is in standards mode and not quirks mode by adding `` - * to the top of your HTML document. - * - * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for - * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. - * - * Here's an example of a binding in a privileged context: - * - *
    - *     
    - *     
    - *
    - * - * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE - * disabled, this application allows the user to render arbitrary HTML into the DIV. - * In a more realistic example, one may be rendering user comments, blog articles, etc. via - * bindings. (HTML is just one example of a context where rendering user controlled input creates - * security vulnerabilities.) - * - * For the case of HTML, you might use a library, either on the client side, or on the server side, - * to sanitize unsafe HTML before binding to the value and rendering it in the document. - * - * How would you ensure that every place that used these types of bindings was bound to a value that - * was sanitized by your library (or returned as safe for rendering by your server?) How can you - * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some - * properties/fields and forgot to update the binding to the sanitized value? - * - * To be secure by default, you want to ensure that any such bindings are disallowed unless you can - * determine that something explicitly says it's safe to use a value for binding in that - * context. You can then audit your code (a simple grep would do) to ensure that this is only done - * for those values that you can easily tell are safe - because they were received from your server, - * sanitized by your library, etc. You can organize your codebase to help with this - perhaps - * allowing only the files in a specific directory to do this. Ensuring that the internal API - * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. - * - * In the case of AngularJS' SCE service, one uses {@link ng.$sce#methods_trustAs $sce.trustAs} - * (and shorthand methods such as {@link ng.$sce#methods_trustAsHtml $sce.trustAsHtml}, etc.) to - * obtain values that will be accepted by SCE / privileged contexts. - * - * - * ## How does it work? - * - * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#methods_getTrusted - * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link - * ng.$sce#methods_parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the - * {@link ng.$sce#methods_getTrusted $sce.getTrusted} behind the scenes on non-constant literals. - * - * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link - * ng.$sce#methods_parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly - * simplified): - * - *
    - *   var ngBindHtmlDirective = ['$sce', function($sce) {
    - *     return function(scope, element, attr) {
    - *       scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) {
    - *         element.html(value || '');
    - *       });
    - *     };
    - *   }];
    - * 
    - * - * ## Impact on loading templates - * - * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as - * `templateUrl`'s specified by {@link guide/directive directives}. - * - * By default, Angular only loads templates from the same domain and protocol as the application - * document. This is done by calling {@link ng.$sce#methods_getTrustedResourceUrl - * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or - * protocols, you may either either {@link ng.$sceDelegateProvider#methods_resourceUrlWhitelist whitelist - * them} or {@link ng.$sce#methods_trustAsResourceUrl wrap it} into a trusted value. - * - * *Please note*: - * The browser's - * {@link https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest - * Same Origin Policy} and {@link http://www.w3.org/TR/cors/ Cross-Origin Resource Sharing (CORS)} - * policy apply in addition to this and may further restrict whether the template is successfully - * loaded. This means that without the right CORS policy, loading templates from a different domain - * won't work on all browsers. Also, loading templates from `file://` URL does not work on some - * browsers. - * - * ## This feels like too much overhead for the developer? - * - * It's important to remember that SCE only applies to interpolation expressions. - * - * If your expressions are constant literals, they're automatically trusted and you don't need to - * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g. - * `
    `) just works. - * - * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them - * through {@link ng.$sce#methods_getTrusted $sce.getTrusted}. SCE doesn't play a role here. - * - * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load - * templates in `ng-include` from your application's domain without having to even know about SCE. - * It blocks loading templates from other domains or loading templates over http from an https - * served document. You can change these by setting your own custom {@link - * ng.$sceDelegateProvider#methods_resourceUrlWhitelist whitelists} and {@link - * ng.$sceDelegateProvider#methods_resourceUrlBlacklist blacklists} for matching such URLs. - * - * This significantly reduces the overhead. It is far easier to pay the small overhead and have an - * application that's secure and can be audited to verify that with much more ease than bolting - * security onto an application later. - * - * - * ## What trusted context types are supported? - * - * | Context | Notes | - * |---------------------|----------------| - * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. | - * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | - * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
    Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | - * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | - * - * ## Format of items in {@link ng.$sceDelegateProvider#methods_resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#methods_resourceUrlBlacklist Blacklist}
    - * - * Each element in these arrays must be one of the following: - * - * - **'self'** - * - The special **string**, `'self'`, can be used to match against all URLs of the **same - * domain** as the application document using the **same protocol**. - * - **String** (except the special value `'self'`) - * - The string is matched against the full *normalized / absolute URL* of the resource - * being tested (substring matches are not good enough.) - * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters - * match themselves. - * - `*`: matches zero or more occurances of any character other than one of the following 6 - * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use - * in a whitelist. - * - `**`: matches zero or more occurances of *any* character. As such, it's not - * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g. - * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might - * not have been the intention.) It's usage at the very end of the path is ok. (e.g. - * http://foo.example.com/templates/**). - * - **RegExp** (*see caveat below*) - * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax - * (and all the inevitable escaping) makes them *harder to maintain*. It's easy to - * accidentally introduce a bug when one updates a complex expression (imho, all regexes should - * have good test coverage.). For instance, the use of `.` in the regex is correct only in a - * small number of cases. A `.` character in the regex used when matching the scheme or a - * subdomain could be matched against a `:` or literal `.` that was likely not intended. It - * is highly recommended to use the string patterns and only fall back to regular expressions - * if they as a last resort. - * - The regular expression must be an instance of RegExp (i.e. not a string.) It is - * matched against the **entire** *normalized / absolute URL* of the resource being tested - * (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags - * present on the RegExp (such as multiline, global, ignoreCase) are ignored. - * - If you are generating your JavaScript from some other templating engine (not - * recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)), - * remember to escape your regular expression (and be aware that you might need more than - * one level of escaping depending on your templating engine and the way you interpolated - * the value.) Do make use of your platform's escaping mechanism as it might be good - * enough before coding your own. e.g. Ruby has - * [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape) - * and Python has [re.escape](http://docs.python.org/library/re.html#re.escape). - * Javascript lacks a similar built in function for escaping. Take a look at Google - * Closure library's [goog.string.regExpEscape(s)]( - * http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962). - * - * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example. - * - * ## Show me an example using SCE. - * - * @example - - -
    -

    - User comments
    - By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when - $sanitize is available. If $sanitize isn't available, this results in an error instead of an - exploit. -
    -
    - {{userComment.name}}: - -
    -
    -
    -
    -
    - - - var mySceApp = angular.module('mySceApp', ['ngSanitize']); - - mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) { - var self = this; - $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { - self.userComments = userComments; - }); - self.explicitlyTrustedHtml = $sce.trustAsHtml( - 'Hover over this text.'); - }); - - - -[ - { "name": "Alice", - "htmlComment": - "Is anyone reading this?" - }, - { "name": "Bob", - "htmlComment": "Yes! Am I the only other one?" - } -] - - - - describe('SCE doc demo', function() { - it('should sanitize untrusted values', function() { - expect(element(by.css('.htmlComment')).getInnerHtml()) - .toBe('Is anyone reading this?'); - }); - - it('should NOT sanitize explicitly trusted values', function() { - expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( - 'Hover over this text.'); - }); - }); - -
    - * - * - * - * ## Can I disable SCE completely? - * - * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits - * for little coding overhead. It will be much harder to take an SCE disabled application and - * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE - * for cases where you have a lot of existing code that was written before SCE was introduced and - * you're migrating them a module at a time. - * - * That said, here's how you can completely disable SCE: - * - *
    - *   angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) {
    - *     // Completely disable SCE.  For demonstration purposes only!
    - *     // Do not use in new projects.
    - *     $sceProvider.enabled(false);
    - *   });
    - * 
    - * - */ -/* jshint maxlen: 100 */ - -function $SceProvider() { - var enabled = true; - - /** - * @ngdoc function - * @name ng.sceProvider#enabled - * @methodOf ng.$sceProvider - * @function - * - * @param {boolean=} value If provided, then enables/disables SCE. - * @return {boolean} true if SCE is enabled, false otherwise. - * - * @description - * Enables/disables SCE and returns the current value. - */ - this.enabled = function (value) { - if (arguments.length) { - enabled = !!value; - } - return enabled; - }; - - - /* Design notes on the default implementation for SCE. - * - * The API contract for the SCE delegate - * ------------------------------------- - * The SCE delegate object must provide the following 3 methods: - * - * - trustAs(contextEnum, value) - * This method is used to tell the SCE service that the provided value is OK to use in the - * contexts specified by contextEnum. It must return an object that will be accepted by - * getTrusted() for a compatible contextEnum and return this value. - * - * - valueOf(value) - * For values that were not produced by trustAs(), return them as is. For values that were - * produced by trustAs(), return the corresponding input value to trustAs. Basically, if - * trustAs is wrapping the given values into some type, this operation unwraps it when given - * such a value. - * - * - getTrusted(contextEnum, value) - * This function should return the a value that is safe to use in the context specified by - * contextEnum or throw and exception otherwise. - * - * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be - * opaque or wrapped in some holder object. That happens to be an implementation detail. For - * instance, an implementation could maintain a registry of all trusted objects by context. In - * such a case, trustAs() would return the same object that was passed in. getTrusted() would - * return the same object passed in if it was found in the registry under a compatible context or - * throw an exception otherwise. An implementation might only wrap values some of the time based - * on some criteria. getTrusted() might return a value and not throw an exception for special - * constants or objects even if not wrapped. All such implementations fulfill this contract. - * - * - * A note on the inheritance model for SCE contexts - * ------------------------------------------------ - * I've used inheritance and made RESOURCE_URL wrapped types a subtype of URL wrapped types. This - * is purely an implementation details. - * - * The contract is simply this: - * - * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) - * will also succeed. - * - * Inheritance happens to capture this in a natural way. In some future, we - * may not use inheritance anymore. That is OK because no code outside of - * sce.js and sceSpecs.js would need to be aware of this detail. - */ - - this.$get = ['$parse', '$sniffer', '$sceDelegate', function( - $parse, $sniffer, $sceDelegate) { - // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows - // the "expression(javascript expression)" syntax which is insecure. - if (enabled && $sniffer.msie && $sniffer.msieDocumentMode < 8) { - throw $sceMinErr('iequirks', - 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' + - 'mode. You can fix this by adding the text to the top of your HTML ' + - 'document. See http://docs.angularjs.org/api/ng.$sce for more information.'); - } - - var sce = copy(SCE_CONTEXTS); - - /** - * @ngdoc function - * @name ng.sce#isEnabled - * @methodOf ng.$sce - * @function - * - * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you - * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. - * - * @description - * Returns a boolean indicating if SCE is enabled. - */ - sce.isEnabled = function () { - return enabled; - }; - sce.trustAs = $sceDelegate.trustAs; - sce.getTrusted = $sceDelegate.getTrusted; - sce.valueOf = $sceDelegate.valueOf; - - if (!enabled) { - sce.trustAs = sce.getTrusted = function(type, value) { return value; }; - sce.valueOf = identity; - } - - /** - * @ngdoc method - * @name ng.$sce#parse - * @methodOf ng.$sce - * - * @description - * Converts Angular {@link guide/expression expression} into a function. This is like {@link - * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it - * wraps the expression in a call to {@link ng.$sce#methods_getTrusted $sce.getTrusted(*type*, - * *result*)} - * - * @param {string} type The kind of SCE context in which this result will be used. - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ - sce.parseAs = function sceParseAs(type, expr) { - var parsed = $parse(expr); - if (parsed.literal && parsed.constant) { - return parsed; - } else { - return function sceParseAsTrusted(self, locals) { - return sce.getTrusted(type, parsed(self, locals)); - }; - } - }; - - /** - * @ngdoc method - * @name ng.$sce#trustAs - * @methodOf ng.$sce - * - * @description - * Delegates to {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs`}. As such, - * returns an object that is trusted by angular for use in specified strict contextual - * escaping contexts (such as ng-bind-html, ng-include, any src attribute - * interpolation, any dom event binding attribute interpolation such as for onclick, etc.) - * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual - * escaping. - * - * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resource_url, html, js and css. - * @param {*} value The value that that should be considered trusted/safe. - * @returns {*} A value that can be used to stand in for the provided `value` in places - * where Angular expects a $sce.trustAs() return value. - */ - - /** - * @ngdoc method - * @name ng.$sce#trustAsHtml - * @methodOf ng.$sce - * - * @description - * Shorthand method. `$sce.trustAsHtml(value)` → - * {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs($sce.HTML, value)`} - * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#methods_getTrustedHtml - * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#methods_trustAs $sce.trustAs}.) - */ - - /** - * @ngdoc method - * @name ng.$sce#trustAsUrl - * @methodOf ng.$sce - * - * @description - * Shorthand method. `$sce.trustAsUrl(value)` → - * {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs($sce.URL, value)`} - * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#methods_getTrustedUrl - * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#methods_trustAs $sce.trustAs}.) - */ - - /** - * @ngdoc method - * @name ng.$sce#trustAsResourceUrl - * @methodOf ng.$sce - * - * @description - * Shorthand method. `$sce.trustAsResourceUrl(value)` → - * {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} - * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#methods_getTrustedResourceUrl - * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the return - * value of {@link ng.$sce#methods_trustAs $sce.trustAs}.) - */ - - /** - * @ngdoc method - * @name ng.$sce#trustAsJs - * @methodOf ng.$sce - * - * @description - * Shorthand method. `$sce.trustAsJs(value)` → - * {@link ng.$sceDelegate#methods_trustAs `$sceDelegate.trustAs($sce.JS, value)`} - * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#methods_getTrustedJs - * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#methods_trustAs $sce.trustAs}.) - */ - - /** - * @ngdoc method - * @name ng.$sce#getTrusted - * @methodOf ng.$sce - * - * @description - * Delegates to {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted`}. As such, - * takes the result of a {@link ng.$sce#methods_trustAs `$sce.trustAs`}() call and returns the - * originally supplied value if the queried context type is a supertype of the created type. - * If this condition isn't satisfied, throws an exception. - * - * @param {string} type The kind of context in which this value is to be used. - * @param {*} maybeTrusted The result of a prior {@link ng.$sce#methods_trustAs `$sce.trustAs`} - * call. - * @returns {*} The value the was originally provided to - * {@link ng.$sce#methods_trustAs `$sce.trustAs`} if valid in this context. - * Otherwise, throws an exception. - */ - - /** - * @ngdoc method - * @name ng.$sce#getTrustedHtml - * @methodOf ng.$sce - * - * @description - * Shorthand method. `$sce.getTrustedHtml(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} - * - * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` - */ - - /** - * @ngdoc method - * @name ng.$sce#getTrustedCss - * @methodOf ng.$sce - * - * @description - * Shorthand method. `$sce.getTrustedCss(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} - * - * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` - */ - - /** - * @ngdoc method - * @name ng.$sce#getTrustedUrl - * @methodOf ng.$sce - * - * @description - * Shorthand method. `$sce.getTrustedUrl(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} - * - * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` - */ - - /** - * @ngdoc method - * @name ng.$sce#getTrustedResourceUrl - * @methodOf ng.$sce - * - * @description - * Shorthand method. `$sce.getTrustedResourceUrl(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} - * - * @param {*} value The value to pass to `$sceDelegate.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` - */ - - /** - * @ngdoc method - * @name ng.$sce#getTrustedJs - * @methodOf ng.$sce - * - * @description - * Shorthand method. `$sce.getTrustedJs(value)` → - * {@link ng.$sceDelegate#methods_getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} - * - * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` - */ - - /** - * @ngdoc method - * @name ng.$sce#parseAsHtml - * @methodOf ng.$sce - * - * @description - * Shorthand method. `$sce.parseAsHtml(expression string)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.HTML, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ - - /** - * @ngdoc method - * @name ng.$sce#parseAsCss - * @methodOf ng.$sce - * - * @description - * Shorthand method. `$sce.parseAsCss(value)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.CSS, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ - - /** - * @ngdoc method - * @name ng.$sce#parseAsUrl - * @methodOf ng.$sce - * - * @description - * Shorthand method. `$sce.parseAsUrl(value)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.URL, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ - - /** - * @ngdoc method - * @name ng.$sce#parseAsResourceUrl - * @methodOf ng.$sce - * - * @description - * Shorthand method. `$sce.parseAsResourceUrl(value)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.RESOURCE_URL, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ - - /** - * @ngdoc method - * @name ng.$sce#parseAsJs - * @methodOf ng.$sce - * - * @description - * Shorthand method. `$sce.parseAsJs(value)` → - * {@link ng.$sce#methods_parse `$sce.parseAs($sce.JS, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ - - // Shorthand delegations. - var parse = sce.parseAs, - getTrusted = sce.getTrusted, - trustAs = sce.trustAs; - - forEach(SCE_CONTEXTS, function (enumValue, name) { - var lName = lowercase(name); - sce[camelCase("parse_as_" + lName)] = function (expr) { - return parse(enumValue, expr); - }; - sce[camelCase("get_trusted_" + lName)] = function (value) { - return getTrusted(enumValue, value); - }; - sce[camelCase("trust_as_" + lName)] = function (value) { - return trustAs(enumValue, value); - }; - }); - - return sce; - }]; -} - -/** - * !!! This is an undocumented "private" service !!! - * - * @name ng.$sniffer - * @requires $window - * @requires $document - * - * @property {boolean} history Does the browser support html5 history api ? - * @property {boolean} hashchange Does the browser support hashchange event ? - * @property {boolean} transitions Does the browser support CSS transition events ? - * @property {boolean} animations Does the browser support CSS animation events ? - * - * @description - * This is very simple implementation of testing browser's features. - */ -function $SnifferProvider() { - this.$get = ['$window', '$document', function($window, $document) { - var eventSupport = {}, - android = - int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), - boxee = /Boxee/i.test(($window.navigator || {}).userAgent), - document = $document[0] || {}, - documentMode = document.documentMode, - vendorPrefix, - vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/, - bodyStyle = document.body && document.body.style, - transitions = false, - animations = false, - match; - - if (bodyStyle) { - for(var prop in bodyStyle) { - if(match = vendorRegex.exec(prop)) { - vendorPrefix = match[0]; - vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); - break; - } - } - - if(!vendorPrefix) { - vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit'; - } - - transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); - animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); - - if (android && (!transitions||!animations)) { - transitions = isString(document.body.style.webkitTransition); - animations = isString(document.body.style.webkitAnimation); - } - } - - - return { - // Android has history.pushState, but it does not update location correctly - // so let's not use the history API at all. - // http://code.google.com/p/android/issues/detail?id=17471 - // https://github.com/angular/angular.js/issues/904 - - // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has - // so let's not use the history API also - // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined - // jshint -W018 - history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), - // jshint +W018 - hashchange: 'onhashchange' in $window && - // IE8 compatible mode lies - (!documentMode || documentMode > 7), - hasEvent: function(event) { - // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have - // it. In particular the event is not fired when backspace or delete key are pressed or - // when cut operation is performed. - if (event == 'input' && msie == 9) return false; - - if (isUndefined(eventSupport[event])) { - var divElm = document.createElement('div'); - eventSupport[event] = 'on' + event in divElm; - } - - return eventSupport[event]; - }, - csp: csp(), - vendorPrefix: vendorPrefix, - transitions : transitions, - animations : animations, - android: android, - msie : msie, - msieDocumentMode: documentMode - }; - }]; -} - -function $TimeoutProvider() { - this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler', - function($rootScope, $browser, $q, $exceptionHandler) { - var deferreds = {}; - - - /** - * @ngdoc function - * @name ng.$timeout - * @requires $browser - * - * @description - * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch - * block and delegates any exceptions to - * {@link ng.$exceptionHandler $exceptionHandler} service. - * - * The return value of registering a timeout function is a promise, which will be resolved when - * the timeout is reached and the timeout function is executed. - * - * To cancel a timeout request, call `$timeout.cancel(promise)`. - * - * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to - * synchronously flush the queue of deferred functions. - * - * @param {function()} fn A function, whose execution should be delayed. - * @param {number=} [delay=0] Delay in milliseconds. - * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#methods_$apply $apply} block. - * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this - * promise will be resolved with is the return value of the `fn` function. - * - */ - function timeout(fn, delay, invokeApply) { - var deferred = $q.defer(), - promise = deferred.promise, - skipApply = (isDefined(invokeApply) && !invokeApply), - timeoutId; - - timeoutId = $browser.defer(function() { - try { - deferred.resolve(fn()); - } catch(e) { - deferred.reject(e); - $exceptionHandler(e); - } - finally { - delete deferreds[promise.$$timeoutId]; - } - - if (!skipApply) $rootScope.$apply(); - }, delay); - - promise.$$timeoutId = timeoutId; - deferreds[timeoutId] = deferred; - - return promise; - } - - - /** - * @ngdoc function - * @name ng.$timeout#cancel - * @methodOf ng.$timeout - * - * @description - * Cancels a task associated with the `promise`. As a result of this, the promise will be - * resolved with a rejection. - * - * @param {Promise=} promise Promise returned by the `$timeout` function. - * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully - * canceled. - */ - timeout.cancel = function(promise) { - if (promise && promise.$$timeoutId in deferreds) { - deferreds[promise.$$timeoutId].reject('canceled'); - delete deferreds[promise.$$timeoutId]; - return $browser.defer.cancel(promise.$$timeoutId); - } - return false; - }; - - return timeout; - }]; -} - -// NOTE: The usage of window and document instead of $window and $document here is -// deliberate. This service depends on the specific behavior of anchor nodes created by the -// browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and -// cause us to break tests. In addition, when the browser resolves a URL for XHR, it -// doesn't know about mocked locations and resolves URLs to the real document - which is -// exactly the behavior needed here. There is little value is mocking these out for this -// service. -var urlParsingNode = document.createElement("a"); -var originUrl = urlResolve(window.location.href, true); - - -/** - * - * Implementation Notes for non-IE browsers - * ---------------------------------------- - * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, - * results both in the normalizing and parsing of the URL. Normalizing means that a relative - * URL will be resolved into an absolute URL in the context of the application document. - * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related - * properties are all populated to reflect the normalized URL. This approach has wide - * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See - * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html - * - * Implementation Notes for IE - * --------------------------- - * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other - * browsers. However, the parsed components will not be set if the URL assigned did not specify - * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We - * work around that by performing the parsing in a 2nd step by taking a previously normalized - * URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the - * properties such as protocol, hostname, port, etc. - * - * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one - * uses the inner HTML approach to assign the URL as part of an HTML snippet - - * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL. - * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception. - * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that - * method and IE < 8 is unsupported. - * - * References: - * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement - * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html - * http://url.spec.whatwg.org/#urlutils - * https://github.com/angular/angular.js/pull/2902 - * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ - * - * @function - * @param {string} url The URL to be parsed. - * @description Normalizes and parses a URL. - * @returns {object} Returns the normalized URL as a dictionary. - * - * | member name | Description | - * |---------------|----------------| - * | href | A normalized version of the provided URL if it was not an absolute URL | - * | protocol | The protocol including the trailing colon | - * | host | The host and port (if the port is non-default) of the normalizedUrl | - * | search | The search params, minus the question mark | - * | hash | The hash string, minus the hash symbol - * | hostname | The hostname - * | port | The port, without ":" - * | pathname | The pathname, beginning with "/" - * - */ -function urlResolve(url, base) { - var href = url; - - if (msie) { - // Normalize before parse. Refer Implementation Notes on why this is - // done in two steps on IE. - urlParsingNode.setAttribute("href", href); - href = urlParsingNode.href; - } - - urlParsingNode.setAttribute('href', href); - - // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils - return { - href: urlParsingNode.href, - protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', - host: urlParsingNode.host, - search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', - hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', - hostname: urlParsingNode.hostname, - port: urlParsingNode.port, - pathname: (urlParsingNode.pathname.charAt(0) === '/') - ? urlParsingNode.pathname - : '/' + urlParsingNode.pathname - }; -} - -/** - * Parse a request URL and determine whether this is a same-origin request as the application document. - * - * @param {string|object} requestUrl The url of the request as a string that will be resolved - * or a parsed URL object. - * @returns {boolean} Whether the request is for the same origin as the application document. - */ -function urlIsSameOrigin(requestUrl) { - var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; - return (parsed.protocol === originUrl.protocol && - parsed.host === originUrl.host); -} - -/** - * @ngdoc object - * @name ng.$window - * - * @description - * A reference to the browser's `window` object. While `window` - * is globally available in JavaScript, it causes testability problems, because - * it is a global variable. In angular we always refer to it through the - * `$window` service, so it may be overridden, removed or mocked for testing. - * - * Expressions, like the one defined for the `ngClick` directive in the example - * below, are evaluated with respect to the current scope. Therefore, there is - * no risk of inadvertently coding in a dependency on a global value in such an - * expression. - * - * @example - - - -
    - - -
    -
    - - it('should display the greeting in the input box', function() { - element(by.model('greeting')).sendKeys('Hello, E2E Tests'); - // If we click the button it will block the test runner - // element(':button').click(); - }); - -
    - */ -function $WindowProvider(){ - this.$get = valueFn(window); -} - -/** - * @ngdoc object - * @name ng.$filterProvider - * @description - * - * Filters are just functions which transform input to an output. However filters need to be - * Dependency Injected. To achieve this a filter definition consists of a factory function which is - * annotated with dependencies and is responsible for creating a filter function. - * - *
    - *   // Filter registration
    - *   function MyModule($provide, $filterProvider) {
    - *     // create a service to demonstrate injection (not always needed)
    - *     $provide.value('greet', function(name){
    - *       return 'Hello ' + name + '!';
    - *     });
    - *
    - *     // register a filter factory which uses the
    - *     // greet service to demonstrate DI.
    - *     $filterProvider.register('greet', function(greet){
    - *       // return the filter function which uses the greet service
    - *       // to generate salutation
    - *       return function(text) {
    - *         // filters need to be forgiving so check input validity
    - *         return text && greet(text) || text;
    - *       };
    - *     });
    - *   }
    - * 
    - * - * The filter function is registered with the `$injector` under the filter name suffix with - * `Filter`. - * - *
    - *   it('should be the same instance', inject(
    - *     function($filterProvider) {
    - *       $filterProvider.register('reverse', function(){
    - *         return ...;
    - *       });
    - *     },
    - *     function($filter, reverseFilter) {
    - *       expect($filter('reverse')).toBe(reverseFilter);
    - *     });
    - * 
    - * - * - * For more information about how angular filters work, and how to create your own filters, see - * {@link guide/filter Filters} in the Angular Developer Guide. - */ -/** - * @ngdoc method - * @name ng.$filterProvider#register - * @methodOf ng.$filterProvider - * @description - * Register filter factory function. - * - * @param {String} name Name of the filter. - * @param {function} fn The filter factory function which is injectable. - */ - - -/** - * @ngdoc function - * @name ng.$filter - * @function - * @description - * Filters are used for formatting data displayed to the user. - * - * The general syntax in templates is as follows: - * - * {{ expression [| filter_name[:parameter_value] ... ] }} - * - * @param {String} name Name of the filter function to retrieve - * @return {Function} the filter function - */ -$FilterProvider.$inject = ['$provide']; -function $FilterProvider($provide) { - var suffix = 'Filter'; - - /** - * @ngdoc function - * @name ng.$controllerProvider#register - * @methodOf ng.$controllerProvider - * @param {string|Object} name Name of the filter function, or an object map of filters where - * the keys are the filter names and the values are the filter factories. - * @returns {Object} Registered filter instance, or if a map of filters was provided then a map - * of the registered filter instances. - */ - function register(name, factory) { - if(isObject(name)) { - var filters = {}; - forEach(name, function(filter, key) { - filters[key] = register(key, filter); - }); - return filters; - } else { - return $provide.factory(name + suffix, factory); - } - } - this.register = register; - - this.$get = ['$injector', function($injector) { - return function(name) { - return $injector.get(name + suffix); - }; - }]; - - //////////////////////////////////////// - - /* global - currencyFilter: false, - dateFilter: false, - filterFilter: false, - jsonFilter: false, - limitToFilter: false, - lowercaseFilter: false, - numberFilter: false, - orderByFilter: false, - uppercaseFilter: false, - */ - - register('currency', currencyFilter); - register('date', dateFilter); - register('filter', filterFilter); - register('json', jsonFilter); - register('limitTo', limitToFilter); - register('lowercase', lowercaseFilter); - register('number', numberFilter); - register('orderBy', orderByFilter); - register('uppercase', uppercaseFilter); -} - -/** - * @ngdoc filter - * @name ng.filter:filter - * @function - * - * @description - * Selects a subset of items from `array` and returns it as a new array. - * - * @param {Array} array The source array. - * @param {string|Object|function()} expression The predicate to be used for selecting items from - * `array`. - * - * Can be one of: - * - * - `string`: The string is evaluated as an expression and the resulting value is used for substring match against - * the contents of the `array`. All strings or objects with string properties in `array` that contain this string - * will be returned. The predicate can be negated by prefixing the string with `!`. - * - * - `Object`: A pattern object can be used to filter specific properties on objects contained - * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items - * which have property `name` containing "M" and property `phone` containing "1". A special - * property name `$` can be used (as in `{$:"text"}`) to accept a match against any - * property of the object. That's equivalent to the simple substring match with a `string` - * as described above. - * - * - `function(value)`: A predicate function can be used to write arbitrary filters. The function is - * called for each element of `array`. The final result is an array of those elements that - * the predicate returned true for. - * - * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in - * determining if the expected value (from the filter expression) and actual value (from - * the object in the array) should be considered a match. - * - * Can be one of: - * - * - `function(actual, expected)`: - * The function will be given the object value and the predicate value to compare and - * should return true if the item should be included in filtered result. - * - * - `true`: A shorthand for `function(actual, expected) { return angular.equals(expected, actual)}`. - * this is essentially strict comparison of expected and actual. - * - * - `false|undefined`: A short hand for a function which will look for a substring match in case - * insensitive way. - * - * @example - - -
    - - Search: - - - - - - -
    NamePhone
    {{friend.name}}{{friend.phone}}
    -
    - Any:
    - Name only
    - Phone only
    - Equality
    - - - - - - -
    NamePhone
    {{friendObj.name}}{{friendObj.phone}}
    -
    - - var expectFriendNames = function(expectedNames, key) { - element.all(by.repeater(key + ' in friends').column(key + '.name')).then(function(arr) { - arr.forEach(function(wd, i) { - expect(wd.getText()).toMatch(expectedNames[i]); - }); - }); - }; - - it('should search across all fields when filtering with a string', function() { - var searchText = element(by.model('searchText')); - searchText.clear(); - searchText.sendKeys('m'); - expectFriendNames(['Mary', 'Mike', 'Adam'], 'friend'); - - searchText.clear(); - searchText.sendKeys('76'); - expectFriendNames(['John', 'Julie'], 'friend'); - }); - - it('should search in specific fields when filtering with a predicate object', function() { - var searchAny = element(by.model('search.$')); - searchAny.clear(); - searchAny.sendKeys('i'); - expectFriendNames(['Mary', 'Mike', 'Julie', 'Juliette'], 'friendObj'); - }); - it('should use a equal comparison when comparator is true', function() { - var searchName = element(by.model('search.name')); - var strict = element(by.model('strict')); - searchName.clear(); - searchName.sendKeys('Julie'); - strict.click(); - expectFriendNames(['Julie'], 'friendObj'); - }); - -
    - */ -function filterFilter() { - return function(array, expression, comparator) { - if (!isArray(array)) return array; - - var comparatorType = typeof(comparator), - predicates = []; - - predicates.check = function(value) { - for (var j = 0; j < predicates.length; j++) { - if(!predicates[j](value)) { - return false; - } - } - return true; - }; - - if (comparatorType !== 'function') { - if (comparatorType === 'boolean' && comparator) { - comparator = function(obj, text) { - return angular.equals(obj, text); - }; - } else { - comparator = function(obj, text) { - if (obj && text && typeof obj === 'object' && typeof text === 'object') { - for (var objKey in obj) { - if (objKey.charAt(0) !== '$' && hasOwnProperty.call(obj, objKey) && - comparator(obj[objKey], text[objKey])) { - return true; - } - } - return false; - } - text = (''+text).toLowerCase(); - return (''+obj).toLowerCase().indexOf(text) > -1; - }; - } - } - - var search = function(obj, text){ - if (typeof text == 'string' && text.charAt(0) === '!') { - return !search(obj, text.substr(1)); - } - switch (typeof obj) { - case "boolean": - case "number": - case "string": - return comparator(obj, text); - case "object": - switch (typeof text) { - case "object": - return comparator(obj, text); - default: - for ( var objKey in obj) { - if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { - return true; - } - } - break; - } - return false; - case "array": - for ( var i = 0; i < obj.length; i++) { - if (search(obj[i], text)) { - return true; - } - } - return false; - default: - return false; - } - }; - switch (typeof expression) { - case "boolean": - case "number": - case "string": - // Set up expression object and fall through - expression = {$:expression}; - // jshint -W086 - case "object": - // jshint +W086 - for (var key in expression) { - (function(path) { - if (typeof expression[path] == 'undefined') return; - predicates.push(function(value) { - return search(path == '$' ? value : (value && value[path]), expression[path]); - }); - })(key); - } - break; - case 'function': - predicates.push(expression); - break; - default: - return array; - } - var filtered = []; - for ( var j = 0; j < array.length; j++) { - var value = array[j]; - if (predicates.check(value)) { - filtered.push(value); - } - } - return filtered; - }; -} - -/** - * @ngdoc filter - * @name ng.filter:currency - * @function - * - * @description - * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default - * symbol for current locale is used. - * - * @param {number} amount Input to filter. - * @param {string=} symbol Currency symbol or identifier to be displayed. - * @returns {string} Formatted number. - * - * - * @example - - - -
    -
    - default currency symbol ($): {{amount | currency}}
    - custom currency identifier (USD$): {{amount | currency:"USD$"}} -
    -
    - - it('should init with 1234.56', function() { - expect(element(by.id('currency-default')).getText()).toBe('$1,234.56'); - expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('USD$1,234.56'); - }); - it('should update', function() { - if (browser.params.browser == 'safari') { - // Safari does not understand the minus key. See - // https://github.com/angular/protractor/issues/481 - return; - } - element(by.model('amount')).clear(); - element(by.model('amount')).sendKeys('-1234'); - expect(element(by.id('currency-default')).getText()).toBe('($1,234.00)'); - expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('(USD$1,234.00)'); - }); - -
    - */ -currencyFilter.$inject = ['$locale']; -function currencyFilter($locale) { - var formats = $locale.NUMBER_FORMATS; - return function(amount, currencySymbol){ - if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; - return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). - replace(/\u00A4/g, currencySymbol); - }; -} - -/** - * @ngdoc filter - * @name ng.filter:number - * @function - * - * @description - * Formats a number as text. - * - * If the input is not a number an empty string is returned. - * - * @param {number|string} number Number to format. - * @param {(number|string)=} fractionSize Number of decimal places to round the number to. - * If this is not provided then the fraction size is computed from the current locale's number - * formatting pattern. In the case of the default locale, it will be 3. - * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. - * - * @example - - - -
    - Enter number:
    - Default formatting: {{val | number}}
    - No fractions: {{val | number:0}}
    - Negative number: {{-val | number:4}} -
    -
    - - it('should format numbers', function() { - expect(element(by.id('number-default')).getText()).toBe('1,234.568'); - expect(element(by.binding('val | number:0')).getText()).toBe('1,235'); - expect(element(by.binding('-val | number:4')).getText()).toBe('-1,234.5679'); - }); - - it('should update', function() { - element(by.model('val')).clear(); - element(by.model('val')).sendKeys('3374.333'); - expect(element(by.id('number-default')).getText()).toBe('3,374.333'); - expect(element(by.binding('val | number:0')).getText()).toBe('3,374'); - expect(element(by.binding('-val | number:4')).getText()).toBe('-3,374.3330'); - }); - -
    - */ - - -numberFilter.$inject = ['$locale']; -function numberFilter($locale) { - var formats = $locale.NUMBER_FORMATS; - return function(number, fractionSize) { - return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, - fractionSize); - }; -} - -var DECIMAL_SEP = '.'; -function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (isNaN(number) || !isFinite(number)) return ''; - - var isNegative = number < 0; - number = Math.abs(number); - var numStr = number + '', - formatedText = '', - parts = []; - - var hasExponent = false; - if (numStr.indexOf('e') !== -1) { - var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); - if (match && match[2] == '-' && match[3] > fractionSize + 1) { - numStr = '0'; - } else { - formatedText = numStr; - hasExponent = true; - } - } - - if (!hasExponent) { - var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; - - // determine fractionSize if it is not specified - if (isUndefined(fractionSize)) { - fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); - } - - var pow = Math.pow(10, fractionSize); - number = Math.round(number * pow) / pow; - var fraction = ('' + number).split(DECIMAL_SEP); - var whole = fraction[0]; - fraction = fraction[1] || ''; - - var i, pos = 0, - lgroup = pattern.lgSize, - group = pattern.gSize; - - if (whole.length >= (lgroup + group)) { - pos = whole.length - lgroup; - for (i = 0; i < pos; i++) { - if ((pos - i)%group === 0 && i !== 0) { - formatedText += groupSep; - } - formatedText += whole.charAt(i); - } - } - - for (i = pos; i < whole.length; i++) { - if ((whole.length - i)%lgroup === 0 && i !== 0) { - formatedText += groupSep; - } - formatedText += whole.charAt(i); - } - - // format fraction part. - while(fraction.length < fractionSize) { - fraction += '0'; - } - - if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); - } else { - - if (fractionSize > 0 && number > -1 && number < 1) { - formatedText = number.toFixed(fractionSize); - } - } - - parts.push(isNegative ? pattern.negPre : pattern.posPre); - parts.push(formatedText); - parts.push(isNegative ? pattern.negSuf : pattern.posSuf); - return parts.join(''); -} - -function padNumber(num, digits, trim) { - var neg = ''; - if (num < 0) { - neg = '-'; - num = -num; - } - num = '' + num; - while(num.length < digits) num = '0' + num; - if (trim) - num = num.substr(num.length - digits); - return neg + num; -} - - -function dateGetter(name, size, offset, trim) { - offset = offset || 0; - return function(date) { - var value = date['get' + name](); - if (offset > 0 || value > -offset) - value += offset; - if (value === 0 && offset == -12 ) value = 12; - return padNumber(value, size, trim); - }; -} - -function dateStrGetter(name, shortForm) { - return function(date, formats) { - var value = date['get' + name](); - var get = uppercase(shortForm ? ('SHORT' + name) : name); - - return formats[get][value]; - }; -} - -function timeZoneGetter(date) { - var zone = -1 * date.getTimezoneOffset(); - var paddedZone = (zone >= 0) ? "+" : ""; - - paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) + - padNumber(Math.abs(zone % 60), 2); - - return paddedZone; -} - -function ampmGetter(date, formats) { - return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; -} - -var DATE_FORMATS = { - yyyy: dateGetter('FullYear', 4), - yy: dateGetter('FullYear', 2, 0, true), - y: dateGetter('FullYear', 1), - MMMM: dateStrGetter('Month'), - MMM: dateStrGetter('Month', true), - MM: dateGetter('Month', 2, 1), - M: dateGetter('Month', 1, 1), - dd: dateGetter('Date', 2), - d: dateGetter('Date', 1), - HH: dateGetter('Hours', 2), - H: dateGetter('Hours', 1), - hh: dateGetter('Hours', 2, -12), - h: dateGetter('Hours', 1, -12), - mm: dateGetter('Minutes', 2), - m: dateGetter('Minutes', 1), - ss: dateGetter('Seconds', 2), - s: dateGetter('Seconds', 1), - // while ISO 8601 requires fractions to be prefixed with `.` or `,` - // we can be just safely rely on using `sss` since we currently don't support single or two digit fractions - sss: dateGetter('Milliseconds', 3), - EEEE: dateStrGetter('Day'), - EEE: dateStrGetter('Day', true), - a: ampmGetter, - Z: timeZoneGetter -}; - -var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, - NUMBER_STRING = /^\-?\d+$/; - -/** - * @ngdoc filter - * @name ng.filter:date - * @function - * - * @description - * Formats `date` to a string based on the requested `format`. - * - * `format` string can be composed of the following elements: - * - * * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) - * * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) - * * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) - * * `'MMMM'`: Month in year (January-December) - * * `'MMM'`: Month in year (Jan-Dec) - * * `'MM'`: Month in year, padded (01-12) - * * `'M'`: Month in year (1-12) - * * `'dd'`: Day in month, padded (01-31) - * * `'d'`: Day in month (1-31) - * * `'EEEE'`: Day in Week,(Sunday-Saturday) - * * `'EEE'`: Day in Week, (Sun-Sat) - * * `'HH'`: Hour in day, padded (00-23) - * * `'H'`: Hour in day (0-23) - * * `'hh'`: Hour in am/pm, padded (01-12) - * * `'h'`: Hour in am/pm, (1-12) - * * `'mm'`: Minute in hour, padded (00-59) - * * `'m'`: Minute in hour (0-59) - * * `'ss'`: Second in minute, padded (00-59) - * * `'s'`: Second in minute (0-59) - * * `'.sss' or ',sss'`: Millisecond in second, padded (000-999) - * * `'a'`: am/pm marker - * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) - * - * `format` string can also be one of the following predefined - * {@link guide/i18n localizable formats}: - * - * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale - * (e.g. Sep 3, 2010 12:05:08 pm) - * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm) - * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale - * (e.g. Friday, September 3, 2010) - * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010) - * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) - * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) - * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) - * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm) - * - * `format` string can contain literal values. These need to be quoted with single quotes (e.g. - * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence - * (e.g. `"h 'o''clock'"`). - * - * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or - * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.SSSZ and its - * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is - * specified in the string input, the time is considered to be in the local timezone. - * @param {string=} format Formatting rules (see Description). If not specified, - * `mediumDate` is used. - * @returns {string} Formatted string or the input if input is not recognized as date/millis. - * - * @example - - - {{1288323623006 | date:'medium'}}: - {{1288323623006 | date:'medium'}}
    - {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: - {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
    - {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: - {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
    -
    - - it('should format date', function() { - expect(element(by.binding("1288323623006 | date:'medium'")).getText()). - toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); - expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()). - toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); - expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). - toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); - }); - -
    - */ -dateFilter.$inject = ['$locale']; -function dateFilter($locale) { - - - var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; - // 1 2 3 4 5 6 7 8 9 10 11 - function jsonStringToDate(string) { - var match; - if (match = string.match(R_ISO8601_STR)) { - var date = new Date(0), - tzHour = 0, - tzMin = 0, - dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear, - timeSetter = match[8] ? date.setUTCHours : date.setHours; - - if (match[9]) { - tzHour = int(match[9] + match[10]); - tzMin = int(match[9] + match[11]); - } - dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3])); - var h = int(match[4]||0) - tzHour; - var m = int(match[5]||0) - tzMin; - var s = int(match[6]||0); - var ms = Math.round(parseFloat('0.' + (match[7]||0)) * 1000); - timeSetter.call(date, h, m, s, ms); - return date; - } - return string; - } - - - return function(date, format) { - var text = '', - parts = [], - fn, match; - - format = format || 'mediumDate'; - format = $locale.DATETIME_FORMATS[format] || format; - if (isString(date)) { - if (NUMBER_STRING.test(date)) { - date = int(date); - } else { - date = jsonStringToDate(date); - } - } - - if (isNumber(date)) { - date = new Date(date); - } - - if (!isDate(date)) { - return date; - } - - while(format) { - match = DATE_FORMATS_SPLIT.exec(format); - if (match) { - parts = concat(parts, match, 1); - format = parts.pop(); - } else { - parts.push(format); - format = null; - } - } - - forEach(parts, function(value){ - fn = DATE_FORMATS[value]; - text += fn ? fn(date, $locale.DATETIME_FORMATS) - : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); - }); - - return text; - }; -} - - -/** - * @ngdoc filter - * @name ng.filter:json - * @function - * - * @description - * Allows you to convert a JavaScript object into JSON string. - * - * This filter is mostly useful for debugging. When using the double curly {{value}} notation - * the binding is automatically converted to JSON. - * - * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. - * @returns {string} JSON string. - * - * - * @example: - - -
    {{ {'name':'value'} | json }}
    -
    - - it('should jsonify filtered objects', function() { - expect(element(by.binding("{'name':'value'}")).getText()).toMatch(/\{\n "name": ?"value"\n}/); - }); - -
    - * - */ -function jsonFilter() { - return function(object) { - return toJson(object, true); - }; -} - - -/** - * @ngdoc filter - * @name ng.filter:lowercase - * @function - * @description - * Converts string to lowercase. - * @see angular.lowercase - */ -var lowercaseFilter = valueFn(lowercase); - - -/** - * @ngdoc filter - * @name ng.filter:uppercase - * @function - * @description - * Converts string to uppercase. - * @see angular.uppercase - */ -var uppercaseFilter = valueFn(uppercase); - -/** - * @ngdoc function - * @name ng.filter:limitTo - * @function - * - * @description - * Creates a new array or string containing only a specified number of elements. The elements - * are taken from either the beginning or the end of the source array or string, as specified by - * the value and sign (positive or negative) of `limit`. - * - * @param {Array|string} input Source array or string to be limited. - * @param {string|number} limit The length of the returned array or string. If the `limit` number - * is positive, `limit` number of items from the beginning of the source array/string are copied. - * If the number is negative, `limit` number of items from the end of the source array/string - * are copied. The `limit` will be trimmed if it exceeds `array.length` - * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array - * had less than `limit` elements. - * - * @example - - - -
    - Limit {{numbers}} to: -

    Output numbers: {{ numbers | limitTo:numLimit }}

    - Limit {{letters}} to: -

    Output letters: {{ letters | limitTo:letterLimit }}

    -
    -
    - - var numLimitInput = element(by.model('numLimit')); - var letterLimitInput = element(by.model('letterLimit')); - var limitedNumbers = element(by.binding('numbers | limitTo:numLimit')); - var limitedLetters = element(by.binding('letters | limitTo:letterLimit')); - - it('should limit the number array to first three items', function() { - expect(numLimitInput.getAttribute('value')).toBe('3'); - expect(letterLimitInput.getAttribute('value')).toBe('3'); - expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]'); - expect(limitedLetters.getText()).toEqual('Output letters: abc'); - }); - - it('should update the output when -3 is entered', function() { - numLimitInput.clear(); - numLimitInput.sendKeys('-3'); - letterLimitInput.clear(); - letterLimitInput.sendKeys('-3'); - expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); - expect(limitedLetters.getText()).toEqual('Output letters: ghi'); - }); - - it('should not exceed the maximum size of input array', function() { - numLimitInput.clear(); - numLimitInput.sendKeys('100'); - letterLimitInput.clear(); - letterLimitInput.sendKeys('100'); - expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]'); - expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi'); - }); - -
    - */ -function limitToFilter(){ - return function(input, limit) { - if (!isArray(input) && !isString(input)) return input; - - limit = int(limit); - - if (isString(input)) { - //NaN check on limit - if (limit) { - return limit >= 0 ? input.slice(0, limit) : input.slice(limit, input.length); - } else { - return ""; - } - } - - var out = [], - i, n; - - // if abs(limit) exceeds maximum length, trim it - if (limit > input.length) - limit = input.length; - else if (limit < -input.length) - limit = -input.length; - - if (limit > 0) { - i = 0; - n = limit; - } else { - i = input.length + limit; - n = input.length; - } - - for (; i} expression A predicate to be - * used by the comparator to determine the order of elements. - * - * Can be one of: - * - * - `function`: Getter function. The result of this function will be sorted using the - * `<`, `=`, `>` operator. - * - `string`: An Angular expression which evaluates to an object to order by, such as 'name' - * to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control - * ascending or descending sort order (for example, +name or -name). - * - `Array`: An array of function or string predicates. The first predicate in the array - * is used for sorting, but when two items are equivalent, the next predicate is used. - * - * @param {boolean=} reverse Reverse the order the array. - * @returns {Array} Sorted copy of the source array. - * - * @example - - - -
    -
    Sorting predicate = {{predicate}}; reverse = {{reverse}}
    -
    - [ unsorted ] - - - - - - - - - - - -
    Name - (^)Phone NumberAge
    {{friend.name}}{{friend.phone}}{{friend.age}}
    -
    -
    -
    - */ -orderByFilter.$inject = ['$parse']; -function orderByFilter($parse){ - return function(array, sortPredicate, reverseOrder) { - if (!isArray(array)) return array; - if (!sortPredicate) return array; - sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; - sortPredicate = map(sortPredicate, function(predicate){ - var descending = false, get = predicate || identity; - if (isString(predicate)) { - if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { - descending = predicate.charAt(0) == '-'; - predicate = predicate.substring(1); - } - get = $parse(predicate); - } - return reverseComparator(function(a,b){ - return compare(get(a),get(b)); - }, descending); - }); - var arrayCopy = []; - for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } - return arrayCopy.sort(reverseComparator(comparator, reverseOrder)); - - function comparator(o1, o2){ - for ( var i = 0; i < sortPredicate.length; i++) { - var comp = sortPredicate[i](o1, o2); - if (comp !== 0) return comp; - } - return 0; - } - function reverseComparator(comp, descending) { - return toBoolean(descending) - ? function(a,b){return comp(b,a);} - : comp; - } - function compare(v1, v2){ - var t1 = typeof v1; - var t2 = typeof v2; - if (t1 == t2) { - if (t1 == "string") { - v1 = v1.toLowerCase(); - v2 = v2.toLowerCase(); - } - if (v1 === v2) return 0; - return v1 < v2 ? -1 : 1; - } else { - return t1 < t2 ? -1 : 1; - } - } - }; -} - -function ngDirective(directive) { - if (isFunction(directive)) { - directive = { - link: directive - }; - } - directive.restrict = directive.restrict || 'AC'; - return valueFn(directive); -} - -/** - * @ngdoc directive - * @name ng.directive:a - * @restrict E - * - * @description - * Modifies the default behavior of the html A tag so that the default action is prevented when - * the href attribute is empty. - * - * This change permits the easy creation of action links with the `ngClick` directive - * without changing the location or causing page reloads, e.g.: - * `Add Item` - */ -var htmlAnchorDirective = valueFn({ - restrict: 'E', - compile: function(element, attr) { - - if (msie <= 8) { - - // turn link into a stylable link in IE - // but only if it doesn't have name attribute, in which case it's an anchor - if (!attr.href && !attr.name) { - attr.$set('href', ''); - } - - // add a comment node to anchors to workaround IE bug that causes element content to be reset - // to new attribute content if attribute is updated with value containing @ and element also - // contains value with @ - // see issue #1949 - element.append(document.createComment('IE fix')); - } - - if (!attr.href && !attr.xlinkHref && !attr.name) { - return function(scope, element) { - // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. - var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? - 'xlink:href' : 'href'; - element.on('click', function(event){ - // if we have no href url, then don't navigate anywhere. - if (!element.attr(href)) { - event.preventDefault(); - } - }); - }; - } - } -}); - -/** - * @ngdoc directive - * @name ng.directive:ngHref - * @restrict A - * @priority 99 - * - * @description - * Using Angular markup like `{{hash}}` in an href attribute will - * make the link go to the wrong URL if the user clicks it before - * Angular has a chance to replace the `{{hash}}` markup with its - * value. Until Angular replaces the markup the link will be broken - * and will most likely return a 404 error. - * - * The `ngHref` directive solves this problem. - * - * The wrong way to write it: - *
    - * 
    - * 
    - * - * The correct way to write it: - *
    - * 
    - * 
    - * - * @element A - * @param {template} ngHref any string which can contain `{{}}` markup. - * - * @example - * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes - * in links and their different behaviors: - - -
    -
    link 1 (link, don't reload)
    - link 2 (link, don't reload)
    - link 3 (link, reload!)
    - anchor (link, don't reload)
    - anchor (no link)
    - link (link, change location) - - - it('should execute ng-click but not reload when href without value', function() { - element(by.id('link-1')).click(); - expect(element(by.model('value')).getAttribute('value')).toEqual('1'); - expect(element(by.id('link-1')).getAttribute('href')).toBe(''); - }); - - it('should execute ng-click but not reload when href empty string', function() { - element(by.id('link-2')).click(); - expect(element(by.model('value')).getAttribute('value')).toEqual('2'); - expect(element(by.id('link-2')).getAttribute('href')).toBe(''); - }); - - it('should execute ng-click and change url when ng-href specified', function() { - expect(element(by.id('link-3')).getAttribute('href')).toMatch(/\/123$/); - - element(by.id('link-3')).click(); - - // At this point, we navigate away from an Angular page, so we need - // to use browser.driver to get the base webdriver. - - browser.wait(function() { - return browser.driver.getCurrentUrl().then(function(url) { - return url.match(/\/123$/); - }); - }, 1000, 'page should navigate to /123'); - }); - - it('should execute ng-click but not reload when href empty string and name specified', function() { - element(by.id('link-4')).click(); - expect(element(by.model('value')).getAttribute('value')).toEqual('4'); - expect(element(by.id('link-4')).getAttribute('href')).toBe(''); - }); - - it('should execute ng-click but not reload when no href but name specified', function() { - element(by.id('link-5')).click(); - expect(element(by.model('value')).getAttribute('value')).toEqual('5'); - expect(element(by.id('link-5')).getAttribute('href')).toBe(null); - }); - - it('should only change url when only ng-href', function() { - element(by.model('value')).clear(); - element(by.model('value')).sendKeys('6'); - expect(element(by.id('link-6')).getAttribute('href')).toMatch(/\/6$/); - - element(by.id('link-6')).click(); - expect(browser.getCurrentUrl()).toMatch(/\/6$/); - }); - - - */ - -/** - * @ngdoc directive - * @name ng.directive:ngSrc - * @restrict A - * @priority 99 - * - * @description - * Using Angular markup like `{{hash}}` in a `src` attribute doesn't - * work right: The browser will fetch from the URL with the literal - * text `{{hash}}` until Angular replaces the expression inside - * `{{hash}}`. The `ngSrc` directive solves this problem. - * - * The buggy way to write it: - *
    - * 
    - * 
    - * - * The correct way to write it: - *
    - * 
    - * 
    - * - * @element IMG - * @param {template} ngSrc any string which can contain `{{}}` markup. - */ - -/** - * @ngdoc directive - * @name ng.directive:ngSrcset - * @restrict A - * @priority 99 - * - * @description - * Using Angular markup like `{{hash}}` in a `srcset` attribute doesn't - * work right: The browser will fetch from the URL with the literal - * text `{{hash}}` until Angular replaces the expression inside - * `{{hash}}`. The `ngSrcset` directive solves this problem. - * - * The buggy way to write it: - *
    - * 
    - * 
    - * - * The correct way to write it: - *
    - * 
    - * 
    - * - * @element IMG - * @param {template} ngSrcset any string which can contain `{{}}` markup. - */ - -/** - * @ngdoc directive - * @name ng.directive:ngDisabled - * @restrict A - * @priority 100 - * - * @description - * - * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: - *
    - * 
    - * - *
    - *
    - * - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as disabled. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngDisabled` directive solves this problem for the `disabled` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. - * - * @example - - - Click me to toggle:
    - -
    - - it('should toggle button', function() { - expect(element(by.css('.doc-example-live button')).getAttribute('disabled')).toBeFalsy(); - element(by.model('checked')).click(); - expect(element(by.css('.doc-example-live button')).getAttribute('disabled')).toBeTruthy(); - }); - -
    - * - * @element INPUT - * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy, - * then special attribute "disabled" will be set on the element - */ - - -/** - * @ngdoc directive - * @name ng.directive:ngChecked - * @restrict A - * @priority 100 - * - * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as checked. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngChecked` directive solves this problem for the `checked` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. - * @example - - - Check me to check both:
    - -
    - - it('should check both checkBoxes', function() { - expect(element(by.id('checkSlave')).getAttribute('checked')).toBeFalsy(); - element(by.model('master')).click(); - expect(element(by.id('checkSlave')).getAttribute('checked')).toBeTruthy(); - }); - -
    - * - * @element INPUT - * @param {expression} ngChecked If the {@link guide/expression expression} is truthy, - * then special attribute "checked" will be set on the element - */ - - -/** - * @ngdoc directive - * @name ng.directive:ngReadonly - * @restrict A - * @priority 100 - * - * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as readonly. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngReadonly` directive solves this problem for the `readonly` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. - * @example - - - Check me to make text readonly:
    - -
    - - it('should toggle readonly attr', function() { - expect(element(by.css('.doc-example-live [type="text"]')).getAttribute('readonly')).toBeFalsy(); - element(by.model('checked')).click(); - expect(element(by.css('.doc-example-live [type="text"]')).getAttribute('readonly')).toBeTruthy(); - }); - -
    - * - * @element INPUT - * @param {expression} ngReadonly If the {@link guide/expression expression} is truthy, - * then special attribute "readonly" will be set on the element - */ - - -/** - * @ngdoc directive - * @name ng.directive:ngSelected - * @restrict A - * @priority 100 - * - * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as selected. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngSelected` directive solves this problem for the `selected` atttribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. - * - * @example - - - Check me to select:
    - -
    - - it('should select Greetings!', function() { - expect(element(by.id('greet')).getAttribute('selected')).toBeFalsy(); - element(by.model('selected')).click(); - expect(element(by.id('greet')).getAttribute('selected')).toBeTruthy(); - }); - -
    - * - * @element OPTION - * @param {expression} ngSelected If the {@link guide/expression expression} is truthy, - * then special attribute "selected" will be set on the element - */ - -/** - * @ngdoc directive - * @name ng.directive:ngOpen - * @restrict A - * @priority 100 - * - * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as open. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngOpen` directive solves this problem for the `open` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. - * @example - - - Check me check multiple:
    -
    - Show/Hide me -
    -
    - - it('should toggle open', function() { - expect(element(by.id('details')).getAttribute('open')).toBeFalsy(); - element(by.model('open')).click(); - expect(element(by.id('details')).getAttribute('open')).toBeTruthy(); - }); - -
    - * - * @element DETAILS - * @param {expression} ngOpen If the {@link guide/expression expression} is truthy, - * then special attribute "open" will be set on the element - */ - -var ngAttributeAliasDirectives = {}; - - -// boolean attrs are evaluated -forEach(BOOLEAN_ATTR, function(propName, attrName) { - // binding to multiple is not supported - if (propName == "multiple") return; - - var normalized = directiveNormalize('ng-' + attrName); - ngAttributeAliasDirectives[normalized] = function() { - return { - priority: 100, - link: function(scope, element, attr) { - scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { - attr.$set(attrName, !!value); - }); - } - }; - }; -}); - - -// ng-src, ng-srcset, ng-href are interpolated -forEach(['src', 'srcset', 'href'], function(attrName) { - var normalized = directiveNormalize('ng-' + attrName); - ngAttributeAliasDirectives[normalized] = function() { - return { - priority: 99, // it needs to run after the attributes are interpolated - link: function(scope, element, attr) { - attr.$observe(normalized, function(value) { - if (!value) - return; - - attr.$set(attrName, value); - - // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist - // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need - // to set the property as well to achieve the desired effect. - // we use attr[attrName] value since $set can sanitize the url. - if (msie) element.prop(attrName, attr[attrName]); - }); - } - }; - }; -}); - -/* global -nullFormCtrl */ -var nullFormCtrl = { - $addControl: noop, - $removeControl: noop, - $setValidity: noop, - $setDirty: noop, - $setPristine: noop -}; - -/** - * @ngdoc object - * @name ng.directive:form.FormController - * - * @property {boolean} $pristine True if user has not interacted with the form yet. - * @property {boolean} $dirty True if user has already interacted with the form. - * @property {boolean} $valid True if all of the containing forms and controls are valid. - * @property {boolean} $invalid True if at least one containing control or form is invalid. - * - * @property {Object} $error Is an object hash, containing references to all invalid controls or - * forms, where: - * - * - keys are validation tokens (error names), - * - values are arrays of controls or forms that are invalid for given error name. - * - * - * Built-in validation tokens: - * - * - `email` - * - `max` - * - `maxlength` - * - `min` - * - `minlength` - * - `number` - * - `pattern` - * - `required` - * - `url` - * - * @description - * `FormController` keeps track of all its controls and nested forms as well as state of them, - * such as being valid/invalid or dirty/pristine. - * - * Each {@link ng.directive:form form} directive creates an instance - * of `FormController`. - * - */ -//asks for $scope to fool the BC controller module -FormController.$inject = ['$element', '$attrs', '$scope']; -function FormController(element, attrs) { - var form = this, - parentForm = element.parent().controller('form') || nullFormCtrl, - invalidCount = 0, // used to easily determine if we are valid - errors = form.$error = {}, - controls = []; - - // init state - form.$name = attrs.name || attrs.ngForm; - form.$dirty = false; - form.$pristine = true; - form.$valid = true; - form.$invalid = false; - - parentForm.$addControl(form); - - // Setup initial state of the control - element.addClass(PRISTINE_CLASS); - toggleValidCss(true); - - // convenience method for easy toggling of classes - function toggleValidCss(isValid, validationErrorKey) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - element. - removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey). - addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); - } - - /** - * @ngdoc function - * @name ng.directive:form.FormController#$addControl - * @methodOf ng.directive:form.FormController - * - * @description - * Register a control with the form. - * - * Input elements using ngModelController do this automatically when they are linked. - */ - form.$addControl = function(control) { - // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored - // and not added to the scope. Now we throw an error. - assertNotHasOwnProperty(control.$name, 'input'); - controls.push(control); - - if (control.$name) { - form[control.$name] = control; - } - }; - - /** - * @ngdoc function - * @name ng.directive:form.FormController#$removeControl - * @methodOf ng.directive:form.FormController - * - * @description - * Deregister a control from the form. - * - * Input elements using ngModelController do this automatically when they are destroyed. - */ - form.$removeControl = function(control) { - if (control.$name && form[control.$name] === control) { - delete form[control.$name]; - } - forEach(errors, function(queue, validationToken) { - form.$setValidity(validationToken, true, control); - }); - - arrayRemove(controls, control); - }; - - /** - * @ngdoc function - * @name ng.directive:form.FormController#$setValidity - * @methodOf ng.directive:form.FormController - * - * @description - * Sets the validity of a form control. - * - * This method will also propagate to parent forms. - */ - form.$setValidity = function(validationToken, isValid, control) { - var queue = errors[validationToken]; - - if (isValid) { - if (queue) { - arrayRemove(queue, control); - if (!queue.length) { - invalidCount--; - if (!invalidCount) { - toggleValidCss(isValid); - form.$valid = true; - form.$invalid = false; - } - errors[validationToken] = false; - toggleValidCss(true, validationToken); - parentForm.$setValidity(validationToken, true, form); - } - } - - } else { - if (!invalidCount) { - toggleValidCss(isValid); - } - if (queue) { - if (includes(queue, control)) return; - } else { - errors[validationToken] = queue = []; - invalidCount++; - toggleValidCss(false, validationToken); - parentForm.$setValidity(validationToken, false, form); - } - queue.push(control); - - form.$valid = false; - form.$invalid = true; - } - }; - - /** - * @ngdoc function - * @name ng.directive:form.FormController#$setDirty - * @methodOf ng.directive:form.FormController - * - * @description - * Sets the form to a dirty state. - * - * This method can be called to add the 'ng-dirty' class and set the form to a dirty - * state (ng-dirty class). This method will also propagate to parent forms. - */ - form.$setDirty = function() { - element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); - form.$dirty = true; - form.$pristine = false; - parentForm.$setDirty(); - }; - - /** - * @ngdoc function - * @name ng.directive:form.FormController#$setPristine - * @methodOf ng.directive:form.FormController - * - * @description - * Sets the form to its pristine state. - * - * This method can be called to remove the 'ng-dirty' class and set the form to its pristine - * state (ng-pristine class). This method will also propagate to all the controls contained - * in this form. - * - * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after - * saving or resetting it. - */ - form.$setPristine = function () { - element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS); - form.$dirty = false; - form.$pristine = true; - forEach(controls, function(control) { - control.$setPristine(); - }); - }; -} - - -/** - * @ngdoc directive - * @name ng.directive:ngForm - * @restrict EAC - * - * @description - * Nestable alias of {@link ng.directive:form `form`} directive. HTML - * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a - * sub-group of controls needs to be determined. - * - * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into - * related scope, under this name. - * - */ - - /** - * @ngdoc directive - * @name ng.directive:form - * @restrict E - * - * @description - * Directive that instantiates - * {@link ng.directive:form.FormController FormController}. - * - * If the `name` attribute is specified, the form controller is published onto the current scope under - * this name. - * - * # Alias: {@link ng.directive:ngForm `ngForm`} - * - * In Angular forms can be nested. This means that the outer form is valid when all of the child - * forms are valid as well. However, browsers do not allow nesting of `
    ` elements, so - * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to - * `` but can be nested. This allows you to have nested forms, which is very useful when - * using Angular validation directives in forms that are dynamically generated using the - * {@link ng.directive:ngRepeat `ngRepeat`} directive. Since you cannot dynamically generate the `name` - * attribute of input elements using interpolation, you have to wrap each set of repeated inputs in an - * `ngForm` directive and nest these in an outer `form` element. - * - * - * # CSS classes - * - `ng-valid` is set if the form is valid. - * - `ng-invalid` is set if the form is invalid. - * - `ng-pristine` is set if the form is pristine. - * - `ng-dirty` is set if the form is dirty. - * - * - * # Submitting a form and preventing the default action - * - * Since the role of forms in client-side Angular applications is different than in classical - * roundtrip apps, it is desirable for the browser not to translate the form submission into a full - * page reload that sends the data to the server. Instead some javascript logic should be triggered - * to handle the form submission in an application-specific way. - * - * For this reason, Angular prevents the default action (form submission to the server) unless the - * `` element has an `action` attribute specified. - * - * You can use one of the following two ways to specify what javascript method should be called when - * a form is submitted: - * - * - {@link ng.directive:ngSubmit ngSubmit} directive on the form element - * - {@link ng.directive:ngClick ngClick} directive on the first - * button or input field of type submit (input[type=submit]) - * - * To prevent double execution of the handler, use only one of the {@link ng.directive:ngSubmit ngSubmit} - * or {@link ng.directive:ngClick ngClick} directives. - * This is because of the following form submission rules in the HTML specification: - * - * - If a form has only one input field then hitting enter in this field triggers form submit - * (`ngSubmit`) - * - if a form has 2+ input fields and no buttons or input[type=submit] then hitting enter - * doesn't trigger submit - * - if a form has one or more input fields and one or more buttons or input[type=submit] then - * hitting enter in any of the input fields will trigger the click handler on the *first* button or - * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`) - * - * @param {string=} name Name of the form. If specified, the form controller will be published into - * related scope, under this name. - * - * @example - - - - - userType: - Required!
    - userType = {{userType}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    - -
    - - it('should initialize to model', function() { - var userType = element(by.binding('userType')); - var valid = element(by.binding('myForm.input.$valid')); - - expect(userType.getText()).toContain('guest'); - expect(valid.getText()).toContain('true'); - }); - - it('should be invalid if empty', function() { - var userType = element(by.binding('userType')); - var valid = element(by.binding('myForm.input.$valid')); - var userInput = element(by.model('userType')); - - userInput.clear(); - userInput.sendKeys(''); - - expect(userType.getText()).toEqual('userType ='); - expect(valid.getText()).toContain('false'); - }); - -
    - */ -var formDirectiveFactory = function(isNgForm) { - return ['$timeout', function($timeout) { - var formDirective = { - name: 'form', - restrict: isNgForm ? 'EAC' : 'E', - controller: FormController, - compile: function() { - return { - pre: function(scope, formElement, attr, controller) { - if (!attr.action) { - // we can't use jq events because if a form is destroyed during submission the default - // action is not prevented. see #1238 - // - // IE 9 is not affected because it doesn't fire a submit event and try to do a full - // page reload if the form was destroyed by submission of the form via a click handler - // on a button in the form. Looks like an IE9 specific bug. - var preventDefaultListener = function(event) { - event.preventDefault - ? event.preventDefault() - : event.returnValue = false; // IE - }; - - addEventListenerFn(formElement[0], 'submit', preventDefaultListener); - - // unregister the preventDefault listener so that we don't not leak memory but in a - // way that will achieve the prevention of the default action. - formElement.on('$destroy', function() { - $timeout(function() { - removeEventListenerFn(formElement[0], 'submit', preventDefaultListener); - }, 0, false); - }); - } - - var parentFormCtrl = formElement.parent().controller('form'), - alias = attr.name || attr.ngForm; - - if (alias) { - setter(scope, alias, controller, alias); - } - if (parentFormCtrl) { - formElement.on('$destroy', function() { - parentFormCtrl.$removeControl(controller); - if (alias) { - setter(scope, alias, undefined, alias); - } - extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards - }); - } - } - }; - } - }; - - return formDirective; - }]; -}; - -var formDirective = formDirectiveFactory(); -var ngFormDirective = formDirectiveFactory(true); - -/* global - - -VALID_CLASS, - -INVALID_CLASS, - -PRISTINE_CLASS, - -DIRTY_CLASS -*/ - -var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; -var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/i; -var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; - -var inputType = { - - /** - * @ngdoc inputType - * @name ng.directive:input.text - * - * @description - * Standard HTML text input with angular data binding. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} required Adds `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than - * minlength. - * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. - * - * @example - - - -
    - Single word: - - Required! - - Single word only! - - text = {{text}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    -
    -
    - - var text = element(by.binding('text')); - var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('text')); - - it('should initialize to model', function() { - expect(text.getText()).toContain('guest'); - expect(valid.getText()).toContain('true'); - }); - - it('should be invalid if empty', function() { - input.clear(); - input.sendKeys(''); - - expect(text.getText()).toEqual('text ='); - expect(valid.getText()).toContain('false'); - }); - - it('should be invalid if multi word', function() { - input.clear(); - input.sendKeys('hello world'); - - expect(valid.getText()).toContain('false'); - }); - -
    - */ - 'text': textInputType, - - - /** - * @ngdoc inputType - * @name ng.directive:input.number - * - * @description - * Text input with number validation and transformation. Sets the `number` validation - * error if not a valid number. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than - * minlength. - * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - - - -
    - Number: - - Required! - - Not valid number! - value = {{value}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    -
    -
    - - var value = element(by.binding('value')); - var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('value')); - - it('should initialize to model', function() { - expect(value.getText()).toContain('12'); - expect(valid.getText()).toContain('true'); - }); - - it('should be invalid if empty', function() { - input.clear(); - input.sendKeys(''); - expect(value.getText()).toEqual('value ='); - expect(valid.getText()).toContain('false'); - }); - - it('should be invalid if over max', function() { - input.clear(); - input.sendKeys('123'); - expect(value.getText()).toEqual('value ='); - expect(valid.getText()).toContain('false'); - }); - -
    - */ - 'number': numberInputType, - - - /** - * @ngdoc inputType - * @name ng.directive:input.url - * - * @description - * Text input with URL validation. Sets the `url` validation error key if the content is not a - * valid URL. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than - * minlength. - * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - - - -
    - URL: - - Required! - - Not valid url! - text = {{text}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    - myForm.$error.url = {{!!myForm.$error.url}}
    -
    -
    - - var text = element(by.binding('text')); - var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('text')); - - it('should initialize to model', function() { - expect(text.getText()).toContain('http://google.com'); - expect(valid.getText()).toContain('true'); - }); - - it('should be invalid if empty', function() { - input.clear(); - input.sendKeys(''); - - expect(text.getText()).toEqual('text ='); - expect(valid.getText()).toContain('false'); - }); - - it('should be invalid if not url', function() { - input.clear(); - input.sendKeys('box'); - - expect(valid.getText()).toContain('false'); - }); - -
    - */ - 'url': urlInputType, - - - /** - * @ngdoc inputType - * @name ng.directive:input.email - * - * @description - * Text input with email validation. Sets the `email` validation error key if not a valid email - * address. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than - * minlength. - * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - - - -
    - Email: - - Required! - - Not valid email! - text = {{text}}
    - myForm.input.$valid = {{myForm.input.$valid}}
    - myForm.input.$error = {{myForm.input.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    - myForm.$error.email = {{!!myForm.$error.email}}
    -
    -
    - - var text = element(by.binding('text')); - var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('text')); - - it('should initialize to model', function() { - expect(text.getText()).toContain('me@example.com'); - expect(valid.getText()).toContain('true'); - }); - - it('should be invalid if empty', function() { - input.clear(); - input.sendKeys(''); - expect(text.getText()).toEqual('text ='); - expect(valid.getText()).toContain('false'); - }); - - it('should be invalid if not email', function() { - input.clear(); - input.sendKeys('xxx'); - - expect(valid.getText()).toContain('false'); - }); - -
    - */ - 'email': emailInputType, - - - /** - * @ngdoc inputType - * @name ng.directive:input.radio - * - * @description - * HTML radio button. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string} value The value to which the expression should be set when selected. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * @param {string} ngValue Angular expression which sets the value to which the expression should - * be set when selected. - * - * @example - - - -
    - Red
    - Green
    - Blue
    - color = {{color | json}}
    -
    - Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`. -
    - - it('should change state', function() { - var color = element(by.binding('color')); - - expect(color.getText()).toContain('blue'); - - element.all(by.model('color')).get(0).click(); - - expect(color.getText()).toContain('red'); - }); - -
    - */ - 'radio': radioInputType, - - - /** - * @ngdoc inputType - * @name ng.directive:input.checkbox - * - * @description - * HTML checkbox. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} ngTrueValue The value to which the expression should be set when selected. - * @param {string=} ngFalseValue The value to which the expression should be set when not selected. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - - - -
    - Value1:
    - Value2:
    - value1 = {{value1}}
    - value2 = {{value2}}
    -
    -
    - - it('should change state', function() { - var value1 = element(by.binding('value1')); - var value2 = element(by.binding('value2')); - - expect(value1.getText()).toContain('true'); - expect(value2.getText()).toContain('YES'); - - element(by.model('value1')).click(); - element(by.model('value2')).click(); - - expect(value1.getText()).toContain('false'); - expect(value2.getText()).toContain('NO'); - }); - -
    - */ - 'checkbox': checkboxInputType, - - 'hidden': noop, - 'button': noop, - 'submit': noop, - 'reset': noop, - 'file': noop -}; - -// A helper function to call $setValidity and return the value / undefined, -// a pattern that is repeated a lot in the input validation logic. -function validate(ctrl, validatorName, validity, value){ - ctrl.$setValidity(validatorName, validity); - return validity ? value : undefined; -} - -function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { - // In composition mode, users are still inputing intermediate text buffer, - // hold the listener until composition is done. - // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent - if (!$sniffer.android) { - var composing = false; - - element.on('compositionstart', function(data) { - composing = true; - }); - - element.on('compositionend', function() { - composing = false; - listener(); - }); - } - - var listener = function() { - if (composing) return; - var value = element.val(); - - // By default we will trim the value - // If the attribute ng-trim exists we will avoid trimming - // e.g. - if (toBoolean(attr.ngTrim || 'T')) { - value = trim(value); - } - - if (ctrl.$viewValue !== value) { - if (scope.$$phase) { - ctrl.$setViewValue(value); - } else { - scope.$apply(function() { - ctrl.$setViewValue(value); - }); - } - } - }; - - // if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the - // input event on backspace, delete or cut - if ($sniffer.hasEvent('input')) { - element.on('input', listener); - } else { - var timeout; - - var deferListener = function() { - if (!timeout) { - timeout = $browser.defer(function() { - listener(); - timeout = null; - }); - } - }; - - element.on('keydown', function(event) { - var key = event.keyCode; - - // ignore - // command modifiers arrows - if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; - - deferListener(); - }); - - // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it - if ($sniffer.hasEvent('paste')) { - element.on('paste cut', deferListener); - } - } - - // if user paste into input using mouse on older browser - // or form autocomplete on newer browser, we need "change" event to catch it - element.on('change', listener); - - ctrl.$render = function() { - element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); - }; - - // pattern validator - var pattern = attr.ngPattern, - patternValidator, - match; - - if (pattern) { - var validateRegex = function(regexp, value) { - return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || regexp.test(value), value); - }; - match = pattern.match(/^\/(.*)\/([gim]*)$/); - if (match) { - pattern = new RegExp(match[1], match[2]); - patternValidator = function(value) { - return validateRegex(pattern, value); - }; - } else { - patternValidator = function(value) { - var patternObj = scope.$eval(pattern); - - if (!patternObj || !patternObj.test) { - throw minErr('ngPattern')('noregexp', - 'Expected {0} to be a RegExp but was {1}. Element: {2}', pattern, - patternObj, startingTag(element)); - } - return validateRegex(patternObj, value); - }; - } - - ctrl.$formatters.push(patternValidator); - ctrl.$parsers.push(patternValidator); - } - - // min length validator - if (attr.ngMinlength) { - var minlength = int(attr.ngMinlength); - var minLengthValidator = function(value) { - return validate(ctrl, 'minlength', ctrl.$isEmpty(value) || value.length >= minlength, value); - }; - - ctrl.$parsers.push(minLengthValidator); - ctrl.$formatters.push(minLengthValidator); - } - - // max length validator - if (attr.ngMaxlength) { - var maxlength = int(attr.ngMaxlength); - var maxLengthValidator = function(value) { - return validate(ctrl, 'maxlength', ctrl.$isEmpty(value) || value.length <= maxlength, value); - }; - - ctrl.$parsers.push(maxLengthValidator); - ctrl.$formatters.push(maxLengthValidator); - } -} - -function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); - - ctrl.$parsers.push(function(value) { - var empty = ctrl.$isEmpty(value); - if (empty || NUMBER_REGEXP.test(value)) { - ctrl.$setValidity('number', true); - return value === '' ? null : (empty ? value : parseFloat(value)); - } else { - ctrl.$setValidity('number', false); - return undefined; - } - }); - - ctrl.$formatters.push(function(value) { - return ctrl.$isEmpty(value) ? '' : '' + value; - }); - - if (attr.min) { - var minValidator = function(value) { - var min = parseFloat(attr.min); - return validate(ctrl, 'min', ctrl.$isEmpty(value) || value >= min, value); - }; - - ctrl.$parsers.push(minValidator); - ctrl.$formatters.push(minValidator); - } - - if (attr.max) { - var maxValidator = function(value) { - var max = parseFloat(attr.max); - return validate(ctrl, 'max', ctrl.$isEmpty(value) || value <= max, value); - }; - - ctrl.$parsers.push(maxValidator); - ctrl.$formatters.push(maxValidator); - } - - ctrl.$formatters.push(function(value) { - return validate(ctrl, 'number', ctrl.$isEmpty(value) || isNumber(value), value); - }); -} - -function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); - - var urlValidator = function(value) { - return validate(ctrl, 'url', ctrl.$isEmpty(value) || URL_REGEXP.test(value), value); - }; - - ctrl.$formatters.push(urlValidator); - ctrl.$parsers.push(urlValidator); -} - -function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); - - var emailValidator = function(value) { - return validate(ctrl, 'email', ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value), value); - }; - - ctrl.$formatters.push(emailValidator); - ctrl.$parsers.push(emailValidator); -} - -function radioInputType(scope, element, attr, ctrl) { - // make the name unique, if not defined - if (isUndefined(attr.name)) { - element.attr('name', nextUid()); - } - - element.on('click', function() { - if (element[0].checked) { - scope.$apply(function() { - ctrl.$setViewValue(attr.value); - }); - } - }); - - ctrl.$render = function() { - var value = attr.value; - element[0].checked = (value == ctrl.$viewValue); - }; - - attr.$observe('value', ctrl.$render); -} - -function checkboxInputType(scope, element, attr, ctrl) { - var trueValue = attr.ngTrueValue, - falseValue = attr.ngFalseValue; - - if (!isString(trueValue)) trueValue = true; - if (!isString(falseValue)) falseValue = false; - - element.on('click', function() { - scope.$apply(function() { - ctrl.$setViewValue(element[0].checked); - }); - }); - - ctrl.$render = function() { - element[0].checked = ctrl.$viewValue; - }; - - // Override the standard `$isEmpty` because a value of `false` means empty in a checkbox. - ctrl.$isEmpty = function(value) { - return value !== trueValue; - }; - - ctrl.$formatters.push(function(value) { - return value === trueValue; - }); - - ctrl.$parsers.push(function(value) { - return value ? trueValue : falseValue; - }); -} - - -/** - * @ngdoc directive - * @name ng.directive:textarea - * @restrict E - * - * @description - * HTML textarea element control with angular data-binding. The data-binding and validation - * properties of this element are exactly the same as those of the - * {@link ng.directive:input input element}. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than - * minlength. - * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - */ - - -/** - * @ngdoc directive - * @name ng.directive:input - * @restrict E - * - * @description - * HTML input element control with angular data-binding. Input control follows HTML5 input types - * and polyfills the HTML5 validation behavior for older browsers. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} required Sets `required` validation error key if the value is not entered. - * @param {boolean=} ngRequired Sets `required` attribute if set to true - * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than - * minlength. - * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - - - -
    -
    - User name: - - Required!
    - Last name: - - Too short! - - Too long!
    -
    -
    - user = {{user}}
    - myForm.userName.$valid = {{myForm.userName.$valid}}
    - myForm.userName.$error = {{myForm.userName.$error}}
    - myForm.lastName.$valid = {{myForm.lastName.$valid}}
    - myForm.lastName.$error = {{myForm.lastName.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    - myForm.$error.minlength = {{!!myForm.$error.minlength}}
    - myForm.$error.maxlength = {{!!myForm.$error.maxlength}}
    -
    -
    - - var user = element(by.binding('{{user}}')); - var userNameValid = element(by.binding('myForm.userName.$valid')); - var lastNameValid = element(by.binding('myForm.lastName.$valid')); - var lastNameError = element(by.binding('myForm.lastName.$error')); - var formValid = element(by.binding('myForm.$valid')); - var userNameInput = element(by.model('user.name')); - var userLastInput = element(by.model('user.last')); - - it('should initialize to model', function() { - expect(user.getText()).toContain('{"name":"guest","last":"visitor"}'); - expect(userNameValid.getText()).toContain('true'); - expect(formValid.getText()).toContain('true'); - }); - - it('should be invalid if empty when required', function() { - userNameInput.clear(); - userNameInput.sendKeys(''); - - expect(user.getText()).toContain('{"last":"visitor"}'); - expect(userNameValid.getText()).toContain('false'); - expect(formValid.getText()).toContain('false'); - }); - - it('should be valid if empty when min length is set', function() { - userLastInput.clear(); - userLastInput.sendKeys(''); - - expect(user.getText()).toContain('{"name":"guest","last":""}'); - expect(lastNameValid.getText()).toContain('true'); - expect(formValid.getText()).toContain('true'); - }); - - it('should be invalid if less than required min length', function() { - userLastInput.clear(); - userLastInput.sendKeys('xx'); - - expect(user.getText()).toContain('{"name":"guest"}'); - expect(lastNameValid.getText()).toContain('false'); - expect(lastNameError.getText()).toContain('minlength'); - expect(formValid.getText()).toContain('false'); - }); - - it('should be invalid if longer than max length', function() { - userLastInput.clear(); - userLastInput.sendKeys('some ridiculously long name'); - - expect(user.getText()).toContain('{"name":"guest"}'); - expect(lastNameValid.getText()).toContain('false'); - expect(lastNameError.getText()).toContain('maxlength'); - expect(formValid.getText()).toContain('false'); - }); - -
    - */ -var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { - return { - restrict: 'E', - require: '?ngModel', - link: function(scope, element, attr, ctrl) { - if (ctrl) { - (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer, - $browser); - } - } - }; -}]; - -var VALID_CLASS = 'ng-valid', - INVALID_CLASS = 'ng-invalid', - PRISTINE_CLASS = 'ng-pristine', - DIRTY_CLASS = 'ng-dirty'; - -/** - * @ngdoc object - * @name ng.directive:ngModel.NgModelController - * - * @property {string} $viewValue Actual string value in the view. - * @property {*} $modelValue The value in the model, that the control is bound to. - * @property {Array.} $parsers Array of functions to execute, as a pipeline, whenever - the control reads value from the DOM. Each function is called, in turn, passing the value - through to the next. Used to sanitize / convert the value as well as validation. - For validation, the parsers should update the validity state using - {@link ng.directive:ngModel.NgModelController#methods_$setValidity $setValidity()}, - and return `undefined` for invalid values. - - * - * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever - the model value changes. Each function is called, in turn, passing the value through to the - next. Used to format / convert values for display in the control and validation. - *
    - *      function formatter(value) {
    - *        if (value) {
    - *          return value.toUpperCase();
    - *        }
    - *      }
    - *      ngModel.$formatters.push(formatter);
    - *      
    - * - * @property {Array.} $viewChangeListeners Array of functions to execute whenever the - * view value has changed. It is called with no arguments, and its return value is ignored. - * This can be used in place of additional $watches against the model value. - * - * @property {Object} $error An object hash with all errors as keys. - * - * @property {boolean} $pristine True if user has not interacted with the control yet. - * @property {boolean} $dirty True if user has already interacted with the control. - * @property {boolean} $valid True if there is no error. - * @property {boolean} $invalid True if at least one error on the control. - * - * @description - * - * `NgModelController` provides API for the `ng-model` directive. The controller contains - * services for data-binding, validation, CSS updates, and value formatting and parsing. It - * purposefully does not contain any logic which deals with DOM rendering or listening to - * DOM events. Such DOM related logic should be provided by other directives which make use of - * `NgModelController` for data-binding. - * - * ## Custom Control Example - * This example shows how to use `NgModelController` with a custom control to achieve - * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`) - * collaborate together to achieve the desired result. - * - * Note that `contenteditable` is an HTML5 attribute, which tells the browser to let the element - * contents be edited in place by the user. This will not work on older browsers. - * - * - - [contenteditable] { - border: 1px solid black; - background-color: white; - min-height: 20px; - } - - .ng-invalid { - border: 1px solid red; - } - - - - angular.module('customControl', []). - directive('contenteditable', function() { - return { - restrict: 'A', // only activate on element attribute - require: '?ngModel', // get a hold of NgModelController - link: function(scope, element, attrs, ngModel) { - if(!ngModel) return; // do nothing if no ng-model - - // Specify how UI should be updated - ngModel.$render = function() { - element.html(ngModel.$viewValue || ''); - }; - - // Listen for change events to enable binding - element.on('blur keyup change', function() { - scope.$apply(read); - }); - read(); // initialize - - // Write data to the model - function read() { - var html = element.html(); - // When we clear the content editable the browser leaves a
    behind - // If strip-br attribute is provided then we strip this out - if( attrs.stripBr && html == '
    ' ) { - html = ''; - } - ngModel.$setViewValue(html); - } - } - }; - }); -
    - -
    -
    Change me!
    - Required! -
    - -
    -
    - - it('should data-bind and become invalid', function() { - if (browser.params.browser = 'safari') { - // SafariDriver can't handle contenteditable. - return; - }; - var contentEditable = element(by.css('.doc-example-live [contenteditable]')); - - expect(contentEditable.getText()).toEqual('Change me!'); - - // Firefox driver doesn't trigger the proper events on 'clear', so do this hack - contentEditable.click(); - contentEditable.sendKeys(protractor.Key.chord(protractor.Key.COMMAND, "a")); - contentEditable.sendKeys(protractor.Key.BACK_SPACE); - - expect(contentEditable.getText()).toEqual(''); - expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/); - }); - - *
    - * - * - */ -var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', - function($scope, $exceptionHandler, $attr, $element, $parse) { - this.$viewValue = Number.NaN; - this.$modelValue = Number.NaN; - this.$parsers = []; - this.$formatters = []; - this.$viewChangeListeners = []; - this.$pristine = true; - this.$dirty = false; - this.$valid = true; - this.$invalid = false; - this.$name = $attr.name; - - var ngModelGet = $parse($attr.ngModel), - ngModelSet = ngModelGet.assign; - - if (!ngModelSet) { - throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}", - $attr.ngModel, startingTag($element)); - } - - /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$render - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * Called when the view needs to be updated. It is expected that the user of the ng-model - * directive will implement this method. - */ - this.$render = noop; - - /** - * @ngdoc function - * @name { ng.directive:ngModel.NgModelController#$isEmpty - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * This is called when we need to determine if the value of the input is empty. - * - * For instance, the required directive does this to work out if the input has data or not. - * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. - * - * You can override this for input directives whose concept of being empty is different to the - * default. The `checkboxInputType` directive does this because in its case a value of `false` - * implies empty. - * - * @param {*} value Reference to check. - * @returns {boolean} True if `value` is empty. - */ - this.$isEmpty = function(value) { - return isUndefined(value) || value === '' || value === null || value !== value; - }; - - var parentForm = $element.inheritedData('$formController') || nullFormCtrl, - invalidCount = 0, // used to easily determine if we are valid - $error = this.$error = {}; // keep invalid keys here - - - // Setup initial state of the control - $element.addClass(PRISTINE_CLASS); - toggleValidCss(true); - - // convenience method for easy toggling of classes - function toggleValidCss(isValid, validationErrorKey) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - $element. - removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey). - addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); - } - - /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$setValidity - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * Change the validity state, and notifies the form when the control changes validity. (i.e. it - * does not notify form if given validator is already marked as invalid). - * - * This method should be called by validators - i.e. the parser or formatter functions. - * - * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign - * to `$error[validationErrorKey]=isValid` so that it is available for data-binding. - * The `validationErrorKey` should be in camelCase and will get converted into dash-case - * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` - * class and can be bound to as `{{someForm.someControl.$error.myError}}` . - * @param {boolean} isValid Whether the current state is valid (true) or invalid (false). - */ - this.$setValidity = function(validationErrorKey, isValid) { - // Purposeful use of ! here to cast isValid to boolean in case it is undefined - // jshint -W018 - if ($error[validationErrorKey] === !isValid) return; - // jshint +W018 - - if (isValid) { - if ($error[validationErrorKey]) invalidCount--; - if (!invalidCount) { - toggleValidCss(true); - this.$valid = true; - this.$invalid = false; - } - } else { - toggleValidCss(false); - this.$invalid = true; - this.$valid = false; - invalidCount++; - } - - $error[validationErrorKey] = !isValid; - toggleValidCss(isValid, validationErrorKey); - - parentForm.$setValidity(validationErrorKey, isValid, this); - }; - - /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$setPristine - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * Sets the control to its pristine state. - * - * This method can be called to remove the 'ng-dirty' class and set the control to its pristine - * state (ng-pristine class). - */ - this.$setPristine = function () { - this.$dirty = false; - this.$pristine = true; - $element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS); - }; - - /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$setViewValue - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * Update the view value. - * - * This method should be called when the view value changes, typically from within a DOM event handler. - * For example {@link ng.directive:input input} and - * {@link ng.directive:select select} directives call it. - * - * It will update the $viewValue, then pass this value through each of the functions in `$parsers`, - * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to - * `$modelValue` and the **expression** specified in the `ng-model` attribute. - * - * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. - * - * Note that calling this function does not trigger a `$digest`. - * - * @param {string} value Value from the view. - */ - this.$setViewValue = function(value) { - this.$viewValue = value; - - // change to dirty - if (this.$pristine) { - this.$dirty = true; - this.$pristine = false; - $element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); - parentForm.$setDirty(); - } - - forEach(this.$parsers, function(fn) { - value = fn(value); - }); - - if (this.$modelValue !== value) { - this.$modelValue = value; - ngModelSet($scope, value); - forEach(this.$viewChangeListeners, function(listener) { - try { - listener(); - } catch(e) { - $exceptionHandler(e); - } - }); - } - }; - - // model -> value - var ctrl = this; - - $scope.$watch(function ngModelWatch() { - var value = ngModelGet($scope); - - // if scope model value and ngModel value are out of sync - if (ctrl.$modelValue !== value) { - - var formatters = ctrl.$formatters, - idx = formatters.length; - - ctrl.$modelValue = value; - while(idx--) { - value = formatters[idx](value); - } - - if (ctrl.$viewValue !== value) { - ctrl.$viewValue = value; - ctrl.$render(); - } - } - - return value; - }); -}]; - - -/** - * @ngdoc directive - * @name ng.directive:ngModel - * - * @element input - * - * @description - * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a - * property on the scope using {@link ng.directive:ngModel.NgModelController NgModelController}, - * which is created and exposed by this directive. - * - * `ngModel` is responsible for: - * - * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` - * require. - * - Providing validation behavior (i.e. required, number, email, url). - * - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors). - * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`). - * - Registering the control with its parent {@link ng.directive:form form}. - * - * Note: `ngModel` will try to bind to the property given by evaluating the expression on the - * current scope. If the property doesn't already exist on this scope, it will be created - * implicitly and added to the scope. - * - * For best practices on using `ngModel`, see: - * - * - {@link https://github.com/angular/angular.js/wiki/Understanding-Scopes} - * - * For basic examples, how to use `ngModel`, see: - * - * - {@link ng.directive:input input} - * - {@link ng.directive:input.text text} - * - {@link ng.directive:input.checkbox checkbox} - * - {@link ng.directive:input.radio radio} - * - {@link ng.directive:input.number number} - * - {@link ng.directive:input.email email} - * - {@link ng.directive:input.url url} - * - {@link ng.directive:select select} - * - {@link ng.directive:textarea textarea} - * - */ -var ngModelDirective = function() { - return { - require: ['ngModel', '^?form'], - controller: NgModelController, - link: function(scope, element, attr, ctrls) { - // notify others, especially parent forms - - var modelCtrl = ctrls[0], - formCtrl = ctrls[1] || nullFormCtrl; - - formCtrl.$addControl(modelCtrl); - - scope.$on('$destroy', function() { - formCtrl.$removeControl(modelCtrl); - }); - } - }; -}; - - -/** - * @ngdoc directive - * @name ng.directive:ngChange - * - * @description - * Evaluate the given expression when the user changes the input. - * The expression is evaluated immediately, unlike the JavaScript onchange event - * which only triggers at the end of a change (usually, when the user leaves the - * form element or presses the return key). - * The expression is not evaluated when the value change is coming from the model. - * - * Note, this directive requires `ngModel` to be present. - * - * @element input - * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change - * in input value. - * - * @example - * - * - * - *
    - * - * - *
    - * debug = {{confirmed}}
    - * counter = {{counter}}
    - *
    - *
    - * - * var counter = element(by.binding('counter')); - * var debug = element(by.binding('confirmed')); - * - * it('should evaluate the expression if changing from view', function() { - * expect(counter.getText()).toContain('0'); - * - * element(by.id('ng-change-example1')).click(); - * - * expect(counter.getText()).toContain('1'); - * expect(debug.getText()).toContain('true'); - * }); - * - * it('should not evaluate the expression if changing from model', function() { - * element(by.id('ng-change-example2')).click(); - - * expect(counter.getText()).toContain('0'); - * expect(debug.getText()).toContain('true'); - * }); - * - *
    - */ -var ngChangeDirective = valueFn({ - require: 'ngModel', - link: function(scope, element, attr, ctrl) { - ctrl.$viewChangeListeners.push(function() { - scope.$eval(attr.ngChange); - }); - } -}); - - -var requiredDirective = function() { - return { - require: '?ngModel', - link: function(scope, elm, attr, ctrl) { - if (!ctrl) return; - attr.required = true; // force truthy in case we are on non input element - - var validator = function(value) { - if (attr.required && ctrl.$isEmpty(value)) { - ctrl.$setValidity('required', false); - return; - } else { - ctrl.$setValidity('required', true); - return value; - } - }; - - ctrl.$formatters.push(validator); - ctrl.$parsers.unshift(validator); - - attr.$observe('required', function() { - validator(ctrl.$viewValue); - }); - } - }; -}; - - -/** - * @ngdoc directive - * @name ng.directive:ngList - * - * @description - * Text input that converts between a delimited string and an array of strings. The delimiter - * can be a fixed string (by default a comma) or a regular expression. - * - * @element input - * @param {string=} ngList optional delimiter that should be used to split the value. If - * specified in form `/something/` then the value will be converted into a regular expression. - * - * @example - - - -
    - List: - - Required! -
    - names = {{names}}
    - myForm.namesInput.$valid = {{myForm.namesInput.$valid}}
    - myForm.namesInput.$error = {{myForm.namesInput.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    -
    -
    - - var listInput = element(by.model('names')); - var names = element(by.binding('{{names}}')); - var valid = element(by.binding('myForm.namesInput.$valid')); - var error = element(by.css('span.error')); - - it('should initialize to model', function() { - expect(names.getText()).toContain('["igor","misko","vojta"]'); - expect(valid.getText()).toContain('true'); - expect(error.getCssValue('display')).toBe('none'); - }); - - it('should be invalid if empty', function() { - listInput.clear(); - listInput.sendKeys(''); - - expect(names.getText()).toContain(''); - expect(valid.getText()).toContain('false'); - expect(error.getCssValue('display')).not.toBe('none'); }); - -
    - */ -var ngListDirective = function() { - return { - require: 'ngModel', - link: function(scope, element, attr, ctrl) { - var match = /\/(.*)\//.exec(attr.ngList), - separator = match && new RegExp(match[1]) || attr.ngList || ','; - - var parse = function(viewValue) { - // If the viewValue is invalid (say required but empty) it will be `undefined` - if (isUndefined(viewValue)) return; - - var list = []; - - if (viewValue) { - forEach(viewValue.split(separator), function(value) { - if (value) list.push(trim(value)); - }); - } - - return list; - }; - - ctrl.$parsers.push(parse); - ctrl.$formatters.push(function(value) { - if (isArray(value)) { - return value.join(', '); - } - - return undefined; - }); - - // Override the standard $isEmpty because an empty array means the input is empty. - ctrl.$isEmpty = function(value) { - return !value || !value.length; - }; - } - }; -}; - - -var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; -/** - * @ngdoc directive - * @name ng.directive:ngValue - * - * @description - * Binds the given expression to the value of `input[select]` or `input[radio]`, so - * that when the element is selected, the `ngModel` of that element is set to the - * bound value. - * - * `ngValue` is useful when dynamically generating lists of radio buttons using `ng-repeat`, as - * shown below. - * - * @element input - * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute - * of the `input` element - * - * @example - - - -
    -

    Which is your favorite?

    - -
    You chose {{my.favorite}}
    -
    -
    - - var favorite = element(by.binding('my.favorite')); - - it('should initialize to model', function() { - expect(favorite.getText()).toContain('unicorns'); - }); - it('should bind the values to the inputs', function() { - element.all(by.model('my.favorite')).get(0).click(); - expect(favorite.getText()).toContain('pizza'); - }); - -
    - */ -var ngValueDirective = function() { - return { - priority: 100, - compile: function(tpl, tplAttr) { - if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { - return function ngValueConstantLink(scope, elm, attr) { - attr.$set('value', scope.$eval(attr.ngValue)); - }; - } else { - return function ngValueLink(scope, elm, attr) { - scope.$watch(attr.ngValue, function valueWatchAction(value) { - attr.$set('value', value); - }); - }; - } - } - }; -}; - -/** - * @ngdoc directive - * @name ng.directive:ngBind - * @restrict AC - * - * @description - * The `ngBind` attribute tells Angular to replace the text content of the specified HTML element - * with the value of a given expression, and to update the text content when the value of that - * expression changes. - * - * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like - * `{{ expression }}` which is similar but less verbose. - * - * It is preferrable to use `ngBind` instead of `{{ expression }}` when a template is momentarily - * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an - * element attribute, it makes the bindings invisible to the user while the page is loading. - * - * An alternative solution to this problem would be using the - * {@link ng.directive:ngCloak ngCloak} directive. - * - * - * @element ANY - * @param {expression} ngBind {@link guide/expression Expression} to evaluate. - * - * @example - * Enter a name in the Live Preview text box; the greeting below the text box changes instantly. - - - -
    - Enter name:
    - Hello ! -
    -
    - - it('should check ng-bind', function() { - var exampleContainer = $('.doc-example-live'); - var nameInput = element(by.model('name')); - - expect(exampleContainer.findElement(by.binding('name')).getText()).toBe('Whirled'); - nameInput.clear(); - nameInput.sendKeys('world'); - expect(exampleContainer.findElement(by.binding('name')).getText()).toBe('world'); - }); - -
    - */ -var ngBindDirective = ngDirective(function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBind); - scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - // We are purposefully using == here rather than === because we want to - // catch when value is "null or undefined" - // jshint -W041 - element.text(value == undefined ? '' : value); - }); -}); - - -/** - * @ngdoc directive - * @name ng.directive:ngBindTemplate - * - * @description - * The `ngBindTemplate` directive specifies that the element - * text content should be replaced with the interpolation of the template - * in the `ngBindTemplate` attribute. - * Unlike `ngBind`, the `ngBindTemplate` can contain multiple `{{` `}}` - * expressions. This directive is needed since some HTML elements - * (such as TITLE and OPTION) cannot contain SPAN elements. - * - * @element ANY - * @param {string} ngBindTemplate template of form - * {{ expression }} to eval. - * - * @example - * Try it here: enter text in text box and watch the greeting change. - - - -
    - Salutation:
    - Name:
    -
    
    -       
    -
    - - it('should check ng-bind', function() { - var salutationElem = element(by.binding('salutation')); - var salutationInput = element(by.model('salutation')); - var nameInput = element(by.model('name')); - - expect(salutationElem.getText()).toBe('Hello World!'); - - salutationInput.clear(); - salutationInput.sendKeys('Greetings'); - nameInput.clear(); - nameInput.sendKeys('user'); - - expect(salutationElem.getText()).toBe('Greetings user!'); - }); - -
    - */ -var ngBindTemplateDirective = ['$interpolate', function($interpolate) { - return function(scope, element, attr) { - // TODO: move this to scenario runner - var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); - element.addClass('ng-binding').data('$binding', interpolateFn); - attr.$observe('ngBindTemplate', function(value) { - element.text(value); - }); - }; -}]; - - -/** - * @ngdoc directive - * @name ng.directive:ngBindHtml - * - * @description - * Creates a binding that will innerHTML the result of evaluating the `expression` into the current - * element in a secure way. By default, the innerHTML-ed content will be sanitized using the {@link - * ngSanitize.$sanitize $sanitize} service. To utilize this functionality, ensure that `$sanitize` - * is available, for example, by including {@link ngSanitize} in your module's dependencies (not in - * core Angular.) You may also bypass sanitization for values you know are safe. To do so, bind to - * an explicitly trusted value via {@link ng.$sce#methods_trustAsHtml $sce.trustAsHtml}. See the example - * under {@link ng.$sce#Example Strict Contextual Escaping (SCE)}. - * - * Note: If a `$sanitize` service is unavailable and the bound value isn't explicitly trusted, you - * will have an exception (instead of an exploit.) - * - * @element ANY - * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. - * - * @example - Try it here: enter text in text box and watch the greeting change. - - - -
    -

    -
    -
    - - - angular.module('ngBindHtmlExample', ['ngSanitize']) - - .controller('ngBindHtmlCtrl', ['$scope', function ngBindHtmlCtrl($scope) { - $scope.myHTML = - 'I am an HTMLstring with links! and other stuff'; - }]); - - - - it('should check ng-bind-html', function() { - expect(element(by.binding('myHTML')).getText()).toBe( - 'I am an HTMLstring with links! and other stuff'); - }); - -
    - */ -var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) { - return function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBindHtml); - - var parsed = $parse(attr.ngBindHtml); - function getStringValue() { return (parsed(scope) || '').toString(); } - - scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) { - element.html($sce.getTrustedHtml(parsed(scope)) || ''); - }); - }; -}]; - -function classDirective(name, selector) { - name = 'ngClass' + name; - return function() { - return { - restrict: 'AC', - link: function(scope, element, attr) { - var oldVal; - - scope.$watch(attr[name], ngClassWatchAction, true); - - attr.$observe('class', function(value) { - ngClassWatchAction(scope.$eval(attr[name])); - }); - - - if (name !== 'ngClass') { - scope.$watch('$index', function($index, old$index) { - // jshint bitwise: false - var mod = $index & 1; - if (mod !== old$index & 1) { - var classes = flattenClasses(scope.$eval(attr[name])); - mod === selector ? - attr.$addClass(classes) : - attr.$removeClass(classes); - } - }); - } - - - function ngClassWatchAction(newVal) { - if (selector === true || scope.$index % 2 === selector) { - var newClasses = flattenClasses(newVal || ''); - if(!oldVal) { - attr.$addClass(newClasses); - } else if(!equals(newVal,oldVal)) { - attr.$updateClass(newClasses, flattenClasses(oldVal)); - } - } - oldVal = copy(newVal); - } - - - function flattenClasses(classVal) { - if(isArray(classVal)) { - return classVal.join(' '); - } else if (isObject(classVal)) { - var classes = [], i = 0; - forEach(classVal, function(v, k) { - if (v) { - classes.push(k); - } - }); - return classes.join(' '); - } - - return classVal; - } - } - }; - }; -} - -/** - * @ngdoc directive - * @name ng.directive:ngClass - * @restrict AC - * - * @description - * The `ngClass` directive allows you to dynamically set CSS classes on an HTML element by databinding - * an expression that represents all classes to be added. - * - * The directive won't add duplicate classes if a particular class was already set. - * - * When the expression changes, the previously added classes are removed and only then the - * new classes are added. - * - * @animations - * add - happens just before the class is applied to the element - * remove - happens just before the class is removed from the element - * - * @element ANY - * @param {expression} ngClass {@link guide/expression Expression} to eval. The result - * of the evaluation can be a string representing space delimited class - * names, an array, or a map of class names to boolean values. In the case of a map, the - * names of the properties whose values are truthy will be added as css classes to the - * element. - * - * @example Example that demonstrates basic bindings via ngClass directive. - - -

    Map Syntax Example

    - deleted (apply "strike" class)
    - important (apply "bold" class)
    - error (apply "red" class) -
    -

    Using String Syntax

    - -
    -

    Using Array Syntax

    -
    -
    -
    -
    - - .strike { - text-decoration: line-through; - } - .bold { - font-weight: bold; - } - .red { - color: red; - } - - - var ps = element.all(by.css('.doc-example-live p')); - - it('should let you toggle the class', function() { - - expect(ps.first().getAttribute('class')).not.toMatch(/bold/); - expect(ps.first().getAttribute('class')).not.toMatch(/red/); - - element(by.model('important')).click(); - expect(ps.first().getAttribute('class')).toMatch(/bold/); - - element(by.model('error')).click(); - expect(ps.first().getAttribute('class')).toMatch(/red/); - }); - - it('should let you toggle string example', function() { - expect(ps.get(1).getAttribute('class')).toBe(''); - element(by.model('style')).clear(); - element(by.model('style')).sendKeys('red'); - expect(ps.get(1).getAttribute('class')).toBe('red'); - }); - - it('array example should have 3 classes', function() { - expect(ps.last().getAttribute('class')).toBe(''); - element(by.model('style1')).sendKeys('bold'); - element(by.model('style2')).sendKeys('strike'); - element(by.model('style3')).sendKeys('red'); - expect(ps.last().getAttribute('class')).toBe('bold strike red'); - }); - -
    - - ## Animations - - The example below demonstrates how to perform animations using ngClass. - - - - - -
    - Sample Text -
    - - .base-class { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - } - - .base-class.my-class { - color: red; - font-size:3em; - } - - - it('should check ng-class', function() { - expect(element(by.css('.base-class')).getAttribute('class')).not. - toMatch(/my-class/); - - element(by.id('setbtn')).click(); - - expect(element(by.css('.base-class')).getAttribute('class')). - toMatch(/my-class/); - - element(by.id('clearbtn')).click(); - - expect(element(by.css('.base-class')).getAttribute('class')).not. - toMatch(/my-class/); - }); - -
    - - - ## ngClass and pre-existing CSS3 Transitions/Animations - The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure. - Upon animation ngAnimate will apply supplementary CSS classes to track the start and end of an animation, but this will not hinder - any pre-existing CSS transitions already on the element. To get an idea of what happens during a class-based animation, be sure - to view the step by step details of {@link ngAnimate.$animate#methods_addclass $animate.addClass} and - {@link ngAnimate.$animate#methods_removeclass $animate.removeClass}. - */ -var ngClassDirective = classDirective('', true); - -/** - * @ngdoc directive - * @name ng.directive:ngClassOdd - * @restrict AC - * - * @description - * The `ngClassOdd` and `ngClassEven` directives work exactly as - * {@link ng.directive:ngClass ngClass}, except they work in - * conjunction with `ngRepeat` and take effect only on odd (even) rows. - * - * This directive can be applied only within the scope of an - * {@link ng.directive:ngRepeat ngRepeat}. - * - * @element ANY - * @param {expression} ngClassOdd {@link guide/expression Expression} to eval. The result - * of the evaluation can be a string representing space delimited class names or an array. - * - * @example - - -
      -
    1. - - {{name}} - -
    2. -
    -
    - - .odd { - color: red; - } - .even { - color: blue; - } - - - it('should check ng-class-odd and ng-class-even', function() { - expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')). - toMatch(/odd/); - expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')). - toMatch(/even/); - }); - -
    - */ -var ngClassOddDirective = classDirective('Odd', 0); - -/** - * @ngdoc directive - * @name ng.directive:ngClassEven - * @restrict AC - * - * @description - * The `ngClassOdd` and `ngClassEven` directives work exactly as - * {@link ng.directive:ngClass ngClass}, except they work in - * conjunction with `ngRepeat` and take effect only on odd (even) rows. - * - * This directive can be applied only within the scope of an - * {@link ng.directive:ngRepeat ngRepeat}. - * - * @element ANY - * @param {expression} ngClassEven {@link guide/expression Expression} to eval. The - * result of the evaluation can be a string representing space delimited class names or an array. - * - * @example - - -
      -
    1. - - {{name}}       - -
    2. -
    -
    - - .odd { - color: red; - } - .even { - color: blue; - } - - - it('should check ng-class-odd and ng-class-even', function() { - expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')). - toMatch(/odd/); - expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')). - toMatch(/even/); - }); - -
    - */ -var ngClassEvenDirective = classDirective('Even', 1); - -/** - * @ngdoc directive - * @name ng.directive:ngCloak - * @restrict AC - * - * @description - * The `ngCloak` directive is used to prevent the Angular html template from being briefly - * displayed by the browser in its raw (uncompiled) form while your application is loading. Use this - * directive to avoid the undesirable flicker effect caused by the html template display. - * - * The directive can be applied to the `` element, but the preferred usage is to apply - * multiple `ngCloak` directives to small portions of the page to permit progressive rendering - * of the browser view. - * - * `ngCloak` works in cooperation with the following css rule embedded within `angular.js` and - * `angular.min.js`. - * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). - * - *
    - * [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
    - *   display: none !important;
    - * }
    - * 
    - * - * When this css rule is loaded by the browser, all html elements (including their children) that - * are tagged with the `ngCloak` directive are hidden. When Angular encounters this directive - * during the compilation of the template it deletes the `ngCloak` element attribute, making - * the compiled element visible. - * - * For the best result, the `angular.js` script must be loaded in the head section of the html - * document; alternatively, the css rule above must be included in the external stylesheet of the - * application. - * - * Legacy browsers, like IE7, do not provide attribute selector support (added in CSS 2.1) so they - * cannot match the `[ng\:cloak]` selector. To work around this limitation, you must add the css - * class `ng-cloak` in addition to the `ngCloak` directive as shown in the example below. - * - * @element ANY - * - * @example - - -
    {{ 'hello' }}
    -
    {{ 'hello IE7' }}
    -
    - - it('should remove the template directive and css class', function() { - expect($('.doc-example-live #template1').getAttribute('ng-cloak')). - toBeNull(); - expect($('.doc-example-live #template2').getAttribute('ng-cloak')). - toBeNull(); - }); - -
    - * - */ -var ngCloakDirective = ngDirective({ - compile: function(element, attr) { - attr.$set('ngCloak', undefined); - element.removeClass('ng-cloak'); - } -}); - -/** - * @ngdoc directive - * @name ng.directive:ngController - * - * @description - * The `ngController` directive attaches a controller class to the view. This is a key aspect of how angular - * supports the principles behind the Model-View-Controller design pattern. - * - * MVC components in angular: - * - * * Model — The Model is scope properties; scopes are attached to the DOM where scope properties - * are accessed through bindings. - * * View — The template (HTML with data bindings) that is rendered into the View. - * * Controller — The `ngController` directive specifies a Controller class; the class contains business - * logic behind the application to decorate the scope with functions and values - * - * Note that you can also attach controllers to the DOM by declaring it in a route definition - * via the {@link ngRoute.$route $route} service. A common mistake is to declare the controller - * again using `ng-controller` in the template itself. This will cause the controller to be attached - * and executed twice. - * - * @element ANY - * @scope - * @param {expression} ngController Name of a globally accessible constructor function or an - * {@link guide/expression expression} that on the current scope evaluates to a - * constructor function. The controller instance can be published into a scope property - * by specifying `as propertyName`. - * - * @example - * Here is a simple form for editing user contact information. Adding, removing, clearing, and - * greeting are methods declared on the controller (see source tab). These methods can - * easily be called from the angular markup. Notice that the scope becomes the `this` for the - * controller's instance. This allows for easy access to the view data from the controller. Also - * notice that any changes to the data are automatically reflected in the View without the need - * for a manual update. The example is shown in two different declaration styles you may use - * according to preference. - - - -
    - Name: - [ greet ]
    - Contact: -
      -
    • - - - [ clear - | X ] -
    • -
    • [ add ]
    • -
    -
    -
    - - it('should check controller as', function() { - var container = element(by.id('ctrl-as-exmpl')); - - expect(container.findElement(by.model('settings.name')) - .getAttribute('value')).toBe('John Smith'); - - var firstRepeat = - container.findElement(by.repeater('contact in settings.contacts').row(0)); - var secondRepeat = - container.findElement(by.repeater('contact in settings.contacts').row(1)); - - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('408 555 1212'); - expect(secondRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('john.smith@example.org'); - - firstRepeat.findElement(by.linkText('clear')).click() - - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe(''); - - container.findElement(by.linkText('add')).click(); - - expect(container.findElement(by.repeater('contact in settings.contacts').row(2)) - .findElement(by.model('contact.value')) - .getAttribute('value')) - .toBe('yourname@example.org'); - }); - -
    - - - -
    - Name: - [ greet ]
    - Contact: -
      -
    • - - - [ clear - | X ] -
    • -
    • [ add ]
    • -
    -
    -
    - - it('should check controller', function() { - var container = element(by.id('ctrl-exmpl')); - - expect(container.findElement(by.model('name')) - .getAttribute('value')).toBe('John Smith'); - - var firstRepeat = - container.findElement(by.repeater('contact in contacts').row(0)); - var secondRepeat = - container.findElement(by.repeater('contact in contacts').row(1)); - - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('408 555 1212'); - expect(secondRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('john.smith@example.org'); - - firstRepeat.findElement(by.linkText('clear')).click() - - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe(''); - - container.findElement(by.linkText('add')).click(); - - expect(container.findElement(by.repeater('contact in contacts').row(2)) - .findElement(by.model('contact.value')) - .getAttribute('value')) - .toBe('yourname@example.org'); - }); - -
    - - */ -var ngControllerDirective = [function() { - return { - scope: true, - controller: '@', - priority: 500 - }; -}]; - -/** - * @ngdoc directive - * @name ng.directive:ngCsp - * - * @element html - * @description - * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support. - * - * This is necessary when developing things like Google Chrome Extensions. - * - * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things). - * For us to be compatible, we just need to implement the "getterFn" in $parse without violating - * any of these restrictions. - * - * AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp` - * directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will - * evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will - * be raised. - * - * CSP forbids JavaScript to inline stylesheet rules. In non CSP mode Angular automatically - * includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}). - * To make those directives work in CSP mode, include the `angular-csp.css` manually. - * - * In order to use this feature put the `ngCsp` directive on the root element of the application. - * - * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.* - * - * @example - * This example shows how to apply the `ngCsp` directive to the `html` tag. -
    -     
    -     
    -     ...
    -     ...
    -     
    -   
    - */ - -// ngCsp is not implemented as a proper directive any more, because we need it be processed while we bootstrap -// the system (before $parse is instantiated), for this reason we just have a csp() fn that looks for ng-csp attribute -// anywhere in the current doc - -/** - * @ngdoc directive - * @name ng.directive:ngClick - * - * @description - * The ngClick directive allows you to specify custom behavior when - * an element is clicked. - * - * @element ANY - * @priority 0 - * @param {expression} ngClick {@link guide/expression Expression} to evaluate upon - * click. (Event object is available as `$event`) - * - * @example - - - - count: {{count}} - - - it('should check ng-click', function() { - expect(element(by.binding('count')).getText()).toMatch('0'); - element(by.css('.doc-example-live button')).click(); - expect(element(by.binding('count')).getText()).toMatch('1'); - }); - - - */ -/* - * A directive that allows creation of custom onclick handlers that are defined as angular - * expressions and are compiled and executed within the current scope. - * - * Events that are handled via these handler are always configured not to propagate further. - */ -var ngEventDirectives = {}; -forEach( - 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), - function(name) { - var directiveName = directiveNormalize('ng-' + name); - ngEventDirectives[directiveName] = ['$parse', function($parse) { - return { - compile: function($element, attr) { - var fn = $parse(attr[directiveName]); - return function(scope, element, attr) { - element.on(lowercase(name), function(event) { - scope.$apply(function() { - fn(scope, {$event:event}); - }); - }); - }; - } - }; - }]; - } -); - -/** - * @ngdoc directive - * @name ng.directive:ngDblclick - * - * @description - * The `ngDblclick` directive allows you to specify custom behavior on a dblclick event. - * - * @element ANY - * @priority 0 - * @param {expression} ngDblclick {@link guide/expression Expression} to evaluate upon - * a dblclick. (The Event object is available as `$event`) - * - * @example - - - - count: {{count}} - - - */ - - -/** - * @ngdoc directive - * @name ng.directive:ngMousedown - * - * @description - * The ngMousedown directive allows you to specify custom behavior on mousedown event. - * - * @element ANY - * @priority 0 - * @param {expression} ngMousedown {@link guide/expression Expression} to evaluate upon - * mousedown. (Event object is available as `$event`) - * - * @example - - - - count: {{count}} - - - */ - - -/** - * @ngdoc directive - * @name ng.directive:ngMouseup - * - * @description - * Specify custom behavior on mouseup event. - * - * @element ANY - * @priority 0 - * @param {expression} ngMouseup {@link guide/expression Expression} to evaluate upon - * mouseup. (Event object is available as `$event`) - * - * @example - - - - count: {{count}} - - - */ - -/** - * @ngdoc directive - * @name ng.directive:ngMouseover - * - * @description - * Specify custom behavior on mouseover event. - * - * @element ANY - * @priority 0 - * @param {expression} ngMouseover {@link guide/expression Expression} to evaluate upon - * mouseover. (Event object is available as `$event`) - * - * @example - - - - count: {{count}} - - - */ - - -/** - * @ngdoc directive - * @name ng.directive:ngMouseenter - * - * @description - * Specify custom behavior on mouseenter event. - * - * @element ANY - * @priority 0 - * @param {expression} ngMouseenter {@link guide/expression Expression} to evaluate upon - * mouseenter. (Event object is available as `$event`) - * - * @example - - - - count: {{count}} - - - */ - - -/** - * @ngdoc directive - * @name ng.directive:ngMouseleave - * - * @description - * Specify custom behavior on mouseleave event. - * - * @element ANY - * @priority 0 - * @param {expression} ngMouseleave {@link guide/expression Expression} to evaluate upon - * mouseleave. (Event object is available as `$event`) - * - * @example - - - - count: {{count}} - - - */ - - -/** - * @ngdoc directive - * @name ng.directive:ngMousemove - * - * @description - * Specify custom behavior on mousemove event. - * - * @element ANY - * @priority 0 - * @param {expression} ngMousemove {@link guide/expression Expression} to evaluate upon - * mousemove. (Event object is available as `$event`) - * - * @example - - - - count: {{count}} - - - */ - - -/** - * @ngdoc directive - * @name ng.directive:ngKeydown - * - * @description - * Specify custom behavior on keydown event. - * - * @element ANY - * @priority 0 - * @param {expression} ngKeydown {@link guide/expression Expression} to evaluate upon - * keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) - * - * @example - - - - key down count: {{count}} - - - */ - - -/** - * @ngdoc directive - * @name ng.directive:ngKeyup - * - * @description - * Specify custom behavior on keyup event. - * - * @element ANY - * @priority 0 - * @param {expression} ngKeyup {@link guide/expression Expression} to evaluate upon - * keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) - * - * @example - - - - key up count: {{count}} - - - */ - - -/** - * @ngdoc directive - * @name ng.directive:ngKeypress - * - * @description - * Specify custom behavior on keypress event. - * - * @element ANY - * @param {expression} ngKeypress {@link guide/expression Expression} to evaluate upon - * keypress. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) - * - * @example - - - - key press count: {{count}} - - - */ - - -/** - * @ngdoc directive - * @name ng.directive:ngSubmit - * - * @description - * Enables binding angular expressions to onsubmit events. - * - * Additionally it prevents the default action (which for form means sending the request to the - * server and reloading the current page), but only if the form does not contain `action`, - * `data-action`, or `x-action` attributes. - * - * @element form - * @priority 0 - * @param {expression} ngSubmit {@link guide/expression Expression} to eval. (Event object is available as `$event`) - * - * @example - - - -
    - Enter text and hit enter: - - -
    list={{list}}
    -
    -
    - - it('should check ng-submit', function() { - expect(element(by.binding('list')).getText()).toBe('list=[]'); - element(by.css('.doc-example-live #submit')).click(); - expect(element(by.binding('list')).getText()).toContain('hello'); - expect(element(by.input('text')).getAttribute('value')).toBe(''); - }); - it('should ignore empty strings', function() { - expect(element(by.binding('list')).getText()).toBe('list=[]'); - element(by.css('.doc-example-live #submit')).click(); - element(by.css('.doc-example-live #submit')).click(); - expect(element(by.binding('list')).getText()).toContain('hello'); - }); - -
    - */ - -/** - * @ngdoc directive - * @name ng.directive:ngFocus - * - * @description - * Specify custom behavior on focus event. - * - * @element window, input, select, textarea, a - * @priority 0 - * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon - * focus. (Event object is available as `$event`) - * - * @example - * See {@link ng.directive:ngClick ngClick} - */ - -/** - * @ngdoc directive - * @name ng.directive:ngBlur - * - * @description - * Specify custom behavior on blur event. - * - * @element window, input, select, textarea, a - * @priority 0 - * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon - * blur. (Event object is available as `$event`) - * - * @example - * See {@link ng.directive:ngClick ngClick} - */ - -/** - * @ngdoc directive - * @name ng.directive:ngCopy - * - * @description - * Specify custom behavior on copy event. - * - * @element window, input, select, textarea, a - * @priority 0 - * @param {expression} ngCopy {@link guide/expression Expression} to evaluate upon - * copy. (Event object is available as `$event`) - * - * @example - - - - copied: {{copied}} - - - */ - -/** - * @ngdoc directive - * @name ng.directive:ngCut - * - * @description - * Specify custom behavior on cut event. - * - * @element window, input, select, textarea, a - * @priority 0 - * @param {expression} ngCut {@link guide/expression Expression} to evaluate upon - * cut. (Event object is available as `$event`) - * - * @example - - - - cut: {{cut}} - - - */ - -/** - * @ngdoc directive - * @name ng.directive:ngPaste - * - * @description - * Specify custom behavior on paste event. - * - * @element window, input, select, textarea, a - * @priority 0 - * @param {expression} ngPaste {@link guide/expression Expression} to evaluate upon - * paste. (Event object is available as `$event`) - * - * @example - - - - pasted: {{paste}} - - - */ - -/** - * @ngdoc directive - * @name ng.directive:ngIf - * @restrict A - * - * @description - * The `ngIf` directive removes or recreates a portion of the DOM tree based on an - * {expression}. If the expression assigned to `ngIf` evaluates to a false - * value then the element is removed from the DOM, otherwise a clone of the - * element is reinserted into the DOM. - * - * `ngIf` differs from `ngShow` and `ngHide` in that `ngIf` completely removes and recreates the - * element in the DOM rather than changing its visibility via the `display` css property. A common - * case when this difference is significant is when using css selectors that rely on an element's - * position within the DOM, such as the `:first-child` or `:last-child` pseudo-classes. - * - * Note that when an element is removed using `ngIf` its scope is destroyed and a new scope - * is created when the element is restored. The scope created within `ngIf` inherits from - * its parent scope using - * {@link https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance prototypal inheritance}. - * An important implication of this is if `ngModel` is used within `ngIf` to bind to - * a javascript primitive defined in the parent scope. In this case any modifications made to the - * variable within the child scope will override (hide) the value in the parent scope. - * - * Also, `ngIf` recreates elements using their compiled state. An example of this behavior - * is if an element's class attribute is directly modified after it's compiled, using something like - * jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element - * the added class will be lost because the original compiled state is used to regenerate the element. - * - * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter` - * and `leave` effects. - * - * @animations - * enter - happens just after the ngIf contents change and a new DOM element is created and injected into the ngIf container - * leave - happens just before the ngIf contents are removed from the DOM - * - * @element ANY - * @scope - * @priority 600 - * @param {expression} ngIf If the {@link guide/expression expression} is falsy then - * the element is removed from the DOM tree. If it is truthy a copy of the compiled - * element is added to the DOM tree. - * - * @example - - - Click me:
    - Show when checked: - - I'm removed when the checkbox is unchecked. - -
    - - .animate-if { - background:white; - border:1px solid black; - padding:10px; - } - - .animate-if.ng-enter, .animate-if.ng-leave { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - } - - .animate-if.ng-enter, - .animate-if.ng-leave.ng-leave-active { - opacity:0; - } - - .animate-if.ng-leave, - .animate-if.ng-enter.ng-enter-active { - opacity:1; - } - -
    - */ -var ngIfDirective = ['$animate', function($animate) { - return { - transclude: 'element', - priority: 600, - terminal: true, - restrict: 'A', - $$tlb: true, - link: function ($scope, $element, $attr, ctrl, $transclude) { - var block, childScope; - $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { - - if (toBoolean(value)) { - if (!childScope) { - childScope = $scope.$new(); - $transclude(childScope, function (clone) { - clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' '); - // Note: We only need the first/last node of the cloned nodes. - // However, we need to keep the reference to the jqlite wrapper as it might be changed later - // by a directive with templateUrl when it's template arrives. - block = { - clone: clone - }; - $animate.enter(clone, $element.parent(), $element); - }); - } - } else { - - if (childScope) { - childScope.$destroy(); - childScope = null; - } - - if (block) { - $animate.leave(getBlockElements(block.clone)); - block = null; - } - } - }); - } - }; -}]; - -/** - * @ngdoc directive - * @name ng.directive:ngInclude - * @restrict ECA - * - * @description - * Fetches, compiles and includes an external HTML fragment. - * - * By default, the template URL is restricted to the same domain and protocol as the - * application document. This is done by calling {@link ng.$sce#methods_getTrustedResourceUrl - * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols - * you may either {@link ng.$sceDelegateProvider#methods_resourceUrlWhitelist whitelist them} or - * {@link ng.$sce#methods_trustAsResourceUrl wrap them} as trusted values. Refer to Angular's {@link - * ng.$sce Strict Contextual Escaping}. - * - * In addition, the browser's - * {@link https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest - * Same Origin Policy} and {@link http://www.w3.org/TR/cors/ Cross-Origin Resource Sharing - * (CORS)} policy may further restrict whether the template is successfully loaded. - * For example, `ngInclude` won't work for cross-domain requests on all browsers and for `file://` - * access on some browsers. - * - * @animations - * enter - animation is used to bring new content into the browser. - * leave - animation is used to animate existing content away. - * - * The enter and leave animation occur concurrently. - * - * @scope - * @priority 400 - * - * @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant, - * make sure you wrap it in quotes, e.g. `src="'myPartialTemplate.html'"`. - * @param {string=} onload Expression to evaluate when a new partial is loaded. - * - * @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll - * $anchorScroll} to scroll the viewport after the content is loaded. - * - * - If the attribute is not set, disable scrolling. - * - If the attribute is set without value, enable scrolling. - * - Otherwise enable scrolling only if the expression evaluates to truthy value. - * - * @example - - -
    - - url of the template: {{template.url}} -
    -
    -
    -
    -
    -
    - - function Ctrl($scope) { - $scope.templates = - [ { name: 'template1.html', url: 'template1.html'} - , { name: 'template2.html', url: 'template2.html'} ]; - $scope.template = $scope.templates[0]; - } - - - Content of template1.html - - - Content of template2.html - - - .slide-animate-container { - position:relative; - background:white; - border:1px solid black; - height:40px; - overflow:hidden; - } - - .slide-animate { - padding:10px; - } - - .slide-animate.ng-enter, .slide-animate.ng-leave { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - - position:absolute; - top:0; - left:0; - right:0; - bottom:0; - display:block; - padding:10px; - } - - .slide-animate.ng-enter { - top:-50px; - } - .slide-animate.ng-enter.ng-enter-active { - top:0; - } - - .slide-animate.ng-leave { - top:0; - } - .slide-animate.ng-leave.ng-leave-active { - top:50px; - } - - - var templateSelect = element(by.model('template')); - var includeElem = element(by.css('.doc-example-live [ng-include]')); - - it('should load template1.html', function() { - expect(includeElem.getText()).toMatch(/Content of template1.html/); - }); - - it('should load template2.html', function() { - if (browser.params.browser == 'firefox') { - // Firefox can't handle using selects - // See https://github.com/angular/protractor/issues/480 - return; - } - templateSelect.click(); - templateSelect.element.all(by.css('option')).get(2).click(); - expect(includeElem.getText()).toMatch(/Content of template2.html/); - }); - - it('should change to blank', function() { - if (browser.params.browser == 'firefox') { - // Firefox can't handle using selects - return; - } - templateSelect.click(); - templateSelect.element.all(by.css('option')).get(0).click(); - expect(includeElem.isPresent()).toBe(false); - }); - -
    - */ - - -/** - * @ngdoc event - * @name ng.directive:ngInclude#$includeContentRequested - * @eventOf ng.directive:ngInclude - * @eventType emit on the scope ngInclude was declared in - * @description - * Emitted every time the ngInclude content is requested. - */ - - -/** - * @ngdoc event - * @name ng.directive:ngInclude#$includeContentLoaded - * @eventOf ng.directive:ngInclude - * @eventType emit on the current ngInclude scope - * @description - * Emitted every time the ngInclude content is reloaded. - */ -var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce', - function($http, $templateCache, $anchorScroll, $animate, $sce) { - return { - restrict: 'ECA', - priority: 400, - terminal: true, - transclude: 'element', - controller: angular.noop, - compile: function(element, attr) { - var srcExp = attr.ngInclude || attr.src, - onloadExp = attr.onload || '', - autoScrollExp = attr.autoscroll; - - return function(scope, $element, $attr, ctrl, $transclude) { - var changeCounter = 0, - currentScope, - currentElement; - - var cleanupLastIncludeContent = function() { - if (currentScope) { - currentScope.$destroy(); - currentScope = null; - } - if(currentElement) { - $animate.leave(currentElement); - currentElement = null; - } - }; - - scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) { - var afterAnimation = function() { - if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { - $anchorScroll(); - } - }; - var thisChangeId = ++changeCounter; - - if (src) { - $http.get(src, {cache: $templateCache}).success(function(response) { - if (thisChangeId !== changeCounter) return; - var newScope = scope.$new(); - ctrl.template = response; - - // Note: This will also link all children of ng-include that were contained in the original - // html. If that content contains controllers, ... they could pollute/change the scope. - // However, using ng-include on an element with additional content does not make sense... - // Note: We can't remove them in the cloneAttchFn of $transclude as that - // function is called before linking the content, which would apply child - // directives to non existing elements. - var clone = $transclude(newScope, function(clone) { - cleanupLastIncludeContent(); - $animate.enter(clone, null, $element, afterAnimation); - }); - - currentScope = newScope; - currentElement = clone; - - currentScope.$emit('$includeContentLoaded'); - scope.$eval(onloadExp); - }).error(function() { - if (thisChangeId === changeCounter) cleanupLastIncludeContent(); - }); - scope.$emit('$includeContentRequested'); - } else { - cleanupLastIncludeContent(); - ctrl.template = null; - } - }); - }; - } - }; -}]; - -// This directive is called during the $transclude call of the first `ngInclude` directive. -// It will replace and compile the content of the element with the loaded template. -// We need this directive so that the element content is already filled when -// the link function of another directive on the same element as ngInclude -// is called. -var ngIncludeFillContentDirective = ['$compile', - function($compile) { - return { - restrict: 'ECA', - priority: -400, - require: 'ngInclude', - link: function(scope, $element, $attr, ctrl) { - $element.html(ctrl.template); - $compile($element.contents())(scope); - } - }; - }]; - -/** - * @ngdoc directive - * @name ng.directive:ngInit - * @restrict AC - * - * @description - * The `ngInit` directive allows you to evaluate an expression in the - * current scope. - * - *
    - * The only appropriate use of `ngInit` is for aliasing special properties of - * {@link api/ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below. Besides this case, you - * should use {@link guide/controller controllers} rather than `ngInit` - * to initialize values on a scope. - *
    - *
    - * **Note**: If you have assignment in `ngInit` along with {@link api/ng.$filter `$filter`}, make - * sure you have parenthesis for correct precedence: - *
    - *   
    - *
    - *
    - * - * @priority 450 - * - * @element ANY - * @param {expression} ngInit {@link guide/expression Expression} to eval. - * - * @example - - - -
    -
    -
    - list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}}; -
    -
    -
    -
    - - it('should alias index positions', function() { - var elements = element.all(by.css('.example-init')); - expect(elements.get(0).getText()).toBe('list[ 0 ][ 0 ] = a;'); - expect(elements.get(1).getText()).toBe('list[ 0 ][ 1 ] = b;'); - expect(elements.get(2).getText()).toBe('list[ 1 ][ 0 ] = c;'); - expect(elements.get(3).getText()).toBe('list[ 1 ][ 1 ] = d;'); - }); - -
    - */ -var ngInitDirective = ngDirective({ - priority: 450, - compile: function() { - return { - pre: function(scope, element, attrs) { - scope.$eval(attrs.ngInit); - } - }; - } -}); - -/** - * @ngdoc directive - * @name ng.directive:ngNonBindable - * @restrict AC - * @priority 1000 - * - * @description - * The `ngNonBindable` directive tells Angular not to compile or bind the contents of the current - * DOM element. This is useful if the element contains what appears to be Angular directives and - * bindings but which should be ignored by Angular. This could be the case if you have a site that - * displays snippets of code, for instance. - * - * @element ANY - * - * @example - * In this example there are two locations where a simple interpolation binding (`{{}}`) is present, - * but the one wrapped in `ngNonBindable` is left alone. - * - * @example - - -
    Normal: {{1 + 2}}
    -
    Ignored: {{1 + 2}}
    -
    - - it('should check ng-non-bindable', function() { - expect(element(by.binding('1 + 2')).getText()).toContain('3'); - expect(element.all(by.css('.doc-example-live div')).last().getText()).toMatch(/1 \+ 2/); - }); - -
    - */ -var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); - -/** - * @ngdoc directive - * @name ng.directive:ngPluralize - * @restrict EA - * - * @description - * # Overview - * `ngPluralize` is a directive that displays messages according to en-US localization rules. - * These rules are bundled with angular.js, but can be overridden - * (see {@link guide/i18n Angular i18n} dev guide). You configure ngPluralize directive - * by specifying the mappings between - * {@link http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html - * plural categories} and the strings to be displayed. - * - * # Plural categories and explicit number rules - * There are two - * {@link http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html - * plural categories} in Angular's default en-US locale: "one" and "other". - * - * While a plural category may match many numbers (for example, in en-US locale, "other" can match - * any number that is not 1), an explicit number rule can only match one number. For example, the - * explicit number rule for "3" matches the number 3. There are examples of plural categories - * and explicit number rules throughout the rest of this documentation. - * - * # Configuring ngPluralize - * You configure ngPluralize by providing 2 attributes: `count` and `when`. - * You can also provide an optional attribute, `offset`. - * - * The value of the `count` attribute can be either a string or an {@link guide/expression - * Angular expression}; these are evaluated on the current scope for its bound value. - * - * The `when` attribute specifies the mappings between plural categories and the actual - * string to be displayed. The value of the attribute should be a JSON object. - * - * The following example shows how to configure ngPluralize: - * - *
    - * 
    - * 
    - *
    - * - * In the example, `"0: Nobody is viewing."` is an explicit number rule. If you did not - * specify this rule, 0 would be matched to the "other" category and "0 people are viewing" - * would be shown instead of "Nobody is viewing". You can specify an explicit number rule for - * other numbers, for example 12, so that instead of showing "12 people are viewing", you can - * show "a dozen people are viewing". - * - * You can use a set of closed braces (`{}`) as a placeholder for the number that you want substituted - * into pluralized strings. In the previous example, Angular will replace `{}` with - * `{{personCount}}`. The closed braces `{}` is a placeholder - * for {{numberExpression}}. - * - * # Configuring ngPluralize with offset - * The `offset` attribute allows further customization of pluralized text, which can result in - * a better user experience. For example, instead of the message "4 people are viewing this document", - * you might display "John, Kate and 2 others are viewing this document". - * The offset attribute allows you to offset a number by any desired value. - * Let's take a look at an example: - * - *
    - * 
    - * 
    - * 
    - * - * Notice that we are still using two plural categories(one, other), but we added - * three explicit number rules 0, 1 and 2. - * When one person, perhaps John, views the document, "John is viewing" will be shown. - * When three people view the document, no explicit number rule is found, so - * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category. - * In this case, plural category 'one' is matched and "John, Marry and one other person are viewing" - * is shown. - * - * Note that when you specify offsets, you must provide explicit number rules for - * numbers from 0 up to and including the offset. If you use an offset of 3, for example, - * you must provide explicit number rules for 0, 1, 2 and 3. You must also provide plural strings for - * plural categories "one" and "other". - * - * @param {string|expression} count The variable to be bounded to. - * @param {string} when The mapping between plural category to its corresponding strings. - * @param {number=} offset Offset to deduct from the total number. - * - * @example - - - -
    - Person 1:
    - Person 2:
    - Number of People:
    - - - Without Offset: - -
    - - - With Offset(2): - - -
    -
    - - it('should show correct pluralized string', function() { - var withoutOffset = element.all(by.css('ng-pluralize')).get(0); - var withOffset = element.all(by.css('ng-pluralize')).get(1); - var countInput = element(by.model('personCount')); - - expect(withoutOffset.getText()).toEqual('1 person is viewing.'); - expect(withOffset.getText()).toEqual('Igor is viewing.'); - - countInput.clear(); - countInput.sendKeys('0'); - - expect(withoutOffset.getText()).toEqual('Nobody is viewing.'); - expect(withOffset.getText()).toEqual('Nobody is viewing.'); - - countInput.clear(); - countInput.sendKeys('2'); - - expect(withoutOffset.getText()).toEqual('2 people are viewing.'); - expect(withOffset.getText()).toEqual('Igor and Misko are viewing.'); - - countInput.clear(); - countInput.sendKeys('3'); - - expect(withoutOffset.getText()).toEqual('3 people are viewing.'); - expect(withOffset.getText()).toEqual('Igor, Misko and one other person are viewing.'); - - countInput.clear(); - countInput.sendKeys('4'); - - expect(withoutOffset.getText()).toEqual('4 people are viewing.'); - expect(withOffset.getText()).toEqual('Igor, Misko and 2 other people are viewing.'); - }); - it('should show data-bound names', function() { - var withOffset = element.all(by.css('ng-pluralize')).get(1); - var personCount = element(by.model('personCount')); - var person1 = element(by.model('person1')); - var person2 = element(by.model('person2')); - personCount.clear(); - personCount.sendKeys('4'); - person1.clear(); - person1.sendKeys('Di'); - person2.clear(); - person2.sendKeys('Vojta'); - expect(withOffset.getText()).toEqual('Di, Vojta and 2 other people are viewing.'); - }); - -
    - */ -var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) { - var BRACE = /{}/g; - return { - restrict: 'EA', - link: function(scope, element, attr) { - var numberExp = attr.count, - whenExp = attr.$attr.when && element.attr(attr.$attr.when), // we have {{}} in attrs - offset = attr.offset || 0, - whens = scope.$eval(whenExp) || {}, - whensExpFns = {}, - startSymbol = $interpolate.startSymbol(), - endSymbol = $interpolate.endSymbol(), - isWhen = /^when(Minus)?(.+)$/; - - forEach(attr, function(expression, attributeName) { - if (isWhen.test(attributeName)) { - whens[lowercase(attributeName.replace('when', '').replace('Minus', '-'))] = - element.attr(attr.$attr[attributeName]); - } - }); - forEach(whens, function(expression, key) { - whensExpFns[key] = - $interpolate(expression.replace(BRACE, startSymbol + numberExp + '-' + - offset + endSymbol)); - }); - - scope.$watch(function ngPluralizeWatch() { - var value = parseFloat(scope.$eval(numberExp)); - - if (!isNaN(value)) { - //if explicit number rule such as 1, 2, 3... is defined, just use it. Otherwise, - //check it against pluralization rules in $locale service - if (!(value in whens)) value = $locale.pluralCat(value - offset); - return whensExpFns[value](scope, element, true); - } else { - return ''; - } - }, function ngPluralizeWatchAction(newVal) { - element.text(newVal); - }); - } - }; -}]; - -/** - * @ngdoc directive - * @name ng.directive:ngRepeat - * - * @description - * The `ngRepeat` directive instantiates a template once per item from a collection. Each template - * instance gets its own scope, where the given loop variable is set to the current collection item, - * and `$index` is set to the item index or key. - * - * Special properties are exposed on the local scope of each template instance, including: - * - * | Variable | Type | Details | - * |-----------|-----------------|-----------------------------------------------------------------------------| - * | `$index` | {@type number} | iterator offset of the repeated element (0..length-1) | - * | `$first` | {@type boolean} | true if the repeated element is first in the iterator. | - * | `$middle` | {@type boolean} | true if the repeated element is between the first and last in the iterator. | - * | `$last` | {@type boolean} | true if the repeated element is last in the iterator. | - * | `$even` | {@type boolean} | true if the iterator position `$index` is even (otherwise false). | - * | `$odd` | {@type boolean} | true if the iterator position `$index` is odd (otherwise false). | - * - * Creating aliases for these properties is possible with {@link api/ng.directive:ngInit `ngInit`}. - * This may be useful when, for instance, nesting ngRepeats. - * - * # Special repeat start and end points - * To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending - * the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively. - * The **ng-repeat-start** directive works the same as **ng-repeat**, but will repeat all the HTML code (including the tag it's defined on) - * up to and including the ending HTML tag where **ng-repeat-end** is placed. - * - * The example below makes use of this feature: - *
    - *   
    - * Header {{ item }} - *
    - *
    - * Body {{ item }} - *
    - *
    - * Footer {{ item }} - *
    - *
    - * - * And with an input of {@type ['A','B']} for the items variable in the example above, the output will evaluate to: - *
    - *   
    - * Header A - *
    - *
    - * Body A - *
    - *
    - * Footer A - *
    - *
    - * Header B - *
    - *
    - * Body B - *
    - *
    - * Footer B - *
    - *
    - * - * The custom start and end points for ngRepeat also support all other HTML directive syntax flavors provided in AngularJS (such - * as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**). - * - * @animations - * enter - when a new item is added to the list or when an item is revealed after a filter - * leave - when an item is removed from the list or when an item is filtered out - * move - when an adjacent item is filtered out causing a reorder or when the item contents are reordered - * - * @element ANY - * @scope - * @priority 1000 - * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. These - * formats are currently supported: - * - * * `variable in expression` – where variable is the user defined loop variable and `expression` - * is a scope expression giving the collection to enumerate. - * - * For example: `album in artist.albums`. - * - * * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers, - * and `expression` is the scope expression giving the collection to enumerate. - * - * For example: `(name, age) in {'adam':10, 'amalie':12}`. - * - * * `variable in expression track by tracking_expression` – You can also provide an optional tracking function - * which can be used to associate the objects in the collection with the DOM elements. If no tracking function - * is specified the ng-repeat associates elements by identity in the collection. It is an error to have - * more than one tracking function to resolve to the same key. (This would mean that two distinct objects are - * mapped to the same DOM element, which is not possible.) Filters should be applied to the expression, - * before specifying a tracking expression. - * - * For example: `item in items` is equivalent to `item in items track by $id(item)'. This implies that the DOM elements - * will be associated by item identity in the array. - * - * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique - * `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements - * with the corresponding item in the array by identity. Moving the same object in array would move the DOM - * element in the same way in the DOM. - * - * For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this - * case the object identity does not matter. Two objects are considered equivalent as long as their `id` - * property is same. - * - * For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter - * to items in conjunction with a tracking expression. - * - * @example - * This example initializes the scope to a list of names and - * then uses `ngRepeat` to display every person: - - -
    - I have {{friends.length}} friends. They are: - -
      -
    • - [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. -
    • -
    -
    -
    - - .example-animate-container { - background:white; - border:1px solid black; - list-style:none; - margin:0; - padding:0 10px; - } - - .animate-repeat { - line-height:40px; - list-style:none; - box-sizing:border-box; - } - - .animate-repeat.ng-move, - .animate-repeat.ng-enter, - .animate-repeat.ng-leave { - -webkit-transition:all linear 0.5s; - transition:all linear 0.5s; - } - - .animate-repeat.ng-leave.ng-leave-active, - .animate-repeat.ng-move, - .animate-repeat.ng-enter { - opacity:0; - max-height:0; - } - - .animate-repeat.ng-leave, - .animate-repeat.ng-move.ng-move-active, - .animate-repeat.ng-enter.ng-enter-active { - opacity:1; - max-height:40px; - } - - - var friends = element(by.css('.doc-example-live')) - .element.all(by.repeater('friend in friends')); - - it('should render initial data set', function() { - expect(friends.count()).toBe(10); - expect(friends.get(0).getText()).toEqual('[1] John who is 25 years old.'); - expect(friends.get(1).getText()).toEqual('[2] Jessie who is 30 years old.'); - expect(friends.last().getText()).toEqual('[10] Samantha who is 60 years old.'); - expect(element(by.binding('friends.length')).getText()) - .toMatch("I have 10 friends. They are:"); - }); - - it('should update repeater when filter predicate changes', function() { - expect(friends.count()).toBe(10); - - element(by.css('.doc-example-live')).element(by.model('q')).sendKeys('ma'); - - expect(friends.count()).toBe(2); - expect(friends.get(0).getText()).toEqual('[1] Mary who is 28 years old.'); - expect(friends.last().getText()).toEqual('[2] Samantha who is 60 years old.'); - }); - -
    - */ -var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { - var NG_REMOVED = '$$NG_REMOVED'; - var ngRepeatMinErr = minErr('ngRepeat'); - return { - transclude: 'element', - priority: 1000, - terminal: true, - $$tlb: true, - link: function($scope, $element, $attr, ctrl, $transclude){ - var expression = $attr.ngRepeat; - var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/), - trackByExp, trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, - lhs, rhs, valueIdentifier, keyIdentifier, - hashFnLocals = {$id: hashKey}; - - if (!match) { - throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", - expression); - } - - lhs = match[1]; - rhs = match[2]; - trackByExp = match[3]; - - if (trackByExp) { - trackByExpGetter = $parse(trackByExp); - trackByIdExpFn = function(key, value, index) { - // assign key, value, and $index to the locals so that they can be used in hash functions - if (keyIdentifier) hashFnLocals[keyIdentifier] = key; - hashFnLocals[valueIdentifier] = value; - hashFnLocals.$index = index; - return trackByExpGetter($scope, hashFnLocals); - }; - } else { - trackByIdArrayFn = function(key, value) { - return hashKey(value); - }; - trackByIdObjFn = function(key) { - return key; - }; - } - - match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); - if (!match) { - throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", - lhs); - } - valueIdentifier = match[3] || match[1]; - keyIdentifier = match[2]; - - // Store a list of elements from previous run. This is a hash where key is the item from the - // iterator, and the value is objects with following properties. - // - scope: bound scope - // - element: previous element. - // - index: position - var lastBlockMap = {}; - - //watch props - $scope.$watchCollection(rhs, function ngRepeatAction(collection){ - var index, length, - previousNode = $element[0], // current position of the node - nextNode, - // Same as lastBlockMap but it has the current state. It will become the - // lastBlockMap on the next iteration. - nextBlockMap = {}, - arrayLength, - childScope, - key, value, // key/value of iteration - trackById, - trackByIdFn, - collectionKeys, - block, // last object information {scope, element, id} - nextBlockOrder = [], - elementsToRemove; - - - if (isArrayLike(collection)) { - collectionKeys = collection; - trackByIdFn = trackByIdExpFn || trackByIdArrayFn; - } else { - trackByIdFn = trackByIdExpFn || trackByIdObjFn; - // if object, extract keys, sort them and use to determine order of iteration over obj props - collectionKeys = []; - for (key in collection) { - if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { - collectionKeys.push(key); - } - } - collectionKeys.sort(); - } - - arrayLength = collectionKeys.length; - - // locate existing items - length = nextBlockOrder.length = collectionKeys.length; - for(index = 0; index < length; index++) { - key = (collection === collectionKeys) ? index : collectionKeys[index]; - value = collection[key]; - trackById = trackByIdFn(key, value, index); - assertNotHasOwnProperty(trackById, '`track by` id'); - if(lastBlockMap.hasOwnProperty(trackById)) { - block = lastBlockMap[trackById]; - delete lastBlockMap[trackById]; - nextBlockMap[trackById] = block; - nextBlockOrder[index] = block; - } else if (nextBlockMap.hasOwnProperty(trackById)) { - // restore lastBlockMap - forEach(nextBlockOrder, function(block) { - if (block && block.scope) lastBlockMap[block.id] = block; - }); - // This is a duplicate and we need to throw an error - throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}", - expression, trackById); - } else { - // new never before seen block - nextBlockOrder[index] = { id: trackById }; - nextBlockMap[trackById] = false; - } - } - - // remove existing items - for (key in lastBlockMap) { - // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn - if (lastBlockMap.hasOwnProperty(key)) { - block = lastBlockMap[key]; - elementsToRemove = getBlockElements(block.clone); - $animate.leave(elementsToRemove); - forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; }); - block.scope.$destroy(); - } - } - - // we are not using forEach for perf reasons (trying to avoid #call) - for (index = 0, length = collectionKeys.length; index < length; index++) { - key = (collection === collectionKeys) ? index : collectionKeys[index]; - value = collection[key]; - block = nextBlockOrder[index]; - if (nextBlockOrder[index - 1]) previousNode = getBlockEnd(nextBlockOrder[index - 1]); - - if (block.scope) { - // if we have already seen this object, then we need to reuse the - // associated scope/element - childScope = block.scope; - - nextNode = previousNode; - do { - nextNode = nextNode.nextSibling; - } while(nextNode && nextNode[NG_REMOVED]); - - if (getBlockStart(block) != nextNode) { - // existing item which got moved - $animate.move(getBlockElements(block.clone), null, jqLite(previousNode)); - } - previousNode = getBlockEnd(block); - } else { - // new item which we don't know about - childScope = $scope.$new(); - } - - childScope[valueIdentifier] = value; - if (keyIdentifier) childScope[keyIdentifier] = key; - childScope.$index = index; - childScope.$first = (index === 0); - childScope.$last = (index === (arrayLength - 1)); - childScope.$middle = !(childScope.$first || childScope.$last); - // jshint bitwise: false - childScope.$odd = !(childScope.$even = (index&1) === 0); - // jshint bitwise: true - - if (!block.scope) { - $transclude(childScope, function(clone) { - clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' '); - $animate.enter(clone, null, jqLite(previousNode)); - previousNode = clone; - block.scope = childScope; - // Note: We only need the first/last node of the cloned nodes. - // However, we need to keep the reference to the jqlite wrapper as it might be changed later - // by a directive with templateUrl when it's template arrives. - block.clone = clone; - nextBlockMap[block.id] = block; - }); - } - } - lastBlockMap = nextBlockMap; - }); - } - }; - - function getBlockStart(block) { - return block.clone[0]; - } - - function getBlockEnd(block) { - return block.clone[block.clone.length - 1]; - } -}]; - -/** - * @ngdoc directive - * @name ng.directive:ngShow - * - * @description - * The `ngShow` directive shows or hides the given HTML element based on the expression - * provided to the ngShow attribute. The element is shown or hidden by removing or adding - * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined - * in AngularJS and sets the display style to none (using an !important flag). - * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). - * - *
    - * 
    - * 
    - * - * - *
    - *
    - * - * When the ngShow expression evaluates to false then the ng-hide CSS class is added to the class attribute - * on the element causing it to become hidden. When true, the ng-hide CSS class is removed - * from the element causing the element not to appear hidden. - * - * ## Why is !important used? - * - * You may be wondering why !important is used for the .ng-hide CSS class. This is because the `.ng-hide` selector - * can be easily overridden by heavier selectors. For example, something as simple - * as changing the display style on a HTML list item would make hidden elements appear visible. - * This also becomes a bigger issue when dealing with CSS frameworks. - * - * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector - * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the - * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. - * - * ### Overriding .ng-hide - * - * If you wish to change the hide behavior with ngShow/ngHide then this can be achieved by - * restating the styles for the .ng-hide class in CSS: - *
    - * .ng-hide {
    - *   //!annotate CSS Specificity|Not to worry, this will override the AngularJS default...
    - *   display:block!important;
    - *
    - *   //this is just another form of hiding an element
    - *   position:absolute;
    - *   top:-9999px;
    - *   left:-9999px;
    - * }
    - * 
    - * - * Just remember to include the important flag so the CSS override will function. - * - *
    - * **Note:** Here is a list of values that ngShow will consider as a falsy value (case insensitive):
    - * "f" / "0" / "false" / "no" / "n" / "[]" - *
    - * - * ## A note about animations with ngShow - * - * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression - * is true and false. This system works like the animation system present with ngClass except that - * you must also include the !important flag to override the display property - * so that you can perform an animation when the element is hidden during the time of the animation. - * - *
    - * //
    - * //a working example can be found at the bottom of this page
    - * //
    - * .my-element.ng-hide-add, .my-element.ng-hide-remove {
    - *   transition:0.5s linear all;
    - *   display:block!important;
    - * }
    - *
    - * .my-element.ng-hide-add { ... }
    - * .my-element.ng-hide-add.ng-hide-add-active { ... }
    - * .my-element.ng-hide-remove { ... }
    - * .my-element.ng-hide-remove.ng-hide-remove-active { ... }
    - * 
    - * - * @animations - * addClass: .ng-hide - happens after the ngShow expression evaluates to a truthy value and the just before contents are set to visible - * removeClass: .ng-hide - happens after the ngShow expression evaluates to a non truthy value and just before the contents are set to hidden - * - * @element ANY - * @param {expression} ngShow If the {@link guide/expression expression} is truthy - * then the element is shown or hidden respectively. - * - * @example - - - Click me:
    -
    - Show: -
    - I show up when your checkbox is checked. -
    -
    -
    - Hide: -
    - I hide when your checkbox is checked. -
    -
    -
    - - .animate-show { - -webkit-transition:all linear 0.5s; - transition:all linear 0.5s; - line-height:20px; - opacity:1; - padding:10px; - border:1px solid black; - background:white; - } - - .animate-show.ng-hide-add, - .animate-show.ng-hide-remove { - display:block!important; - } - - .animate-show.ng-hide { - line-height:0; - opacity:0; - padding:0 10px; - } - - .check-element { - padding:10px; - border:1px solid black; - background:white; - } - - - var thumbsUp = element(by.css('.doc-example-live span.icon-thumbs-up')); - var thumbsDown = element(by.css('.doc-example-live span.icon-thumbs-down')); - - it('should check ng-show / ng-hide', function() { - expect(thumbsUp.isDisplayed()).toBeFalsy(); - expect(thumbsDown.isDisplayed()).toBeTruthy(); - - element(by.model('checked')).click(); - - expect(thumbsUp.isDisplayed()).toBeTruthy(); - expect(thumbsDown.isDisplayed()).toBeFalsy(); - }); - -
    - */ -var ngShowDirective = ['$animate', function($animate) { - return function(scope, element, attr) { - scope.$watch(attr.ngShow, function ngShowWatchAction(value){ - $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide'); - }); - }; -}]; - - -/** - * @ngdoc directive - * @name ng.directive:ngHide - * - * @description - * The `ngHide` directive shows or hides the given HTML element based on the expression - * provided to the ngHide attribute. The element is shown or hidden by removing or adding - * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined - * in AngularJS and sets the display style to none (using an !important flag). - * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). - * - *
    - * 
    - * 
    - * - * - *
    - *
    - * - * When the ngHide expression evaluates to true then the .ng-hide CSS class is added to the class attribute - * on the element causing it to become hidden. When false, the ng-hide CSS class is removed - * from the element causing the element not to appear hidden. - * - * ## Why is !important used? - * - * You may be wondering why !important is used for the .ng-hide CSS class. This is because the `.ng-hide` selector - * can be easily overridden by heavier selectors. For example, something as simple - * as changing the display style on a HTML list item would make hidden elements appear visible. - * This also becomes a bigger issue when dealing with CSS frameworks. - * - * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector - * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the - * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. - * - * ### Overriding .ng-hide - * - * If you wish to change the hide behavior with ngShow/ngHide then this can be achieved by - * restating the styles for the .ng-hide class in CSS: - *
    - * .ng-hide {
    - *   //!annotate CSS Specificity|Not to worry, this will override the AngularJS default...
    - *   display:block!important;
    - *
    - *   //this is just another form of hiding an element
    - *   position:absolute;
    - *   top:-9999px;
    - *   left:-9999px;
    - * }
    - * 
    - * - * Just remember to include the important flag so the CSS override will function. - * - *
    - * **Note:** Here is a list of values that ngHide will consider as a falsy value (case insensitive):
    - * "f" / "0" / "false" / "no" / "n" / "[]" - *
    - * - * ## A note about animations with ngHide - * - * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression - * is true and false. This system works like the animation system present with ngClass, except that - * you must also include the !important flag to override the display property so - * that you can perform an animation when the element is hidden during the time of the animation. - * - *
    - * //
    - * //a working example can be found at the bottom of this page
    - * //
    - * .my-element.ng-hide-add, .my-element.ng-hide-remove {
    - *   transition:0.5s linear all;
    - *   display:block!important;
    - * }
    - *
    - * .my-element.ng-hide-add { ... }
    - * .my-element.ng-hide-add.ng-hide-add-active { ... }
    - * .my-element.ng-hide-remove { ... }
    - * .my-element.ng-hide-remove.ng-hide-remove-active { ... }
    - * 
    - * - * @animations - * removeClass: .ng-hide - happens after the ngHide expression evaluates to a truthy value and just before the contents are set to hidden - * addClass: .ng-hide - happens after the ngHide expression evaluates to a non truthy value and just before the contents are set to visible - * - * @element ANY - * @param {expression} ngHide If the {@link guide/expression expression} is truthy then - * the element is shown or hidden respectively. - * - * @example - - - Click me:
    -
    - Show: -
    - I show up when your checkbox is checked. -
    -
    -
    - Hide: -
    - I hide when your checkbox is checked. -
    -
    -
    - - .animate-hide { - -webkit-transition:all linear 0.5s; - transition:all linear 0.5s; - line-height:20px; - opacity:1; - padding:10px; - border:1px solid black; - background:white; - } - - .animate-hide.ng-hide-add, - .animate-hide.ng-hide-remove { - display:block!important; - } - - .animate-hide.ng-hide { - line-height:0; - opacity:0; - padding:0 10px; - } - - .check-element { - padding:10px; - border:1px solid black; - background:white; - } - - - var thumbsUp = element(by.css('.doc-example-live span.icon-thumbs-up')); - var thumbsDown = element(by.css('.doc-example-live span.icon-thumbs-down')); - - it('should check ng-show / ng-hide', function() { - expect(thumbsUp.isDisplayed()).toBeFalsy(); - expect(thumbsDown.isDisplayed()).toBeTruthy(); - - element(by.model('checked')).click(); - - expect(thumbsUp.isDisplayed()).toBeTruthy(); - expect(thumbsDown.isDisplayed()).toBeFalsy(); - }); - -
    - */ -var ngHideDirective = ['$animate', function($animate) { - return function(scope, element, attr) { - scope.$watch(attr.ngHide, function ngHideWatchAction(value){ - $animate[toBoolean(value) ? 'addClass' : 'removeClass'](element, 'ng-hide'); - }); - }; -}]; - -/** - * @ngdoc directive - * @name ng.directive:ngStyle - * @restrict AC - * - * @description - * The `ngStyle` directive allows you to set CSS style on an HTML element conditionally. - * - * @element ANY - * @param {expression} ngStyle {@link guide/expression Expression} which evals to an - * object whose keys are CSS style names and values are corresponding values for those CSS - * keys. - * - * @example - - - - -
    - Sample Text -
    myStyle={{myStyle}}
    -
    - - span { - color: black; - } - - - var colorSpan = element(by.css('.doc-example-live span')); - - it('should check ng-style', function() { - expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)'); - element(by.css('.doc-example-live input[value=set]')).click(); - expect(colorSpan.getCssValue('color')).toBe('rgba(255, 0, 0, 1)'); - element(by.css('.doc-example-live input[value=clear]')).click(); - expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)'); - }); - -
    - */ -var ngStyleDirective = ngDirective(function(scope, element, attr) { - scope.$watch(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) { - if (oldStyles && (newStyles !== oldStyles)) { - forEach(oldStyles, function(val, style) { element.css(style, '');}); - } - if (newStyles) element.css(newStyles); - }, true); -}); - -/** - * @ngdoc directive - * @name ng.directive:ngSwitch - * @restrict EA - * - * @description - * The `ngSwitch` directive is used to conditionally swap DOM structure on your template based on a scope expression. - * Elements within `ngSwitch` but without `ngSwitchWhen` or `ngSwitchDefault` directives will be preserved at the location - * as specified in the template. - * - * The directive itself works similar to ngInclude, however, instead of downloading template code (or loading it - * from the template cache), `ngSwitch` simply chooses one of the nested elements and makes it visible based on which element - * matches the value obtained from the evaluated expression. In other words, you define a container element - * (where you place the directive), place an expression on the **`on="..."` attribute** - * (or the **`ng-switch="..."` attribute**), define any inner elements inside of the directive and place - * a when attribute per element. The when attribute is used to inform ngSwitch which element to display when the on - * expression is evaluated. If a matching expression is not found via a when attribute then an element with the default - * attribute is displayed. - * - *
    - * Be aware that the attribute values to match against cannot be expressions. They are interpreted - * as literal string values to match against. - * For example, **`ng-switch-when="someVal"`** will match against the string `"someVal"` not against the - * value of the expression `$scope.someVal`. - *
    - - * @animations - * enter - happens after the ngSwitch contents change and the matched child element is placed inside the container - * leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM - * - * @usage - * - * ... - * ... - * ... - * - * - * - * @scope - * @priority 800 - * @param {*} ngSwitch|on expression to match against ng-switch-when. - * @paramDescription - * On child elements add: - * - * * `ngSwitchWhen`: the case statement to match against. If match then this - * case will be displayed. If the same match appears multiple times, all the - * elements will be displayed. - * * `ngSwitchDefault`: the default case when no other case match. If there - * are multiple default cases, all of them will be displayed when no other - * case match. - * - * - * @example - - -
    - - selection={{selection}} -
    -
    -
    Settings Div
    -
    Home Span
    -
    default
    -
    -
    -
    - - function Ctrl($scope) { - $scope.items = ['settings', 'home', 'other']; - $scope.selection = $scope.items[0]; - } - - - .animate-switch-container { - position:relative; - background:white; - border:1px solid black; - height:40px; - overflow:hidden; - } - - .animate-switch { - padding:10px; - } - - .animate-switch.ng-animate { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - - position:absolute; - top:0; - left:0; - right:0; - bottom:0; - } - - .animate-switch.ng-leave.ng-leave-active, - .animate-switch.ng-enter { - top:-50px; - } - .animate-switch.ng-leave, - .animate-switch.ng-enter.ng-enter-active { - top:0; - } - - - var switchElem = element(by.css('.doc-example-live [ng-switch]')); - var select = element(by.model('selection')); - - it('should start in settings', function() { - expect(switchElem.getText()).toMatch(/Settings Div/); - }); - it('should change to home', function() { - select.element.all(by.css('option')).get(1).click(); - expect(switchElem.getText()).toMatch(/Home Span/); - }); - it('should select default', function() { - select.element.all(by.css('option')).get(2).click(); - expect(switchElem.getText()).toMatch(/default/); - }); - -
    - */ -var ngSwitchDirective = ['$animate', function($animate) { - return { - restrict: 'EA', - require: 'ngSwitch', - - // asks for $scope to fool the BC controller module - controller: ['$scope', function ngSwitchController() { - this.cases = {}; - }], - link: function(scope, element, attr, ngSwitchController) { - var watchExpr = attr.ngSwitch || attr.on, - selectedTranscludes, - selectedElements, - selectedScopes = []; - - scope.$watch(watchExpr, function ngSwitchWatchAction(value) { - for (var i= 0, ii=selectedScopes.length; i - - -
    -
    -
    - {{text}} -
    -
    - - it('should have transcluded', function() { - var titleElement = element(by.model('title')); - titleElement.clear(); - titleElement.sendKeys('TITLE'); - var textElement = element(by.model('text')); - textElement.clear(); - textElement.sendKeys('TEXT'); - expect(element(by.binding('title')).getText()).toEqual('TITLE'); - expect(element(by.binding('text')).getText()).toEqual('TEXT'); - }); - - - * - */ -var ngTranscludeDirective = ngDirective({ - link: function($scope, $element, $attrs, controller, $transclude) { - if (!$transclude) { - throw minErr('ngTransclude')('orphan', - 'Illegal use of ngTransclude directive in the template! ' + - 'No parent directive that requires a transclusion found. ' + - 'Element: {0}', - startingTag($element)); - } - - $transclude(function(clone) { - $element.empty(); - $element.append(clone); - }); - } -}); - -/** - * @ngdoc directive - * @name ng.directive:script - * @restrict E - * - * @description - * Load the content of a ` - - Load inlined template -
    - - - it('should load template defined inside script tag', function() { - element(by.css('#tpl-link')).click(); - expect(element(by.css('#tpl-content')).getText()).toMatch(/Content of the template/); - }); - - - */ -var scriptDirective = ['$templateCache', function($templateCache) { - return { - restrict: 'E', - terminal: true, - compile: function(element, attr) { - if (attr.type == 'text/ng-template') { - var templateUrl = attr.id, - // IE is not consistent, in scripts we have to read .text but in other nodes we have to read .textContent - text = element[0].text; - - $templateCache.put(templateUrl, text); - } - } - }; -}]; - -var ngOptionsMinErr = minErr('ngOptions'); -/** - * @ngdoc directive - * @name ng.directive:select - * @restrict E - * - * @description - * HTML `SELECT` element with angular data-binding. - * - * # `ngOptions` - * - * The `ngOptions` attribute can be used to dynamically generate a list of `` - * DOM element. - * * `trackexpr`: Used when working with an array of objects. The result of this expression will be - * used to identify the objects in the array. The `trackexpr` will most likely refer to the - * `value` variable (e.g. `value.propertyName`). - * - * @example - - - -
    -
      -
    • - Name: - [X] -
    • -
    • - [add] -
    • -
    -
    - Color (null not allowed): -
    - - Color (null allowed): - - -
    - - Color grouped by shade: -
    - - - Select bogus.
    -
    - Currently selected: {{ {selected_color:color} }} -
    -
    -
    -
    - - it('should check ng-options', function() { - expect(element(by.binding('{selected_color:color}')).getText()).toMatch('red'); - element.all(by.select('color')).first().click(); - element.all(by.css('select[ng-model="color"] option')).first().click(); - expect(element(by.binding('{selected_color:color}')).getText()).toMatch('black'); - element(by.css('.nullable select[ng-model="color"]')).click(); - element.all(by.css('.nullable select[ng-model="color"] option')).first().click(); - expect(element(by.binding('{selected_color:color}')).getText()).toMatch('null'); - }); - -
    - */ - -var ngOptionsDirective = valueFn({ terminal: true }); -// jshint maxlen: false -var selectDirective = ['$compile', '$parse', function($compile, $parse) { - //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888 - var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/, - nullModelCtrl = {$setViewValue: noop}; -// jshint maxlen: 100 - - return { - restrict: 'E', - require: ['select', '?ngModel'], - controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) { - var self = this, - optionsMap = {}, - ngModelCtrl = nullModelCtrl, - nullOption, - unknownOption; - - - self.databound = $attrs.ngModel; - - - self.init = function(ngModelCtrl_, nullOption_, unknownOption_) { - ngModelCtrl = ngModelCtrl_; - nullOption = nullOption_; - unknownOption = unknownOption_; - }; - - - self.addOption = function(value) { - assertNotHasOwnProperty(value, '"option value"'); - optionsMap[value] = true; - - if (ngModelCtrl.$viewValue == value) { - $element.val(value); - if (unknownOption.parent()) unknownOption.remove(); - } - }; - - - self.removeOption = function(value) { - if (this.hasOption(value)) { - delete optionsMap[value]; - if (ngModelCtrl.$viewValue == value) { - this.renderUnknownOption(value); - } - } - }; - - - self.renderUnknownOption = function(val) { - var unknownVal = '? ' + hashKey(val) + ' ?'; - unknownOption.val(unknownVal); - $element.prepend(unknownOption); - $element.val(unknownVal); - unknownOption.prop('selected', true); // needed for IE - }; - - - self.hasOption = function(value) { - return optionsMap.hasOwnProperty(value); - }; - - $scope.$on('$destroy', function() { - // disable unknown option so that we don't do work when the whole select is being destroyed - self.renderUnknownOption = noop; - }); - }], - - link: function(scope, element, attr, ctrls) { - // if ngModel is not defined, we don't need to do anything - if (!ctrls[1]) return; - - var selectCtrl = ctrls[0], - ngModelCtrl = ctrls[1], - multiple = attr.multiple, - optionsExp = attr.ngOptions, - nullOption = false, // if false, user will not be able to select it (used by ngOptions) - emptyOption, - // we can't just jqLite('