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

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Steur <tsteur@users.noreply.github.com>2016-11-15 04:03:59 +0300
committerMatthieu Aubry <mattab@users.noreply.github.com>2016-11-15 04:03:59 +0300
commit587cc39e0362719332d410b7a4d5ddcc68788eeb (patch)
treec982c369cdda542c3a4de08be11c893e5364838c
parent64314b26dbc6619d535002bdb79b9e55d1fc87db (diff)
Update Marketplace to work with new API (#10799)
* starting to port marketplace to piwik 3 * updating tests * fix translation key * fix various issues * use material select * fix plugin upload * deprecate license_homepage plugin metadata and link to a LICENSE[.md|.txt] file if found (#10756) * deprecate license_homepage plugin metadata, and link to a LICENSE[.md|.txt] file if found * Make license view HTML only without menu * fix tests and update * fix some links did not work * we need to show warnings even when plugin is installed, not only when activated. otherwise it is not clear why something is not downloadable * fix install was not working * improved responsiveness of marketplace * fix more tests * fix search was shown when only a few plugins are there * fix ui tests * fix some translations * fix tests and remove duplicated test
-rw-r--r--CHANGELOG.md5
-rw-r--r--composer.json3
-rw-r--r--composer.lock64
-rw-r--r--config/global.ini.php12
-rw-r--r--core/API/DocumentationGenerator.php2
-rw-r--r--core/Http.php15
-rw-r--r--core/Notification.php7
-rw-r--r--core/Plugin.php2
-rw-r--r--core/Plugin/ControllerAdmin.php47
-rw-r--r--core/Plugin/Dependency.php81
-rw-r--r--core/Plugin/Manager.php9
-rw-r--r--core/Plugin/MetadataLoader.php37
-rw-r--r--core/Plugin/ReleaseChannels.php12
-rw-r--r--core/ProxyHttp.php12
-rw-r--r--core/SettingsPiwik.php37
-rwxr-xr-xcore/Twig.php26
-rw-r--r--core/UpdateCheck.php7
-rw-r--r--core/UpdateCheck/ReleaseChannel.php9
-rw-r--r--core/Updates/3.0.0-b2.php24
-rw-r--r--core/Updates/3.0.0-b3.php65
-rw-r--r--core/View.php2
-rw-r--r--libs/bower_components/iframe-resizer/.bower.json55
-rw-r--r--libs/bower_components/iframe-resizer/.gitignore9
-rw-r--r--libs/bower_components/iframe-resizer/.travis.yml6
-rw-r--r--libs/bower_components/iframe-resizer/LICENSE21
-rw-r--r--libs/bower_components/iframe-resizer/bower.json45
-rw-r--r--libs/bower_components/iframe-resizer/index.js4
-rw-r--r--libs/bower_components/iframe-resizer/js/ie8.polyfils.map1
-rw-r--r--libs/bower_components/iframe-resizer/js/ie8.polyfils.min.js4
-rw-r--r--libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.js1108
-rw-r--r--libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.map1
-rw-r--r--libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.min.js10
-rw-r--r--libs/bower_components/iframe-resizer/js/iframeResizer.js1002
-rw-r--r--libs/bower_components/iframe-resizer/js/iframeResizer.map1
-rw-r--r--libs/bower_components/iframe-resizer/js/iframeResizer.min.js9
-rw-r--r--libs/bower_components/iframe-resizer/js/index.js2
-rw-r--r--libs/bower_components/iframe-resizer/karma.conf.js92
-rw-r--r--libs/bower_components/iframe-resizer/src/ie8.polyfils.js64
-rw-r--r--libs/bower_components/iframe-resizer/src/iframeResizer.contentWindow.js1123
-rw-r--r--libs/bower_components/iframe-resizer/src/iframeResizer.js1002
-rw-r--r--libs/bower_components/iframe-resizer/test-main.js33
-rw-r--r--plugins/API/Menu.php2
-rw-r--r--plugins/CoreAdminHome/Controller.php5
-rw-r--r--plugins/CoreAdminHome/templates/home.twig2
-rw-r--r--plugins/CoreConsole/Commands/GeneratePlugin.php5
-rw-r--r--plugins/CoreConsole/Commands/GeneratePluginBase.php14
-rw-r--r--plugins/CoreHome/Controller.php5
-rw-r--r--plugins/CoreHome/CoreHome.php3
-rw-r--r--plugins/CoreHome/lang/en.json1
-rw-r--r--plugins/CoreHome/templates/_headerMessage.twig8
-rw-r--r--plugins/CorePluginsAdmin/Controller.php257
-rw-r--r--plugins/CorePluginsAdmin/CorePluginsAdmin.php12
-rw-r--r--plugins/CorePluginsAdmin/Marketplace.php199
-rw-r--r--plugins/CorePluginsAdmin/MarketplaceApiClient.php200
-rw-r--r--plugins/CorePluginsAdmin/Menu.php43
-rw-r--r--plugins/CorePluginsAdmin/PluginInstaller.php85
-rw-r--r--plugins/CorePluginsAdmin/angularjs/marketplace/marketplace.directive.js59
-rw-r--r--plugins/CorePluginsAdmin/angularjs/plugins/plugin-management.directive.js20
-rw-r--r--plugins/CorePluginsAdmin/config/config.php7
-rw-r--r--plugins/CorePluginsAdmin/lang/en.json64
-rw-r--r--plugins/CorePluginsAdmin/lang/hr.json5
-rw-r--r--plugins/CorePluginsAdmin/stylesheets/plugins_admin.less32
-rw-r--r--plugins/CorePluginsAdmin/templates/installPlugin.twig41
-rw-r--r--plugins/CorePluginsAdmin/templates/license.twig4
-rw-r--r--plugins/CorePluginsAdmin/templates/macros.twig315
-rw-r--r--plugins/CorePluginsAdmin/templates/marketplace/plugin-list.twig82
-rw-r--r--plugins/CorePluginsAdmin/templates/pluginDetails.twig209
-rw-r--r--plugins/CorePluginsAdmin/templates/plugins.twig55
-rw-r--r--plugins/CorePluginsAdmin/templates/updatePlugin.twig41
-rw-r--r--plugins/CorePluginsAdmin/templates/uploadPlugin.twig18
-rw-r--r--plugins/CoreUpdater/Controller.php16
-rw-r--r--plugins/CoreUpdater/ReleaseChannel/Latest2XBeta.php5
-rw-r--r--plugins/CoreUpdater/ReleaseChannel/LatestBeta.php5
-rw-r--r--plugins/CoreUpdater/SystemSettings.php2
-rw-r--r--plugins/CoreUpdater/Test/Integration/ReleaseChannelTest.php5
-rw-r--r--plugins/CoreUpdater/Updater.php41
-rw-r--r--plugins/CoreUpdater/templates/layout.twig2
-rw-r--r--plugins/ExamplePlugin/plugin.json2
-rw-r--r--plugins/ExampleTheme/plugin.json2
-rw-r--r--plugins/Marketplace/API.php106
-rw-r--r--plugins/Marketplace/Api/Client.php326
-rw-r--r--plugins/Marketplace/Api/Exception.php (renamed from plugins/CorePluginsAdmin/MarketplaceApiException.php)4
-rw-r--r--plugins/Marketplace/Api/Service.php158
-rw-r--r--plugins/Marketplace/Api/Service/Exception.php19
-rw-r--r--plugins/Marketplace/Consumer.php68
-rw-r--r--plugins/Marketplace/Controller.php459
-rw-r--r--plugins/Marketplace/Environment.php104
-rw-r--r--plugins/Marketplace/Input/Mode.php29
-rw-r--r--plugins/Marketplace/Input/PluginName.php42
-rw-r--r--plugins/Marketplace/Input/PurchaseType.php20
-rw-r--r--plugins/Marketplace/Input/Sort.php41
-rw-r--r--plugins/Marketplace/LicenseKey.php44
-rw-r--r--plugins/Marketplace/Marketplace.php63
-rw-r--r--plugins/Marketplace/Menu.php28
-rw-r--r--plugins/Marketplace/Plugins.php318
-rw-r--r--plugins/Marketplace/Plugins/InvalidLicenses.php238
-rw-r--r--plugins/Marketplace/Tasks.php (renamed from plugins/CorePluginsAdmin/Tasks.php)18
-rw-r--r--plugins/Marketplace/UpdateCommunication.php (renamed from plugins/CorePluginsAdmin/UpdateCommunication.php)14
-rw-r--r--plugins/Marketplace/Widgets/GetNewPlugins.php (renamed from plugins/CorePluginsAdmin/Widgets/GetNewPlugins.php)15
-rw-r--r--plugins/Marketplace/angularjs/licensekey/licensekey.controller.js63
-rw-r--r--plugins/Marketplace/angularjs/marketplace/marketplace.controller.js (renamed from plugins/CorePluginsAdmin/angularjs/marketplace/marketplace.controller.js)0
-rw-r--r--plugins/Marketplace/angularjs/marketplace/marketplace.directive.js132
-rw-r--r--plugins/Marketplace/angularjs/plugins/plugin-name.directive.js (renamed from plugins/CorePluginsAdmin/angularjs/plugins/plugin-name.directive.js)2
-rw-r--r--plugins/Marketplace/config/config.php32
-rw-r--r--plugins/Marketplace/config/test.php144
-rwxr-xr-xplugins/Marketplace/images/rating_important.png (renamed from plugins/CorePluginsAdmin/images/rating_important.png)bin673 -> 673 bytes
-rw-r--r--plugins/Marketplace/lang/en.json112
-rw-r--r--plugins/Marketplace/stylesheets/marketplace-widget.less (renamed from plugins/CorePluginsAdmin/stylesheets/marketplace-widget.less)0
-rw-r--r--plugins/Marketplace/stylesheets/marketplace.less (renamed from plugins/CorePluginsAdmin/stylesheets/marketplace.less)78
-rw-r--r--plugins/Marketplace/stylesheets/plugin-details.less (renamed from plugins/CorePluginsAdmin/stylesheets/plugin-details.less)64
-rw-r--r--plugins/Marketplace/templates/getNewPlugins.twig (renamed from plugins/CorePluginsAdmin/templates/getNewPlugins.twig)0
-rw-r--r--plugins/Marketplace/templates/getNewPluginsAdmin.twig (renamed from plugins/CorePluginsAdmin/templates/getNewPluginsAdmin.twig)0
-rw-r--r--plugins/Marketplace/templates/installPlugin.twig41
-rw-r--r--plugins/Marketplace/templates/licenseform.twig75
-rw-r--r--plugins/Marketplace/templates/macros.twig25
-rw-r--r--plugins/Marketplace/templates/overview.twig (renamed from plugins/CorePluginsAdmin/templates/marketplace.twig)69
-rw-r--r--plugins/Marketplace/templates/paid-plugins-install-list.twig22
-rw-r--r--plugins/Marketplace/templates/plugin-details.twig361
-rw-r--r--plugins/Marketplace/templates/plugin-list.twig159
-rw-r--r--plugins/Marketplace/templates/subscription-overview.twig101
-rw-r--r--plugins/Marketplace/templates/updatePlugin.twig41
-rw-r--r--plugins/Marketplace/tests/Fixtures/SimpleFixtureTrackFewVisits.php50
-rw-r--r--plugins/Marketplace/tests/Framework/Mock/Client.php22
-rw-r--r--plugins/Marketplace/tests/Framework/Mock/Consumer.php91
-rw-r--r--plugins/Marketplace/tests/Framework/Mock/Environment.php53
-rw-r--r--plugins/Marketplace/tests/Framework/Mock/Service.php122
-rw-r--r--plugins/Marketplace/tests/Integration/ApiTest.php198
-rw-r--r--plugins/Marketplace/tests/Integration/ClientTest.php72
-rw-r--r--plugins/Marketplace/tests/Integration/EnvironmentTest.php87
-rw-r--r--plugins/Marketplace/tests/Integration/Input/PluginNameTest.php53
-rw-r--r--plugins/Marketplace/tests/Integration/LicenseKeyTest.php126
-rw-r--r--plugins/Marketplace/tests/Integration/Plugins/InvalidLicensesTest.php290
-rw-r--r--plugins/Marketplace/tests/Integration/PluginsTest.php486
-rw-r--r--plugins/Marketplace/tests/Integration/ServiceTest.php67
-rw-r--r--plugins/Marketplace/tests/Integration/UpdateCommunicationTest.php (renamed from plugins/CorePluginsAdmin/tests/Integration/UpdateCommunicationTest.php)9
-rw-r--r--plugins/Marketplace/tests/System/Api/ClientTest.php299
-rw-r--r--plugins/Marketplace/tests/System/Api/ServiceTest.php165
-rw-r--r--plugins/Marketplace/tests/Unit/ConsumerTest.php153
-rw-r--r--plugins/Marketplace/tests/resources/emptyObjectResponse.json1
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer1_paid2_custom1.json21
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer2_paid1.json7
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer3_paid1_custom2.json27
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_consumer-access_token-notexistingtoken.json1
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_consumer-access_token-validbutnolicense.json1
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_consumer-num_users-201-access_token-consumer1_paid2_custom1.json21
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer1_paid2_custom1.json1
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer2_paid1.json1
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer3_paid1_custom2.json1
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-notexistingtoken.json1
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-validbutnolicense.json1
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_consumer_validate.json1
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_info.json3
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-consumer1_paid2_custom1.json142
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-consumer2_paid1.json89
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-notexistingtoken.json83
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-num_users-201-access_token-consumer1_paid2_custom1.json141
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-num_users-201-access_token-consumer2_paid1.json90
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_plugins-query-nomatchforthisquery.json1
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_plugins.json3011
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_plugins_Barometer_info.json57
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_plugins_CustomPlugin1_info-access_token-notexistingtoken.json1
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info-access_token-consumer3_paid1_custom2.json90
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info-purchase_type-paid-num_users-201-access_token-consumer2_paid1.json90
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info.json83
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_plugins_TreemapVisualization_info.json36
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_plugins_checkUpdates-pluginspluginsnameAnonymousPi.json17
-rw-r--r--plugins/Marketplace/tests/resources/v2.0_themes.json182
-rw-r--r--plugins/Morpheus/templates/javascriptCode.twig2
-rw-r--r--plugins/Widgetize/Menu.php2
-rw-r--r--plugins/Widgetize/tests/System/WidgetTest.php4
-rw-r--r--tests/PHPUnit/Integration/DependencyTest.php22
-rw-r--r--tests/PHPUnit/Integration/Plugin/ManagerTest.php4
-rw-r--r--tests/PHPUnit/Integration/Tracker/TrackerCodeGeneratorTest.php2
-rw-r--r--tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getWidgetMetadata.xml238
-rw-r--r--tests/UI/expected-screenshots/DashboardManager_expanded.png2
-rw-r--r--tests/UI/expected-screenshots/DashboardManager_removed.png4
-rw-r--r--tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png4
-rw-r--r--tests/UI/expected-screenshots/DashboardManager_widget_preview.png4
-rw-r--r--tests/UI/expected-screenshots/Dashboard_removed.png4
-rw-r--r--tests/UI/expected-screenshots/Marketplace_free_plugin_details_multiUserEnvironment.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_free_plugin_details_superuser.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_free_plugin_details_user.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_notification_plugincheck_exceededLicense.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_notification_plugincheck_expiredLicense.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_notification_plugincheck_noLicense.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_multiUserEnvironment.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_superuser.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_user.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_multiUserEnvironment.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_superuser.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_user.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_multiUserEnvironment.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_multiUserEnvironment_installed.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_superuser.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_superuser_installed.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_user.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_user_installed.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_multiUserEnvironment.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_superuser.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_user.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_multiUserEnvironment.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_superuser.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_user.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_multiUserEnvironment.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_superuser.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_user.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_subscription_overview_exceededLicense.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_subscription_overview_expiredLicense.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_subscription_overview_noLicense.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_subscription_overview_validLicense.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_superuser_enable_plugins_admin.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_superuser_enable_plugins_admin_with_multiserver_enabled.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_superuser_install_all_paid_plugins_at_once.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_superuser_invalid_license_key_entered.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_superuser_remove_license_key_confirmation.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_superuser_remove_license_key_confirmed.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_superuser_valid_license_key_entered.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_multiUserEnvironment.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_superuser.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_user.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_updates_multiUserEnvironment.png3
-rw-r--r--tests/UI/expected-screenshots/Marketplace_updates_superuser.png3
-rw-r--r--tests/UI/expected-screenshots/Theme_home.png4
-rw-r--r--tests/UI/expected-screenshots/UIIntegrationTest_admin_diagnostics_configfile.png4
-rw-r--r--tests/UI/expected-screenshots/UIIntegrationTest_admin_home.png4
-rw-r--r--tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins.png4
-rw-r--r--tests/UI/expected-screenshots/UIIntegrationTest_admin_themes.png4
-rw-r--r--tests/UI/expected-screenshots/UIIntegrationTest_api_listing.png4
-rw-r--r--tests/UI/expected-screenshots/UIIntegrationTest_dashboard1.png4
-rw-r--r--tests/UI/expected-screenshots/UIIntegrationTest_dashboard2.png4
-rw-r--r--tests/UI/expected-screenshots/UIIntegrationTest_menu_apidisallowed.png4
-rw-r--r--tests/UI/expected-screenshots/UIIntegrationTest_widgets_listing.png4
-rw-r--r--tests/UI/specs/Marketplace_spec.js265
-rw-r--r--tests/lib/screenshot-testing/support/page-renderer.js2
234 files changed, 16149 insertions, 1701 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 609cca5e49..2cf95ce416 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -144,6 +144,11 @@ The folder containing expected screenshots was renamed from `expected-ui-screens
* Tracking API: by default, when tracking a Page URL, Piwik will now remove the URL query string parameter `sid` if it is found.
* In the JavaScript tracker, the function `setDomains` will not anymore attempt to set a cookie path. Learn more about [configuring the tracker correctly](http://developer.piwik.org/guides/tracking-javascript-guide#tracking-one-domain) when tracking one or several domains and/or paths.
+## Piwik 2.16.1
+
+### Internal change
+ * The setting `[General]enable_marketplace=0/1` was removed, instead the new plugin Marketplace can be disabled/enabled. The updater should automatically migrate an existing setting.
+
## Piwik 2.16.0
### New features
diff --git a/composer.json b/composer.json
index c6f15b0b67..ef6b953aaf 100644
--- a/composer.json
+++ b/composer.json
@@ -44,7 +44,8 @@
"piwik/referrer-spam-blacklist": "~1.0",
"piwik/searchengine-and-social-list": "~1.0",
"tecnickcom/tcpdf": "~6.0",
- "piwik/piwik-php-tracker": "^1.0"
+ "piwik/piwik-php-tracker": "^1.0",
+ "composer/semver": "~1.3.0"
},
"require-dev": {
"aws/aws-sdk-php": "2.7.1",
diff --git a/composer.lock b/composer.lock
index d1164b4d32..79ce5d3c01 100644
--- a/composer.lock
+++ b/composer.lock
@@ -8,6 +8,68 @@
"content-hash": "6c07c2bb4f82daef6519c9ac18c642c4",
"packages": [
{
+ "name": "composer/semver",
+ "version": "1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/semver.git",
+ "reference": "df4463baa9f44fe6cf0a6da4fde2934d4c0a2747"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/semver/zipball/df4463baa9f44fe6cf0a6da4fde2934d4c0a2747",
+ "reference": "df4463baa9f44fe6cf0a6da4fde2934d4c0a2747",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.5 || ^5.0.5",
+ "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Semver\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "keywords": [
+ "semantic",
+ "semver",
+ "validation",
+ "versioning"
+ ],
+ "time": "2016-02-25 22:23:39"
+ },
+ {
"name": "container-interop/container-interop",
"version": "1.1.0",
"source": {
@@ -1443,7 +1505,7 @@
"performance",
"profiling"
],
- "time": "2015-02-26 14:37:51"
+ "time": "2014-08-28 17:34:52"
},
{
"name": "guzzle/guzzle",
diff --git a/config/global.ini.php b/config/global.ini.php
index 761e059459..8d83121739 100644
--- a/config/global.ini.php
+++ b/config/global.ini.php
@@ -441,6 +441,12 @@ multisites_refresh_after_seconds = 300
; set the HTTPS environment variable.
assume_secure_protocol = 0
+; Set to 1 if you're using more than one server for your Piwik installation. For example if you are using Piwik in a
+; load balanced environment, if you have configured failover or if you're just using multiple servers in general.
+; By enabling this flag we will for example not allow the installation of a plugin via the UI as a plugin would be only
+; installed on one server or a config one change would be only made on one server instead of all servers.
+multi_server_environment = 0
+
; List of proxy headers for client IP addresses
; Piwik will determine the user IP by extracting the first IP address found in this proxy header.
;
@@ -526,14 +532,9 @@ absolute_chroot_path =
; This may for example be useful when doing Mysql AWS replication
enable_load_data_infile = 1
-; By setting this option to 0, you can disable the Piwik marketplace. This is useful to prevent giving the Super user
-; the access to disk and install custom PHP code (Piwik plugins).
-enable_marketplace = 1
-
; By setting this option to 0:
; - links to Enable/Disable/Uninstall plugins will be hidden and disabled
; - links to Uninstall themes will be disabled (but user can still enable/disable themes)
-; - as well as disabling plugins admin actions (such as "Upload new plugin"), setting this to 1 will have same effect as setting enable_marketplace=1
enable_plugins_admin = 1
; By setting this option to 0, you can prevent Super User from editing the Geolocation settings.
@@ -805,6 +806,7 @@ Plugins[] = Resolution
Plugins[] = DevicePlugins
Plugins[] = Heartbeat
Plugins[] = Intl
+Plugins[] = Marketplace
Plugins[] = ProfessionalServices
Plugins[] = UserId
Plugins[] = CustomPiwikJs
diff --git a/core/API/DocumentationGenerator.php b/core/API/DocumentationGenerator.php
index 456e487f00..e738c6c4c8 100644
--- a/core/API/DocumentationGenerator.php
+++ b/core/API/DocumentationGenerator.php
@@ -285,6 +285,8 @@ class DocumentationGenerator
'addGoal',
'updateGoal',
'deleteGoal',
+ //Marketplace
+ 'deleteLicenseKey'
);
if (in_array($methodName, $doNotPrintExampleForTheseMethods)) {
diff --git a/core/Http.php b/core/Http.php
index 7e7602c20e..373c4815d9 100644
--- a/core/Http.php
+++ b/core/Http.php
@@ -94,17 +94,24 @@ class Http
$httpPassword = null)
{
// create output file
- $file = null;
+ $file = self::ensureDestinationDirectoryExists($destinationPath);
+
+ $acceptLanguage = $acceptLanguage ? 'Accept-Language: ' . $acceptLanguage : '';
+ return self::sendHttpRequestBy(self::getTransportMethod(), $aUrl, $timeout, $userAgent, $destinationPath, $file, $followDepth, $acceptLanguage, $acceptInvalidSslCertificate = false, $byteRange, $getExtendedInfo, $httpMethod, $httpUsername, $httpPassword);
+ }
+
+ public static function ensureDestinationDirectoryExists($destinationPath)
+ {
if ($destinationPath) {
- // Ensure destination directory exists
Filesystem::mkdir(dirname($destinationPath));
if (($file = @fopen($destinationPath, 'wb')) === false || !is_resource($file)) {
throw new Exception('Error while creating the file: ' . $destinationPath);
}
+
+ return $file;
}
- $acceptLanguage = $acceptLanguage ? 'Accept-Language: ' . $acceptLanguage : '';
- return self::sendHttpRequestBy(self::getTransportMethod(), $aUrl, $timeout, $userAgent, $destinationPath, $file, $followDepth, $acceptLanguage, $acceptInvalidSslCertificate = false, $byteRange, $getExtendedInfo, $httpMethod, $httpUsername, $httpPassword);
+ return null;
}
/**
diff --git a/core/Notification.php b/core/Notification.php
index b7c576eba0..7672263ab3 100644
--- a/core/Notification.php
+++ b/core/Notification.php
@@ -81,6 +81,13 @@ class Notification
const FLAG_NO_CLEAR = 1;
/**
+ * If this flag is applied, a close icon will be displayed.
+ *
+ * See {@link $flags}.
+ */
+ const FLAG_CLEAR = 0;
+
+ /**
* Notifications of this type will be displayed for a few seconds and then faded out.
*/
const TYPE_TOAST = 'toast';
diff --git a/core/Plugin.php b/core/Plugin.php
index 82ef4f2fec..949895b70e 100644
--- a/core/Plugin.php
+++ b/core/Plugin.php
@@ -42,7 +42,6 @@ require_once PIWIK_INCLUDE_PATH . '/core/Plugin/MetadataLoader.php';
* - **homepage**: The URL to the plugin's website.
* - **authors**: A list of author arrays with keys for 'name', 'email' and 'homepage'
* - **license**: The license the code uses (eg, GPL, MIT, etc.).
- * - **license_homepage**: URL to website describing the license used.
* - **version**: The plugin version (eg, 1.0.1).
* - **theme**: `true` or `false`. If `true`, the plugin will be treated as a theme.
*
@@ -184,7 +183,6 @@ class Plugin
* - 'author_homepage' => string // author homepage URL (or email "mailto:youremail@example.org")
* - 'homepage' => string // plugin homepage URL
* - 'license' => string // plugin license
- * - 'license_homepage' => string // license homepage URL
* - 'version' => string // plugin version number; examples and 3rd party plugins must not use Version::VERSION; 3rd party plugins must increment the version number with each plugin release
* - 'theme' => bool // Whether this plugin is a theme (a theme is a plugin, but a plugin is not necessarily a theme)
*
diff --git a/core/Plugin/ControllerAdmin.php b/core/Plugin/ControllerAdmin.php
index 092a963b8a..73bf47d32b 100644
--- a/core/Plugin/ControllerAdmin.php
+++ b/core/Plugin/ControllerAdmin.php
@@ -10,12 +10,14 @@ namespace Piwik\Plugin;
use Piwik\Config as PiwikConfig;
use Piwik\Config;
+use Piwik\Container\StaticContainer;
use Piwik\Development;
use Piwik\Menu\MenuAdmin;
use Piwik\Menu\MenuTop;
use Piwik\Notification;
use Piwik\Notification\Manager as NotificationManager;
use Piwik\Piwik;
+use Piwik\Plugins\Marketplace\Marketplace;
use Piwik\Tracker\TrackerConfig;
use Piwik\Url;
use Piwik\Version;
@@ -40,6 +42,50 @@ abstract class ControllerAdmin extends Controller
}
}
+ private static function notifyAnyInvalidLicense()
+ {
+ if (!Marketplace::isMarketplaceEnabled()) {
+ return;
+ }
+
+ if (Piwik::isUserIsAnonymous()) {
+ return;
+ }
+
+ if (!Piwik::isUserHasSomeAdminAccess()) {
+ return;
+ }
+
+ $expired = StaticContainer::get('Piwik\Plugins\Marketplace\Plugins\InvalidLicenses');
+
+ $messageLicenseMissing = $expired->getMessageNoLicense();
+ if (!empty($messageLicenseMissing)) {
+ $notification = new Notification($messageLicenseMissing);
+ $notification->raw = true;
+ $notification->context = Notification::CONTEXT_ERROR;
+ $notification->title = Piwik::translate('Marketplace_LicenseMissing');
+ Notification\Manager::notify('ControllerAdmin_LicenseMissingWarning', $notification);
+ }
+
+ $messageExceeded = $expired->getMessageExceededLicenses();
+ if (!empty($messageExceeded)) {
+ $notification = new Notification($messageExceeded);
+ $notification->raw = true;
+ $notification->context = Notification::CONTEXT_WARNING;
+ $notification->title = Piwik::translate('Marketplace_LicenseExceeded');
+ Notification\Manager::notify('ControllerAdmin_LicenseExceededWarning', $notification);
+ }
+
+ $messageExpired = $expired->getMessageExpiredLicenses();
+ if (!empty($messageExpired)) {
+ $notification = new Notification($messageExpired);
+ $notification->raw = true;
+ $notification->context = Notification::CONTEXT_WARNING;
+ $notification->title = Piwik::translate('Marketplace_LicenseExpired');
+ Notification\Manager::notify('ControllerAdmin_LicenseExpiredWarning', $notification);
+ }
+ }
+
private static function notifyAnyInvalidPlugin()
{
$missingPlugins = \Piwik\Plugin\Manager::getInstance()->getMissingPlugins();
@@ -272,6 +318,7 @@ abstract class ControllerAdmin extends Controller
$view->isSuperUser = Piwik::hasUserSuperUserAccess();
+ self::notifyAnyInvalidLicense();
self::notifyAnyInvalidPlugin();
self::notifyWhenPhpVersionIsEOL();
self::notifyWhenPhpVersionIsNotCompatibleWithNextMajorPiwik();
diff --git a/core/Plugin/Dependency.php b/core/Plugin/Dependency.php
index e1ccc1c92c..127d32eabf 100644
--- a/core/Plugin/Dependency.php
+++ b/core/Plugin/Dependency.php
@@ -8,7 +8,9 @@
*/
namespace Piwik\Plugin;
+use Composer\Semver\VersionParser;
use Piwik\Plugin\Manager as PluginManager;
+use Piwik\Plugins\Marketplace\Environment;
use Piwik\Version;
/**
@@ -17,10 +19,18 @@ use Piwik\Version;
class Dependency
{
private $piwikVersion;
+ private $phpVersion;
public function __construct()
{
$this->setPiwikVersion(Version::VERSION);
+ $this->setPhpVersion(PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION);
+ }
+
+ public function setEnvironment(Environment $environment)
+ {
+ $this->setPiwikVersion($environment->getPiwikVersion());
+ $this->setPhpVersion($environment->getPhpVersion());
}
public function getMissingDependencies($requires)
@@ -50,40 +60,91 @@ class Dependency
public function getMissingVersions($currentVersion, $requiredVersion)
{
- $currentVersion = trim($currentVersion);
- $requiredVersions = explode(',', (string) $requiredVersion);
+ $currentVersion = trim($currentVersion);
$missingVersions = array();
+ if (empty($currentVersion)) {
+ if (!empty($requiredVersion)) {
+ $missingVersions[] = (string) $requiredVersion;
+ }
+
+ return $missingVersions;
+ }
+
+ $requiredVersion = $this->makeVersionBackwardsCompatibleIfNoComparisonDefined($requiredVersion);
+
+ $version = new VersionParser();
+ $constraintsExisting = $version->parseConstraints($currentVersion);
+
+ $requiredVersions = explode(',', (string) $requiredVersion);
+
foreach ($requiredVersions as $required) {
- $comparison = '>=';
- $required = trim($required);
+ $required = trim($required);
- if (preg_match('{^(<>|!=|>=?|<=?|==?)\s*(.*)}', $required, $matches)) {
- $required = $matches[2];
- $comparison = trim($matches[1]);
+ if (empty($required)) {
+ continue;
}
- if (false === version_compare($currentVersion, $required, $comparison)) {
- $missingVersions[] = $comparison . $required;
+ $required = $this->makeVersionBackwardsCompatibleIfNoComparisonDefined($required);
+ $constraintRequired = $version->parseConstraints($required);
+
+ if (!$constraintRequired->matches($constraintsExisting)) {
+ $missingVersions[] = $required;
}
}
return $missingVersions;
}
+ private function makeVersionBackwardsCompatibleIfNoComparisonDefined($version)
+ {
+ if (!empty($version) && preg_match('/^(\d+)\.(\d+)/', $version)) {
+ // TODO: we should remove this from piwik 3. To stay BC we add >= if no >= is defined yet
+ $version = '>=' . $version;
+ }
+
+ return $version;
+ }
+
public function setPiwikVersion($piwikVersion)
{
$this->piwikVersion = $piwikVersion;
}
+ public function setPhpVersion($phpVersion)
+ {
+ $this->phpVersion = $phpVersion;
+ }
+
+ public function hasDependencyToDisabledPlugin($requires)
+ {
+ if (empty($requires)) {
+ return false;
+ }
+
+ foreach ($requires as $name => $requiredVersion) {
+ $nameLower = strtolower($name);
+ $isPluginRequire = !in_array($nameLower, array('piwik', 'php'));
+ if ($isPluginRequire) {
+ // we do not check version, only whether it's activated. Everything that is not piwik or php is assumed
+ // a plugin so far.
+ if (!PluginManager::getInstance()->isPluginActivated($name)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
private function getCurrentVersion($name)
{
switch (strtolower($name)) {
case 'piwik':
return $this->piwikVersion;
case 'php':
- return PHP_VERSION;
+ return $this->phpVersion;
default:
try {
$pluginNames = PluginManager::getAllPluginsNames();
diff --git a/core/Plugin/Manager.php b/core/Plugin/Manager.php
index 9455f06c0b..846de60086 100644
--- a/core/Plugin/Manager.php
+++ b/core/Plugin/Manager.php
@@ -658,7 +658,12 @@ class Manager
|| $name == self::DEFAULT_THEME;
}
- protected function isPluginThirdPartyAndBogus($pluginName)
+ /**
+ * @param $pluginName
+ * @return bool
+ * @ignore
+ */
+ public function isPluginThirdPartyAndBogus($pluginName)
{
if ($this->isPluginBundledWithCore($pluginName)) {
return false;
@@ -915,7 +920,7 @@ class Manager
public function isValidPluginName($pluginName)
{
- return (bool) preg_match('/^[a-zA-Z]([a-zA-Z0-9]*)$/D', $pluginName);
+ return (bool) preg_match('/^[a-zA-Z]([a-zA-Z0-9_]*)$/D', $pluginName);
}
/**
diff --git a/core/Plugin/MetadataLoader.php b/core/Plugin/MetadataLoader.php
index 5a6b2617e6..bde849ff7e 100644
--- a/core/Plugin/MetadataLoader.php
+++ b/core/Plugin/MetadataLoader.php
@@ -57,6 +57,12 @@ class MetadataLoader
unset($plugin['description']);
}
+ // look for a license file
+ $licenseFile = $this->getPathToLicenseFile();
+ if(!empty($licenseFile)) {
+ $plugin['license_file'] = $licenseFile;
+ }
+
return array_merge(
$defaults,
$plugin
@@ -78,7 +84,6 @@ class MetadataLoader
'homepage' => 'http://piwik.org/',
'authors' => array(array('name' => 'Piwik', 'homepage' => 'http://piwik.org/')),
'license' => 'GPL v3+',
- 'license_homepage' => 'http://www.gnu.org/licenses/gpl.html',
'version' => Version::VERSION,
'theme' => false,
'require' => array()
@@ -87,7 +92,7 @@ class MetadataLoader
private function loadPluginInfoJson()
{
- $path = \Piwik\Plugin\Manager::getPluginsDirectory() . $this->pluginName . '/' . self::PLUGIN_JSON_FILENAME;
+ $path = $this->getPathToPluginFolder() . '/' . self::PLUGIN_JSON_FILENAME;
return $this->loadJsonMetadata($path);
}
@@ -111,4 +116,32 @@ class MetadataLoader
return $info;
}
+
+ /**
+ * @return string
+ */
+ private function getPathToPluginFolder()
+ {
+ return \Piwik\Plugin\Manager::getPluginsDirectory() . $this->pluginName;
+ }
+
+ /**
+ * @return null|string
+ */
+ public function getPathToLicenseFile()
+ {
+ $prefixPath = $this->getPathToPluginFolder() . '/';
+ $licenseFiles = array(
+ 'LICENSE',
+ 'LICENSE.md',
+ 'LICENSE.txt'
+ );
+ foreach ($licenseFiles as $licenseFile) {
+ $pathToLicense = $prefixPath . $licenseFile;
+ if (is_file($pathToLicense) && is_readable($pathToLicense)) {
+ return $pathToLicense;
+ }
+ }
+ return null;
+ }
}
diff --git a/core/Plugin/ReleaseChannels.php b/core/Plugin/ReleaseChannels.php
index f108a32cdf..08d95d9bb2 100644
--- a/core/Plugin/ReleaseChannels.php
+++ b/core/Plugin/ReleaseChannels.php
@@ -68,6 +68,18 @@ class ReleaseChannels
return reset($channels);
}
+ /**
+ * Sets the given release channel in config but does not save id. $config->forceSave() still needs to be called
+ * @internal tests only
+ * @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);
diff --git a/core/ProxyHttp.php b/core/ProxyHttp.php
index 790d62229d..bf3bbf43e3 100644
--- a/core/ProxyHttp.php
+++ b/core/ProxyHttp.php
@@ -60,9 +60,12 @@ class ProxyHttp
* of the file will be served.
* @param int|false $byteEnd The ending byte in the file to serve. If false, the data from $byteStart to the
* end of the file will be served.
+ * @param string|false $filename By default the filename of $file is reused as Content-Disposition. If the
+ * file should be sent as a different filename to the client you can specify
+ * a custom filename here.
*/
public static function serverStaticFile($file, $contentType, $expireFarFutureDays = 100, $byteStart = false,
- $byteEnd = false)
+ $byteEnd = false, $filename = false)
{
// if the file cannot be found return HTTP status code '404'
if (!file_exists($file)) {
@@ -78,7 +81,12 @@ class ProxyHttp
// set some HTTP response headers
self::overrideCacheControlHeaders('public');
Common::sendHeader('Vary: Accept-Encoding');
- Common::sendHeader('Content-Disposition: inline; filename=' . basename($file));
+
+ if (false === $filename) {
+ $filename = basename($file);
+ }
+
+ Common::sendHeader('Content-Disposition: inline; filename=' . $filename);
if ($expireFarFutureDays) {
// Required by proxy caches potentially in between the browser and server to cache the request indeed
diff --git a/core/SettingsPiwik.php b/core/SettingsPiwik.php
index c0b2d8e177..52c4f3e16f 100644
--- a/core/SettingsPiwik.php
+++ b/core/SettingsPiwik.php
@@ -233,6 +233,43 @@ class SettingsPiwik
}
/**
+ * Detect whether user has enabled auto updates. Please note this config is a bit misleading. It is currently
+ * actually used for 2 things: To disable making any connections back to Piwik, and to actually disable the auto
+ * update of core and plugins.
+ * @return bool
+ */
+ public static function isAutoUpdateEnabled()
+ {
+ return (bool) Config::getInstance()->General['enable_auto_update'];
+ }
+
+ /**
+ * Detects whether an auto update can be made. An update is possible if the user is not on multiple servers and if
+ * automatic updates are actually enabled. If a user is running Piwik on multiple servers an update is not possible
+ * as it would be installed only on one server instead of all of them. Also if a user has disabled automatic updates
+ * we cannot perform any automatic updates.
+ *
+ * @return bool
+ */
+ public static function isAutoUpdatePossible()
+ {
+ return !self::isMultiServerEnvironment() && self::isAutoUpdateEnabled();
+ }
+
+ /**
+ * Returns `true` if Piwik is running on more than one server. For example in a load balanced environment. In this
+ * case we should not make changes to the config and not install a plugin via the UI as it would be only executed
+ * on one server.
+ * @return bool
+ */
+ public static function isMultiServerEnvironment()
+ {
+ $is = Config::getInstance()->General['multi_server_environment'];
+
+ return !empty($is);
+ }
+
+ /**
* Returns `true` if segmentation is allowed for this user, `false` if otherwise.
*
* @return bool
diff --git a/core/Twig.php b/core/Twig.php
index 5ac84993f1..ce8085f23c 100755
--- a/core/Twig.php
+++ b/core/Twig.php
@@ -176,6 +176,9 @@ class Twig
$this->addFilter_prettyDate();
$this->addFilter_safeDecodeRaw();
$this->addFilter_number();
+ $this->addFilter_nonce();
+ $this->addFilter_md5();
+ $this->addFilter_onlyDomain();
$this->twig->addFilter(new Twig_SimpleFilter('implode', 'implode'));
$this->twig->addFilter(new Twig_SimpleFilter('ucwords', 'ucwords'));
$this->twig->addFilter(new Twig_SimpleFilter('lcfirst', 'lcfirst'));
@@ -428,6 +431,29 @@ class Twig
$this->twig->addFilter($formatter);
}
+ protected function addFilter_nonce()
+ {
+ $nonce = new Twig_SimpleFilter('nonce', array('Piwik\\Nonce', 'getNonce'));
+ $this->twig->addFilter($nonce);
+ }
+
+ private function addFilter_md5()
+ {
+ $md5 = new \Twig_SimpleFilter('md5', function ($value) {
+ return md5($value);
+ });
+ $this->twig->addFilter($md5);
+ }
+
+ private function addFilter_onlyDomain()
+ {
+ $domainOnly = new \Twig_SimpleFilter('domainOnly', function ($url) {
+ $parsed = parse_url($url);
+ return $parsed['scheme'] . '://' . $parsed['host'];
+ });
+ $this->twig->addFilter($domainOnly);
+ }
+
protected function addFilter_truncate()
{
$truncateFilter = new Twig_SimpleFilter('truncate', function ($string, $size) {
diff --git a/core/UpdateCheck.php b/core/UpdateCheck.php
index e2ce130c4a..403fa90b36 100644
--- a/core/UpdateCheck.php
+++ b/core/UpdateCheck.php
@@ -21,11 +21,6 @@ class UpdateCheck
const LATEST_VERSION = 'UpdateCheck_LatestVersion';
const SOCKET_TIMEOUT = 2;
- private static function isAutoUpdateEnabled()
- {
- return (bool) Config::getInstance()->General['enable_auto_update'];
- }
-
/**
* Check for a newer version
*
@@ -34,7 +29,7 @@ class UpdateCheck
*/
public static function check($force = false, $interval = null)
{
- if (!self::isAutoUpdateEnabled()) {
+ if (!SettingsPiwik::isAutoUpdateEnabled()) {
return;
}
diff --git a/core/UpdateCheck/ReleaseChannel.php b/core/UpdateCheck/ReleaseChannel.php
index b398ea22c3..0744378623 100644
--- a/core/UpdateCheck/ReleaseChannel.php
+++ b/core/UpdateCheck/ReleaseChannel.php
@@ -34,6 +34,15 @@ abstract class ReleaseChannel
abstract public function getName();
/**
+ * Whether only stable versions are wanted or also beta versions.
+ * @return bool
+ */
+ public function doesPreferStable()
+ {
+ return true;
+ }
+
+ /**
* Get the latest available version number for this release channel. Eg '2.15.0-b4' or '2.15.0'. Should be
* a semantic version number in format MAJOR.MINOR.PATCH (http://semver.org/). Returning an empty string in case
* one cannot connect to the remote server can be acceptable.
diff --git a/core/Updates/3.0.0-b2.php b/core/Updates/3.0.0-b2.php
deleted file mode 100644
index 17c0fce85b..0000000000
--- a/core/Updates/3.0.0-b2.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-/**
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- *
- */
-
-namespace Piwik\Updates;
-
-use Piwik\Db;
-use Piwik\Plugins\Dashboard;
-use Piwik\Updater;
-use Piwik\Updater\Migration;
-use Piwik\Updates;
-
-class Updates_3_0_0_b2 extends Updates
-{
- public static function isMajorUpdate()
- {
- return true;
- }
-}
diff --git a/core/Updates/3.0.0-b3.php b/core/Updates/3.0.0-b3.php
new file mode 100644
index 0000000000..898f669eeb
--- /dev/null
+++ b/core/Updates/3.0.0-b3.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Updates;
+
+use Piwik\Config;
+use Piwik\Plugin;
+use Piwik\Updater;
+use Piwik\Updates as PiwikUpdates;
+use Piwik\Updates;
+
+class Updates_3_0_0_b3 extends Updates
+{
+ private $marketplaceEnabledConfigSetting = 'enable_marketplace';
+
+ public static function isMajorUpdate()
+ {
+ return true;
+ }
+
+ public function doUpdate(Updater $updater)
+ {
+ $general = $this->getConfig()->General;
+
+ // need to check against int(0) value, as if the config setting is not set at all its value is null
+ if (isset($general[$this->marketplaceEnabledConfigSetting])) {
+ $isMarketplaceEnabled = 0 !== $general[$this->marketplaceEnabledConfigSetting];
+
+ $this->removeOldMarketplaceEnabledConfig();
+
+ if ($isMarketplaceEnabled) {
+ $pluginManager = Plugin\Manager::getInstance();
+ $pluginName = 'Marketplace';
+
+ if (!$pluginManager->isPluginActivated($pluginName)) {
+ $pluginManager->activatePlugin($pluginName);
+ }
+ }
+ }
+ }
+
+ private function getConfig()
+ {
+ return Config::getInstance();
+ }
+
+ private function removeOldMarketplaceEnabledConfig()
+ {
+ $config = $this->getConfig();
+ $general = $config->General;
+
+ if (array_key_exists($this->marketplaceEnabledConfigSetting, $general)) {
+ unset($general[$this->marketplaceEnabledConfigSetting]);
+
+ $config->General = $general;
+ $config->forceSave();
+ }
+ }
+}
diff --git a/core/View.php b/core/View.php
index 0c9b379a67..06b6db5f4a 100644
--- a/core/View.php
+++ b/core/View.php
@@ -87,6 +87,7 @@ if (!defined('PIWIK_USER_PATH')) {
* - **isPluginLoaded**: Returns true if the supplied plugin is loaded, false if otherwise.
* `{% if isPluginLoaded('Goals') %}...{% endif %}`
* - **areAdsForProfessionalServicesEnabled**: Returns true if it is ok to show some advertising in the UI for providers of Professional Support for Piwik (from Piwik 2.16.0)
+ * - **isMultiServerEnvironment**: Returns true if Piwik is used on more than one server (since Piwik 2.16.1)
*
* ### Examples
*
@@ -237,6 +238,7 @@ class View implements ViewInterface
$this->latest_version_available = UpdateCheck::isNewestVersionAvailable();
$this->disableLink = Common::getRequestVar('disableLink', 0, 'int');
$this->isWidget = Common::getRequestVar('widget', 0, 'int');
+ $this->isMultiServerEnvironment = SettingsPiwik::isMultiServerEnvironment();
$piwikAds = StaticContainer::get('Piwik\ProfessionalServices\Advertising');
$this->areAdsForProfessionalServicesEnabled = $piwikAds->areAdsForProfessionalServicesEnabled();
diff --git a/libs/bower_components/iframe-resizer/.bower.json b/libs/bower_components/iframe-resizer/.bower.json
new file mode 100644
index 0000000000..5fe4004c75
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/.bower.json
@@ -0,0 +1,55 @@
+{
+ "name": "iframe-resizer",
+ "version": "3.5.5",
+ "homepage": "https://github.com/davidjbradshaw/iframe-resizer",
+ "authors": [
+ "David J. Bradshaw <dave@bradshaw.net>"
+ ],
+ "description": "Responsively keep same and cross domain iFrames sized to their content with support for window/content resizing, multiple and nested iFrames. (Dependacy free and works with IE8+)",
+ "main": [
+ "js/iframeResizer.js",
+ "js/iframeResizer.contentWindow.js"
+ ],
+ "keywords": [
+ "CrossDomain",
+ "Cross-Domain",
+ "iFrame",
+ "Resizing",
+ "Resizer",
+ "postMessage",
+ "content",
+ "resize",
+ "height",
+ "autoheight",
+ "auto-height",
+ "iframe-auto-height",
+ "height-iframe",
+ "heightiframe",
+ "width",
+ "mutationObserver",
+ "RWD",
+ "responsive",
+ "responsiveiframes",
+ "responsive-iframes"
+ ],
+ "license": "MIT",
+ "ignore": [
+ "example",
+ "test",
+ "gruntfile.js",
+ "*.md",
+ "*.json"
+ ],
+ "dependencies": {},
+ "devDependencies": {},
+ "_release": "3.5.5",
+ "_resolution": {
+ "type": "version",
+ "tag": "v3.5.5",
+ "commit": "ddfe8e77c1fd7cc36e2b88b057ef2ab04a45c666"
+ },
+ "_source": "https://github.com/davidjbradshaw/iframe-resizer.git",
+ "_target": "~3.5.5",
+ "_originalSource": "iframe-resizer",
+ "_direct": true
+} \ No newline at end of file
diff --git a/libs/bower_components/iframe-resizer/.gitignore b/libs/bower_components/iframe-resizer/.gitignore
new file mode 100644
index 0000000000..352f695b0f
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/.gitignore
@@ -0,0 +1,9 @@
+.DS_Store
+.coveralls.yml
+node_modules
+bin
+example/test.html
+test/*.off
+npm-debug.log
+bower_components
+coverage* \ No newline at end of file
diff --git a/libs/bower_components/iframe-resizer/.travis.yml b/libs/bower_components/iframe-resizer/.travis.yml
new file mode 100644
index 0000000000..6f671aef5e
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/.travis.yml
@@ -0,0 +1,6 @@
+language: node_js
+node_js:
+ - "4.1"
+before_script:
+ - npm install -g grunt-cli
+sudo: false
diff --git a/libs/bower_components/iframe-resizer/LICENSE b/libs/bower_components/iframe-resizer/LICENSE
new file mode 100644
index 0000000000..1c74bdc837
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2013-2015 David J. Bradshaw
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. \ No newline at end of file
diff --git a/libs/bower_components/iframe-resizer/bower.json b/libs/bower_components/iframe-resizer/bower.json
new file mode 100644
index 0000000000..3749495987
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/bower.json
@@ -0,0 +1,45 @@
+{
+ "name": "iframe-resizer",
+ "version": "3.5.5",
+ "homepage": "https://github.com/davidjbradshaw/iframe-resizer",
+ "authors": [
+ "David J. Bradshaw <dave@bradshaw.net>"
+ ],
+ "description": "Responsively keep same and cross domain iFrames sized to their content with support for window/content resizing, multiple and nested iFrames. (Dependacy free and works with IE8+)",
+ "main": [
+ "js/iframeResizer.js",
+ "js/iframeResizer.contentWindow.js"
+ ],
+ "keywords": [
+ "CrossDomain",
+ "Cross-Domain",
+ "iFrame",
+ "Resizing",
+ "Resizer",
+ "postMessage",
+ "content",
+ "resize",
+ "height",
+ "autoheight",
+ "auto-height",
+ "iframe-auto-height",
+ "height-iframe",
+ "heightiframe",
+ "width",
+ "mutationObserver",
+ "RWD",
+ "responsive",
+ "responsiveiframes",
+ "responsive-iframes"
+ ],
+ "license": "MIT",
+ "ignore": [
+ "example",
+ "test",
+ "gruntfile.js",
+ "*.md",
+ "*.json"
+ ],
+ "dependencies": {},
+ "devDependencies": {}
+}
diff --git a/libs/bower_components/iframe-resizer/index.js b/libs/bower_components/iframe-resizer/index.js
new file mode 100644
index 0000000000..c08035a32b
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/index.js
@@ -0,0 +1,4 @@
+
+'use strict';
+
+module.exports = require('./js');
diff --git a/libs/bower_components/iframe-resizer/js/ie8.polyfils.map b/libs/bower_components/iframe-resizer/js/ie8.polyfils.map
new file mode 100644
index 0000000000..eb0ef37713
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/js/ie8.polyfils.map
@@ -0,0 +1 @@
+{"version":3,"file":"ie8.polyfils.min.js","sources":["../src/ie8.polyfils.js"],"names":["Array","prototype","forEach","fun","this","TypeError","t","Object","len","length","thisArg","arguments","i","call","Function","bind","oThis","aArgs","slice","fToBind","fNOP","fBound","apply","concat","callback","O","k"],"mappings":";;AAOMA,MAAMC,UAAUC,UACrBF,MAAMC,UAAUC,QAAU,SAASC,GAClC,YACA,IAAa,SAATC,MAA4B,OAATA,MAAgC,kBAARD,GAAoB,KAAM,IAAIE,UAO7E,KAAK,GAJJC,GAAIC,OAAOH,MACXI,EAAMF,EAAEG,SAAW,EACnBC,EAAUC,UAAUF,QAAU,EAAIE,UAAU,GAAK,OAEzCC,EAAI,EAAOJ,EAAJI,EAASA,IACpBA,IAAKN,IACRH,EAAIU,KAAKH,EAASJ,EAAEM,GAAIA,EAAGN,KAK1BQ,SAASb,UAAUc,OACtBD,SAASb,UAAUc,KAAO,SAASC,GACjC,GAAoB,kBAATZ,MAGT,KAAM,IAAIC,WAAU,uEAGtB,IAAIY,GAAUjB,MAAMC,UAAUiB,MAAML,KAAKF,UAAW,GAChDQ,EAAUf,KACVgB,EAAU,aACVC,EAAU,WACR,MAAOF,GAAQG,MAAMlB,eAAgBgB,GAAOhB,KAAOY,EAC5CC,EAAMM,OAAOvB,MAAMC,UAAUiB,MAAML,KAAKF,aAMrD,OAHAS,GAAKnB,UAAYG,KAAKH,UACtBoB,EAAOpB,UAAY,GAAImB,GAEhBC,IAINrB,MAAMC,UAAUC,UACnBF,MAAMC,UAAUC,QAAU,SAASsB,EAAUd,GAC3C,GAAa,OAATN,KAAe,KAAM,IAAIC,WAAU,+BACvC,IAAwB,kBAAbmB,GAAyB,KAAM,IAAInB,WAAUmB,EAAW,qBAMnE,KAAK,GAHHC,GAAIlB,OAAOH,MACXI,EAAMiB,EAAEhB,SAAW,EAEZiB,EAAE,EAAQlB,EAAJkB,EAAUA,IACnBA,IAAKD,IACPD,EAASX,KAAKH,EAASe,EAAEC,GAAIA,EAAGD","sourcesContent":["/*\n * IE8 Polyfils for iframeResizer.js\n *\n * Public domain code - Mozilla Contributors\n * https://developer.mozilla.org/\n */\n\n if (!Array.prototype.forEach){\n\tArray.prototype.forEach = function(fun /*, thisArg */){\n\t\t\"use strict\";\n\t\tif (this === void 0 || this === null || typeof fun !== \"function\") throw new TypeError();\n\n\t\tvar\n\t\t\tt = Object(this),\n\t\t\tlen = t.length >>> 0,\n\t\t\tthisArg = arguments.length >= 2 ? arguments[1] : void 0;\n\n\t\tfor (var i = 0; i < len; i++)\n\t\t\tif (i in t)\n\t\t\t\tfun.call(thisArg, t[i], i, t);\n\t};\n}\n\n\nif (!Function.prototype.bind) {\n Function.prototype.bind = function(oThis) {\n if (typeof this !== 'function') {\n // closest thing possible to the ECMAScript 5\n // internal IsCallable function\n throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');\n }\n\n var aArgs = Array.prototype.slice.call(arguments, 1),\n fToBind = this,\n fNOP = function() {},\n fBound = function() {\n return fToBind.apply(this instanceof fNOP ? this : oThis,\n aArgs.concat(Array.prototype.slice.call(arguments)));\n };\n\n fNOP.prototype = this.prototype;\n fBound.prototype = new fNOP();\n\n return fBound;\n };\n}\n\nif (!Array.prototype.forEach) {\n Array.prototype.forEach = function(callback, thisArg) {\n if (this === null) throw new TypeError(' this is null or not defined');\n if (typeof callback !== 'function') throw new TypeError(callback + ' is not a function');\n\n var\n O = Object(this),\n len = O.length >>> 0;\n\n for (var k=0 ; k < len ; k++) {\n if (k in O)\n callback.call(thisArg, O[k], k, O);\n }\n };\n}\n\n\n"]} \ No newline at end of file
diff --git a/libs/bower_components/iframe-resizer/js/ie8.polyfils.min.js b/libs/bower_components/iframe-resizer/js/ie8.polyfils.min.js
new file mode 100644
index 0000000000..5836bacdb4
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/js/ie8.polyfils.min.js
@@ -0,0 +1,4 @@
+// IE8 polyfils for iframeResizer.js
+
+Array.prototype.forEach||(Array.prototype.forEach=function(a){"use strict";if(void 0===this||null===this||"function"!=typeof a)throw new TypeError;for(var b=Object(this),c=b.length>>>0,d=arguments.length>=2?arguments[1]:void 0,e=0;c>e;e++)e in b&&a.call(d,b[e],e,b)}),Function.prototype.bind||(Function.prototype.bind=function(a){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var b=Array.prototype.slice.call(arguments,1),c=this,d=function(){},e=function(){return c.apply(this instanceof d?this:a,b.concat(Array.prototype.slice.call(arguments)))};return d.prototype=this.prototype,e.prototype=new d,e}),Array.prototype.forEach||(Array.prototype.forEach=function(a,b){if(null===this)throw new TypeError(" this is null or not defined");if("function"!=typeof a)throw new TypeError(a+" is not a function");for(var c=Object(this),d=c.length>>>0,e=0;d>e;e++)e in c&&a.call(b,c[e],e,c)});
+//# sourceMappingURL=ie8.polyfils.map \ No newline at end of file
diff --git a/libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.js b/libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.js
new file mode 100644
index 0000000000..bfb4a416e3
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.js
@@ -0,0 +1,1108 @@
+/*
+ * File: iframeResizer.contentWindow.js
+ * Desc: Include this file in any page being loaded into an iframe
+ * to force the iframe to resize to the content size.
+ * Requires: iframeResizer.js on host page.
+ * Doc: https://github.com/davidjbradshaw/iframe-resizer
+ * Author: David J. Bradshaw - dave@bradshaw.net
+ * Contributor: Jure Mav - jure.mav@gmail.com
+ * Contributor: Ian Caunce - ian@hallnet.co.uk
+ */
+
+
+;(function(window, undefined) {
+ 'use strict';
+
+ var
+ autoResize = true,
+ base = 10,
+ bodyBackground = '',
+ bodyMargin = 0,
+ bodyMarginStr = '',
+ bodyObserver = null,
+ bodyPadding = '',
+ calculateWidth = false,
+ doubleEventList = {'resize':1,'click':1},
+ eventCancelTimer = 128,
+ firstRun = true,
+ height = 1,
+ heightCalcModeDefault = 'bodyOffset',
+ heightCalcMode = heightCalcModeDefault,
+ initLock = true,
+ initMsg = '',
+ inPageLinks = {},
+ interval = 32,
+ intervalTimer = null,
+ logging = false,
+ msgID = '[iFrameSizer]', //Must match host page msg ID
+ msgIdLen = msgID.length,
+ myID = '',
+ observer = null,
+ resetRequiredMethods = {max:1,min:1,bodyScroll:1,documentElementScroll:1},
+ resizeFrom = 'child',
+ sendPermit = true,
+ target = window.parent,
+ targetOriginDefault = '*',
+ tolerance = 0,
+ triggerLocked = false,
+ triggerLockedTimer = null,
+ throttledTimer = 16,
+ width = 1,
+ widthCalcModeDefault = 'scroll',
+ widthCalcMode = widthCalcModeDefault,
+ win = window,
+ messageCallback = function(){ warn('MessageCallback function not defined'); },
+ readyCallback = function(){},
+ pageInfoCallback = function(){},
+ customCalcMethods = {
+ height: function(){
+ warn('Custom height calculation function not defined');
+ return document.documentElement.offsetHeight;
+ },
+ width: function(){
+ warn('Custom width calculation function not defined');
+ return document.body.scrollWidth;
+ }
+ };
+
+
+ function addEventListener(el,evt,func){
+ /* istanbul ignore else */ // Not testable in phantonJS
+ if ('addEventListener' in window){
+ el.addEventListener(evt,func, false);
+ } else if ('attachEvent' in window){ //IE
+ el.attachEvent('on'+evt,func);
+ }
+ }
+
+ function removeEventListener(el,evt,func){
+ /* istanbul ignore else */ // Not testable in phantonJS
+ if ('removeEventListener' in window){
+ el.removeEventListener(evt,func, false);
+ } else if ('detachEvent' in window){ //IE
+ el.detachEvent('on'+evt,func);
+ }
+ }
+
+ function capitalizeFirstLetter(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+ }
+
+ //Based on underscore.js
+ function throttle(func) {
+ var
+ context, args, result,
+ timeout = null,
+ previous = 0,
+ later = function() {
+ previous = getNow();
+ timeout = null;
+ result = func.apply(context, args);
+ if (!timeout) {
+ context = args = null;
+ }
+ };
+
+ return function() {
+ var now = getNow();
+
+ if (!previous) {
+ previous = now;
+ }
+
+ var remaining = throttledTimer - (now - previous);
+
+ context = this;
+ args = arguments;
+
+ if (remaining <= 0 || remaining > throttledTimer) {
+ if (timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+
+ previous = now;
+ result = func.apply(context, args);
+
+ if (!timeout) {
+ context = args = null;
+ }
+
+ } else if (!timeout) {
+ timeout = setTimeout(later, remaining);
+ }
+
+ return result;
+ };
+ }
+
+ var getNow = Date.now || function() {
+ /* istanbul ignore next */ // Not testable in PhantonJS
+ return new Date().getTime();
+ };
+
+ function formatLogMsg(msg){
+ return msgID + '[' + myID + ']' + ' ' + msg;
+ }
+
+ function log(msg){
+ if (logging && ('object' === typeof window.console)){
+ console.log(formatLogMsg(msg));
+ }
+ }
+
+ function warn(msg){
+ if ('object' === typeof window.console){
+ console.warn(formatLogMsg(msg));
+ }
+ }
+
+
+ function init(){
+ readDataFromParent();
+ log('Initialising iFrame ('+location.href+')');
+ readDataFromPage();
+ setMargin();
+ setBodyStyle('background',bodyBackground);
+ setBodyStyle('padding',bodyPadding);
+ injectClearFixIntoBodyElement();
+ checkHeightMode();
+ checkWidthMode();
+ stopInfiniteResizingOfIFrame();
+ setupPublicMethods();
+ startEventListeners();
+ inPageLinks = setupInPageLinks();
+ sendSize('init','Init message from host page');
+ readyCallback();
+ }
+
+ function readDataFromParent(){
+
+ function strBool(str){
+ return 'true' === str ? true : false;
+ }
+
+ var data = initMsg.substr(msgIdLen).split(':');
+
+ myID = data[0];
+ bodyMargin = (undefined !== data[1]) ? Number(data[1]) : bodyMargin; //For V1 compatibility
+ calculateWidth = (undefined !== data[2]) ? strBool(data[2]) : calculateWidth;
+ logging = (undefined !== data[3]) ? strBool(data[3]) : logging;
+ interval = (undefined !== data[4]) ? Number(data[4]) : interval;
+ autoResize = (undefined !== data[6]) ? strBool(data[6]) : autoResize;
+ bodyMarginStr = data[7];
+ heightCalcMode = (undefined !== data[8]) ? data[8] : heightCalcMode;
+ bodyBackground = data[9];
+ bodyPadding = data[10];
+ tolerance = (undefined !== data[11]) ? Number(data[11]) : tolerance;
+ inPageLinks.enable = (undefined !== data[12]) ? strBool(data[12]): false;
+ resizeFrom = (undefined !== data[13]) ? data[13] : resizeFrom;
+ widthCalcMode = (undefined !== data[14]) ? data[14] : widthCalcMode;
+ }
+
+ function readDataFromPage(){
+ function readData(){
+ var data = window.iFrameResizer;
+
+ log('Reading data from page: ' + JSON.stringify(data));
+
+ messageCallback = ('messageCallback' in data) ? data.messageCallback : messageCallback;
+ readyCallback = ('readyCallback' in data) ? data.readyCallback : readyCallback;
+ targetOriginDefault = ('targetOrigin' in data) ? data.targetOrigin : targetOriginDefault;
+ heightCalcMode = ('heightCalculationMethod' in data) ? data.heightCalculationMethod : heightCalcMode;
+ widthCalcMode = ('widthCalculationMethod' in data) ? data.widthCalculationMethod : widthCalcMode;
+ }
+
+ function setupCustomCalcMethods(calcMode, calcFunc){
+ if ('function' === typeof calcMode) {
+ log('Setup custom ' + calcFunc + 'CalcMethod');
+ customCalcMethods[calcFunc] = calcMode;
+ calcMode = 'custom';
+ }
+
+ return calcMode;
+ }
+
+ if(('iFrameResizer' in window) && (Object === window.iFrameResizer.constructor)) {
+ readData();
+ heightCalcMode = setupCustomCalcMethods(heightCalcMode, 'height');
+ widthCalcMode = setupCustomCalcMethods(widthCalcMode, 'width');
+ }
+
+ log('TargetOrigin for parent set to: ' + targetOriginDefault);
+ }
+
+
+ function chkCSS(attr,value){
+ if (-1 !== value.indexOf('-')){
+ warn('Negative CSS value ignored for '+attr);
+ value='';
+ }
+ return value;
+ }
+
+ function setBodyStyle(attr,value){
+ if ((undefined !== value) && ('' !== value) && ('null' !== value)){
+ document.body.style[attr] = value;
+ log('Body '+attr+' set to "'+value+'"');
+ }
+ }
+
+ function setMargin(){
+ //If called via V1 script, convert bodyMargin from int to str
+ if (undefined === bodyMarginStr){
+ bodyMarginStr = bodyMargin+'px';
+ }
+
+ setBodyStyle('margin',chkCSS('margin',bodyMarginStr));
+ }
+
+ function stopInfiniteResizingOfIFrame(){
+ document.documentElement.style.height = '';
+ document.body.style.height = '';
+ log('HTML & body height set to "auto"');
+ }
+
+
+ function manageTriggerEvent(options){
+ function handleEvent(){
+ sendSize(options.eventName,options.eventType);
+ }
+
+ var listener = {
+ add: function(eventName){
+ addEventListener(window,eventName,handleEvent);
+ },
+ remove: function(eventName){
+ removeEventListener(window,eventName,handleEvent);
+ }
+ };
+
+ if(options.eventNames && Array.prototype.map){
+ options.eventName = options.eventNames[0];
+ options.eventNames.map(listener[options.method]);
+ } else {
+ listener[options.method](options.eventName);
+ }
+
+ log(capitalizeFirstLetter(options.method) + ' event listener: ' + options.eventType);
+ }
+
+ function manageEventListeners(method){
+ manageTriggerEvent({method:method, eventType: 'Animation Start', eventNames: ['animationstart','webkitAnimationStart'] });
+ manageTriggerEvent({method:method, eventType: 'Animation Iteration', eventNames: ['animationiteration','webkitAnimationIteration'] });
+ manageTriggerEvent({method:method, eventType: 'Animation End', eventNames: ['animationend','webkitAnimationEnd'] });
+ manageTriggerEvent({method:method, eventType: 'Input', eventName: 'input' });
+ manageTriggerEvent({method:method, eventType: 'Mouse Up', eventName: 'mouseup' });
+ manageTriggerEvent({method:method, eventType: 'Mouse Down', eventName: 'mousedown' });
+ manageTriggerEvent({method:method, eventType: 'Orientation Change', eventName: 'orientationchange' });
+ manageTriggerEvent({method:method, eventType: 'Print', eventName: ['afterprint', 'beforeprint'] });
+ manageTriggerEvent({method:method, eventType: 'Ready State Change', eventName: 'readystatechange' });
+ manageTriggerEvent({method:method, eventType: 'Touch Start', eventName: 'touchstart' });
+ manageTriggerEvent({method:method, eventType: 'Touch End', eventName: 'touchend' });
+ manageTriggerEvent({method:method, eventType: 'Touch Cancel', eventName: 'touchcancel' });
+ manageTriggerEvent({method:method, eventType: 'Transition Start', eventNames: ['transitionstart','webkitTransitionStart','MSTransitionStart','oTransitionStart','otransitionstart'] });
+ manageTriggerEvent({method:method, eventType: 'Transition Iteration', eventNames: ['transitioniteration','webkitTransitionIteration','MSTransitionIteration','oTransitionIteration','otransitioniteration'] });
+ manageTriggerEvent({method:method, eventType: 'Transition End', eventNames: ['transitionend','webkitTransitionEnd','MSTransitionEnd','oTransitionEnd','otransitionend'] });
+ if('child' === resizeFrom){
+ manageTriggerEvent({method:method, eventType: 'IFrame Resized', eventName: 'resize' });
+ }
+ }
+
+ function checkCalcMode(calcMode,calcModeDefault,modes,type){
+ if (calcModeDefault !== calcMode){
+ if (!(calcMode in modes)){
+ warn(calcMode + ' is not a valid option for '+type+'CalculationMethod.');
+ calcMode=calcModeDefault;
+ }
+ log(type+' calculation method set to "'+calcMode+'"');
+ }
+
+ return calcMode;
+ }
+
+ function checkHeightMode(){
+ heightCalcMode = checkCalcMode(heightCalcMode,heightCalcModeDefault,getHeight,'height');
+ }
+
+ function checkWidthMode(){
+ widthCalcMode = checkCalcMode(widthCalcMode,widthCalcModeDefault,getWidth,'width');
+ }
+
+ function startEventListeners(){
+ if ( true === autoResize ) {
+ manageEventListeners('add');
+ setupMutationObserver();
+ }
+ else {
+ log('Auto Resize disabled');
+ }
+ }
+
+ function stopMsgsToParent(){
+ log('Disable outgoing messages');
+ sendPermit = false;
+ }
+
+ function removeMsgListener(){
+ log('Remove event listener: Message');
+ removeEventListener(window, 'message', receiver);
+ }
+
+ function disconnectMutationObserver(){
+ if (null !== bodyObserver){
+ /* istanbul ignore next */ // Not testable in PhantonJS
+ bodyObserver.disconnect();
+ }
+ }
+
+ function stopEventListeners(){
+ manageEventListeners('remove');
+ disconnectMutationObserver();
+ clearInterval(intervalTimer);
+ }
+
+ function teardown(){
+ stopMsgsToParent();
+ removeMsgListener();
+ if (true === autoResize) stopEventListeners();
+ }
+
+ function injectClearFixIntoBodyElement(){
+ var clearFix = document.createElement('div');
+ clearFix.style.clear = 'both';
+ clearFix.style.display = 'block'; //Guard against this having been globally redefined in CSS.
+ document.body.appendChild(clearFix);
+ }
+
+ function setupInPageLinks(){
+
+ function getPagePosition (){
+ return {
+ x: (window.pageXOffset !== undefined) ? window.pageXOffset : document.documentElement.scrollLeft,
+ y: (window.pageYOffset !== undefined) ? window.pageYOffset : document.documentElement.scrollTop
+ };
+ }
+
+ function getElementPosition(el){
+ var
+ elPosition = el.getBoundingClientRect(),
+ pagePosition = getPagePosition();
+
+ return {
+ x: parseInt(elPosition.left,10) + parseInt(pagePosition.x,10),
+ y: parseInt(elPosition.top,10) + parseInt(pagePosition.y,10)
+ };
+ }
+
+ function findTarget(location){
+ function jumpToTarget(target){
+ var jumpPosition = getElementPosition(target);
+
+ log('Moving to in page link (#'+hash+') at x: '+jumpPosition.x+' y: '+jumpPosition.y);
+ sendMsg(jumpPosition.y, jumpPosition.x, 'scrollToOffset'); // X&Y reversed at sendMsg uses height/width
+ }
+
+ var
+ hash = location.split('#')[1] || location, //Remove # if present
+ hashData = decodeURIComponent(hash),
+ target = document.getElementById(hashData) || document.getElementsByName(hashData)[0];
+
+ if (undefined !== target){
+ jumpToTarget(target);
+ } else {
+ log('In page link (#' + hash + ') not found in iFrame, so sending to parent');
+ sendMsg(0,0,'inPageLink','#'+hash);
+ }
+ }
+
+ function checkLocationHash(){
+ if ('' !== location.hash && '#' !== location.hash){
+ findTarget(location.href);
+ }
+ }
+
+ function bindAnchors(){
+ function setupLink(el){
+ function linkClicked(e){
+ e.preventDefault();
+
+ /*jshint validthis:true */
+ findTarget(this.getAttribute('href'));
+ }
+
+ if ('#' !== el.getAttribute('href')){
+ addEventListener(el,'click',linkClicked);
+ }
+ }
+
+ Array.prototype.forEach.call( document.querySelectorAll( 'a[href^="#"]' ), setupLink );
+ }
+
+ function bindLocationHash(){
+ addEventListener(window,'hashchange',checkLocationHash);
+ }
+
+ function initCheck(){ //check if page loaded with location hash after init resize
+ setTimeout(checkLocationHash,eventCancelTimer);
+ }
+
+ function enableInPageLinks(){
+ /* istanbul ignore else */ // Not testable in phantonJS
+ if(Array.prototype.forEach && document.querySelectorAll){
+ log('Setting up location.hash handlers');
+ bindAnchors();
+ bindLocationHash();
+ initCheck();
+ } else {
+ warn('In page linking not fully supported in this browser! (See README.md for IE8 workaround)');
+ }
+ }
+
+ if(inPageLinks.enable){
+ enableInPageLinks();
+ } else {
+ log('In page linking not enabled');
+ }
+
+ return {
+ findTarget:findTarget
+ };
+ }
+
+ function setupPublicMethods(){
+ log('Enable public methods');
+
+ win.parentIFrame = {
+
+ autoResize: function autoResizeF(resize){
+ if (true === resize && false === autoResize) {
+ autoResize=true;
+ startEventListeners();
+ //sendSize('autoResize','Auto Resize enabled');
+ } else if (false === resize && true === autoResize) {
+ autoResize=false;
+ stopEventListeners();
+ }
+
+ return autoResize;
+ },
+
+ close: function closeF(){
+ sendMsg(0,0,'close');
+ teardown();
+ },
+
+ getId: function getIdF(){
+ return myID;
+ },
+
+ getPageInfo: function getPageInfoF(callback){
+ if ('function' === typeof callback){
+ pageInfoCallback = callback;
+ sendMsg(0,0,'pageInfo');
+ } else {
+ pageInfoCallback = function(){};
+ sendMsg(0,0,'pageInfoStop');
+ }
+ },
+
+ moveToAnchor: function moveToAnchorF(hash){
+ inPageLinks.findTarget(hash);
+ },
+
+ reset: function resetF(){
+ resetIFrame('parentIFrame.reset');
+ },
+
+ scrollTo: function scrollToF(x,y){
+ sendMsg(y,x,'scrollTo'); // X&Y reversed at sendMsg uses height/width
+ },
+
+ scrollToOffset: function scrollToF(x,y){
+ sendMsg(y,x,'scrollToOffset'); // X&Y reversed at sendMsg uses height/width
+ },
+
+ sendMessage: function sendMessageF(msg,targetOrigin){
+ sendMsg(0,0,'message',JSON.stringify(msg),targetOrigin);
+ },
+
+ setHeightCalculationMethod: function setHeightCalculationMethodF(heightCalculationMethod){
+ heightCalcMode = heightCalculationMethod;
+ checkHeightMode();
+ },
+
+ setWidthCalculationMethod: function setWidthCalculationMethodF(widthCalculationMethod){
+ widthCalcMode = widthCalculationMethod;
+ checkWidthMode();
+ },
+
+ setTargetOrigin: function setTargetOriginF(targetOrigin){
+ log('Set targetOrigin: '+targetOrigin);
+ targetOriginDefault = targetOrigin;
+ },
+
+ size: function sizeF(customHeight, customWidth){
+ var valString = ''+(customHeight?customHeight:'')+(customWidth?','+customWidth:'');
+ //lockTrigger();
+ sendSize('size','parentIFrame.size('+valString+')', customHeight, customWidth);
+ }
+ };
+ }
+
+ function initInterval(){
+ if ( 0 !== interval ){
+ log('setInterval: '+interval+'ms');
+ intervalTimer = setInterval(function(){
+ sendSize('interval','setInterval: '+interval);
+ },Math.abs(interval));
+ }
+ }
+
+ /* istanbul ignore next */ //Not testable in PhantomJS
+ function setupBodyMutationObserver(){
+ function addImageLoadListners(mutation) {
+ function addImageLoadListener(element){
+ if (false === element.complete) {
+ log('Attach listeners to ' + element.src);
+ element.addEventListener('load', imageLoaded, false);
+ element.addEventListener('error', imageError, false);
+ elements.push(element);
+ }
+ }
+
+ if (mutation.type === 'attributes' && mutation.attributeName === 'src'){
+ addImageLoadListener(mutation.target);
+ } else if (mutation.type === 'childList'){
+ Array.prototype.forEach.call(
+ mutation.target.querySelectorAll('img'),
+ addImageLoadListener
+ );
+ }
+ }
+
+ function removeFromArray(element){
+ elements.splice(elements.indexOf(element),1);
+ }
+
+ function removeImageLoadListener(element){
+ log('Remove listeners from ' + element.src);
+ element.removeEventListener('load', imageLoaded, false);
+ element.removeEventListener('error', imageError, false);
+ removeFromArray(element);
+ }
+
+ function imageEventTriggered(event,type,typeDesc){
+ removeImageLoadListener(event.target);
+ sendSize(type, typeDesc + ': ' + event.target.src, undefined, undefined);
+ }
+
+ function imageLoaded(event) {
+ imageEventTriggered(event,'imageLoad','Image loaded');
+ }
+
+ function imageError(event) {
+ imageEventTriggered(event,'imageLoadFailed','Image load failed');
+ }
+
+ function mutationObserved(mutations) {
+ sendSize('mutationObserver','mutationObserver: ' + mutations[0].target + ' ' + mutations[0].type);
+
+ //Deal with WebKit asyncing image loading when tags are injected into the page
+ mutations.forEach(addImageLoadListners);
+ }
+
+ function createMutationObserver(){
+ var
+ target = document.querySelector('body'),
+
+ config = {
+ attributes : true,
+ attributeOldValue : false,
+ characterData : true,
+ characterDataOldValue : false,
+ childList : true,
+ subtree : true
+ };
+
+ observer = new MutationObserver(mutationObserved);
+
+ log('Create body MutationObserver');
+ observer.observe(target, config);
+
+ return observer;
+ }
+
+ var
+ elements = [],
+ MutationObserver = window.MutationObserver || window.WebKitMutationObserver,
+ observer = createMutationObserver();
+
+ return {
+ disconnect: function (){
+ if ('disconnect' in observer){
+ log('Disconnect body MutationObserver');
+ observer.disconnect();
+ elements.forEach(removeImageLoadListener);
+ }
+ }
+ };
+ }
+
+ function setupMutationObserver(){
+ var forceIntervalTimer = 0 > interval;
+
+ /* istanbul ignore if */ // Not testable in PhantomJS
+ if (window.MutationObserver || window.WebKitMutationObserver){
+ if (forceIntervalTimer) {
+ initInterval();
+ } else {
+ bodyObserver = setupBodyMutationObserver();
+ }
+ } else {
+ log('MutationObserver not supported in this browser!');
+ initInterval();
+ }
+ }
+
+
+ // document.documentElement.offsetHeight is not reliable, so
+ // we have to jump through hoops to get a better value.
+ function getComputedStyle(prop,el) {
+ /* istanbul ignore next */ //Not testable in PhantomJS
+ function convertUnitsToPxForIE8(value) {
+ var PIXEL = /^\d+(px)?$/i;
+
+ if (PIXEL.test(value)) {
+ return parseInt(value,base);
+ }
+
+ var
+ style = el.style.left,
+ runtimeStyle = el.runtimeStyle.left;
+
+ el.runtimeStyle.left = el.currentStyle.left;
+ el.style.left = value || 0;
+ value = el.style.pixelLeft;
+ el.style.left = style;
+ el.runtimeStyle.left = runtimeStyle;
+
+ return value;
+ }
+
+ var retVal = 0;
+ el = el || document.body;
+
+ /* istanbul ignore else */ // Not testable in phantonJS
+ if (('defaultView' in document) && ('getComputedStyle' in document.defaultView)) {
+ retVal = document.defaultView.getComputedStyle(el, null);
+ retVal = (null !== retVal) ? retVal[prop] : 0;
+ } else {//IE8
+ retVal = convertUnitsToPxForIE8(el.currentStyle[prop]);
+ }
+
+ return parseInt(retVal,base);
+ }
+
+ function chkEventThottle(timer){
+ if(timer > throttledTimer/2){
+ throttledTimer = 2*timer;
+ log('Event throttle increased to ' + throttledTimer + 'ms');
+ }
+ }
+
+ //Idea from https://github.com/guardian/iframe-messenger
+ function getMaxElement(side,elements) {
+ var
+ elementsLength = elements.length,
+ elVal = 0,
+ maxVal = 0,
+ Side = capitalizeFirstLetter(side),
+ timer = getNow();
+
+ for (var i = 0; i < elementsLength; i++) {
+ elVal = elements[i].getBoundingClientRect()[side] + getComputedStyle('margin'+Side,elements[i]);
+ if (elVal > maxVal) {
+ maxVal = elVal;
+ }
+ }
+
+ timer = getNow() - timer;
+
+ log('Parsed '+elementsLength+' HTML elements');
+ log('Element position calculated in ' + timer + 'ms');
+
+ chkEventThottle(timer);
+
+ return maxVal;
+ }
+
+ function getAllMeasurements(dimention){
+ return [
+ dimention.bodyOffset(),
+ dimention.bodyScroll(),
+ dimention.documentElementOffset(),
+ dimention.documentElementScroll()
+ ];
+ }
+
+ function getTaggedElements(side,tag){
+ function noTaggedElementsFound(){
+ warn('No tagged elements ('+tag+') found on page');
+ return height; //current height
+ }
+
+ var elements = document.querySelectorAll('['+tag+']');
+
+ return 0 === elements.length ? noTaggedElementsFound() : getMaxElement(side,elements);
+ }
+
+ function getAllElements(){
+ return document.querySelectorAll('body *');
+ }
+
+ var
+ getHeight = {
+ bodyOffset: function getBodyOffsetHeight(){
+ return document.body.offsetHeight + getComputedStyle('marginTop') + getComputedStyle('marginBottom');
+ },
+
+ offset: function(){
+ return getHeight.bodyOffset(); //Backwards compatability
+ },
+
+ bodyScroll: function getBodyScrollHeight(){
+ return document.body.scrollHeight;
+ },
+
+ custom: function getCustomWidth(){
+ return customCalcMethods.height();
+ },
+
+ documentElementOffset: function getDEOffsetHeight(){
+ return document.documentElement.offsetHeight;
+ },
+
+ documentElementScroll: function getDEScrollHeight(){
+ return document.documentElement.scrollHeight;
+ },
+
+ max: function getMaxHeight(){
+ return Math.max.apply(null,getAllMeasurements(getHeight));
+ },
+
+ min: function getMinHeight(){
+ return Math.min.apply(null,getAllMeasurements(getHeight));
+ },
+
+ grow: function growHeight(){
+ return getHeight.max(); //Run max without the forced downsizing
+ },
+
+ lowestElement: function getBestHeight(){
+ return Math.max(getHeight.bodyOffset(), getMaxElement('bottom',getAllElements()));
+ },
+
+ taggedElement: function getTaggedElementsHeight(){
+ return getTaggedElements('bottom','data-iframe-height');
+ }
+ },
+
+ getWidth = {
+ bodyScroll: function getBodyScrollWidth(){
+ return document.body.scrollWidth;
+ },
+
+ bodyOffset: function getBodyOffsetWidth(){
+ return document.body.offsetWidth;
+ },
+
+ custom: function getCustomWidth(){
+ return customCalcMethods.width();
+ },
+
+ documentElementScroll: function getDEScrollWidth(){
+ return document.documentElement.scrollWidth;
+ },
+
+ documentElementOffset: function getDEOffsetWidth(){
+ return document.documentElement.offsetWidth;
+ },
+
+ scroll: function getMaxWidth(){
+ return Math.max(getWidth.bodyScroll(), getWidth.documentElementScroll());
+ },
+
+ max: function getMaxWidth(){
+ return Math.max.apply(null,getAllMeasurements(getWidth));
+ },
+
+ min: function getMinWidth(){
+ return Math.min.apply(null,getAllMeasurements(getWidth));
+ },
+
+ rightMostElement: function rightMostElement(){
+ return getMaxElement('right', getAllElements());
+ },
+
+ taggedElement: function getTaggedElementsWidth(){
+ return getTaggedElements('right', 'data-iframe-width');
+ }
+ };
+
+
+ function sizeIFrame(triggerEvent, triggerEventDesc, customHeight, customWidth){
+
+ function resizeIFrame(){
+ height = currentHeight;
+ width = currentWidth;
+
+ sendMsg(height,width,triggerEvent);
+ }
+
+ function isSizeChangeDetected(){
+ function checkTolarance(a,b){
+ var retVal = Math.abs(a-b) <= tolerance;
+ return !retVal;
+ }
+
+ currentHeight = (undefined !== customHeight) ? customHeight : getHeight[heightCalcMode]();
+ currentWidth = (undefined !== customWidth ) ? customWidth : getWidth[widthCalcMode]();
+
+ return checkTolarance(height,currentHeight) || (calculateWidth && checkTolarance(width,currentWidth));
+ }
+
+ function isForceResizableEvent(){
+ return !(triggerEvent in {'init':1,'interval':1,'size':1});
+ }
+
+ function isForceResizableCalcMode(){
+ return (heightCalcMode in resetRequiredMethods) || (calculateWidth && widthCalcMode in resetRequiredMethods);
+ }
+
+ function logIgnored(){
+ log('No change in size detected');
+ }
+
+ function checkDownSizing(){
+ if (isForceResizableEvent() && isForceResizableCalcMode()){
+ resetIFrame(triggerEventDesc);
+ } else if (!(triggerEvent in {'interval':1})){
+ logIgnored();
+ }
+ }
+
+ var currentHeight,currentWidth;
+
+ if (isSizeChangeDetected() || 'init' === triggerEvent){
+ lockTrigger();
+ resizeIFrame();
+ } else {
+ checkDownSizing();
+ }
+ }
+
+ var sizeIFrameThrottled = throttle(sizeIFrame);
+
+ function sendSize(triggerEvent, triggerEventDesc, customHeight, customWidth){
+ function recordTrigger(){
+ if (!(triggerEvent in {'reset':1,'resetPage':1,'init':1})){
+ log( 'Trigger event: ' + triggerEventDesc );
+ }
+ }
+
+ function isDoubleFiredEvent(){
+ return triggerLocked && (triggerEvent in doubleEventList);
+ }
+
+ if (!isDoubleFiredEvent()){
+ recordTrigger();
+ sizeIFrameThrottled(triggerEvent, triggerEventDesc, customHeight, customWidth);
+ } else {
+ log('Trigger event cancelled: '+triggerEvent);
+ }
+ }
+
+ function lockTrigger(){
+ if (!triggerLocked){
+ triggerLocked = true;
+ log('Trigger event lock on');
+ }
+ clearTimeout(triggerLockedTimer);
+ triggerLockedTimer = setTimeout(function(){
+ triggerLocked = false;
+ log('Trigger event lock off');
+ log('--');
+ },eventCancelTimer);
+ }
+
+ function triggerReset(triggerEvent){
+ height = getHeight[heightCalcMode]();
+ width = getWidth[widthCalcMode]();
+
+ sendMsg(height,width,triggerEvent);
+ }
+
+ function resetIFrame(triggerEventDesc){
+ var hcm = heightCalcMode;
+ heightCalcMode = heightCalcModeDefault;
+
+ log('Reset trigger event: ' + triggerEventDesc);
+ lockTrigger();
+ triggerReset('reset');
+
+ heightCalcMode = hcm;
+ }
+
+ function sendMsg(height,width,triggerEvent,msg,targetOrigin){
+ function setTargetOrigin(){
+ if (undefined === targetOrigin){
+ targetOrigin = targetOriginDefault;
+ } else {
+ log('Message targetOrigin: '+targetOrigin);
+ }
+ }
+
+ function sendToParent(){
+ var
+ size = height + ':' + width,
+ message = myID + ':' + size + ':' + triggerEvent + (undefined !== msg ? ':' + msg : '');
+
+ log('Sending message to host page (' + message + ')');
+ target.postMessage( msgID + message, targetOrigin);
+ }
+
+ if(true === sendPermit){
+ setTargetOrigin();
+ sendToParent();
+ }
+ }
+
+ function receiver(event) {
+ function isMessageForUs(){
+ return msgID === (''+event.data).substr(0,msgIdLen); //''+ Protects against non-string messages
+ }
+
+ function initFromParent(){
+ function fireInit(){
+ initMsg = event.data;
+ target = event.source;
+
+ init();
+ firstRun = false;
+ setTimeout(function(){ initLock = false;},eventCancelTimer);
+ }
+
+ if (document.body){
+ fireInit();
+ } else {
+ log('Waiting for page ready');
+ addEventListener(window,'readystatechange',initFromParent);
+ }
+ }
+
+ function resetFromParent(){
+ if (!initLock){
+ log('Page size reset by host page');
+ triggerReset('resetPage');
+ } else {
+ log('Page reset ignored by init');
+ }
+ }
+
+ function resizeFromParent(){
+ sendSize('resizeParent','Parent window requested size check');
+ }
+
+ function moveToAnchor(){
+ var anchor = getData();
+ inPageLinks.findTarget(anchor);
+ }
+
+ function getMessageType(){
+ return event.data.split(']')[1].split(':')[0];
+ }
+
+ function getData(){
+ return event.data.substr(event.data.indexOf(':')+1);
+ }
+
+ function isMiddleTier(){
+ return ('iFrameResize' in window);
+ }
+
+ function messageFromParent(){
+ var msgBody = getData();
+
+ log('MessageCallback called from parent: ' + msgBody );
+ messageCallback(JSON.parse(msgBody));
+ log(' --');
+ }
+
+ function pageInfoFromParent(){
+ var msgBody = getData();
+ log('PageInfoFromParent called from parent: ' + msgBody );
+ pageInfoCallback(JSON.parse(msgBody));
+ log(' --');
+ }
+
+ function isInitMsg(){
+ //Test if this message is from a child below us. This is an ugly test, however, updating
+ //the message format would break backwards compatibity.
+ return event.data.split(':')[2] in {'true':1,'false':1};
+ }
+
+ function callFromParent(){
+ switch (getMessageType()){
+ case 'reset':
+ resetFromParent();
+ break;
+ case 'resize':
+ resizeFromParent();
+ break;
+ case 'inPageLink':
+ case 'moveToAnchor':
+ moveToAnchor();
+ break;
+ case 'message':
+ messageFromParent();
+ break;
+ case 'pageInfo':
+ pageInfoFromParent();
+ break;
+ default:
+ if (!isMiddleTier() && !isInitMsg()){
+ warn('Unexpected message ('+event.data+')');
+ }
+ }
+ }
+
+ function processMessage(){
+ if (false === firstRun) {
+ callFromParent();
+ } else if (isInitMsg()) {
+ initFromParent();
+ } else {
+ log('Ignored message of type "' + getMessageType() + '". Received before initialization.');
+ }
+ }
+
+ if (isMessageForUs()){
+ processMessage();
+ }
+ }
+
+ //Normally the parent kicks things off when it detects the iFrame has loaded.
+ //If this script is async-loaded, then tell parent page to retry init.
+ function chkLateLoaded(){
+ if('loading' !== document.readyState){
+ window.parent.postMessage('[iFrameResizerChild]Ready','*');
+ }
+ }
+
+ addEventListener(window, 'message', receiver);
+ chkLateLoaded();
+
+
+
+})(window || {});
diff --git a/libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.map b/libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.map
new file mode 100644
index 0000000000..ab9fe2d7eb
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.map
@@ -0,0 +1 @@
+{"version":3,"file":"iframeResizer.contentWindow.min.js","sources":["iframeResizer.contentWindow.js"],"names":["window","undefined","addEventListener","el","evt","func","attachEvent","removeEventListener","detachEvent","capitalizeFirstLetter","string","charAt","toUpperCase","slice","throttle","context","args","result","timeout","previous","later","getNow","apply","now","remaining","throttledTimer","this","arguments","clearTimeout","setTimeout","formatLogMsg","msg","msgID","myID","log","logging","console","warn","init","readDataFromParent","location","href","readDataFromPage","setMargin","setBodyStyle","bodyBackground","bodyPadding","injectClearFixIntoBodyElement","checkHeightMode","checkWidthMode","stopInfiniteResizingOfIFrame","setupPublicMethods","startEventListeners","inPageLinks","setupInPageLinks","sendSize","readyCallback","strBool","str","data","initMsg","substr","msgIdLen","split","bodyMargin","Number","calculateWidth","interval","autoResize","bodyMarginStr","heightCalcMode","tolerance","enable","resizeFrom","widthCalcMode","readData","iFrameResizer","JSON","stringify","messageCallback","targetOriginDefault","targetOrigin","heightCalculationMethod","widthCalculationMethod","setupCustomCalcMethods","calcMode","calcFunc","customCalcMethods","Object","constructor","chkCSS","attr","value","indexOf","document","body","style","documentElement","height","manageTriggerEvent","options","handleEvent","eventName","eventType","listener","add","remove","eventNames","Array","prototype","map","method","manageEventListeners","checkCalcMode","calcModeDefault","modes","type","heightCalcModeDefault","getHeight","widthCalcModeDefault","getWidth","setupMutationObserver","stopMsgsToParent","sendPermit","removeMsgListener","receiver","disconnectMutationObserver","bodyObserver","disconnect","stopEventListeners","clearInterval","intervalTimer","teardown","clearFix","createElement","clear","display","appendChild","getPagePosition","x","pageXOffset","scrollLeft","y","pageYOffset","scrollTop","getElementPosition","elPosition","getBoundingClientRect","pagePosition","parseInt","left","top","findTarget","jumpToTarget","target","jumpPosition","hash","sendMsg","hashData","decodeURIComponent","getElementById","getElementsByName","checkLocationHash","bindAnchors","setupLink","linkClicked","e","preventDefault","getAttribute","forEach","call","querySelectorAll","bindLocationHash","initCheck","eventCancelTimer","enableInPageLinks","win","parentIFrame","resize","close","getId","getPageInfo","callback","pageInfoCallback","moveToAnchor","reset","resetIFrame","scrollTo","scrollToOffset","sendMessage","setHeightCalculationMethod","setWidthCalculationMethod","setTargetOrigin","size","customHeight","customWidth","valString","initInterval","setInterval","Math","abs","setupBodyMutationObserver","addImageLoadListners","mutation","addImageLoadListener","element","complete","src","imageLoaded","imageError","elements","push","attributeName","removeFromArray","splice","removeImageLoadListener","imageEventTriggered","event","typeDesc","mutationObserved","mutations","createMutationObserver","querySelector","config","attributes","attributeOldValue","characterData","characterDataOldValue","childList","subtree","observer","MutationObserver","observe","WebKitMutationObserver","forceIntervalTimer","getComputedStyle","prop","convertUnitsToPxForIE8","PIXEL","test","base","runtimeStyle","currentStyle","pixelLeft","retVal","defaultView","chkEventThottle","timer","getMaxElement","side","elementsLength","length","elVal","maxVal","Side","i","getAllMeasurements","dimention","bodyOffset","bodyScroll","documentElementOffset","documentElementScroll","getTaggedElements","tag","noTaggedElementsFound","getAllElements","sizeIFrame","triggerEvent","triggerEventDesc","resizeIFrame","currentHeight","width","currentWidth","isSizeChangeDetected","checkTolarance","a","b","isForceResizableEvent","isForceResizableCalcMode","resetRequiredMethods","logIgnored","checkDownSizing","lockTrigger","recordTrigger","resetPage","isDoubleFiredEvent","triggerLocked","doubleEventList","sizeIFrameThrottled","triggerLockedTimer","triggerReset","hcm","sendToParent","message","postMessage","isMessageForUs","initFromParent","fireInit","source","firstRun","initLock","resetFromParent","resizeFromParent","anchor","getData","getMessageType","isMiddleTier","messageFromParent","msgBody","parse","pageInfoFromParent","isInitMsg","true","false","callFromParent","processMessage","chkLateLoaded","readyState","parent","click","max","min","offsetHeight","scrollWidth","Date","getTime","offset","scrollHeight","custom","grow","lowestElement","taggedElement","offsetWidth","scroll","rightMostElement"],"mappings":";;;;;;;;CAYC,SAAUA,EAAQC,GAClB,YAuDA,SAASC,GAAiBC,EAAGC,EAAIC,GAE5B,oBAAsBL,GACzBG,EAAGD,iBAAiBE,EAAIC,GAAM,GACpB,eAAiBL,IAC3BG,EAAGG,YAAY,KAAKF,EAAIC,GAI1B,QAASE,GAAoBJ,EAAGC,EAAIC,GAE/B,uBAAyBL,GAC5BG,EAAGI,oBAAoBH,EAAIC,GAAM,GACvB,eAAiBL,IAC3BG,EAAGK,YAAY,KAAKJ,EAAIC,GAI1B,QAASI,GAAsBC,GAC9B,MAAOA,GAAOC,OAAO,GAAGC,cAAgBF,EAAOG,MAAM,GAItD,QAASC,GAAST,GACjB,GACCU,GAASC,EAAMC,EACfC,EAAU,KACVC,EAAW,EACXC,EAAQ,WACPD,EAAWE,KACXH,EAAU,KACVD,EAASZ,EAAKiB,MAAMP,EAASC,GACxBE,IACJH,EAAUC,EAAO,MAIpB,OAAO,YACN,GAAIO,GAAMF,IAELF,KACJA,EAAWI,EAGZ,IAAIC,GAAYC,IAAkBF,EAAMJ,EAsBxC,OApBAJ,GAAUW,KACVV,EAAOW,UAEU,GAAbH,GAAkBA,EAAYC,IAC7BP,IACHU,aAAaV,GACbA,EAAU,MAGXC,EAAWI,EACXN,EAASZ,EAAKiB,MAAMP,EAASC,GAExBE,IACJH,EAAUC,EAAO,OAGPE,IACXA,EAAUW,WAAWT,EAAOI,IAGtBP,GAST,QAASa,GAAaC,GACrB,MAAOC,IAAQ,IAAMC,GAAO,KAAYF,EAGzC,QAASG,GAAIH,GACRI,IAAY,gBAAoBnC,GAAOoC,SAC1CA,QAAQF,IAAIJ,EAAaC,IAI3B,QAASM,GAAKN,GACT,gBAAoB/B,GAAOoC,SAC9BA,QAAQC,KAAKP,EAAaC,IAK5B,QAASO,KACRC,IACAL,EAAI,wBAAwBM,SAASC,KAAK,KAC1CC,IACAC,IACAC,EAAa,aAAaC,GAC1BD,EAAa,UAAUE,GACvBC,IACAC,IACAC,IACAC,IACAC,IACAC,IACAC,GAAcC,IACdC,EAAS,OAAO,+BAChBC,KAGD,QAASjB,KAER,QAASkB,GAAQC,GAChB,MAAO,SAAWA,GAAM,GAAO,EAGhC,GAAIC,GAAOC,GAAQC,OAAOC,IAAUC,MAAM,IAE1C9B,IAAqB0B,EAAK,GAC1BK,EAAsB/D,IAAc0D,EAAK,GAAMM,OAAON,EAAK,IAAQK,EACnEE,GAAsBjE,IAAc0D,EAAK,GAAMF,EAAQE,EAAK,IAAOO,GACnE/B,GAAsBlC,IAAc0D,EAAK,GAAMF,EAAQE,EAAK,IAAOxB,GACnEgC,GAAsBlE,IAAc0D,EAAK,GAAMM,OAAON,EAAK,IAAQQ,GACnEC,EAAsBnE,IAAc0D,EAAK,GAAMF,EAAQE,EAAK,IAAOS,EACnEC,EAAqBV,EAAK,GAC1BW,GAAsBrE,IAAc0D,EAAK,GAAMA,EAAK,GAAeW,GACnEzB,EAAqBc,EAAK,GAC1Bb,EAAqBa,EAAK,IAC1BY,GAAsBtE,IAAc0D,EAAK,IAAOM,OAAON,EAAK,KAAOY,GACnElB,GAAYmB,OAAUvE,IAAc0D,EAAK,IAAOF,EAAQE,EAAK,MAAM,EACnEc,GAAsBxE,IAAc0D,EAAK,IAAOA,EAAK,IAAcc,GACnEC,GAAsBzE,IAAc0D,EAAK,IAAOA,EAAK,IAAce,GAGpE,QAAShC,KACR,QAASiC,KACR,GAAIhB,GAAO3D,EAAO4E,aAElB1C,GAAI,2BAA6B2C,KAAKC,UAAUnB,IAEhDoB,GAAuB,mBAA6BpB,GAAQA,EAAKoB,gBAA0BA,GAC3FvB,GAAuB,iBAA6BG,GAAQA,EAAKH,cAA0BA,GAC3FwB,GAAuB,gBAA6BrB,GAAQA,EAAKsB,aAA0BD,GAC3FV,GAAuB,2BAA6BX,GAAQA,EAAKuB,wBAA0BZ,GAC3FI,GAAuB,0BAA6Bf,GAAQA,EAAKwB,uBAA0BT,GAG5F,QAASU,GAAuBC,EAAUC,GAOzC,MANI,kBAAsBD,KACzBnD,EAAI,gBAAkBoD,EAAW,cACjCC,GAAkBD,GAAYD,EAC9BA,EAAW,UAGLA,EAGJ,iBAAmBrF,IAAYwF,SAAWxF,EAAO4E,cAAca,cAClEd,IACAL,GAAiBc,EAAuBd,GAAgB,UACxDI,GAAiBU,EAAuBV,GAAgB,UAGzDxC,EAAI,mCAAqC8C,IAI1C,QAASU,GAAOC,EAAKC,GAKpB,MAJI,KAAOA,EAAMC,QAAQ,OACxBxD,EAAK,kCAAkCsD,GACvCC,EAAM,IAEAA,EAGR,QAAShD,GAAa+C,EAAKC,GACrB3F,IAAc2F,GAAW,KAAOA,GAAW,SAAWA,IAC1DE,SAASC,KAAKC,MAAML,GAAQC,EAC5B1D,EAAI,QAAQyD,EAAK,YAAYC,EAAM,MAIrC,QAASjD,KAEJ1C,IAAcoE,IACjBA,EAAgBL,EAAW,MAG5BpB,EAAa,SAAS8C,EAAO,SAASrB,IAGvC,QAASnB,KACR4C,SAASG,gBAAgBD,MAAME,OAAS,GACxCJ,SAASC,KAAKC,MAAME,OAAS,GAC7BhE,EAAI,oCAIL,QAASiE,GAAmBC,GAC3B,QAASC,KACR9C,EAAS6C,EAAQE,UAAUF,EAAQG,WAGpC,GAAIC,IACHC,IAAQ,SAASH,GAChBpG,EAAiBF,EAAOsG,EAAUD,IAEnCK,OAAQ,SAASJ,GAChB/F,EAAoBP,EAAOsG,EAAUD,IAIpCD,GAAQO,YAAcC,MAAMC,UAAUC,KACxCV,EAAQE,UAAYF,EAAQO,WAAW,GACvCP,EAAQO,WAAWG,IAAIN,EAASJ,EAAQW,UAExCP,EAASJ,EAAQW,QAAQX,EAAQE,WAGlCpE,EAAIzB,EAAsB2F,EAAQW,QAAU,oBAAsBX,EAAQG,WAG3E,QAASS,GAAqBD,GAC7BZ,GAAoBY,OAAOA,EAAQR,UAAW,kBAA6BI,YAAa,iBAAiB,0BACzGR,GAAoBY,OAAOA,EAAQR,UAAW,sBAA6BI,YAAa,qBAAqB,8BAC7GR,GAAoBY,OAAOA,EAAQR,UAAW,gBAA6BI,YAAa,eAAe,wBACvGR,GAAoBY,OAAOA,EAAQR,UAAW,QAA6BD,UAAY,UACvFH,GAAoBY,OAAOA,EAAQR,UAAW,WAA6BD,UAAY,YACvFH,GAAoBY,OAAOA,EAAQR,UAAW,aAA6BD,UAAY,cACvFH,GAAoBY,OAAOA,EAAQR,UAAW,qBAA6BD,UAAY,sBACvFH,GAAoBY,OAAOA,EAAQR,UAAW,QAA6BD,WAAa,aAAc,iBACtGH,GAAoBY,OAAOA,EAAQR,UAAW,qBAA6BD,UAAY,qBACvFH,GAAoBY,OAAOA,EAAQR,UAAW,cAA6BD,UAAY,eACvFH,GAAoBY,OAAOA,EAAQR,UAAW,YAA6BD,UAAY,aACvFH,GAAoBY,OAAOA,EAAQR,UAAW,eAA6BD,UAAY,gBACvFH,GAAoBY,OAAOA,EAAQR,UAAW,mBAA6BI,YAAa,kBAAkB,wBAAwB,oBAAoB,mBAAmB,sBACzKR,GAAoBY,OAAOA,EAAQR,UAAW,uBAA6BI,YAAa,sBAAsB,4BAA4B,wBAAwB,uBAAuB,0BACzLR,GAAoBY,OAAOA,EAAQR,UAAW,iBAA6BI,YAAa,gBAAgB,sBAAsB,kBAAkB,iBAAiB,oBAC9J,UAAYlC,IACd0B,GAAoBY,OAAOA,EAAQR,UAAW,iBAAyBD,UAAY,WAIrF,QAASW,GAAc5B,EAAS6B,EAAgBC,EAAMC,GASrD,MARIF,KAAoB7B,IACjBA,IAAY8B,KACjB9E,EAAKgD,EAAW,8BAA8B+B,EAAK,sBACnD/B,EAAS6B,GAEVhF,EAAIkF,EAAK,+BAA+B/B,EAAS,MAG3CA,EAGR,QAASrC,KACRsB,GAAiB2C,EAAc3C,GAAe+C,GAAsBC,GAAU,UAG/E,QAASrE,KACRyB,GAAgBuC,EAAcvC,GAAc6C,GAAqBC,GAAS,SAG3E,QAASpE,MACH,IAASgB,GACb4C,EAAqB,OACrBS,KAGAvF,EAAI,wBAIN,QAASwF,KACRxF,EAAI,6BACJyF,IAAa,EAGd,QAASC,KACR1F,EAAI,kCACJ3B,EAAoBP,EAAQ,UAAW6H,GAGxC,QAASC,KACJ,OAASC,GAEZA,EAAaC,aAIf,QAASC,KACRjB,EAAqB,UACrBc,IACAI,cAAcC,IAGf,QAASC,KACRV,IACAE,KACI,IAASxD,GAAY6D,IAG1B,QAASlF,KACR,GAAIsF,GAAWvC,SAASwC,cAAc,MACtCD,GAASrC,MAAMuC,MAAU,OACzBF,EAASrC,MAAMwC,QAAU,QACzB1C,SAASC,KAAK0C,YAAYJ,GAG3B,QAAS/E,KAER,QAASoF,KACR,OACCC,EAAI3I,EAAO4I,cAAgB3I,EAAaD,EAAO4I,YAAc9C,SAASG,gBAAgB4C,WACtFC,EAAI9I,EAAO+I,cAAgB9I,EAAaD,EAAO+I,YAAcjD,SAASG,gBAAgB+C,WAIxF,QAASC,GAAmB9I,GAC3B,GACC+I,GAAe/I,EAAGgJ,wBAClBC,EAAeV,GAEhB,QACCC,EAAGU,SAASH,EAAWI,KAAK,IAAMD,SAASD,EAAaT,EAAE,IAC1DG,EAAGO,SAASH,EAAWK,IAAI,IAAOF,SAASD,EAAaN,EAAE,KAI5D,QAASU,GAAWhH,GACnB,QAASiH,GAAaC,GACrB,GAAIC,GAAeV,EAAmBS,EAEtCxH,GAAI,4BAA4B0H,EAAK,WAAWD,EAAahB,EAAE,OAAOgB,EAAab,GACnFe,EAAQF,EAAab,EAAGa,EAAahB,EAAG,kBAGzC,GACCiB,GAAWpH,EAASuB,MAAM,KAAK,IAAMvB,EACrCsH,EAAWC,mBAAmBH,GAC9BF,EAAW5D,SAASkE,eAAeF,IAAahE,SAASmE,kBAAkBH,GAAU,EAElF7J,KAAcyJ,EACjBD,EAAaC,IAEbxH,EAAI,kBAAoB0H,EAAO,+CAC/BC,EAAQ,EAAE,EAAE,aAAa,IAAID,IAI/B,QAASM,KACJ,KAAO1H,SAASoH,MAAQ,MAAQpH,SAASoH,MAC5CJ,EAAWhH,SAASC,MAItB,QAAS0H,KACR,QAASC,GAAUjK,GAClB,QAASkK,GAAYC,GACpBA,EAAEC,iBAGFf,EAAW9H,KAAK8I,aAAa,SAG1B,MAAQrK,EAAGqK,aAAa,SAC3BtK,EAAiBC,EAAG,QAAQkK,GAI9BzD,MAAMC,UAAU4D,QAAQC,KAAM5E,SAAS6E,iBAAkB,gBAAkBP,GAG5E,QAASQ,KACR1K,EAAiBF,EAAO,aAAakK,GAGtC,QAASW,KACRhJ,WAAWqI,EAAkBY,IAG9B,QAASC,KAELnE,MAAMC,UAAU4D,SAAW3E,SAAS6E,kBACtCzI,EAAI,qCACJiI,IACAS,IACAC,KAEAxI,EAAK,2FAUP,MANGgB,IAAYmB,OACduG,IAEA7I,EAAI,gCAIJsH,WAAWA,GAIb,QAASrG,KACRjB,EAAI,yBAEJ8I,GAAIC,cAEH7G,WAAY,SAAqB8G,GAUhC,OATI,IAASA,IAAU,IAAU9G,GAChCA,GAAW,EACXhB,MAEU,IAAU8H,IAAU,IAAS9G,IACvCA,GAAW,EACX6D,KAGM7D,GAGR+G,MAAO,WACNtB,EAAQ,EAAE,EAAE,SACZzB,KAGDgD,MAAO,WACN,MAAOnJ,KAGRoJ,YAAa,SAAsBC,GAC9B,kBAAsBA,IACzBC,GAAmBD,EACnBzB,EAAQ,EAAE,EAAE,cAEZ0B,GAAmB,aACnB1B,EAAQ,EAAE,EAAE,kBAId2B,aAAc,SAAuB5B,GACpCvG,GAAYmG,WAAWI,IAGxB6B,MAAO,WACNC,EAAY,uBAGbC,SAAU,SAAmBhD,EAAEG,GAC9Be,EAAQf,EAAEH,EAAE,aAGbiD,eAAgB,SAAmBjD,EAAEG,GACpCe,EAAQf,EAAEH,EAAE,mBAGbkD,YAAa,SAAsB9J,EAAIkD,GACtC4E,EAAQ,EAAE,EAAE,UAAUhF,KAAKC,UAAU/C,GAAKkD,IAG3C6G,2BAA4B,SAAqC5G,GAChEZ,GAAiBY,EACjBlC,KAGD+I,0BAA2B,SAAoC5G,GAC9DT,GAAgBS,EAChBlC,KAGD+I,gBAAiB,SAA0B/G,GAC1C/C,EAAI,qBAAqB+C,GACzBD,GAAsBC,GAGvBgH,KAAM,SAAeC,EAAcC,GAClC,GAAIC,GAAY,IAAIF,EAAaA,EAAa,KAAKC,EAAY,IAAIA,EAAY,GAE/E5I,GAAS,OAAO,qBAAqB6I,EAAU,IAAKF,EAAcC,KAKrE,QAASE,KACH,IAAMlI,KACVjC,EAAI,gBAAgBiC,GAAS,MAC7BgE,GAAgBmE,YAAY,WAC3B/I,EAAS,WAAW,gBAAgBY,KACnCoI,KAAKC,IAAIrI,MAKb,QAASsI,KACR,QAASC,GAAqBC,GAC7B,QAASC,GAAqBC,IACzB,IAAUA,EAAQC,WACrB5K,EAAI,uBAAyB2K,EAAQE,KACrCF,EAAQ3M,iBAAiB,OAAQ8M,GAAa,GAC9CH,EAAQ3M,iBAAiB,QAAS+M,GAAY,GAC9CC,EAASC,KAAKN,IAIM,eAAlBF,EAASvF,MAAoD,QAA3BuF,EAASS,cAC9CR,EAAqBD,EAASjD,QACF,cAAlBiD,EAASvF,MACnBR,MAAMC,UAAU4D,QAAQC,KACvBiC,EAASjD,OAAOiB,iBAAiB,OACjCiC,GAKH,QAASS,GAAgBR,GACxBK,EAASI,OAAOJ,EAASrH,QAAQgH,GAAS,GAG3C,QAASU,GAAwBV,GAChC3K,EAAI,yBAA2B2K,EAAQE,KACvCF,EAAQtM,oBAAoB,OAAQyM,GAAa,GACjDH,EAAQtM,oBAAoB,QAAS0M,GAAY,GACjDI,EAAgBR,GAGjB,QAASW,GAAoBC,EAAMrG,EAAKsG,GACvCH,EAAwBE,EAAM/D,QAC9BnG,EAAS6D,EAAMsG,EAAW,KAAOD,EAAM/D,OAAOqD,IAAK9M,EAAWA,GAG/D,QAAS+M,GAAYS,GACpBD,EAAoBC,EAAM,YAAY,gBAGvC,QAASR,GAAWQ,GACnBD,EAAoBC,EAAM,kBAAkB,qBAG7C,QAASE,GAAiBC,GACzBrK,EAAS,mBAAmB,qBAAuBqK,EAAU,GAAGlE,OAAS,IAAMkE,EAAU,GAAGxG,MAG5FwG,EAAUnD,QAAQiC,GAGnB,QAASmB,KACR,GACCnE,GAAS5D,SAASgI,cAAc,QAEhCC,GACCC,YAAwB,EACxBC,mBAAwB,EACxBC,eAAwB,EACxBC,uBAAwB,EACxBC,WAAwB,EACxBC,SAAwB,EAQ1B,OALAC,GAAW,GAAIC,GAAiBZ,GAEhCzL,EAAI,gCACJoM,EAASE,QAAQ9E,EAAQqE,GAElBO,EAGR,GACCpB,MACAqB,EAAmBvO,EAAOuO,kBAAoBvO,EAAOyO,uBACrDH,EAAmBT,GAEpB,QACC7F,WAAY,WACP,cAAgBsG,KACnBpM,EAAI,oCACJoM,EAAStG,aACTkF,EAASzC,QAAQ8C,MAMrB,QAAS9F,KACR,GAAIiH,GAAqB,EAAIvK,EAGzBnE,GAAOuO,kBAAoBvO,EAAOyO,uBACjCC,EACHrC,IAEAtE,EAAe0E,KAGhBvK,EAAI,mDACJmK,KAOF,QAASsC,GAAiBC,EAAKzO,GAE9B,QAAS0O,GAAuBjJ,GAC/B,GAAIkJ,GAAQ,aAEZ,IAAIA,EAAMC,KAAKnJ,GACd,MAAOyD,UAASzD,EAAMoJ,EAGvB,IACChJ,GAAQ7F,EAAG6F,MAAMsD,KACjB2F,EAAe9O,EAAG8O,aAAa3F,IAQhC,OANAnJ,GAAG8O,aAAa3F,KAAOnJ,EAAG+O,aAAa5F,KACvCnJ,EAAG6F,MAAMsD,KAAO1D,GAAS,EACzBA,EAAQzF,EAAG6F,MAAMmJ,UACjBhP,EAAG6F,MAAMsD,KAAOtD,EAChB7F,EAAG8O,aAAa3F,KAAO2F,EAEhBrJ,EAGR,GAAIwJ,GAAS,CAWb,OAVAjP,GAAMA,GAAM2F,SAASC,KAGhB,eAAiBD,WAAc,oBAAsBA,UAASuJ,aAClED,EAAStJ,SAASuJ,YAAYV,iBAAiBxO,EAAI,MACnDiP,EAAU,OAASA,EAAUA,EAAOR,GAAQ,GAE5CQ,EAAUP,EAAuB1O,EAAG+O,aAAaN,IAG3CvF,SAAS+F,EAAOJ,GAGxB,QAASM,GAAgBC,GACrBA,EAAQ9N,GAAe,IACzBA,GAAiB,EAAE8N,EACnBrN,EAAI,+BAAiCT,GAAiB,OAKxD,QAAS+N,GAAcC,EAAKvC,GAQ3B,IAAK,GANJwC,GAAiBxC,EAASyC,OAC1BC,EAAiB,EACjBC,EAAiB,EACjBC,EAAiBrP,EAAsBgP,GACvCF,EAAiBlO,KAET0O,EAAI,EAAOL,EAAJK,EAAoBA,IACnCH,EAAQ1C,EAAS6C,GAAG5G,wBAAwBsG,GAAQd,EAAiB,SAASmB,EAAK5C,EAAS6C,IACxFH,EAAQC,IACXA,EAASD,EAWX,OAPAL,GAAQlO,KAAWkO,EAEnBrN,EAAI,UAAUwN,EAAe,kBAC7BxN,EAAI,kCAAoCqN,EAAQ,MAEhDD,EAAgBC,GAETM,EAGR,QAASG,GAAmBC,GAC3B,OACCA,EAAUC,aACVD,EAAUE,aACVF,EAAUG,wBACVH,EAAUI,yBAIZ,QAASC,GAAkBb,EAAKc,GAC/B,QAASC,KAER,MADAnO,GAAK,uBAAuBkO,EAAI,mBACzBrK,GAGR,GAAIgH,GAAWpH,SAAS6E,iBAAiB,IAAI4F,EAAI,IAEjD,OAAO,KAAMrD,EAASyC,OAAUa,IAA0BhB,EAAcC,EAAKvC,GAG9E,QAASuD,KACR,MAAO3K,UAAS6E,iBAAiB,UA6FlC,QAAS+F,GAAWC,EAAcC,EAAkB1E,EAAcC,GAEjE,QAAS0E,KACR3K,GAAS4K,EACTC,GAASC,EAETnH,EAAQ3D,GAAO6K,GAAMJ,GAGtB,QAASM,KACR,QAASC,GAAeC,EAAEC,GACzB,GAAIhC,GAAS7C,KAAKC,IAAI2E,EAAEC,IAAM7M,EAC9B,QAAQ6K,EAMT,MAHA0B,GAAiB7Q,IAAciM,EAAiBA,EAAe5E,GAAUhD,MACzE0M,EAAiB/Q,IAAckM,EAAiBA,EAAe3E,GAAS9C,MAEjEwM,EAAehL,GAAO4K,IAAmB5M,IAAkBgN,EAAeH,GAAMC,GAGxF,QAASK,KACR,QAASV,KAAiBrO,KAAO,EAAE6B,SAAW,EAAE8H,KAAO,IAGxD,QAASqF,KACR,MAAQhN,MAAkBiN,KAA0BrN,IAAkBQ,KAAiB6M,IAGxF,QAASC,KACRtP,EAAI,8BAGL,QAASuP,KACJJ,KAA2BC,IAC9B5F,EAAYkF,GACAD,KAAiBxM,SAAW,IACxCqN,IAIF,GAAIV,GAAcE,CAEdC,MAA0B,SAAWN,GACxCe,IACAb,KAEAY,IAMF,QAASlO,GAASoN,EAAcC,EAAkB1E,EAAcC,GAC/D,QAASwF,KACFhB,KAAiBlF,MAAQ,EAAEmG,UAAY,EAAEtP,KAAO,IACrDJ,EAAK,kBAAoB0O,GAI3B,QAASiB,KACR,MAAQC,KAAkBnB,IAAgBoB,IAGtCF,IAIJ3P,EAAI,4BAA4ByO,IAHhCgB,IACAK,GAAoBrB,EAAcC,EAAkB1E,EAAcC,IAMpE,QAASuF,KACHI,KACJA,IAAgB,EAChB5P,EAAI,0BAELN,aAAaqQ,IACbA,GAAqBpQ,WAAW,WAC/BiQ,IAAgB,EAChB5P,EAAI,0BACJA,EAAI,OACH4I,IAGH,QAASoH,GAAavB,GACrBzK,GAASoB,GAAUhD,MACnByM,GAASvJ,GAAS9C,MAElBmF,EAAQ3D,GAAO6K,GAAMJ,GAGtB,QAASjF,GAAYkF,GACpB,GAAIuB,GAAM7N,EACVA,IAAiB+C,GAEjBnF,EAAI,wBAA0B0O,GAC9Bc,IACAQ,EAAa,SAEb5N,GAAiB6N,EAGlB,QAAStI,GAAQ3D,EAAO6K,EAAMJ,EAAa5O,EAAIkD,GAC9C,QAAS+G,KACJ/L,IAAcgF,EACjBA,EAAeD,GAEf9C,EAAI,yBAAyB+C,GAI/B,QAASmN,KACR,GACCnG,GAAQ/F,EAAS,IAAM6K,EACvBsB,EAAUpQ,GAAO,IAAOgK,EAAO,IAAM0E,GAAgB1Q,IAAc8B,EAAM,IAAMA,EAAM,GAEtFG,GAAI,iCAAmCmQ,EAAU,KACjD3I,GAAO4I,YAAatQ,GAAQqQ,EAASpN,IAGnC,IAAS0C,KACXqE,IACAoG,KAIF,QAASvK,GAAS4F,GACjB,QAAS8E,KACR,MAAOvQ,OAAW,GAAGyL,EAAM9J,MAAME,OAAO,EAAEC,IAG3C,QAAS0O,KACR,QAASC,KACR7O,GAAU6J,EAAM9J,KAChB+F,GAAU+D,EAAMiF,OAEhBpQ,IACAqQ,IAAW,EACX9Q,WAAW,WAAY+Q,IAAW,GAAQ9H,IAGvChF,SAASC,KACZ0M,KAEAvQ,EAAI,0BACJhC,EAAiBF,EAAO,mBAAmBwS,IAI7C,QAASK,KACHD,GAIJ1Q,EAAI,+BAHJA,EAAI,gCACJgQ,EAAa,cAMf,QAASY,KACRvP,EAAS,eAAe,sCAGzB,QAASiI,KACR,GAAIuH,GAASC,GACb3P,IAAYmG,WAAWuJ,GAGxB,QAASE,KACR,MAAOxF,GAAM9J,KAAKI,MAAM,KAAK,GAAGA,MAAM,KAAK,GAG5C,QAASiP,KACR,MAAOvF,GAAM9J,KAAKE,OAAO4J,EAAM9J,KAAKkC,QAAQ,KAAK,GAGlD,QAASqN,KACR,MAAQ,gBAAkBlT,GAG3B,QAASmT,KACR,GAAIC,GAAUJ,GAEd9Q,GAAI,uCAAyCkR,GAC7CrO,GAAgBF,KAAKwO,MAAMD,IAC3BlR,EAAI,OAGL,QAASoR,KACR,GAAIF,GAAUJ,GACd9Q,GAAI,0CAA4CkR,GAChD7H,GAAiB1G,KAAKwO,MAAMD,IAC5BlR,EAAI,OAGL,QAASqR,KAGR,MAAO9F,GAAM9J,KAAKI,MAAM,KAAK,KAAOyP,OAAO,EAAEC,QAAQ,GAGtD,QAASC,KACR,OAAQT,KACR,IAAK,QACJJ,GACA,MACD,KAAK,SACJC,GACA,MACD,KAAK,aACL,IAAK,eACJtH,GACA,MACD,KAAK,UACJ2H,GACA,MACD,KAAK,WACJG,GACA,MACD,SACMJ,KAAmBK,KACvBlR,EAAK,uBAAuBoL,EAAM9J,KAAK,MAK1C,QAASgQ,MACJ,IAAUhB,GACbe,IACUH,IACVf,IAEAtQ,EAAI,4BAA8B+Q,IAAmB,sCAInDV,KACHoB,IAMF,QAASC,KACL,YAAc9N,SAAS+N,YACzB7T,EAAO8T,OAAOxB,YAAY,4BAA4B,KA3jCxD,GACClO,IAAwB,EACxB4K,EAAwB,GACxBnM,EAAwB,GACxBmB,EAAwB,EACxBK,EAAwB,GACxB0D,EAAwB,KACxBjF,EAAwB,GACxBoB,IAAwB,EACxB6N,IAAyB7G,OAAS,EAAE6I,MAAQ,GAC5CjJ,GAAwB,IACxB6H,IAAwB,EACxBzM,GAAwB,EACxBmB,GAAwB,aACxB/C,GAAwB+C,GACxBuL,IAAwB,EACxBhP,GAAwB,GACxBP,MACAc,GAAwB,GACxBgE,GAAwB,KACxBhG,IAAwB,EACxBH,GAAwB,gBACxB8B,GAAwB9B,GAAM2N,OAC9B1N,GAAwB,GAExBsP,IAAyByC,IAAI,EAAEC,IAAI,EAAE9D,WAAW,EAAEE,sBAAsB,GACxE5L,GAAwB,QACxBkD,IAAwB,EACxB+B,GAAwB1J,EAAO8T,OAC/B9O,GAAwB,IACxBT,GAAwB,EACxBuN,IAAwB,EACxBG,GAAwB,KACxBxQ,GAAwB,GACxBsP,GAAwB,EACxBxJ,GAAwB,SACxB7C,GAAwB6C,GACxByD,GAAwBhL,EACxB+E,GAAwB,WAAY1C,EAAK,yCACzCmB,GAAwB,aACxB+H,GAAwB,aACxBhG,IACCW,OAAQ,WAEP,MADA7D,GAAK,kDACEyD,SAASG,gBAAgBiO,cAEjCnD,MAAO,WAEN,MADA1O,GAAK,iDACEyD,SAASC,KAAKoO,cA2EpB9S,GAAS+S,KAAK7S,KAAO,WAExB,OAAO,GAAI6S,OAAOC,WAgnBlB/M,IACC4I,WAAY,WACX,MAAQpK,UAASC,KAAKmO,aAAevF,EAAiB,aAAeA,EAAiB,iBAGvF2F,OAAQ,WACP,MAAOhN,IAAU4I,cAGlBC,WAAY,WACX,MAAOrK,UAASC,KAAKwO,cAGtBC,OAAQ,WACP,MAAOjP,IAAkBW,UAG1BkK,sBAAuB,WACtB,MAAOtK,UAASG,gBAAgBiO,cAGjC7D,sBAAuB,WACtB,MAAOvK,UAASG,gBAAgBsO,cAGjCP,IAAK,WACJ,MAAOzH,MAAKyH,IAAI1S,MAAM,KAAK0O,EAAmB1I,MAG/C2M,IAAK,WACJ,MAAO1H,MAAK0H,IAAI3S,MAAM,KAAK0O,EAAmB1I,MAG/CmN,KAAM,WACL,MAAOnN,IAAU0M,OAGlBU,cAAe,WACd,MAAOnI,MAAKyH,IAAI1M,GAAU4I,aAAcV,EAAc,SAASiB,OAGhEkE,cAAe,WACd,MAAOrE,GAAkB,SAAS,wBAIpC9I,IACC2I,WAAY,WACX,MAAOrK,UAASC,KAAKoO,aAGtBjE,WAAY,WACX,MAAOpK,UAASC,KAAK6O,aAGtBJ,OAAQ,WACP,MAAOjP,IAAkBwL,SAG1BV,sBAAuB,WACtB,MAAOvK,UAASG,gBAAgBkO,aAGjC/D,sBAAuB,WACtB,MAAOtK,UAASG,gBAAgB2O,aAGjCC,OAAQ,WACP,MAAOtI,MAAKyH,IAAIxM,GAAS2I,aAAc3I,GAAS6I,0BAGjD2D,IAAK,WACJ,MAAOzH,MAAKyH,IAAI1S,MAAM,KAAK0O,EAAmBxI,MAG/CyM,IAAK,WACJ,MAAO1H,MAAK0H,IAAI3S,MAAM,KAAK0O,EAAmBxI,MAG/CsN,iBAAkB,WACjB,MAAOtF,GAAc,QAASiB,MAG/BkE,cAAe,WACd,MAAOrE,GAAkB,QAAS,uBAwDjC0B,GAAsBlR,EAAS4P,EAsMnCxQ,GAAiBF,EAAQ,UAAW6H,GACpC+L,KAIE5T","sourcesContent":["/*\n * File: iframeResizer.contentWindow.js\n * Desc: Include this file in any page being loaded into an iframe\n * to force the iframe to resize to the content size.\n * Requires: iframeResizer.js on host page.\n * Doc: https://github.com/davidjbradshaw/iframe-resizer\n * Author: David J. Bradshaw - dave@bradshaw.net\n * Contributor: Jure Mav - jure.mav@gmail.com\n * Contributor: Ian Caunce - ian@hallnet.co.uk\n */\n\n\n;(function(window, undefined) {\n\t'use strict';\n\n\tvar\n\t\tautoResize = true,\n\t\tbase = 10,\n\t\tbodyBackground = '',\n\t\tbodyMargin = 0,\n\t\tbodyMarginStr = '',\n\t\tbodyObserver = null,\n\t\tbodyPadding = '',\n\t\tcalculateWidth = false,\n\t\tdoubleEventList = {'resize':1,'click':1},\n\t\teventCancelTimer = 128,\n\t\tfirstRun = true,\n\t\theight = 1,\n\t\theightCalcModeDefault = 'bodyOffset',\n\t\theightCalcMode = heightCalcModeDefault,\n\t\tinitLock = true,\n\t\tinitMsg = '',\n\t\tinPageLinks = {},\n\t\tinterval = 32,\n\t\tintervalTimer = null,\n\t\tlogging = false,\n\t\tmsgID = '[iFrameSizer]', //Must match host page msg ID\n\t\tmsgIdLen = msgID.length,\n\t\tmyID = '',\n\t\tobserver = null,\n\t\tresetRequiredMethods = {max:1,min:1,bodyScroll:1,documentElementScroll:1},\n\t\tresizeFrom = 'child',\n\t\tsendPermit = true,\n\t\ttarget = window.parent,\n\t\ttargetOriginDefault = '*',\n\t\ttolerance = 0,\n\t\ttriggerLocked = false,\n\t\ttriggerLockedTimer = null,\n\t\tthrottledTimer = 16,\n\t\twidth = 1,\n\t\twidthCalcModeDefault = 'scroll',\n\t\twidthCalcMode = widthCalcModeDefault,\n\t\twin = window,\n\t\tmessageCallback = function(){ warn('MessageCallback function not defined'); },\n\t\treadyCallback = function(){},\n\t\tpageInfoCallback = function(){},\n\t\tcustomCalcMethods = {\n\t\t\theight: function(){\n\t\t\t\twarn('Custom height calculation function not defined');\n\t\t\t\treturn document.documentElement.offsetHeight;\n\t\t\t}, \n\t\t\twidth: function(){\n\t\t\t\twarn('Custom width calculation function not defined');\n\t\t\t\treturn document.body.scrollWidth;\n\t\t\t}\n\t\t};\n\n\n\tfunction addEventListener(el,evt,func){\n\t\t/* istanbul ignore else */ // Not testable in phantonJS\n\t\tif ('addEventListener' in window){\n\t\t\tel.addEventListener(evt,func, false);\n\t\t} else if ('attachEvent' in window){ //IE\n\t\t\tel.attachEvent('on'+evt,func);\n\t\t}\n\t}\n\n\tfunction removeEventListener(el,evt,func){\n\t\t/* istanbul ignore else */ // Not testable in phantonJS\n\t\tif ('removeEventListener' in window){\n\t\t\tel.removeEventListener(evt,func, false);\n\t\t} else if ('detachEvent' in window){ //IE\n\t\t\tel.detachEvent('on'+evt,func);\n\t\t}\n\t}\n\n\tfunction capitalizeFirstLetter(string) {\n\t\treturn string.charAt(0).toUpperCase() + string.slice(1);\n\t}\n\n\t//Based on underscore.js\n\tfunction throttle(func) {\n\t\tvar\n\t\t\tcontext, args, result,\n\t\t\ttimeout = null,\n\t\t\tprevious = 0,\n\t\t\tlater = function() {\n\t\t\t\tprevious = getNow();\n\t\t\t\ttimeout = null;\n\t\t\t\tresult = func.apply(context, args);\n\t\t\t\tif (!timeout) {\n\t\t\t\t\tcontext = args = null;\n\t\t\t\t}\n\t\t\t};\n\n\t\treturn function() {\n\t\t\tvar now = getNow();\n\n\t\t\tif (!previous) {\n\t\t\t\tprevious = now;\n\t\t\t}\n\n\t\t\tvar remaining = throttledTimer - (now - previous);\n\n\t\t\tcontext = this;\n\t\t\targs = arguments;\n\n\t\t\tif (remaining <= 0 || remaining > throttledTimer) {\n\t\t\t\tif (timeout) {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\ttimeout = null;\n\t\t\t\t}\n\n\t\t\t\tprevious = now;\n\t\t\t\tresult = func.apply(context, args);\n\n\t\t\t\tif (!timeout) {\n\t\t\t\t\tcontext = args = null;\n\t\t\t\t}\n\n\t\t\t} else if (!timeout) {\n\t\t\t\ttimeout = setTimeout(later, remaining);\n\t\t\t}\n\n\t\t\treturn result;\n\t\t};\n\t}\n\n\tvar getNow = Date.now || function() {\n\t\t/* istanbul ignore next */ // Not testable in PhantonJS\n\t\treturn new Date().getTime();\n\t};\n\n\tfunction formatLogMsg(msg){\n\t\treturn msgID + '[' + myID + ']' + ' ' + msg;\n\t}\n\n\tfunction log(msg){\n\t\tif (logging && ('object' === typeof window.console)){\n\t\t\tconsole.log(formatLogMsg(msg));\n\t\t}\n\t}\n\n\tfunction warn(msg){\n\t\tif ('object' === typeof window.console){\n\t\t\tconsole.warn(formatLogMsg(msg));\n\t\t}\n\t}\n\n\n\tfunction init(){\n\t\treadDataFromParent();\n\t\tlog('Initialising iFrame ('+location.href+')');\n\t\treadDataFromPage();\n\t\tsetMargin();\n\t\tsetBodyStyle('background',bodyBackground);\n\t\tsetBodyStyle('padding',bodyPadding);\n\t\tinjectClearFixIntoBodyElement();\n\t\tcheckHeightMode();\n\t\tcheckWidthMode();\n\t\tstopInfiniteResizingOfIFrame();\n\t\tsetupPublicMethods();\n\t\tstartEventListeners();\n\t\tinPageLinks = setupInPageLinks();\n\t\tsendSize('init','Init message from host page');\n\t\treadyCallback();\n\t}\n\n\tfunction readDataFromParent(){\n\n\t\tfunction strBool(str){\n\t\t\treturn 'true' === str ? true : false;\n\t\t}\n\n\t\tvar data = initMsg.substr(msgIdLen).split(':');\n\n\t\tmyID = data[0];\n\t\tbodyMargin = (undefined !== data[1]) ? Number(data[1]) : bodyMargin; //For V1 compatibility\n\t\tcalculateWidth = (undefined !== data[2]) ? strBool(data[2]) : calculateWidth;\n\t\tlogging = (undefined !== data[3]) ? strBool(data[3]) : logging;\n\t\tinterval = (undefined !== data[4]) ? Number(data[4]) : interval;\n\t\tautoResize = (undefined !== data[6]) ? strBool(data[6]) : autoResize;\n\t\tbodyMarginStr = data[7];\n\t\theightCalcMode = (undefined !== data[8]) ? data[8] : heightCalcMode;\n\t\tbodyBackground = data[9];\n\t\tbodyPadding = data[10];\n\t\ttolerance = (undefined !== data[11]) ? Number(data[11]) : tolerance;\n\t\tinPageLinks.enable = (undefined !== data[12]) ? strBool(data[12]): false;\n\t\tresizeFrom = (undefined !== data[13]) ? data[13] : resizeFrom;\n\t\twidthCalcMode = (undefined !== data[14]) ? data[14] : widthCalcMode;\n\t}\n\n\tfunction readDataFromPage(){\n\t\tfunction readData(){\n\t\t\tvar data = window.iFrameResizer;\n\n\t\t\tlog('Reading data from page: ' + JSON.stringify(data));\n\n\t\t\tmessageCallback = ('messageCallback' in data) ? data.messageCallback : messageCallback;\n\t\t\treadyCallback = ('readyCallback' in data) ? data.readyCallback : readyCallback;\n\t\t\ttargetOriginDefault = ('targetOrigin' in data) ? data.targetOrigin : targetOriginDefault;\n\t\t\theightCalcMode = ('heightCalculationMethod' in data) ? data.heightCalculationMethod : heightCalcMode;\n\t\t\twidthCalcMode = ('widthCalculationMethod' in data) ? data.widthCalculationMethod : widthCalcMode;\n\t\t}\n\n\t\tfunction setupCustomCalcMethods(calcMode, calcFunc){\n\t\t\tif ('function' === typeof calcMode) {\n\t\t\t\tlog('Setup custom ' + calcFunc + 'CalcMethod');\n\t\t\t\tcustomCalcMethods[calcFunc] = calcMode;\n\t\t\t\tcalcMode = 'custom';\n\t\t\t}\n\n\t\t\treturn calcMode;\n\t\t}\n\n\t\tif(('iFrameResizer' in window) && (Object === window.iFrameResizer.constructor)) {\n\t\t\treadData();\n\t\t\theightCalcMode = setupCustomCalcMethods(heightCalcMode, 'height');\n\t\t\twidthCalcMode = setupCustomCalcMethods(widthCalcMode, 'width');\n\t\t}\n\n\t\tlog('TargetOrigin for parent set to: ' + targetOriginDefault);\n\t}\n\n\n\tfunction chkCSS(attr,value){\n\t\tif (-1 !== value.indexOf('-')){\n\t\t\twarn('Negative CSS value ignored for '+attr);\n\t\t\tvalue='';\n\t\t}\n\t\treturn value;\n\t}\n\n\tfunction setBodyStyle(attr,value){\n\t\tif ((undefined !== value) && ('' !== value) && ('null' !== value)){\n\t\t\tdocument.body.style[attr] = value;\n\t\t\tlog('Body '+attr+' set to \"'+value+'\"');\n\t\t}\n\t}\n\n\tfunction setMargin(){\n\t\t//If called via V1 script, convert bodyMargin from int to str\n\t\tif (undefined === bodyMarginStr){\n\t\t\tbodyMarginStr = bodyMargin+'px';\n\t\t}\n\n\t\tsetBodyStyle('margin',chkCSS('margin',bodyMarginStr));\n\t}\n\n\tfunction stopInfiniteResizingOfIFrame(){\n\t\tdocument.documentElement.style.height = '';\n\t\tdocument.body.style.height = '';\n\t\tlog('HTML & body height set to \"auto\"');\n\t}\n\n\n\tfunction manageTriggerEvent(options){\n\t\tfunction handleEvent(){\n\t\t\tsendSize(options.eventName,options.eventType);\n\t\t}\n\n\t\tvar listener = {\n\t\t\tadd: function(eventName){\n\t\t\t\taddEventListener(window,eventName,handleEvent);\n\t\t\t},\n\t\t\tremove: function(eventName){\n\t\t\t\tremoveEventListener(window,eventName,handleEvent);\n\t\t\t}\n\t\t};\n\n\t\tif(options.eventNames && Array.prototype.map){\n\t\t\toptions.eventName = options.eventNames[0];\n\t\t\toptions.eventNames.map(listener[options.method]);\n\t\t} else {\n\t\t\tlistener[options.method](options.eventName);\n\t\t}\n\n\t\tlog(capitalizeFirstLetter(options.method) + ' event listener: ' + options.eventType);\n\t}\n\n\tfunction manageEventListeners(method){\n\t\tmanageTriggerEvent({method:method, eventType: 'Animation Start', eventNames: ['animationstart','webkitAnimationStart'] });\n\t\tmanageTriggerEvent({method:method, eventType: 'Animation Iteration', eventNames: ['animationiteration','webkitAnimationIteration'] });\n\t\tmanageTriggerEvent({method:method, eventType: 'Animation End', eventNames: ['animationend','webkitAnimationEnd'] });\n\t\tmanageTriggerEvent({method:method, eventType: 'Input', eventName: 'input' });\n\t\tmanageTriggerEvent({method:method, eventType: 'Mouse Up', eventName: 'mouseup' });\n\t\tmanageTriggerEvent({method:method, eventType: 'Mouse Down', eventName: 'mousedown' });\n\t\tmanageTriggerEvent({method:method, eventType: 'Orientation Change', eventName: 'orientationchange' });\n\t\tmanageTriggerEvent({method:method, eventType: 'Print', eventName: ['afterprint', 'beforeprint'] });\n\t\tmanageTriggerEvent({method:method, eventType: 'Ready State Change', eventName: 'readystatechange' });\n\t\tmanageTriggerEvent({method:method, eventType: 'Touch Start', eventName: 'touchstart' });\n\t\tmanageTriggerEvent({method:method, eventType: 'Touch End', eventName: 'touchend' });\n\t\tmanageTriggerEvent({method:method, eventType: 'Touch Cancel', eventName: 'touchcancel' });\n\t\tmanageTriggerEvent({method:method, eventType: 'Transition Start', eventNames: ['transitionstart','webkitTransitionStart','MSTransitionStart','oTransitionStart','otransitionstart'] });\n\t\tmanageTriggerEvent({method:method, eventType: 'Transition Iteration', eventNames: ['transitioniteration','webkitTransitionIteration','MSTransitionIteration','oTransitionIteration','otransitioniteration'] });\n\t\tmanageTriggerEvent({method:method, eventType: 'Transition End', eventNames: ['transitionend','webkitTransitionEnd','MSTransitionEnd','oTransitionEnd','otransitionend'] });\n\t\tif('child' === resizeFrom){\n\t\t\tmanageTriggerEvent({method:method, eventType: 'IFrame Resized', eventName: 'resize' });\n\t\t}\n\t}\n\n\tfunction checkCalcMode(calcMode,calcModeDefault,modes,type){\n\t\tif (calcModeDefault !== calcMode){\n\t\t\tif (!(calcMode in modes)){\n\t\t\t\twarn(calcMode + ' is not a valid option for '+type+'CalculationMethod.');\n\t\t\t\tcalcMode=calcModeDefault;\n\t\t\t}\n\t\t\tlog(type+' calculation method set to \"'+calcMode+'\"');\n\t\t}\n\n\t\treturn calcMode;\n\t}\n\n\tfunction checkHeightMode(){\n\t\theightCalcMode = checkCalcMode(heightCalcMode,heightCalcModeDefault,getHeight,'height');\n\t}\n\n\tfunction checkWidthMode(){\n\t\twidthCalcMode = checkCalcMode(widthCalcMode,widthCalcModeDefault,getWidth,'width');\n\t}\n\n\tfunction startEventListeners(){\n\t\tif ( true === autoResize ) {\n\t\t\tmanageEventListeners('add');\n\t\t\tsetupMutationObserver();\n\t\t}\n\t\telse {\n\t\t\tlog('Auto Resize disabled');\n\t\t}\n\t}\n\n\tfunction stopMsgsToParent(){\n\t\tlog('Disable outgoing messages');\n\t\tsendPermit = false;\n\t}\n\n\tfunction removeMsgListener(){\n\t\tlog('Remove event listener: Message');\n\t\tremoveEventListener(window, 'message', receiver);\n\t}\n\n\tfunction disconnectMutationObserver(){\n\t\tif (null !== bodyObserver){\n\t\t\t/* istanbul ignore next */ // Not testable in PhantonJS\n\t\t\tbodyObserver.disconnect();\n\t\t}\n\t}\n\n\tfunction stopEventListeners(){\n\t\tmanageEventListeners('remove');\n\t\tdisconnectMutationObserver();\n\t\tclearInterval(intervalTimer);\n\t}\n\n\tfunction teardown(){\n\t\tstopMsgsToParent();\n\t\tremoveMsgListener();\n\t\tif (true === autoResize) stopEventListeners();\n\t}\n\n\tfunction injectClearFixIntoBodyElement(){\n\t\tvar clearFix = document.createElement('div');\n\t\tclearFix.style.clear = 'both';\n\t\tclearFix.style.display = 'block'; //Guard against this having been globally redefined in CSS.\n\t\tdocument.body.appendChild(clearFix);\n\t}\n\n\tfunction setupInPageLinks(){\n\n\t\tfunction getPagePosition (){\n\t\t\treturn {\n\t\t\t\tx: (window.pageXOffset !== undefined) ? window.pageXOffset : document.documentElement.scrollLeft,\n\t\t\t\ty: (window.pageYOffset !== undefined) ? window.pageYOffset : document.documentElement.scrollTop\n\t\t\t};\n\t\t}\n\n\t\tfunction getElementPosition(el){\n\t\t\tvar\n\t\t\t\telPosition = el.getBoundingClientRect(),\n\t\t\t\tpagePosition = getPagePosition();\n\n\t\t\treturn {\n\t\t\t\tx: parseInt(elPosition.left,10) + parseInt(pagePosition.x,10),\n\t\t\t\ty: parseInt(elPosition.top,10) + parseInt(pagePosition.y,10)\n\t\t\t};\n\t\t}\n\n\t\tfunction findTarget(location){\n\t\t\tfunction jumpToTarget(target){\n\t\t\t\tvar jumpPosition = getElementPosition(target);\n\n\t\t\t\tlog('Moving to in page link (#'+hash+') at x: '+jumpPosition.x+' y: '+jumpPosition.y);\n\t\t\t\tsendMsg(jumpPosition.y, jumpPosition.x, 'scrollToOffset'); // X&Y reversed at sendMsg uses height/width\n\t\t\t}\n\n\t\t\tvar\n\t\t\t\thash = location.split('#')[1] || location, //Remove # if present\n\t\t\t\thashData = decodeURIComponent(hash),\n\t\t\t\ttarget = document.getElementById(hashData) || document.getElementsByName(hashData)[0];\n\n\t\t\tif (undefined !== target){\n\t\t\t\tjumpToTarget(target);\n\t\t\t} else {\n\t\t\t\tlog('In page link (#' + hash + ') not found in iFrame, so sending to parent');\n\t\t\t\tsendMsg(0,0,'inPageLink','#'+hash);\n\t\t\t}\n\t\t}\n\n\t\tfunction checkLocationHash(){\n\t\t\tif ('' !== location.hash && '#' !== location.hash){\n\t\t\t\tfindTarget(location.href);\n\t\t\t}\n\t\t}\n\n\t\tfunction bindAnchors(){\n\t\t\tfunction setupLink(el){\n\t\t\t\tfunction linkClicked(e){\n\t\t\t\t\te.preventDefault();\n\n\t\t\t\t\t/*jshint validthis:true */\n\t\t\t\t\tfindTarget(this.getAttribute('href'));\n\t\t\t\t}\n\n\t\t\t\tif ('#' !== el.getAttribute('href')){\n\t\t\t\t\taddEventListener(el,'click',linkClicked);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tArray.prototype.forEach.call( document.querySelectorAll( 'a[href^=\"#\"]' ), setupLink );\n\t\t}\n\n\t\tfunction bindLocationHash(){\n\t\t\taddEventListener(window,'hashchange',checkLocationHash);\n\t\t}\n\n\t\tfunction initCheck(){ //check if page loaded with location hash after init resize\n\t\t\tsetTimeout(checkLocationHash,eventCancelTimer);\n\t\t}\n\n\t\tfunction enableInPageLinks(){\n\t\t\t/* istanbul ignore else */ // Not testable in phantonJS\n\t\t\tif(Array.prototype.forEach && document.querySelectorAll){\n\t\t\t\tlog('Setting up location.hash handlers');\n\t\t\t\tbindAnchors();\n\t\t\t\tbindLocationHash();\n\t\t\t\tinitCheck();\n\t\t\t} else {\n\t\t\t\twarn('In page linking not fully supported in this browser! (See README.md for IE8 workaround)');\n\t\t\t}\n\t\t}\n\n\t\tif(inPageLinks.enable){\n\t\t\tenableInPageLinks();\n\t\t} else {\n\t\t\tlog('In page linking not enabled');\n\t\t}\n\n\t\treturn {\n\t\t\tfindTarget:findTarget\n\t\t};\n\t}\n\n\tfunction setupPublicMethods(){\n\t\tlog('Enable public methods');\n\n\t\twin.parentIFrame = {\n\n\t\t\tautoResize: function autoResizeF(resize){\n\t\t\t\tif (true === resize && false === autoResize) {\n\t\t\t\t\tautoResize=true;\n\t\t\t\t\tstartEventListeners();\n\t\t\t\t\t//sendSize('autoResize','Auto Resize enabled');\n\t\t\t\t} else if (false === resize && true === autoResize) {\n\t\t\t\t\tautoResize=false;\n\t\t\t\t\tstopEventListeners();\n\t\t\t\t}\n\n\t\t\t\treturn autoResize;\n\t\t\t},\n\n\t\t\tclose: function closeF(){\n\t\t\t\tsendMsg(0,0,'close');\n\t\t\t\tteardown();\n\t\t\t},\n\n\t\t\tgetId: function getIdF(){\n\t\t\t\treturn myID;\n\t\t\t},\n\n\t\t\tgetPageInfo: function getPageInfoF(callback){\n\t\t\t\tif ('function' === typeof callback){\n\t\t\t\t\tpageInfoCallback = callback;\n\t\t\t\t\tsendMsg(0,0,'pageInfo');\n\t\t\t\t} else {\n\t\t\t\t\tpageInfoCallback = function(){};\n\t\t\t\t\tsendMsg(0,0,'pageInfoStop');\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tmoveToAnchor: function moveToAnchorF(hash){\n\t\t\t\tinPageLinks.findTarget(hash);\n\t\t\t},\n\n\t\t\treset: function resetF(){\n\t\t\t\tresetIFrame('parentIFrame.reset');\n\t\t\t},\n\n\t\t\tscrollTo: function scrollToF(x,y){\n\t\t\t\tsendMsg(y,x,'scrollTo'); // X&Y reversed at sendMsg uses height/width\n\t\t\t},\n\n\t\t\tscrollToOffset: function scrollToF(x,y){\n\t\t\t\tsendMsg(y,x,'scrollToOffset'); // X&Y reversed at sendMsg uses height/width\n\t\t\t},\n\n\t\t\tsendMessage: function sendMessageF(msg,targetOrigin){\n\t\t\t\tsendMsg(0,0,'message',JSON.stringify(msg),targetOrigin);\n\t\t\t},\n\n\t\t\tsetHeightCalculationMethod: function setHeightCalculationMethodF(heightCalculationMethod){\n\t\t\t\theightCalcMode = heightCalculationMethod;\n\t\t\t\tcheckHeightMode();\n\t\t\t},\n\n\t\t\tsetWidthCalculationMethod: function setWidthCalculationMethodF(widthCalculationMethod){\n\t\t\t\twidthCalcMode = widthCalculationMethod;\n\t\t\t\tcheckWidthMode();\n\t\t\t},\n\n\t\t\tsetTargetOrigin: function setTargetOriginF(targetOrigin){\n\t\t\t\tlog('Set targetOrigin: '+targetOrigin);\n\t\t\t\ttargetOriginDefault = targetOrigin;\n\t\t\t},\n\n\t\t\tsize: function sizeF(customHeight, customWidth){\n\t\t\t\tvar valString = ''+(customHeight?customHeight:'')+(customWidth?','+customWidth:'');\n\t\t\t\t//lockTrigger();\n\t\t\t\tsendSize('size','parentIFrame.size('+valString+')', customHeight, customWidth);\n\t\t\t}\n\t\t};\n\t}\n\n\tfunction initInterval(){\n\t\tif ( 0 !== interval ){\n\t\t\tlog('setInterval: '+interval+'ms');\n\t\t\tintervalTimer = setInterval(function(){\n\t\t\t\tsendSize('interval','setInterval: '+interval);\n\t\t\t},Math.abs(interval));\n\t\t}\n\t}\n\n\t/* istanbul ignore next */ //Not testable in PhantomJS\n\tfunction setupBodyMutationObserver(){\n\t\tfunction addImageLoadListners(mutation) {\n\t\t\tfunction addImageLoadListener(element){\n\t\t\t\tif (false === element.complete) {\n\t\t\t\t\tlog('Attach listeners to ' + element.src);\n\t\t\t\t\telement.addEventListener('load', imageLoaded, false);\n\t\t\t\t\telement.addEventListener('error', imageError, false);\n\t\t\t\t\telements.push(element);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (mutation.type === 'attributes' && mutation.attributeName === 'src'){\n\t\t\t\taddImageLoadListener(mutation.target);\n\t\t\t} else if (mutation.type === 'childList'){\n\t\t\t\tArray.prototype.forEach.call(\n\t\t\t\t\tmutation.target.querySelectorAll('img'),\n\t\t\t\t\taddImageLoadListener\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tfunction removeFromArray(element){\n\t\t\telements.splice(elements.indexOf(element),1);\n\t\t}\n\n\t\tfunction removeImageLoadListener(element){\n\t\t\tlog('Remove listeners from ' + element.src);\n\t\t\telement.removeEventListener('load', imageLoaded, false);\n\t\t\telement.removeEventListener('error', imageError, false);\n\t\t\tremoveFromArray(element);\n\t\t}\n\n\t\tfunction imageEventTriggered(event,type,typeDesc){\n\t\t\tremoveImageLoadListener(event.target);\n\t\t\tsendSize(type, typeDesc + ': ' + event.target.src, undefined, undefined);\n\t\t}\n\n\t\tfunction imageLoaded(event) {\n\t\t\timageEventTriggered(event,'imageLoad','Image loaded');\n\t\t}\n\n\t\tfunction imageError(event) {\n\t\t\timageEventTriggered(event,'imageLoadFailed','Image load failed');\n\t\t}\n\n\t\tfunction mutationObserved(mutations) {\n\t\t\tsendSize('mutationObserver','mutationObserver: ' + mutations[0].target + ' ' + mutations[0].type);\n\n\t\t\t//Deal with WebKit asyncing image loading when tags are injected into the page\n\t\t\tmutations.forEach(addImageLoadListners);\n\t\t}\n\n\t\tfunction createMutationObserver(){\n\t\t\tvar\n\t\t\t\ttarget = document.querySelector('body'),\n\n\t\t\t\tconfig = {\n\t\t\t\t\tattributes : true,\n\t\t\t\t\tattributeOldValue : false,\n\t\t\t\t\tcharacterData : true,\n\t\t\t\t\tcharacterDataOldValue : false,\n\t\t\t\t\tchildList : true,\n\t\t\t\t\tsubtree : true\n\t\t\t\t};\n\n\t\t\tobserver = new MutationObserver(mutationObserved);\n\n\t\t\tlog('Create body MutationObserver');\n\t\t\tobserver.observe(target, config);\n\n\t\t\treturn observer;\n\t\t}\n\n\t\tvar\n\t\t\telements = [],\n\t\t\tMutationObserver = window.MutationObserver || window.WebKitMutationObserver,\n\t\t\tobserver = createMutationObserver();\n\n\t\treturn {\n\t\t\tdisconnect: function (){\n\t\t\t\tif ('disconnect' in observer){\n\t\t\t\t\tlog('Disconnect body MutationObserver');\n\t\t\t\t\tobserver.disconnect();\n\t\t\t\t\telements.forEach(removeImageLoadListener);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\tfunction setupMutationObserver(){\n\t\tvar\tforceIntervalTimer = 0 > interval;\n\n\t\t/* istanbul ignore if */ // Not testable in PhantomJS\n\t\tif (window.MutationObserver || window.WebKitMutationObserver){\n\t\t\tif (forceIntervalTimer) {\n\t\t\t\tinitInterval();\n\t\t\t} else {\n\t\t\t\tbodyObserver = setupBodyMutationObserver();\n\t\t\t}\n\t\t} else {\n\t\t\tlog('MutationObserver not supported in this browser!');\n\t\t\tinitInterval();\n\t\t}\n\t}\n\n\n\t// document.documentElement.offsetHeight is not reliable, so\n\t// we have to jump through hoops to get a better value.\n\tfunction getComputedStyle(prop,el) {\n\t\t/* istanbul ignore next */ //Not testable in PhantomJS\n\t\tfunction convertUnitsToPxForIE8(value) {\n\t\t\tvar PIXEL = /^\\d+(px)?$/i;\n\n\t\t\tif (PIXEL.test(value)) {\n\t\t\t\treturn parseInt(value,base);\n\t\t\t}\n\n\t\t\tvar\n\t\t\t\tstyle = el.style.left,\n\t\t\t\truntimeStyle = el.runtimeStyle.left;\n\n\t\t\tel.runtimeStyle.left = el.currentStyle.left;\n\t\t\tel.style.left = value || 0;\n\t\t\tvalue = el.style.pixelLeft;\n\t\t\tel.style.left = style;\n\t\t\tel.runtimeStyle.left = runtimeStyle;\n\n\t\t\treturn value;\n\t\t}\n\n\t\tvar retVal = 0;\n\t\tel = el || document.body;\n\n\t\t/* istanbul ignore else */ // Not testable in phantonJS\n\t\tif (('defaultView' in document) && ('getComputedStyle' in document.defaultView)) {\n\t\t\tretVal = document.defaultView.getComputedStyle(el, null);\n\t\t\tretVal = (null !== retVal) ? retVal[prop] : 0;\n\t\t} else {//IE8\n\t\t\tretVal = convertUnitsToPxForIE8(el.currentStyle[prop]);\n\t\t}\n\n\t\treturn parseInt(retVal,base);\n\t}\n\n\tfunction chkEventThottle(timer){\n\t\tif(timer > throttledTimer/2){\n\t\t\tthrottledTimer = 2*timer;\n\t\t\tlog('Event throttle increased to ' + throttledTimer + 'ms');\n\t\t}\n\t}\n\n\t//Idea from https://github.com/guardian/iframe-messenger\n\tfunction getMaxElement(side,elements) {\n\t\tvar\n\t\t\telementsLength = elements.length,\n\t\t\telVal = 0,\n\t\t\tmaxVal = 0,\n\t\t\tSide = capitalizeFirstLetter(side),\n\t\t\ttimer = getNow();\n\n\t\tfor (var i = 0; i < elementsLength; i++) {\n\t\t\telVal = elements[i].getBoundingClientRect()[side] + getComputedStyle('margin'+Side,elements[i]);\n\t\t\tif (elVal > maxVal) {\n\t\t\t\tmaxVal = elVal;\n\t\t\t}\n\t\t}\n\n\t\ttimer = getNow() - timer;\n\n\t\tlog('Parsed '+elementsLength+' HTML elements');\n\t\tlog('Element position calculated in ' + timer + 'ms');\n\n\t\tchkEventThottle(timer);\n\n\t\treturn maxVal;\n\t}\n\n\tfunction getAllMeasurements(dimention){\n\t\treturn [\n\t\t\tdimention.bodyOffset(),\n\t\t\tdimention.bodyScroll(),\n\t\t\tdimention.documentElementOffset(),\n\t\t\tdimention.documentElementScroll()\n\t\t];\n\t}\n\n\tfunction getTaggedElements(side,tag){\n\t\tfunction noTaggedElementsFound(){\n\t\t\twarn('No tagged elements ('+tag+') found on page');\n\t\t\treturn height; //current height\n\t\t}\n\n\t\tvar elements = document.querySelectorAll('['+tag+']');\n\n\t\treturn 0 === elements.length ? noTaggedElementsFound() : getMaxElement(side,elements);\n\t}\n\n\tfunction getAllElements(){\n\t\treturn document.querySelectorAll('body *');\n\t}\n\n\tvar\n\t\tgetHeight = {\n\t\t\tbodyOffset: function getBodyOffsetHeight(){\n\t\t\t\treturn document.body.offsetHeight + getComputedStyle('marginTop') + getComputedStyle('marginBottom');\n\t\t\t},\n\n\t\t\toffset: function(){\n\t\t\t\treturn getHeight.bodyOffset(); //Backwards compatability\n\t\t\t},\n\n\t\t\tbodyScroll: function getBodyScrollHeight(){\n\t\t\t\treturn document.body.scrollHeight;\n\t\t\t},\n\n\t\t\tcustom: function getCustomWidth(){\n\t\t\t\treturn customCalcMethods.height();\n\t\t\t},\n\n\t\t\tdocumentElementOffset: function getDEOffsetHeight(){\n\t\t\t\treturn document.documentElement.offsetHeight;\n\t\t\t},\n\n\t\t\tdocumentElementScroll: function getDEScrollHeight(){\n\t\t\t\treturn document.documentElement.scrollHeight;\n\t\t\t},\n\n\t\t\tmax: function getMaxHeight(){\n\t\t\t\treturn Math.max.apply(null,getAllMeasurements(getHeight));\n\t\t\t},\n\n\t\t\tmin: function getMinHeight(){\n\t\t\t\treturn Math.min.apply(null,getAllMeasurements(getHeight));\n\t\t\t},\n\n\t\t\tgrow: function growHeight(){\n\t\t\t\treturn getHeight.max(); //Run max without the forced downsizing\n\t\t\t},\n\n\t\t\tlowestElement: function getBestHeight(){\n\t\t\t\treturn Math.max(getHeight.bodyOffset(), getMaxElement('bottom',getAllElements()));\n\t\t\t},\n\n\t\t\ttaggedElement: function getTaggedElementsHeight(){\n\t\t\t\treturn getTaggedElements('bottom','data-iframe-height');\n\t\t\t}\n\t\t},\n\n\t\tgetWidth = {\n\t\t\tbodyScroll: function getBodyScrollWidth(){\n\t\t\t\treturn document.body.scrollWidth;\n\t\t\t},\n\n\t\t\tbodyOffset: function getBodyOffsetWidth(){\n\t\t\t\treturn document.body.offsetWidth;\n\t\t\t},\n\n\t\t\tcustom: function getCustomWidth(){\n\t\t\t\treturn customCalcMethods.width();\n\t\t\t},\n\n\t\t\tdocumentElementScroll: function getDEScrollWidth(){\n\t\t\t\treturn document.documentElement.scrollWidth;\n\t\t\t},\n\n\t\t\tdocumentElementOffset: function getDEOffsetWidth(){\n\t\t\t\treturn document.documentElement.offsetWidth;\n\t\t\t},\n\n\t\t\tscroll: function getMaxWidth(){\n\t\t\t\treturn Math.max(getWidth.bodyScroll(), getWidth.documentElementScroll());\n\t\t\t},\n\n\t\t\tmax: function getMaxWidth(){\n\t\t\t\treturn Math.max.apply(null,getAllMeasurements(getWidth));\n\t\t\t},\n\n\t\t\tmin: function getMinWidth(){\n\t\t\t\treturn Math.min.apply(null,getAllMeasurements(getWidth));\n\t\t\t},\n\n\t\t\trightMostElement: function rightMostElement(){\n\t\t\t\treturn getMaxElement('right', getAllElements());\n\t\t\t},\n\n\t\t\ttaggedElement: function getTaggedElementsWidth(){\n\t\t\t\treturn getTaggedElements('right', 'data-iframe-width');\n\t\t\t}\n\t\t};\n\n\n\tfunction sizeIFrame(triggerEvent, triggerEventDesc, customHeight, customWidth){\n\n\t\tfunction resizeIFrame(){\n\t\t\theight = currentHeight;\n\t\t\twidth = currentWidth;\n\n\t\t\tsendMsg(height,width,triggerEvent);\n\t\t}\n\n\t\tfunction isSizeChangeDetected(){\n\t\t\tfunction checkTolarance(a,b){\n\t\t\t\tvar retVal = Math.abs(a-b) <= tolerance;\n\t\t\t\treturn !retVal;\n\t\t\t}\n\n\t\t\tcurrentHeight = (undefined !== customHeight) ? customHeight : getHeight[heightCalcMode]();\n\t\t\tcurrentWidth = (undefined !== customWidth ) ? customWidth : getWidth[widthCalcMode]();\n\n\t\t\treturn\tcheckTolarance(height,currentHeight) || (calculateWidth && checkTolarance(width,currentWidth));\n\t\t}\n\n\t\tfunction isForceResizableEvent(){\n\t\t\treturn !(triggerEvent in {'init':1,'interval':1,'size':1});\n\t\t}\n\n\t\tfunction isForceResizableCalcMode(){\n\t\t\treturn (heightCalcMode in resetRequiredMethods) || (calculateWidth && widthCalcMode in resetRequiredMethods);\n\t\t}\n\n\t\tfunction logIgnored(){\n\t\t\tlog('No change in size detected');\n\t\t}\n\n\t\tfunction checkDownSizing(){\n\t\t\tif (isForceResizableEvent() && isForceResizableCalcMode()){\n\t\t\t\tresetIFrame(triggerEventDesc);\n\t\t\t} else if (!(triggerEvent in {'interval':1})){\n\t\t\t\tlogIgnored();\n\t\t\t}\n\t\t}\n\n\t\tvar\tcurrentHeight,currentWidth;\n\n\t\tif (isSizeChangeDetected() || 'init' === triggerEvent){\n\t\t\tlockTrigger();\n\t\t\tresizeIFrame();\n\t\t} else {\n\t\t\tcheckDownSizing();\n\t\t}\n\t}\n\n\tvar sizeIFrameThrottled = throttle(sizeIFrame);\n\n\tfunction sendSize(triggerEvent, triggerEventDesc, customHeight, customWidth){\n\t\tfunction recordTrigger(){\n\t\t\tif (!(triggerEvent in {'reset':1,'resetPage':1,'init':1})){\n\t\t\t\tlog( 'Trigger event: ' + triggerEventDesc );\n\t\t\t}\n\t\t}\n\n\t\tfunction isDoubleFiredEvent(){\n\t\t\treturn triggerLocked && (triggerEvent in doubleEventList);\n\t\t}\n\n\t\tif (!isDoubleFiredEvent()){\n\t\t\trecordTrigger();\n\t\t\tsizeIFrameThrottled(triggerEvent, triggerEventDesc, customHeight, customWidth);\n\t\t} else {\n\t\t\tlog('Trigger event cancelled: '+triggerEvent);\n\t\t}\n\t}\n\n\tfunction lockTrigger(){\n\t\tif (!triggerLocked){\n\t\t\ttriggerLocked = true;\n\t\t\tlog('Trigger event lock on');\n\t\t}\n\t\tclearTimeout(triggerLockedTimer);\n\t\ttriggerLockedTimer = setTimeout(function(){\n\t\t\ttriggerLocked = false;\n\t\t\tlog('Trigger event lock off');\n\t\t\tlog('--');\n\t\t},eventCancelTimer);\n\t}\n\n\tfunction triggerReset(triggerEvent){\n\t\theight = getHeight[heightCalcMode]();\n\t\twidth = getWidth[widthCalcMode]();\n\n\t\tsendMsg(height,width,triggerEvent);\n\t}\n\n\tfunction resetIFrame(triggerEventDesc){\n\t\tvar hcm = heightCalcMode;\n\t\theightCalcMode = heightCalcModeDefault;\n\n\t\tlog('Reset trigger event: ' + triggerEventDesc);\n\t\tlockTrigger();\n\t\ttriggerReset('reset');\n\n\t\theightCalcMode = hcm;\n\t}\n\n\tfunction sendMsg(height,width,triggerEvent,msg,targetOrigin){\n\t\tfunction setTargetOrigin(){\n\t\t\tif (undefined === targetOrigin){\n\t\t\t\ttargetOrigin = targetOriginDefault;\n\t\t\t} else {\n\t\t\t\tlog('Message targetOrigin: '+targetOrigin);\n\t\t\t}\n\t\t}\n\n\t\tfunction sendToParent(){\n\t\t\tvar\n\t\t\t\tsize = height + ':' + width,\n\t\t\t\tmessage = myID + ':' + size + ':' + triggerEvent + (undefined !== msg ? ':' + msg : '');\n\n\t\t\tlog('Sending message to host page (' + message + ')');\n\t\t\ttarget.postMessage( msgID + message, targetOrigin);\n\t\t}\n\n\t\tif(true === sendPermit){\n\t\t\tsetTargetOrigin();\n\t\t\tsendToParent();\n\t\t}\n\t}\n\n\tfunction receiver(event) {\n\t\tfunction isMessageForUs(){\n\t\t\treturn msgID === (''+event.data).substr(0,msgIdLen); //''+ Protects against non-string messages\n\t\t}\n\n\t\tfunction initFromParent(){\n\t\t\tfunction fireInit(){\n\t\t\t\tinitMsg = event.data;\n\t\t\t\ttarget = event.source;\n\n\t\t\t\tinit();\n\t\t\t\tfirstRun = false;\n\t\t\t\tsetTimeout(function(){ initLock = false;},eventCancelTimer);\n\t\t\t}\n\n\t\t\tif (document.body){\n\t\t\t\tfireInit();\n\t\t\t} else {\n\t\t\t\tlog('Waiting for page ready');\n\t\t\t\taddEventListener(window,'readystatechange',initFromParent);\n\t\t\t}\n\t\t}\n\n\t\tfunction resetFromParent(){\n\t\t\tif (!initLock){\n\t\t\t\tlog('Page size reset by host page');\n\t\t\t\ttriggerReset('resetPage');\n\t\t\t} else {\n\t\t\t\tlog('Page reset ignored by init');\n\t\t\t}\n\t\t}\n\n\t\tfunction resizeFromParent(){\n\t\t\tsendSize('resizeParent','Parent window requested size check');\n\t\t}\n\n\t\tfunction moveToAnchor(){\n\t\t\tvar anchor = getData();\n\t\t\tinPageLinks.findTarget(anchor);\n\t\t}\n\n\t\tfunction getMessageType(){\n\t\t\treturn event.data.split(']')[1].split(':')[0];\n\t\t}\n\n\t\tfunction getData(){\n\t\t\treturn event.data.substr(event.data.indexOf(':')+1);\n\t\t}\n\n\t\tfunction isMiddleTier(){\n\t\t\treturn ('iFrameResize' in window);\n\t\t}\n\n\t\tfunction messageFromParent(){\n\t\t\tvar msgBody = getData();\n\n\t\t\tlog('MessageCallback called from parent: ' + msgBody );\n\t\t\tmessageCallback(JSON.parse(msgBody));\n\t\t\tlog(' --');\n\t\t}\n\n\t\tfunction pageInfoFromParent(){\n\t\t\tvar msgBody = getData();\n\t\t\tlog('PageInfoFromParent called from parent: ' + msgBody );\n\t\t\tpageInfoCallback(JSON.parse(msgBody));\n\t\t\tlog(' --');\n\t\t}\n\n\t\tfunction isInitMsg(){\n\t\t\t//Test if this message is from a child below us. This is an ugly test, however, updating\n\t\t\t//the message format would break backwards compatibity.\n\t\t\treturn event.data.split(':')[2] in {'true':1,'false':1};\n\t\t}\n\n\t\tfunction callFromParent(){\n\t\t\tswitch (getMessageType()){\n\t\t\tcase 'reset':\n\t\t\t\tresetFromParent();\n\t\t\t\tbreak;\n\t\t\tcase 'resize':\n\t\t\t\tresizeFromParent();\n\t\t\t\tbreak;\n\t\t\tcase 'inPageLink':\n\t\t\tcase 'moveToAnchor':\n\t\t\t\tmoveToAnchor();\n\t\t\t\tbreak;\n\t\t\tcase 'message':\n\t\t\t\tmessageFromParent();\n\t\t\t\tbreak;\n\t\t\tcase 'pageInfo':\n\t\t\t\tpageInfoFromParent();\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tif (!isMiddleTier() && !isInitMsg()){\n\t\t\t\t\twarn('Unexpected message ('+event.data+')');\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfunction processMessage(){\n\t\t\tif (false === firstRun) {\n\t\t\t\tcallFromParent();\n\t\t\t} else if (isInitMsg()) {\n\t\t\t\tinitFromParent();\n\t\t\t} else {\n\t\t\t\tlog('Ignored message of type \"' + getMessageType() + '\". Received before initialization.');\n\t\t\t}\n\t\t}\n\n\t\tif (isMessageForUs()){\n\t\t\tprocessMessage();\n\t\t}\n\t}\n\n\t//Normally the parent kicks things off when it detects the iFrame has loaded.\n\t//If this script is async-loaded, then tell parent page to retry init.\n\tfunction chkLateLoaded(){\n\t\tif('loading' !== document.readyState){\n\t\t\twindow.parent.postMessage('[iFrameResizerChild]Ready','*');\n\t\t}\n\t}\n\n\taddEventListener(window, 'message', receiver);\n\tchkLateLoaded();\n\n\t\n\n})(window || {});\n"]} \ No newline at end of file
diff --git a/libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.min.js b/libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.min.js
new file mode 100644
index 0000000000..5cfd4e276b
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.min.js
@@ -0,0 +1,10 @@
+/*! iFrame Resizer (iframeSizer.contentWindow.min.js) - v3.5.5 - 2016-06-16
+ * Desc: Include this file in any page being loaded into an iframe
+ * to force the iframe to resize to the content size.
+ * Requires: iframeResizer.min.js on host page.
+ * Copyright: (c) 2016 David J. Bradshaw - dave@bradshaw.net
+ * License: MIT
+ */
+
+!function(a,b){"use strict";function c(b,c,d){"addEventListener"in a?b.addEventListener(c,d,!1):"attachEvent"in a&&b.attachEvent("on"+c,d)}function d(b,c,d){"removeEventListener"in a?b.removeEventListener(c,d,!1):"detachEvent"in a&&b.detachEvent("on"+c,d)}function e(a){return a.charAt(0).toUpperCase()+a.slice(1)}function f(a){var b,c,d,e=null,f=0,g=function(){f=Ha(),e=null,d=a.apply(b,c),e||(b=c=null)};return function(){var h=Ha();f||(f=h);var i=ya-(h-f);return b=this,c=arguments,0>=i||i>ya?(e&&(clearTimeout(e),e=null),f=h,d=a.apply(b,c),e||(b=c=null)):e||(e=setTimeout(g,i)),d}}function g(a){return na+"["+pa+"] "+a}function h(b){ma&&"object"==typeof a.console&&console.log(g(b))}function i(b){"object"==typeof a.console&&console.warn(g(b))}function j(){k(),h("Initialising iFrame ("+location.href+")"),l(),o(),n("background",X),n("padding",_),B(),t(),u(),p(),D(),v(),ja=C(),O("init","Init message from host page"),Ea()}function k(){function a(a){return"true"===a?!0:!1}var c=ia.substr(oa).split(":");pa=c[0],Y=b!==c[1]?Number(c[1]):Y,aa=b!==c[2]?a(c[2]):aa,ma=b!==c[3]?a(c[3]):ma,ka=b!==c[4]?Number(c[4]):ka,V=b!==c[6]?a(c[6]):V,Z=c[7],ga=b!==c[8]?c[8]:ga,X=c[9],_=c[10],va=b!==c[11]?Number(c[11]):va,ja.enable=b!==c[12]?a(c[12]):!1,ra=b!==c[13]?c[13]:ra,Ba=b!==c[14]?c[14]:Ba}function l(){function b(){var b=a.iFrameResizer;h("Reading data from page: "+JSON.stringify(b)),Da="messageCallback"in b?b.messageCallback:Da,Ea="readyCallback"in b?b.readyCallback:Ea,ua="targetOrigin"in b?b.targetOrigin:ua,ga="heightCalculationMethod"in b?b.heightCalculationMethod:ga,Ba="widthCalculationMethod"in b?b.widthCalculationMethod:Ba}function c(a,b){return"function"==typeof a&&(h("Setup custom "+b+"CalcMethod"),Ga[b]=a,a="custom"),a}"iFrameResizer"in a&&Object===a.iFrameResizer.constructor&&(b(),ga=c(ga,"height"),Ba=c(Ba,"width")),h("TargetOrigin for parent set to: "+ua)}function m(a,b){return-1!==b.indexOf("-")&&(i("Negative CSS value ignored for "+a),b=""),b}function n(a,c){b!==c&&""!==c&&"null"!==c&&(document.body.style[a]=c,h("Body "+a+' set to "'+c+'"'))}function o(){b===Z&&(Z=Y+"px"),n("margin",m("margin",Z))}function p(){document.documentElement.style.height="",document.body.style.height="",h('HTML & body height set to "auto"')}function q(b){function f(){O(b.eventName,b.eventType)}var g={add:function(b){c(a,b,f)},remove:function(b){d(a,b,f)}};b.eventNames&&Array.prototype.map?(b.eventName=b.eventNames[0],b.eventNames.map(g[b.method])):g[b.method](b.eventName),h(e(b.method)+" event listener: "+b.eventType)}function r(a){q({method:a,eventType:"Animation Start",eventNames:["animationstart","webkitAnimationStart"]}),q({method:a,eventType:"Animation Iteration",eventNames:["animationiteration","webkitAnimationIteration"]}),q({method:a,eventType:"Animation End",eventNames:["animationend","webkitAnimationEnd"]}),q({method:a,eventType:"Input",eventName:"input"}),q({method:a,eventType:"Mouse Up",eventName:"mouseup"}),q({method:a,eventType:"Mouse Down",eventName:"mousedown"}),q({method:a,eventType:"Orientation Change",eventName:"orientationchange"}),q({method:a,eventType:"Print",eventName:["afterprint","beforeprint"]}),q({method:a,eventType:"Ready State Change",eventName:"readystatechange"}),q({method:a,eventType:"Touch Start",eventName:"touchstart"}),q({method:a,eventType:"Touch End",eventName:"touchend"}),q({method:a,eventType:"Touch Cancel",eventName:"touchcancel"}),q({method:a,eventType:"Transition Start",eventNames:["transitionstart","webkitTransitionStart","MSTransitionStart","oTransitionStart","otransitionstart"]}),q({method:a,eventType:"Transition Iteration",eventNames:["transitioniteration","webkitTransitionIteration","MSTransitionIteration","oTransitionIteration","otransitioniteration"]}),q({method:a,eventType:"Transition End",eventNames:["transitionend","webkitTransitionEnd","MSTransitionEnd","oTransitionEnd","otransitionend"]}),"child"===ra&&q({method:a,eventType:"IFrame Resized",eventName:"resize"})}function s(a,b,c,d){return b!==a&&(a in c||(i(a+" is not a valid option for "+d+"CalculationMethod."),a=b),h(d+' calculation method set to "'+a+'"')),a}function t(){ga=s(ga,fa,Ia,"height")}function u(){Ba=s(Ba,Aa,Ja,"width")}function v(){!0===V?(r("add"),G()):h("Auto Resize disabled")}function w(){h("Disable outgoing messages"),sa=!1}function x(){h("Remove event listener: Message"),d(a,"message",T)}function y(){null!==$&&$.disconnect()}function z(){r("remove"),y(),clearInterval(la)}function A(){w(),x(),!0===V&&z()}function B(){var a=document.createElement("div");a.style.clear="both",a.style.display="block",document.body.appendChild(a)}function C(){function d(){return{x:a.pageXOffset!==b?a.pageXOffset:document.documentElement.scrollLeft,y:a.pageYOffset!==b?a.pageYOffset:document.documentElement.scrollTop}}function e(a){var b=a.getBoundingClientRect(),c=d();return{x:parseInt(b.left,10)+parseInt(c.x,10),y:parseInt(b.top,10)+parseInt(c.y,10)}}function f(a){function c(a){var b=e(a);h("Moving to in page link (#"+d+") at x: "+b.x+" y: "+b.y),S(b.y,b.x,"scrollToOffset")}var d=a.split("#")[1]||a,f=decodeURIComponent(d),g=document.getElementById(f)||document.getElementsByName(f)[0];b!==g?c(g):(h("In page link (#"+d+") not found in iFrame, so sending to parent"),S(0,0,"inPageLink","#"+d))}function g(){""!==location.hash&&"#"!==location.hash&&f(location.href)}function j(){function a(a){function b(a){a.preventDefault(),f(this.getAttribute("href"))}"#"!==a.getAttribute("href")&&c(a,"click",b)}Array.prototype.forEach.call(document.querySelectorAll('a[href^="#"]'),a)}function k(){c(a,"hashchange",g)}function l(){setTimeout(g,ca)}function m(){Array.prototype.forEach&&document.querySelectorAll?(h("Setting up location.hash handlers"),j(),k(),l()):i("In page linking not fully supported in this browser! (See README.md for IE8 workaround)")}return ja.enable?m():h("In page linking not enabled"),{findTarget:f}}function D(){h("Enable public methods"),Ca.parentIFrame={autoResize:function(a){return!0===a&&!1===V?(V=!0,v()):!1===a&&!0===V&&(V=!1,z()),V},close:function(){S(0,0,"close"),A()},getId:function(){return pa},getPageInfo:function(a){"function"==typeof a?(Fa=a,S(0,0,"pageInfo")):(Fa=function(){},S(0,0,"pageInfoStop"))},moveToAnchor:function(a){ja.findTarget(a)},reset:function(){R("parentIFrame.reset")},scrollTo:function(a,b){S(b,a,"scrollTo")},scrollToOffset:function(a,b){S(b,a,"scrollToOffset")},sendMessage:function(a,b){S(0,0,"message",JSON.stringify(a),b)},setHeightCalculationMethod:function(a){ga=a,t()},setWidthCalculationMethod:function(a){Ba=a,u()},setTargetOrigin:function(a){h("Set targetOrigin: "+a),ua=a},size:function(a,b){var c=""+(a?a:"")+(b?","+b:"");O("size","parentIFrame.size("+c+")",a,b)}}}function E(){0!==ka&&(h("setInterval: "+ka+"ms"),la=setInterval(function(){O("interval","setInterval: "+ka)},Math.abs(ka)))}function F(){function c(a){function b(a){!1===a.complete&&(h("Attach listeners to "+a.src),a.addEventListener("load",g,!1),a.addEventListener("error",i,!1),l.push(a))}"attributes"===a.type&&"src"===a.attributeName?b(a.target):"childList"===a.type&&Array.prototype.forEach.call(a.target.querySelectorAll("img"),b)}function d(a){l.splice(l.indexOf(a),1)}function e(a){h("Remove listeners from "+a.src),a.removeEventListener("load",g,!1),a.removeEventListener("error",i,!1),d(a)}function f(a,c,d){e(a.target),O(c,d+": "+a.target.src,b,b)}function g(a){f(a,"imageLoad","Image loaded")}function i(a){f(a,"imageLoadFailed","Image load failed")}function j(a){O("mutationObserver","mutationObserver: "+a[0].target+" "+a[0].type),a.forEach(c)}function k(){var a=document.querySelector("body"),b={attributes:!0,attributeOldValue:!1,characterData:!0,characterDataOldValue:!1,childList:!0,subtree:!0};return n=new m(j),h("Create body MutationObserver"),n.observe(a,b),n}var l=[],m=a.MutationObserver||a.WebKitMutationObserver,n=k();return{disconnect:function(){"disconnect"in n&&(h("Disconnect body MutationObserver"),n.disconnect(),l.forEach(e))}}}function G(){var b=0>ka;a.MutationObserver||a.WebKitMutationObserver?b?E():$=F():(h("MutationObserver not supported in this browser!"),E())}function H(a,b){function c(a){var c=/^\d+(px)?$/i;if(c.test(a))return parseInt(a,W);var d=b.style.left,e=b.runtimeStyle.left;return b.runtimeStyle.left=b.currentStyle.left,b.style.left=a||0,a=b.style.pixelLeft,b.style.left=d,b.runtimeStyle.left=e,a}var d=0;return b=b||document.body,"defaultView"in document&&"getComputedStyle"in document.defaultView?(d=document.defaultView.getComputedStyle(b,null),d=null!==d?d[a]:0):d=c(b.currentStyle[a]),parseInt(d,W)}function I(a){a>ya/2&&(ya=2*a,h("Event throttle increased to "+ya+"ms"))}function J(a,b){for(var c=b.length,d=0,f=0,g=e(a),i=Ha(),j=0;c>j;j++)d=b[j].getBoundingClientRect()[a]+H("margin"+g,b[j]),d>f&&(f=d);return i=Ha()-i,h("Parsed "+c+" HTML elements"),h("Element position calculated in "+i+"ms"),I(i),f}function K(a){return[a.bodyOffset(),a.bodyScroll(),a.documentElementOffset(),a.documentElementScroll()]}function L(a,b){function c(){return i("No tagged elements ("+b+") found on page"),ea}var d=document.querySelectorAll("["+b+"]");return 0===d.length?c():J(a,d)}function M(){return document.querySelectorAll("body *")}function N(a,c,d,e){function f(){ea=m,za=n,S(ea,za,a)}function g(){function a(a,b){var c=Math.abs(a-b)<=va;return!c}return m=b!==d?d:Ia[ga](),n=b!==e?e:Ja[Ba](),a(ea,m)||aa&&a(za,n)}function i(){return!(a in{init:1,interval:1,size:1})}function j(){return ga in qa||aa&&Ba in qa}function k(){h("No change in size detected")}function l(){i()&&j()?R(c):a in{interval:1}||k()}var m,n;g()||"init"===a?(P(),f()):l()}function O(a,b,c,d){function e(){a in{reset:1,resetPage:1,init:1}||h("Trigger event: "+b)}function f(){return wa&&a in ba}f()?h("Trigger event cancelled: "+a):(e(),Ka(a,b,c,d))}function P(){wa||(wa=!0,h("Trigger event lock on")),clearTimeout(xa),xa=setTimeout(function(){wa=!1,h("Trigger event lock off"),h("--")},ca)}function Q(a){ea=Ia[ga](),za=Ja[Ba](),S(ea,za,a)}function R(a){var b=ga;ga=fa,h("Reset trigger event: "+a),P(),Q("reset"),ga=b}function S(a,c,d,e,f){function g(){b===f?f=ua:h("Message targetOrigin: "+f)}function i(){var g=a+":"+c,i=pa+":"+g+":"+d+(b!==e?":"+e:"");h("Sending message to host page ("+i+")"),ta.postMessage(na+i,f)}!0===sa&&(g(),i())}function T(b){function d(){return na===(""+b.data).substr(0,oa)}function e(){function d(){ia=b.data,ta=b.source,j(),da=!1,setTimeout(function(){ha=!1},ca)}document.body?d():(h("Waiting for page ready"),c(a,"readystatechange",e))}function f(){ha?h("Page reset ignored by init"):(h("Page size reset by host page"),Q("resetPage"))}function g(){O("resizeParent","Parent window requested size check")}function k(){var a=m();ja.findTarget(a)}function l(){return b.data.split("]")[1].split(":")[0]}function m(){return b.data.substr(b.data.indexOf(":")+1)}function n(){return"iFrameResize"in a}function o(){var a=m();h("MessageCallback called from parent: "+a),Da(JSON.parse(a)),h(" --")}function p(){var a=m();h("PageInfoFromParent called from parent: "+a),Fa(JSON.parse(a)),h(" --")}function q(){return b.data.split(":")[2]in{"true":1,"false":1}}function r(){switch(l()){case"reset":f();break;case"resize":g();break;case"inPageLink":case"moveToAnchor":k();break;case"message":o();break;case"pageInfo":p();break;default:n()||q()||i("Unexpected message ("+b.data+")")}}function s(){!1===da?r():q()?e():h('Ignored message of type "'+l()+'". Received before initialization.')}d()&&s()}function U(){"loading"!==document.readyState&&a.parent.postMessage("[iFrameResizerChild]Ready","*")}var V=!0,W=10,X="",Y=0,Z="",$=null,_="",aa=!1,ba={resize:1,click:1},ca=128,da=!0,ea=1,fa="bodyOffset",ga=fa,ha=!0,ia="",ja={},ka=32,la=null,ma=!1,na="[iFrameSizer]",oa=na.length,pa="",qa={max:1,min:1,bodyScroll:1,documentElementScroll:1},ra="child",sa=!0,ta=a.parent,ua="*",va=0,wa=!1,xa=null,ya=16,za=1,Aa="scroll",Ba=Aa,Ca=a,Da=function(){i("MessageCallback function not defined")},Ea=function(){},Fa=function(){},Ga={height:function(){return i("Custom height calculation function not defined"),document.documentElement.offsetHeight},width:function(){return i("Custom width calculation function not defined"),document.body.scrollWidth}},Ha=Date.now||function(){return(new Date).getTime()},Ia={bodyOffset:function(){return document.body.offsetHeight+H("marginTop")+H("marginBottom")},offset:function(){return Ia.bodyOffset()},bodyScroll:function(){return document.body.scrollHeight},custom:function(){return Ga.height()},documentElementOffset:function(){return document.documentElement.offsetHeight},documentElementScroll:function(){return document.documentElement.scrollHeight},max:function(){return Math.max.apply(null,K(Ia))},min:function(){return Math.min.apply(null,K(Ia))},grow:function(){return Ia.max()},lowestElement:function(){return Math.max(Ia.bodyOffset(),J("bottom",M()))},taggedElement:function(){return L("bottom","data-iframe-height")}},Ja={bodyScroll:function(){return document.body.scrollWidth},bodyOffset:function(){return document.body.offsetWidth},custom:function(){return Ga.width()},documentElementScroll:function(){return document.documentElement.scrollWidth},documentElementOffset:function(){return document.documentElement.offsetWidth},scroll:function(){return Math.max(Ja.bodyScroll(),Ja.documentElementScroll())},max:function(){return Math.max.apply(null,K(Ja))},min:function(){return Math.min.apply(null,K(Ja))},rightMostElement:function(){return J("right",M())},taggedElement:function(){return L("right","data-iframe-width")}},Ka=f(N);c(a,"message",T),U()}(window||{});
+//# sourceMappingURL=iframeResizer.contentWindow.map \ No newline at end of file
diff --git a/libs/bower_components/iframe-resizer/js/iframeResizer.js b/libs/bower_components/iframe-resizer/js/iframeResizer.js
new file mode 100644
index 0000000000..897a632ce4
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/js/iframeResizer.js
@@ -0,0 +1,1002 @@
+/*
+ * File: iframeResizer.js
+ * Desc: Force iframes to size to content.
+ * Requires: iframeResizer.contentWindow.js to be loaded into the target frame.
+ * Doc: https://github.com/davidjbradshaw/iframe-resizer
+ * Author: David J. Bradshaw - dave@bradshaw.net
+ * Contributor: Jure Mav - jure.mav@gmail.com
+ * Contributor: Reed Dadoune - reed@dadoune.com
+ */
+
+
+;(function(window) {
+ 'use strict';
+
+ var
+ count = 0,
+ logEnabled = false,
+ hiddenCheckEnabled = false,
+ msgHeader = 'message',
+ msgHeaderLen = msgHeader.length,
+ msgId = '[iFrameSizer]', //Must match iframe msg ID
+ msgIdLen = msgId.length,
+ pagePosition = null,
+ requestAnimationFrame = window.requestAnimationFrame,
+ resetRequiredMethods = {max:1,scroll:1,bodyScroll:1,documentElementScroll:1},
+ settings = {},
+ timer = null,
+ logId = 'Host Page',
+
+ defaults = {
+ autoResize : true,
+ bodyBackground : null,
+ bodyMargin : null,
+ bodyMarginV1 : 8,
+ bodyPadding : null,
+ checkOrigin : true,
+ inPageLinks : false,
+ enablePublicMethods : true,
+ heightCalculationMethod : 'bodyOffset',
+ id : 'iFrameResizer',
+ interval : 32,
+ log : false,
+ maxHeight : Infinity,
+ maxWidth : Infinity,
+ minHeight : 0,
+ minWidth : 0,
+ resizeFrom : 'parent',
+ scrolling : false,
+ sizeHeight : true,
+ sizeWidth : false,
+ tolerance : 0,
+ widthCalculationMethod : 'scroll',
+ closedCallback : function(){},
+ initCallback : function(){},
+ messageCallback : function(){warn('MessageCallback function not defined');},
+ resizedCallback : function(){},
+ scrollCallback : function(){return true;}
+ };
+
+ function addEventListener(obj,evt,func){
+ /* istanbul ignore else */ // Not testable in PhantonJS
+ if ('addEventListener' in window){
+ obj.addEventListener(evt,func, false);
+ } else if ('attachEvent' in window){//IE
+ obj.attachEvent('on'+evt,func);
+ }
+ }
+
+ function removeEventListener(el,evt,func){
+ /* istanbul ignore else */ // Not testable in phantonJS
+ if ('removeEventListener' in window){
+ el.removeEventListener(evt,func, false);
+ } else if ('detachEvent' in window){ //IE
+ el.detachEvent('on'+evt,func);
+ }
+ }
+
+ function setupRequestAnimationFrame(){
+ var
+ vendors = ['moz', 'webkit', 'o', 'ms'],
+ x;
+
+ // Remove vendor prefixing if prefixed and break early if not
+ for (x = 0; x < vendors.length && !requestAnimationFrame; x += 1) {
+ requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
+ }
+
+ if (!(requestAnimationFrame)){
+ log('setup','RequestAnimationFrame not supported');
+ }
+ }
+
+ function getMyID(iframeId){
+ var retStr = 'Host page: '+iframeId;
+
+ if (window.top!==window.self){
+ if (window.parentIFrame && window.parentIFrame.getId){
+ retStr = window.parentIFrame.getId()+': '+iframeId;
+ } else {
+ retStr = 'Nested host page: '+iframeId;
+ }
+ }
+
+ return retStr;
+ }
+
+ function formatLogHeader(iframeId){
+ return msgId + '[' + getMyID(iframeId) + ']';
+ }
+
+ function isLogEnabled(iframeId){
+ return settings[iframeId] ? settings[iframeId].log : logEnabled;
+ }
+
+ function log(iframeId,msg){
+ output('log',iframeId,msg,isLogEnabled(iframeId));
+ }
+
+ function info(iframeId,msg){
+ output('info',iframeId,msg,isLogEnabled(iframeId));
+ }
+
+ function warn(iframeId,msg){
+ output('warn',iframeId,msg,true);
+ }
+
+ function output(type,iframeId,msg,enabled){
+ if (true === enabled && 'object' === typeof window.console){
+ console[type](formatLogHeader(iframeId),msg);
+ }
+ }
+
+ function iFrameListener(event){
+ function resizeIFrame(){
+ function resize(){
+ setSize(messageData);
+ setPagePosition(iframeId);
+ }
+
+ ensureInRange('Height');
+ ensureInRange('Width');
+
+ syncResize(resize,messageData,'init');
+ }
+
+ function processMsg(){
+ var data = msg.substr(msgIdLen).split(':');
+
+ return {
+ iframe: settings[data[0]].iframe,
+ id: data[0],
+ height: data[1],
+ width: data[2],
+ type: data[3]
+ };
+ }
+
+ function ensureInRange(Dimension){
+ var
+ max = Number(settings[iframeId]['max' + Dimension]),
+ min = Number(settings[iframeId]['min' + Dimension]),
+ dimension = Dimension.toLowerCase(),
+ size = Number(messageData[dimension]);
+
+ log(iframeId,'Checking ' + dimension + ' is in range ' + min + '-' + max);
+
+ if (size<min) {
+ size=min;
+ log(iframeId,'Set ' + dimension + ' to min value');
+ }
+
+ if (size>max) {
+ size=max;
+ log(iframeId,'Set ' + dimension + ' to max value');
+ }
+
+ messageData[dimension] = '' + size;
+ }
+
+
+ function isMessageFromIFrame(){
+ function checkAllowedOrigin(){
+ function checkList(){
+ var
+ i = 0,
+ retCode = false;
+
+ log(iframeId,'Checking connection is from allowed list of origins: ' + checkOrigin);
+
+ for (; i < checkOrigin.length; i++) {
+ if (checkOrigin[i] === origin) {
+ retCode = true;
+ break;
+ }
+ }
+ return retCode;
+ }
+
+ function checkSingle(){
+ var remoteHost = settings[iframeId].remoteHost;
+ log(iframeId,'Checking connection is from: '+remoteHost);
+ return origin === remoteHost;
+ }
+
+ return checkOrigin.constructor === Array ? checkList() : checkSingle();
+ }
+
+ var
+ origin = event.origin,
+ checkOrigin = settings[iframeId].checkOrigin;
+
+ if (checkOrigin && (''+origin !== 'null') && !checkAllowedOrigin()) {
+ throw new Error(
+ 'Unexpected message received from: ' + origin +
+ ' for ' + messageData.iframe.id +
+ '. Message was: ' + event.data +
+ '. This error can be disabled by setting the checkOrigin: false option or by providing of array of trusted domains.'
+ );
+ }
+
+ return true;
+ }
+
+ function isMessageForUs(){
+ return msgId === (('' + msg).substr(0,msgIdLen)) && (msg.substr(msgIdLen).split(':')[0] in settings); //''+Protects against non-string msg
+ }
+
+ function isMessageFromMetaParent(){
+ //Test if this message is from a parent above us. This is an ugly test, however, updating
+ //the message format would break backwards compatibity.
+ var retCode = messageData.type in {'true':1,'false':1,'undefined':1};
+
+ if (retCode){
+ log(iframeId,'Ignoring init message from meta parent page');
+ }
+
+ return retCode;
+ }
+
+ function getMsgBody(offset){
+ return msg.substr(msg.indexOf(':')+msgHeaderLen+offset);
+ }
+
+ function forwardMsgFromIFrame(msgBody){
+ log(iframeId,'MessageCallback passed: {iframe: '+ messageData.iframe.id + ', message: ' + msgBody + '}');
+ callback('messageCallback',{
+ iframe: messageData.iframe,
+ message: JSON.parse(msgBody)
+ });
+ log(iframeId,'--');
+ }
+
+ function getPageInfo(){
+ var
+ bodyPosition = document.body.getBoundingClientRect(),
+ iFramePosition = messageData.iframe.getBoundingClientRect();
+
+ return JSON.stringify({
+ iframeHeight: iFramePosition.height,
+ iframeWidth: iFramePosition.width,
+ clientHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
+ clientWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
+ offsetTop: parseInt(iFramePosition.top - bodyPosition.top, 10),
+ offsetLeft: parseInt(iFramePosition.left - bodyPosition.left, 10),
+ scrollTop: window.pageYOffset,
+ scrollLeft: window.pageXOffset
+ });
+ }
+
+ function sendPageInfoToIframe(iframe,iframeId){
+ function debouncedTrigger(){
+ trigger(
+ 'Send Page Info',
+ 'pageInfo:' + getPageInfo(),
+ iframe,
+ iframeId
+ );
+ }
+
+ debouce(debouncedTrigger,32);
+ }
+
+
+ function startPageInfoMonitor(){
+ function setListener(type,func){
+ function sendPageInfo(){
+ if (settings[id]){
+ sendPageInfoToIframe(settings[id].iframe,id);
+ } else {
+ stop();
+ }
+ }
+
+ ['scroll','resize'].forEach(function(evt){
+ log(id, type + evt + ' listener for sendPageInfo');
+ func(window,evt,sendPageInfo);
+ });
+ }
+
+ function stop(){
+ setListener('Remove ', removeEventListener);
+ }
+
+ function start(){
+ setListener('Add ', addEventListener);
+ }
+
+ var id = iframeId; //Create locally scoped copy of iFrame ID
+
+ start();
+
+ settings[id].stopPageInfo = stop;
+ }
+
+ function stopPageInfoMonitor(){
+ if (settings[iframeId] && settings[iframeId].stopPageInfo){
+ settings[iframeId].stopPageInfo();
+ delete settings[iframeId].stopPageInfo;
+ }
+ }
+
+ function checkIFrameExists(){
+ var retBool = true;
+
+ if (null === messageData.iframe) {
+ warn(iframeId,'IFrame ('+messageData.id+') not found');
+ retBool = false;
+ }
+ return retBool;
+ }
+
+ function getElementPosition(target){
+ var iFramePosition = target.getBoundingClientRect();
+
+ getPagePosition(iframeId);
+
+ return {
+ x: Math.floor( Number(iFramePosition.left) + Number(pagePosition.x) ),
+ y: Math.floor( Number(iFramePosition.top) + Number(pagePosition.y) )
+ };
+ }
+
+ function scrollRequestFromChild(addOffset){
+ /* istanbul ignore next */ //Not testable in Karma
+ function reposition(){
+ pagePosition = newPosition;
+ scrollTo();
+ log(iframeId,'--');
+ }
+
+ function calcOffset(){
+ return {
+ x: Number(messageData.width) + offset.x,
+ y: Number(messageData.height) + offset.y
+ };
+ }
+
+ function scrollParent(){
+ if (window.parentIFrame){
+ window.parentIFrame['scrollTo'+(addOffset?'Offset':'')](newPosition.x,newPosition.y);
+ } else {
+ warn(iframeId,'Unable to scroll to requested position, window.parentIFrame not found');
+ }
+ }
+
+ var
+ offset = addOffset ? getElementPosition(messageData.iframe) : {x:0,y:0},
+ newPosition = calcOffset();
+
+ log(iframeId,'Reposition requested from iFrame (offset x:'+offset.x+' y:'+offset.y+')');
+
+ if(window.top!==window.self){
+ scrollParent();
+ } else {
+ reposition();
+ }
+ }
+
+ function scrollTo(){
+ if (false !== callback('scrollCallback',pagePosition)){
+ setPagePosition(iframeId);
+ } else {
+ unsetPagePosition();
+ }
+ }
+
+ function findTarget(location){
+ function jumpToTarget(){
+ var jumpPosition = getElementPosition(target);
+
+ log(iframeId,'Moving to in page link (#'+hash+') at x: '+jumpPosition.x+' y: '+jumpPosition.y);
+ pagePosition = {
+ x: jumpPosition.x,
+ y: jumpPosition.y
+ };
+
+ scrollTo();
+ log(iframeId,'--');
+ }
+
+ function jumpToParent(){
+ if (window.parentIFrame){
+ window.parentIFrame.moveToAnchor(hash);
+ } else {
+ log(iframeId,'In page link #'+hash+' not found and window.parentIFrame not found');
+ }
+ }
+
+ var
+ hash = location.split('#')[1] || '',
+ hashData = decodeURIComponent(hash),
+ target = document.getElementById(hashData) || document.getElementsByName(hashData)[0];
+
+ if (target){
+ jumpToTarget();
+ } else if(window.top!==window.self){
+ jumpToParent();
+ } else {
+ log(iframeId,'In page link #'+hash+' not found');
+ }
+ }
+
+ function callback(funcName,val){
+ return chkCallback(iframeId,funcName,val);
+ }
+
+ function actionMsg(){
+
+ if(settings[iframeId].firstRun) firstRun();
+
+ switch(messageData.type){
+ case 'close':
+ closeIFrame(messageData.iframe);
+ break;
+ case 'message':
+ forwardMsgFromIFrame(getMsgBody(6));
+ break;
+ case 'scrollTo':
+ scrollRequestFromChild(false);
+ break;
+ case 'scrollToOffset':
+ scrollRequestFromChild(true);
+ break;
+ case 'pageInfo':
+ sendPageInfoToIframe(settings[iframeId].iframe,iframeId);
+ startPageInfoMonitor();
+ break;
+ case 'pageInfoStop':
+ stopPageInfoMonitor();
+ break;
+ case 'inPageLink':
+ findTarget(getMsgBody(9));
+ break;
+ case 'reset':
+ resetIFrame(messageData);
+ break;
+ case 'init':
+ resizeIFrame();
+ callback('initCallback',messageData.iframe);
+ callback('resizedCallback',messageData);
+ break;
+ default:
+ resizeIFrame();
+ callback('resizedCallback',messageData);
+ }
+ }
+
+ function hasSettings(iframeId){
+ var retBool = true;
+
+ if (!settings[iframeId]){
+ retBool = false;
+ warn(messageData.type + ' No settings for ' + iframeId + '. Message was: ' + msg);
+ }
+
+ return retBool;
+ }
+
+ function iFrameReadyMsgReceived(){
+ for (var iframeId in settings){
+ trigger('iFrame requested init',createOutgoingMsg(iframeId),document.getElementById(iframeId),iframeId);
+ }
+ }
+
+ function firstRun() {
+ settings[iframeId].firstRun = false;
+ }
+
+ var
+ msg = event.data,
+ messageData = {},
+ iframeId = null;
+
+ if('[iFrameResizerChild]Ready' === msg){
+ iFrameReadyMsgReceived();
+ } else if (isMessageForUs()){
+ messageData = processMsg();
+ iframeId = logId = messageData.id;
+
+ if (!isMessageFromMetaParent() && hasSettings(iframeId)){
+ log(iframeId,'Received: '+msg);
+
+ if ( checkIFrameExists() && isMessageFromIFrame() ){
+ actionMsg();
+ }
+ }
+ } else {
+ info(iframeId,'Ignored: '+msg);
+ }
+
+ }
+
+
+ function chkCallback(iframeId,funcName,val){
+ var
+ func = null,
+ retVal = null;
+
+ if(settings[iframeId]){
+ func = settings[iframeId][funcName];
+
+ if( 'function' === typeof func){
+ retVal = func(val);
+ } else {
+ throw new TypeError(funcName+' on iFrame['+iframeId+'] is not a function');
+ }
+ }
+
+ return retVal;
+ }
+
+ function closeIFrame(iframe){
+ var iframeId = iframe.id;
+
+ log(iframeId,'Removing iFrame: '+iframeId);
+ iframe.parentNode.removeChild(iframe);
+ chkCallback(iframeId,'closedCallback',iframeId);
+ log(iframeId,'--');
+ delete settings[iframeId];
+ }
+
+ function getPagePosition(iframeId){
+ if(null === pagePosition){
+ pagePosition = {
+ x: (window.pageXOffset !== undefined) ? window.pageXOffset : document.documentElement.scrollLeft,
+ y: (window.pageYOffset !== undefined) ? window.pageYOffset : document.documentElement.scrollTop
+ };
+ log(iframeId,'Get page position: '+pagePosition.x+','+pagePosition.y);
+ }
+ }
+
+ function setPagePosition(iframeId){
+ if(null !== pagePosition){
+ window.scrollTo(pagePosition.x,pagePosition.y);
+ log(iframeId,'Set page position: '+pagePosition.x+','+pagePosition.y);
+ unsetPagePosition();
+ }
+ }
+
+ function unsetPagePosition(){
+ pagePosition = null;
+ }
+
+ function resetIFrame(messageData){
+ function reset(){
+ setSize(messageData);
+ trigger('reset','reset',messageData.iframe,messageData.id);
+ }
+
+ log(messageData.id,'Size reset requested by '+('init'===messageData.type?'host page':'iFrame'));
+ getPagePosition(messageData.id);
+ syncResize(reset,messageData,'reset');
+ }
+
+ function setSize(messageData){
+ function setDimension(dimension){
+ messageData.iframe.style[dimension] = messageData[dimension] + 'px';
+ log(
+ messageData.id,
+ 'IFrame (' + iframeId +
+ ') ' + dimension +
+ ' set to ' + messageData[dimension] + 'px'
+ );
+ }
+
+ function chkZero(dimension){
+ //FireFox sets dimension of hidden iFrames to zero.
+ //So if we detect that set up an event to check for
+ //when iFrame becomes visible.
+
+ /* istanbul ignore next */ //Not testable in PhantomJS
+ if (!hiddenCheckEnabled && '0' === messageData[dimension]){
+ hiddenCheckEnabled = true;
+ log(iframeId,'Hidden iFrame detected, creating visibility listener');
+ fixHiddenIFrames();
+ }
+ }
+
+ function processDimension(dimension){
+ setDimension(dimension);
+ chkZero(dimension);
+ }
+
+ var iframeId = messageData.iframe.id;
+
+ if(settings[iframeId]){
+ if( settings[iframeId].sizeHeight) { processDimension('height'); }
+ if( settings[iframeId].sizeWidth ) { processDimension('width'); }
+ }
+ }
+
+ function syncResize(func,messageData,doNotSync){
+ /* istanbul ignore if */ //Not testable in PhantomJS
+ if(doNotSync!==messageData.type && requestAnimationFrame){
+ log(messageData.id,'Requesting animation frame');
+ requestAnimationFrame(func);
+ } else {
+ func();
+ }
+ }
+
+ function trigger(calleeMsg,msg,iframe,id){
+ function postMessageToIFrame(){
+ var target = settings[id].targetOrigin;
+ log(id,'[' + calleeMsg + '] Sending msg to iframe['+id+'] ('+msg+') targetOrigin: '+target);
+ iframe.contentWindow.postMessage( msgId + msg, target );
+ }
+
+ function iFrameNotFound(){
+ info(id,'[' + calleeMsg + '] IFrame('+id+') not found');
+ if(settings[id]) {
+ delete settings[id];
+ }
+ }
+
+ function chkAndSend(){
+ if(iframe && 'contentWindow' in iframe && (null !== iframe.contentWindow)){ //Null test for PhantomJS
+ postMessageToIFrame();
+ } else {
+ iFrameNotFound();
+ }
+ }
+
+ id = id || iframe.id;
+
+ if(settings[id]) {
+ chkAndSend();
+ }
+
+ }
+
+ function createOutgoingMsg(iframeId){
+ return iframeId +
+ ':' + settings[iframeId].bodyMarginV1 +
+ ':' + settings[iframeId].sizeWidth +
+ ':' + settings[iframeId].log +
+ ':' + settings[iframeId].interval +
+ ':' + settings[iframeId].enablePublicMethods +
+ ':' + settings[iframeId].autoResize +
+ ':' + settings[iframeId].bodyMargin +
+ ':' + settings[iframeId].heightCalculationMethod +
+ ':' + settings[iframeId].bodyBackground +
+ ':' + settings[iframeId].bodyPadding +
+ ':' + settings[iframeId].tolerance +
+ ':' + settings[iframeId].inPageLinks +
+ ':' + settings[iframeId].resizeFrom +
+ ':' + settings[iframeId].widthCalculationMethod;
+ }
+
+ function setupIFrame(iframe,options){
+ function setLimits(){
+ function addStyle(style){
+ if ((Infinity !== settings[iframeId][style]) && (0 !== settings[iframeId][style])){
+ iframe.style[style] = settings[iframeId][style] + 'px';
+ log(iframeId,'Set '+style+' = '+settings[iframeId][style]+'px');
+ }
+ }
+
+ function chkMinMax(dimension){
+ if (settings[iframeId]['min'+dimension]>settings[iframeId]['max'+dimension]){
+ throw new Error('Value for min'+dimension+' can not be greater than max'+dimension);
+ }
+ }
+
+ chkMinMax('Height');
+ chkMinMax('Width');
+
+ addStyle('maxHeight');
+ addStyle('minHeight');
+ addStyle('maxWidth');
+ addStyle('minWidth');
+ }
+
+ function newId(){
+ var id = ((options && options.id) || defaults.id + count++);
+ if (null!==document.getElementById(id)){
+ id = id + count++;
+ }
+ return id;
+ }
+
+ function ensureHasId(iframeId){
+ logId=iframeId;
+ if (''===iframeId){
+ iframe.id = iframeId = newId();
+ logEnabled = (options || {}).log;
+ logId=iframeId;
+ log(iframeId,'Added missing iframe ID: '+ iframeId +' (' + iframe.src + ')');
+ }
+
+
+ return iframeId;
+ }
+
+ function setScrolling(){
+ log(iframeId,'IFrame scrolling ' + (settings[iframeId].scrolling ? 'enabled' : 'disabled') + ' for ' + iframeId);
+ iframe.style.overflow = false === settings[iframeId].scrolling ? 'hidden' : 'auto';
+ iframe.scrolling = false === settings[iframeId].scrolling ? 'no' : 'yes';
+ }
+
+ //The V1 iFrame script expects an int, where as in V2 expects a CSS
+ //string value such as '1px 3em', so if we have an int for V2, set V1=V2
+ //and then convert V2 to a string PX value.
+ function setupBodyMarginValues(){
+ if (('number'===typeof(settings[iframeId].bodyMargin)) || ('0'===settings[iframeId].bodyMargin)){
+ settings[iframeId].bodyMarginV1 = settings[iframeId].bodyMargin;
+ settings[iframeId].bodyMargin = '' + settings[iframeId].bodyMargin + 'px';
+ }
+ }
+
+ function checkReset(){
+ // Reduce scope of firstRun to function, because IE8's JS execution
+ // context stack is borked and this value gets externally
+ // changed midway through running this function!!!
+ var
+ firstRun = settings[iframeId].firstRun,
+ resetRequertMethod = settings[iframeId].heightCalculationMethod in resetRequiredMethods;
+
+ if (!firstRun && resetRequertMethod){
+ resetIFrame({iframe:iframe, height:0, width:0, type:'init'});
+ }
+ }
+
+ function setupIFrameObject(){
+ if(Function.prototype.bind){ //Ignore unpolyfilled IE8.
+ settings[iframeId].iframe.iFrameResizer = {
+
+ close : closeIFrame.bind(null,settings[iframeId].iframe),
+
+ resize : trigger.bind(null,'Window resize', 'resize', settings[iframeId].iframe),
+
+ moveToAnchor : function(anchor){
+ trigger('Move to anchor','moveToAnchor:'+anchor, settings[iframeId].iframe,iframeId);
+ },
+
+ sendMessage : function(message){
+ message = JSON.stringify(message);
+ trigger('Send Message','message:'+message, settings[iframeId].iframe,iframeId);
+ }
+ };
+ }
+ }
+
+ //We have to call trigger twice, as we can not be sure if all
+ //iframes have completed loading when this code runs. The
+ //event listener also catches the page changing in the iFrame.
+ function init(msg){
+ function iFrameLoaded(){
+ trigger('iFrame.onload',msg,iframe);
+ checkReset();
+ }
+
+ addEventListener(iframe,'load',iFrameLoaded);
+ trigger('init',msg,iframe);
+ }
+
+ function checkOptions(options){
+ if ('object' !== typeof options){
+ throw new TypeError('Options is not an object');
+ }
+ }
+
+ function copyOptions(options){
+ for (var option in defaults) {
+ if (defaults.hasOwnProperty(option)){
+ settings[iframeId][option] = options.hasOwnProperty(option) ? options[option] : defaults[option];
+ }
+ }
+ }
+
+ function getTargetOrigin (remoteHost){
+ return ('' === remoteHost || 'file://' === remoteHost) ? '*' : remoteHost;
+ }
+
+ function processOptions(options){
+ options = options || {};
+ settings[iframeId] = {
+ firstRun : true,
+ iframe : iframe,
+ remoteHost : iframe.src.split('/').slice(0,3).join('/')
+ };
+
+ checkOptions(options);
+ copyOptions(options);
+
+ settings[iframeId].targetOrigin = true === settings[iframeId].checkOrigin ? getTargetOrigin(settings[iframeId].remoteHost) : '*';
+ }
+
+ function beenHere(){
+ return (iframeId in settings && 'iFrameResizer' in iframe);
+ }
+
+ var iframeId = ensureHasId(iframe.id);
+
+ if (!beenHere()){
+ processOptions(options);
+ setScrolling();
+ setLimits();
+ setupBodyMarginValues();
+ init(createOutgoingMsg(iframeId));
+ setupIFrameObject();
+ } else {
+ warn(iframeId,'Ignored iFrame, already setup.');
+ }
+ }
+
+ function debouce(fn,time){
+ if (null === timer){
+ timer = setTimeout(function(){
+ timer = null;
+ fn();
+ }, time);
+ }
+ }
+
+ /* istanbul ignore next */ //Not testable in PhantomJS
+ function fixHiddenIFrames(){
+ function checkIFrames(){
+ function checkIFrame(settingId){
+ function chkDimension(dimension){
+ return '0px' === settings[settingId].iframe.style[dimension];
+ }
+
+ function isVisible(el) {
+ return (null !== el.offsetParent);
+ }
+
+ if (isVisible(settings[settingId].iframe) && (chkDimension('height') || chkDimension('width'))){
+ trigger('Visibility change', 'resize', settings[settingId].iframe,settingId);
+ }
+ }
+
+ for (var settingId in settings){
+ checkIFrame(settingId);
+ }
+ }
+
+ function mutationObserved(mutations){
+ log('window','Mutation observed: ' + mutations[0].target + ' ' + mutations[0].type);
+ debouce(checkIFrames,16);
+ }
+
+ function createMutationObserver(){
+ var
+ target = document.querySelector('body'),
+
+ config = {
+ attributes : true,
+ attributeOldValue : false,
+ characterData : true,
+ characterDataOldValue : false,
+ childList : true,
+ subtree : true
+ },
+
+ observer = new MutationObserver(mutationObserved);
+
+ observer.observe(target, config);
+ }
+
+ var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
+
+ if (MutationObserver) createMutationObserver();
+ }
+
+
+ function resizeIFrames(event){
+ function resize(){
+ sendTriggerMsg('Window '+event,'resize');
+ }
+
+ log('window','Trigger event: '+event);
+ debouce(resize,16);
+ }
+
+ /* istanbul ignore next */ //Not testable in PhantomJS
+ function tabVisible() {
+ function resize(){
+ sendTriggerMsg('Tab Visable','resize');
+ }
+
+ if('hidden' !== document.visibilityState) {
+ log('document','Trigger event: Visiblity change');
+ debouce(resize,16);
+ }
+ }
+
+ function sendTriggerMsg(eventName,event){
+ function isIFrameResizeEnabled(iframeId) {
+ return 'parent' === settings[iframeId].resizeFrom &&
+ settings[iframeId].autoResize &&
+ !settings[iframeId].firstRun;
+ }
+
+ for (var iframeId in settings){
+ if(isIFrameResizeEnabled(iframeId)){
+ trigger(eventName,event,document.getElementById(iframeId),iframeId);
+ }
+ }
+ }
+
+ function setupEventListeners(){
+ addEventListener(window,'message',iFrameListener);
+
+ addEventListener(window,'resize', function(){resizeIFrames('resize');});
+
+ addEventListener(document,'visibilitychange',tabVisible);
+ addEventListener(document,'-webkit-visibilitychange',tabVisible); //Andriod 4.4
+ addEventListener(window,'focusin',function(){resizeIFrames('focus');}); //IE8-9
+ addEventListener(window,'focus',function(){resizeIFrames('focus');});
+ }
+
+
+ function factory(){
+ function init(options,element){
+ function chkType(){
+ if(!element.tagName) {
+ throw new TypeError('Object is not a valid DOM element');
+ } else if ('IFRAME' !== element.tagName.toUpperCase()) {
+ throw new TypeError('Expected <IFRAME> tag, found <'+element.tagName+'>');
+ }
+ }
+
+ if(element) {
+ chkType();
+ setupIFrame(element, options);
+ iFrames.push(element);
+ }
+ }
+
+ var iFrames;
+
+ setupRequestAnimationFrame();
+ setupEventListeners();
+
+ return function iFrameResizeF(options,target){
+ iFrames = []; //Only return iFrames past in on this call
+
+ switch (typeof(target)){
+ case 'undefined':
+ case 'string':
+ Array.prototype.forEach.call(
+ document.querySelectorAll( target || 'iframe' ),
+ init.bind(undefined, options)
+ );
+ break;
+ case 'object':
+ init(options,target);
+ break;
+ default:
+ throw new TypeError('Unexpected data type ('+typeof(target)+')');
+ }
+
+ return iFrames;
+ };
+ }
+
+ function createJQueryPublicMethod($){
+ if (!$.fn) {
+ info('','Unable to bind to jQuery, it is not fully loaded.');
+ } else {
+ $.fn.iFrameResize = function $iFrameResizeF(options) {
+ function init(index, element) {
+ setupIFrame(element, options);
+ }
+
+ return this.filter('iframe').each(init).end();
+ };
+ }
+ }
+
+ if (window.jQuery) { createJQueryPublicMethod(jQuery); }
+
+ if (typeof define === 'function' && define.amd) {
+ define([],factory);
+ } else if (typeof module === 'object' && typeof module.exports === 'object') { //Node for browserfy
+ module.exports = factory();
+ } else {
+ window.iFrameResize = window.iFrameResize || factory();
+ }
+
+})(window || {});
diff --git a/libs/bower_components/iframe-resizer/js/iframeResizer.map b/libs/bower_components/iframe-resizer/js/iframeResizer.map
new file mode 100644
index 0000000000..c26b45efb4
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/js/iframeResizer.map
@@ -0,0 +1 @@
+{"version":3,"file":"iframeResizer.min.js","sources":["iframeResizer.js"],"names":["window","addEventListener","obj","evt","func","attachEvent","removeEventListener","el","detachEvent","setupRequestAnimationFrame","x","vendors","length","requestAnimationFrame","log","getMyID","iframeId","retStr","top","self","parentIFrame","getId","formatLogHeader","msgId","isLogEnabled","settings","logEnabled","msg","output","info","warn","type","enabled","console","iFrameListener","event","resizeIFrame","resize","setSize","messageData","setPagePosition","ensureInRange","syncResize","processMsg","data","substr","msgIdLen","split","iframe","id","height","width","Dimension","max","Number","min","dimension","toLowerCase","size","isMessageFromIFrame","checkAllowedOrigin","checkList","i","retCode","checkOrigin","origin","checkSingle","remoteHost","constructor","Array","Error","isMessageForUs","isMessageFromMetaParent","true","false","undefined","getMsgBody","offset","indexOf","msgHeaderLen","forwardMsgFromIFrame","msgBody","callback","message","JSON","parse","getPageInfo","bodyPosition","document","body","getBoundingClientRect","iFramePosition","stringify","iframeHeight","iframeWidth","clientHeight","Math","documentElement","innerHeight","clientWidth","innerWidth","offsetTop","parseInt","offsetLeft","left","scrollTop","pageYOffset","scrollLeft","pageXOffset","sendPageInfoToIframe","debouncedTrigger","trigger","debouce","startPageInfoMonitor","setListener","sendPageInfo","stop","forEach","start","stopPageInfo","stopPageInfoMonitor","checkIFrameExists","retBool","getElementPosition","target","getPagePosition","floor","pagePosition","y","scrollRequestFromChild","addOffset","reposition","newPosition","scrollTo","calcOffset","scrollParent","unsetPagePosition","findTarget","location","jumpToTarget","jumpPosition","hash","jumpToParent","moveToAnchor","hashData","decodeURIComponent","getElementById","getElementsByName","funcName","val","chkCallback","actionMsg","firstRun","closeIFrame","resetIFrame","hasSettings","iFrameReadyMsgReceived","createOutgoingMsg","logId","retVal","TypeError","parentNode","removeChild","reset","setDimension","style","chkZero","hiddenCheckEnabled","fixHiddenIFrames","processDimension","sizeHeight","sizeWidth","doNotSync","calleeMsg","postMessageToIFrame","targetOrigin","contentWindow","postMessage","iFrameNotFound","chkAndSend","bodyMarginV1","interval","enablePublicMethods","autoResize","bodyMargin","heightCalculationMethod","bodyBackground","bodyPadding","tolerance","inPageLinks","resizeFrom","widthCalculationMethod","setupIFrame","options","setLimits","addStyle","Infinity","chkMinMax","newId","defaults","count","ensureHasId","src","setScrolling","scrolling","overflow","setupBodyMarginValues","checkReset","resetRequertMethod","resetRequiredMethods","setupIFrameObject","Function","prototype","bind","iFrameResizer","close","anchor","sendMessage","init","iFrameLoaded","checkOptions","copyOptions","option","hasOwnProperty","getTargetOrigin","processOptions","slice","join","beenHere","fn","time","timer","setTimeout","checkIFrames","checkIFrame","settingId","chkDimension","isVisible","offsetParent","mutationObserved","mutations","createMutationObserver","querySelector","config","attributes","attributeOldValue","characterData","characterDataOldValue","childList","subtree","observer","MutationObserver","observe","WebKitMutationObserver","resizeIFrames","sendTriggerMsg","tabVisible","visibilityState","eventName","isIFrameResizeEnabled","setupEventListeners","factory","element","chkType","tagName","toUpperCase","iFrames","push","call","querySelectorAll","createJQueryPublicMethod","$","iFrameResize","index","this","filter","each","end","msgHeader","scroll","bodyScroll","documentElementScroll","maxHeight","maxWidth","minHeight","minWidth","closedCallback","initCallback","messageCallback","resizedCallback","scrollCallback","jQuery","define","amd","module","exports"],"mappings":";;;;;;;CAWC,SAAUA,GACV,YA+CA,SAASC,GAAiBC,EAAIC,EAAIC,GAE7B,oBAAsBJ,GACzBE,EAAID,iBAAiBE,EAAIC,GAAM,GACrB,eAAiBJ,IAC3BE,EAAIG,YAAY,KAAKF,EAAIC,GAI3B,QAASE,GAAoBC,EAAGJ,EAAIC,GAE/B,uBAAyBJ,GAC5BO,EAAGD,oBAAoBH,EAAIC,GAAM,GACvB,eAAiBJ,IAC3BO,EAAGC,YAAY,KAAKL,EAAIC,GAI1B,QAASK,KACR,GAECC,GADAC,GAAW,MAAO,SAAU,IAAK,KAIlC,KAAKD,EAAI,EAAGA,EAAIC,EAAQC,SAAWC,EAAuBH,GAAK,EAC9DG,EAAwBb,EAAOW,EAAQD,GAAK,wBAGxC,IACJI,EAAI,QAAQ,uCAId,QAASC,GAAQC,GAChB,GAAIC,GAAS,cAAcD,CAU3B,OARIhB,GAAOkB,MAAMlB,EAAOmB,OAEtBF,EADGjB,EAAOoB,cAAgBpB,EAAOoB,aAAaC,MACrCrB,EAAOoB,aAAaC,QAAQ,KAAKL,EAEjC,qBAAqBA,GAIzBC,EAGR,QAASK,GAAgBN,GACxB,MAAOO,GAAQ,IAAMR,EAAQC,GAAY,IAG1C,QAASQ,GAAaR,GACrB,MAAOS,GAAST,GAAYS,EAAST,GAAUF,IAAMY,EAGtD,QAASZ,GAAIE,EAASW,GACrBC,EAAO,MAAMZ,EAASW,EAAIH,EAAaR,IAGxC,QAASa,GAAKb,EAASW,GACtBC,EAAO,OAAOZ,EAASW,EAAIH,EAAaR,IAGzC,QAASc,GAAKd,EAASW,GACtBC,EAAO,OAAOZ,EAASW,GAAI,GAG5B,QAASC,GAAOG,EAAKf,EAASW,EAAIK,IAC7B,IAASA,GAAW,gBAAoBhC,GAAOiC,SAClDA,QAAQF,GAAMT,EAAgBN,GAAUW,GAI1C,QAASO,GAAeC,GACvB,QAASC,KACR,QAASC,KACRC,EAAQC,GACRC,EAAgBxB,GAGjByB,EAAc,UACdA,EAAc,SAEdC,EAAWL,EAAOE,EAAY,QAG/B,QAASI,KACR,GAAIC,GAAOjB,EAAIkB,OAAOC,GAAUC,MAAM,IAEtC,QACCC,OAAQvB,EAASmB,EAAK,IAAII,OAC1BC,GAAQL,EAAK,GACbM,OAAQN,EAAK,GACbO,MAAQP,EAAK,GACbb,KAAQa,EAAK,IAIf,QAASH,GAAcW,GACtB,GACCC,GAAOC,OAAO7B,EAAST,GAAU,MAAQoC,IACzCG,EAAOD,OAAO7B,EAAST,GAAU,MAAQoC,IACzCI,EAAYJ,EAAUK,cACtBC,EAAOJ,OAAOf,EAAYiB,GAE3B1C,GAAIE,EAAS,YAAcwC,EAAY,gBAAkBD,EAAM,IAAMF,GAE5DE,EAALG,IACHA,EAAKH,EACLzC,EAAIE,EAAS,OAASwC,EAAY,kBAG/BE,EAAKL,IACRK,EAAKL,EACLvC,EAAIE,EAAS,OAASwC,EAAY,kBAGnCjB,EAAYiB,GAAa,GAAKE,EAI/B,QAASC,KACR,QAASC,KACR,QAASC,KACR,GACCC,GAAI,EACJC,GAAU,CAIX,KAFAjD,EAAIE,EAAS,wDAA0DgD,GAEhEF,EAAIE,EAAYpD,OAAQkD,IAC9B,GAAIE,EAAYF,KAAOG,EAAQ,CAC9BF,GAAU,CACV,OAGF,MAAOA,GAGR,QAASG,KACR,GAAIC,GAAc1C,EAAST,GAAUmD,UAErC,OADArD,GAAIE,EAAS,gCAAgCmD,GACtCF,IAAWE,EAGnB,MAAOH,GAAYI,cAAgBC,MAAQR,IAAcK,IAG1D,GACCD,GAAc9B,EAAM8B,OACpBD,EAAcvC,EAAST,GAAUgD,WAElC,IAAIA,GAAgB,GAAGC,GAAW,SAAYL,IAC7C,KAAM,IAAIU,OACT,qCAAuCL,EACvC,QAAU1B,EAAYS,OAAOC,GAC7B,kBAAoBd,EAAMS,KAC1B,qHAIF,QAAO,EAGR,QAAS2B,KACR,MAAOhD,MAAY,GAAKI,GAAKkB,OAAO,EAAEC,IAAenB,EAAIkB,OAAOC,GAAUC,MAAM,KAAK,IAAMtB,GAG5F,QAAS+C,KAGR,GAAIT,GAAUxB,EAAYR,QAAS0C,OAAO,EAAEC,QAAQ,EAAEC,UAAY,EAMlE,OAJIZ,IACHjD,EAAIE,EAAS,+CAGP+C,EAGR,QAASa,GAAWC,GACnB,MAAOlD,GAAIkB,OAAOlB,EAAImD,QAAQ,KAAKC,EAAaF,GAGjD,QAASG,GAAqBC,GAC7BnE,EAAIE,EAAS,oCAAqCuB,EAAYS,OAAOC,GAAK,cAAgBgC,EAAU,KACpGC,EAAS,mBACRlC,OAAQT,EAAYS,OACpBmC,QAASC,KAAKC,MAAMJ,KAErBnE,EAAIE,EAAS,MAGd,QAASsE,KACR,GACCC,GAAiBC,SAASC,KAAKC,wBAC/BC,EAAiBpD,EAAYS,OAAO0C,uBAErC,OAAON,MAAKQ,WACXC,aAAcF,EAAezC,OAC7B4C,YAAcH,EAAexC,MAC7B4C,aAAcC,KAAK3C,IAAImC,SAASS,gBAAgBF,aAAc/F,EAAOkG,aAAe,GACpFC,YAAcH,KAAK3C,IAAImC,SAASS,gBAAgBE,YAAcnG,EAAOoG,YAAe,GACpFC,UAAcC,SAASX,EAAezE,IAAOqE,EAAarE,IAAM,IAChEqF,WAAcD,SAASX,EAAea,KAAOjB,EAAaiB,KAAM,IAChEC,UAAczG,EAAO0G,YACrBC,WAAc3G,EAAO4G,cAIvB,QAASC,GAAqB7D,EAAOhC,GACpC,QAAS8F,KACRC,EACC,iBACA,YAAczB,IACdtC,EACAhC,GAIFgG,EAAQF,EAAiB,IAI1B,QAASG,KACR,QAASC,GAAYnF,EAAK3B,GACzB,QAAS+G,KACJ1F,EAASwB,GACZ4D,EAAqBpF,EAASwB,GAAID,OAAOC,GAEzCmE,KAID,SAAS,UAAUC,QAAQ,SAASlH,GACpCW,EAAImC,EAAIlB,EAAQ5B,EAAM,8BACtBC,EAAKJ,EAAOG,EAAIgH,KAIlB,QAASC,KACRF,EAAY,UAAW5G,GAGxB,QAASgH,KACRJ,EAAY,OAAQjH,GAGrB,GAAIgD,GAAKjC,CAETsG,KAEA7F,EAASwB,GAAIsE,aAAeH,EAG7B,QAASI,KACJ/F,EAAST,IAAaS,EAAST,GAAUuG,eAC5C9F,EAAST,GAAUuG,qBACZ9F,GAAST,GAAUuG,cAI5B,QAASE,KACR,GAAIC,IAAU,CAMd,OAJI,QAASnF,EAAYS,SACxBlB,EAAKd,EAAS,WAAWuB,EAAYU,GAAG,eACxCyE,GAAU,GAEJA,EAGR,QAASC,GAAmBC,GAC3B,GAAIjC,GAAiBiC,EAAOlC,uBAI5B,OAFAmC,GAAgB7G,IAGfN,EAAGsF,KAAK8B,MAAOxE,OAAOqC,EAAea,MAAQlD,OAAOyE,EAAarH,IACjEsH,EAAGhC,KAAK8B,MAAOxE,OAAOqC,EAAezE,KAAQoC,OAAOyE,EAAaC,KAInE,QAASC,GAAuBC,GAE/B,QAASC,KACRJ,EAAeK,EACfC,IACAvH,EAAIE,EAAS,MAGd,QAASsH,KACR,OACC5H,EAAG4C,OAAOf,EAAYY,OAAS0B,EAAOnE,EACtCsH,EAAG1E,OAAOf,EAAYW,QAAU2B,EAAOmD,GAIzC,QAASO,KACJvI,EAAOoB,aACVpB,EAAOoB,aAAa,YAAY8G,EAAU,SAAS,KAAKE,EAAY1H,EAAE0H,EAAYJ,GAElFlG,EAAKd,EAAS,yEAIhB,GACC6D,GAASqD,EAAYP,EAAmBpF,EAAYS,SAAWtC,EAAE,EAAEsH,EAAE,GACrEI,EAAcE,GAEfxH,GAAIE,EAAS,8CAA8C6D,EAAOnE,EAAE,MAAMmE,EAAOmD,EAAE,KAEhFhI,EAAOkB,MAAMlB,EAAOmB,KACtBoH,IAEAJ,IAIF,QAASE,MACJ,IAAUnD,EAAS,iBAAiB6C,GACvCvF,EAAgBxB,GAEhBwH,IAIF,QAASC,GAAWC,GACnB,QAASC,KACR,GAAIC,GAAejB,EAAmBC,EAEtC9G,GAAIE,EAAS,4BAA4B6H,EAAK,WAAWD,EAAalI,EAAE,OAAOkI,EAAaZ,GAC5FD,GACCrH,EAAGkI,EAAalI,EAChBsH,EAAGY,EAAaZ,GAGjBK,IACAvH,EAAIE,EAAS,MAGd,QAAS8H,KACJ9I,EAAOoB,aACVpB,EAAOoB,aAAa2H,aAAaF,GAEjC/H,EAAIE,EAAS,iBAAiB6H,EAAK,gDAIrC,GACCA,GAAWH,EAAS3F,MAAM,KAAK,IAAM,GACrCiG,EAAWC,mBAAmBJ,GAC9BjB,EAAWpC,SAAS0D,eAAeF,IAAaxD,SAAS2D,kBAAkBH,GAAU,EAElFpB,GACHe,IACS3I,EAAOkB,MAAMlB,EAAOmB,KAC7B2H,IAEAhI,EAAIE,EAAS,iBAAiB6H,EAAK,cAIrC,QAAS3D,GAASkE,EAASC,GAC1B,MAAOC,GAAYtI,EAASoI,EAASC,GAGtC,QAASE,KAIR,OAFG9H,EAAST,GAAUwI,UAAUA,IAEzBjH,EAAYR,MACnB,IAAK,QACJ0H,EAAYlH,EAAYS,OACxB,MACD,KAAK,UACJgC,EAAqBJ,EAAW,GAChC,MACD,KAAK,WACJqD,GAAuB,EACvB,MACD,KAAK,iBACJA,GAAuB,EACvB,MACD,KAAK,WACJpB,EAAqBpF,EAAST,GAAUgC,OAAOhC,GAC/CiG,GACA,MACD,KAAK,eACJO,GACA,MACD,KAAK,aACJiB,EAAW7D,EAAW,GACtB,MACD,KAAK,QACJ8E,EAAYnH,EACZ,MACD,KAAK,OACJH,IACA8C,EAAS,eAAe3C,EAAYS,QACpCkC,EAAS,kBAAkB3C,EAC3B,MACD,SACCH,IACA8C,EAAS,kBAAkB3C,IAI7B,QAASoH,GAAY3I,GACpB,GAAI0G,IAAU,CAOd,OALKjG,GAAST,KACb0G,GAAU,EACV5F,EAAKS,EAAYR,KAAO,oBAAsBf,EAAW,kBAAoBW,IAGvE+F,EAGR,QAASkC,KACR,IAAK,GAAI5I,KAAYS,GACpBsF,EAAQ,wBAAwB8C,EAAkB7I,GAAUwE,SAAS0D,eAAelI,GAAUA,GAIhG,QAASwI,KACR/H,EAAST,GAAUwI,UAAW,EAG/B,GACC7H,GAAMQ,EAAMS,KACZL,KACAvB,EAAW,IAET,+BAAgCW,EAClCiI,IACUrF,KACVhC,EAAcI,IACd3B,EAAc8I,EAAQvH,EAAYU,IAE7BuB,KAA6BmF,EAAY3I,KAC7CF,EAAIE,EAAS,aAAaW,GAErB8F,KAAuB9D,KAC3B4F,MAIF1H,EAAKb,EAAS,YAAYW,GAM5B,QAAS2H,GAAYtI,EAASoI,EAASC,GACtC,GACCjJ,GAAO,KACP2J,EAAS,IAEV,IAAGtI,EAAST,GAAU,CAGrB,GAFAZ,EAAOqB,EAAST,GAAUoI,GAEtB,kBAAsBhJ,GAGzB,KAAM,IAAI4J,WAAUZ,EAAS,cAAcpI,EAAS,sBAFpD+I,GAAS3J,EAAKiJ,GAMhB,MAAOU,GAGR,QAASN,GAAYzG,GACpB,GAAIhC,GAAWgC,EAAOC,EAEtBnC,GAAIE,EAAS,oBAAoBA,GACjCgC,EAAOiH,WAAWC,YAAYlH,GAC9BsG,EAAYtI,EAAS,iBAAiBA,GACtCF,EAAIE,EAAS,YACNS,GAAST,GAGjB,QAAS6G,GAAgB7G,GACrB,OAAS+G,IACXA,GACCrH,EAA2BiE,SAAvB3E,EAAO4G,YAA6B5G,EAAO4G,YAAcpB,SAASS,gBAAgBU,WACtFqB,EAA2BrD,SAAvB3E,EAAO0G,YAA6B1G,EAAO0G,YAAclB,SAASS,gBAAgBQ,WAEvF3F,EAAIE,EAAS,sBAAsB+G,EAAarH,EAAE,IAAIqH,EAAaC,IAIrE,QAASxF,GAAgBxB,GACrB,OAAS+G,IACX/H,EAAOqI,SAASN,EAAarH,EAAEqH,EAAaC,GAC5ClH,EAAIE,EAAS,sBAAsB+G,EAAarH,EAAE,IAAIqH,EAAaC,GACnEQ,KAIF,QAASA,KACRT,EAAe,KAGhB,QAAS2B,GAAYnH,GACpB,QAAS4H,KACR7H,EAAQC,GACRwE,EAAQ,QAAQ,QAAQxE,EAAYS,OAAOT,EAAYU,IAGxDnC,EAAIyB,EAAYU,GAAG,4BAA4B,SAASV,EAAYR,KAAK,YAAY,WACrF8F,EAAgBtF,EAAYU,IAC5BP,EAAWyH,EAAM5H,EAAY,SAG9B,QAASD,GAAQC,GAChB,QAAS6H,GAAa5G,GACrBjB,EAAYS,OAAOqH,MAAM7G,GAAajB,EAAYiB,GAAa,KAC/D1C,EACCyB,EAAYU,GACZ,WAAajC,EACb,KAAOwC,EACP,WAAajB,EAAYiB,GAAa,MAIxC,QAAS8G,GAAQ9G,GAMX+G,GAAsB,MAAQhI,EAAYiB,KAC9C+G,GAAqB,EACrBzJ,EAAIE,EAAS,wDACbwJ,KAIF,QAASC,GAAiBjH,GACzB4G,EAAa5G,GACb8G,EAAQ9G,GAGT,GAAIxC,GAAWuB,EAAYS,OAAOC,EAE/BxB,GAAST,KACPS,EAAST,GAAU0J,YAAcD,EAAiB,UAClDhJ,EAAST,GAAU2J,WAAcF,EAAiB,UAIxD,QAAS/H,GAAWtC,EAAKmC,EAAYqI,GAEjCA,IAAYrI,EAAYR,MAAQlB,GAClCC,EAAIyB,EAAYU,GAAG,8BACnBpC,EAAsBT,IAEtBA,IAIF,QAAS2G,GAAQ8D,EAAUlJ,EAAIqB,EAAOC,GACrC,QAAS6H,KACR,GAAIlD,GAASnG,EAASwB,GAAI8H,YAC1BjK,GAAImC,EAAG,IAAM4H,EAAY,2BAA2B5H,EAAG,MAAMtB,EAAI,mBAAmBiG,GACpF5E,EAAOgI,cAAcC,YAAa1J,EAAQI,EAAKiG,GAGhD,QAASsD,KACRrJ,EAAKoB,EAAG,IAAM4H,EAAY,YAAY5H,EAAG,eACtCxB,EAASwB,UACJxB,GAASwB,GAIlB,QAASkI,KACLnI,GAAU,iBAAmBA,IAAW,OAASA,EAAOgI,cAC1DF,IAEAI,IAIFjI,EAAKA,GAAMD,EAAOC,GAEfxB,EAASwB,IACXkI,IAKF,QAAStB,GAAkB7I,GAC1B,MAAOA,GACN,IAAMS,EAAST,GAAUoK,aACzB,IAAM3J,EAAST,GAAU2J,UACzB,IAAMlJ,EAAST,GAAUF,IACzB,IAAMW,EAAST,GAAUqK,SACzB,IAAM5J,EAAST,GAAUsK,oBACzB,IAAM7J,EAAST,GAAUuK,WACzB,IAAM9J,EAAST,GAAUwK,WACzB,IAAM/J,EAAST,GAAUyK,wBACzB,IAAMhK,EAAST,GAAU0K,eACzB,IAAMjK,EAAST,GAAU2K,YACzB,IAAMlK,EAAST,GAAU4K,UACzB,IAAMnK,EAAST,GAAU6K,YACzB,IAAMpK,EAAST,GAAU8K,WACzB,IAAMrK,EAAST,GAAU+K,uBAG3B,QAASC,GAAYhJ,EAAOiJ,GAC3B,QAASC,KACR,QAASC,GAAS9B,GACZ+B,EAAAA,IAAa3K,EAAST,GAAUqJ,IAAY,IAAM5I,EAAST,GAAUqJ,KACzErH,EAAOqH,MAAMA,GAAS5I,EAAST,GAAUqJ,GAAS,KAClDvJ,EAAIE,EAAS,OAAOqJ,EAAM,MAAM5I,EAAST,GAAUqJ,GAAO,OAI5D,QAASgC,GAAU7I,GAClB,GAAI/B,EAAST,GAAU,MAAMwC,GAAW/B,EAAST,GAAU,MAAMwC,GAChE,KAAM,IAAIc,OAAM,gBAAgBd,EAAU,+BAA+BA,GAI3E6I,EAAU,UACVA,EAAU,SAEVF,EAAS,aACTA,EAAS,aACTA,EAAS,YACTA,EAAS,YAGV,QAASG,KACR,GAAIrJ,GAAOgJ,GAAWA,EAAQhJ,IAAOsJ,EAAStJ,GAAKuJ,GAInD,OAHK,QAAOhH,SAAS0D,eAAejG,KACnCA,GAAUuJ,KAEJvJ,EAGR,QAASwJ,GAAYzL,GAUpB,MATA8I,GAAM9I,EACF,KAAKA,IACRgC,EAAOC,GAAKjC,EAAYsL,IACxB5K,GAAcuK,OAAenL,IAC7BgJ,EAAM9I,EACNF,EAAIE,EAAS,4BAA6BA,EAAU,KAAOgC,EAAO0J,IAAM,MAIlE1L,EAGR,QAAS2L,KACR7L,EAAIE,EAAS,qBAAuBS,EAAST,GAAU4L,UAAY,UAAY,YAAc,QAAU5L,GACvGgC,EAAOqH,MAAMwC,UAAW,IAAUpL,EAAST,GAAU4L,UAAY,SAAW,OAC5E5J,EAAO4J,WAAiB,IAAUnL,EAAST,GAAU4L,UAAY,KAAO,MAMzE,QAASE,MACH,gBAAkBrL,GAAST,GAAoB,YAAO,MAAMS,EAAST,GAAUwK,cACnF/J,EAAST,GAAUoK,aAAe3J,EAAST,GAAUwK,WACrD/J,EAAST,GAAUwK,WAAe,GAAK/J,EAAST,GAAUwK,WAAa,MAIzE,QAASuB,KAIR,GACCvD,GAAqB/H,EAAST,GAAUwI,SACxCwD,EAAqBvL,EAAST,GAAUyK,0BAA2BwB,IAE/DzD,GAAYwD,GAChBtD,GAAa1G,OAAOA,EAAQE,OAAO,EAAGC,MAAM,EAAGpB,KAAK,SAItD,QAASmL,KACLC,SAASC,UAAUC,OACrB5L,EAAST,GAAUgC,OAAOsK,eAEzBC,MAAe9D,EAAY4D,KAAK,KAAK5L,EAAST,GAAUgC,QAExDX,OAAe0E,EAAQsG,KAAK,KAAK,gBAAiB,SAAU5L,EAAST,GAAUgC,QAE/E+F,aAAe,SAASyE,GACvBzG,EAAQ,iBAAiB,gBAAgByG,EAAQ/L,EAAST,GAAUgC,OAAOhC,IAG5EyM,YAAe,SAAStI,GACvBA,EAAUC,KAAKQ,UAAUT,GACzB4B,EAAQ,eAAe,WAAW5B,EAAS1D,EAAST,GAAUgC,OAAOhC,MASzE,QAAS0M,GAAK/L,GACb,QAASgM,KACR5G,EAAQ,gBAAgBpF,EAAIqB,GAC5B+J,IAGD9M,EAAiB+C,EAAO,OAAO2K,GAC/B5G,EAAQ,OAAOpF,EAAIqB,GAGpB,QAAS4K,GAAa3B,GACrB,GAAI,gBAAoBA,GACvB,KAAM,IAAIjC,WAAU,4BAItB,QAAS6D,GAAY5B,GACpB,IAAK,GAAI6B,KAAUvB,GACdA,EAASwB,eAAeD,KAC3BrM,EAAST,GAAU8M,GAAU7B,EAAQ8B,eAAeD,GAAU7B,EAAQ6B,GAAUvB,EAASuB,IAK5F,QAASE,GAAiB7J,GACzB,MAAQ,KAAOA,GAAc,YAAcA,EAAc,IAAMA,EAGhE,QAAS8J,GAAehC,GACvBA,EAAUA,MACVxK,EAAST,IACRwI,UAAW,EACXxG,OAAUA,EACVmB,WAAanB,EAAO0J,IAAI3J,MAAM,KAAKmL,MAAM,EAAE,GAAGC,KAAK,MAGpDP,EAAa3B,GACb4B,EAAY5B,GAEZxK,EAAST,GAAU+J,cAAe,IAAStJ,EAAST,GAAUgD,YAAcgK,EAAgBvM,EAAST,GAAUmD,YAAc,IAG9H,QAASiK,KACR,MAAQpN,KAAYS,IAAY,iBAAmBuB,GAGpD,GAAIhC,GAAWyL,EAAYzJ,EAAOC,GAE7BmL,KAQJtM,EAAKd,EAAS,mCAPdiN,EAAehC,GACfU,IACAT,IACAY,IACAY,EAAK7D,EAAkB7I,IACvBkM,KAMF,QAASlG,GAAQqH,EAAGC,GACf,OAASC,IACZA,EAAQC,WAAW,WAClBD,EAAQ,KACRF,KACEC,IAKL,QAAS9D,KACR,QAASiE,KACR,QAASC,GAAYC,GACpB,QAASC,GAAapL,GACrB,MAAO,QAAU/B,EAASkN,GAAW3L,OAAOqH,MAAM7G,GAGnD,QAASqL,GAAUtO,GAClB,MAAQ,QAASA,EAAGuO,aAGjBD,EAAUpN,EAASkN,GAAW3L,UAAY4L,EAAa,WAAaA,EAAa,WACpF7H,EAAQ,oBAAqB,SAAUtF,EAASkN,GAAW3L,OAAO2L,GAIpE,IAAK,GAAIA,KAAalN,GACrBiN,EAAYC,GAId,QAASI,GAAiBC,GACzBlO,EAAI,SAAS,sBAAwBkO,EAAU,GAAGpH,OAAS,IAAMoH,EAAU,GAAGjN,MAC9EiF,EAAQyH,EAAa,IAGtB,QAASQ,KACR,GACCrH,GAASpC,SAAS0J,cAAc,QAEhCC,GACCC,YAAwB,EACxBC,mBAAwB,EACxBC,eAAwB,EACxBC,uBAAwB,EACxBC,WAAwB,EACxBC,SAAwB,GAGzBC,EAAW,GAAIC,GAAiBZ,EAEjCW,GAASE,QAAQhI,EAAQuH,GAG1B,GAAIQ,GAAmB3P,EAAO2P,kBAAoB3P,EAAO6P,sBAErDF,IAAkBV,IAIvB,QAASa,GAAc3N,GACtB,QAASE,KACR0N,EAAe,UAAU5N,EAAM,UAGhCrB,EAAI,SAAS,kBAAkBqB,GAC/B6E,EAAQ3E,EAAO,IAIhB,QAAS2N,KACR,QAAS3N,KACR0N,EAAe,cAAc,UAG3B,WAAavK,SAASyK,kBACxBnP,EAAI,WAAW,mCACfkG,EAAQ3E,EAAO,KAIjB,QAAS0N,GAAeG,EAAU/N,GACjC,QAASgO,GAAsBnP,GAC9B,MAAO,WAAaS,EAAST,GAAU8K,YACrCrK,EAAST,GAAUuK,aAClB9J,EAAST,GAAUwI,SAGvB,IAAK,GAAIxI,KAAYS,GACjB0O,EAAsBnP,IACxB+F,EAAQmJ,EAAU/N,EAAMqD,SAAS0D,eAAelI,GAAUA,GAK7D,QAASoP,KACRnQ,EAAiBD,EAAO,UAAUkC,GAElCjC,EAAiBD,EAAO,SAAU,WAAW8P,EAAc,YAE3D7P,EAAiBuF,SAAS,mBAAmBwK,GAC7C/P,EAAiBuF,SAAS,2BAA2BwK,GACrD/P,EAAiBD,EAAO,UAAU,WAAW8P,EAAc,WAC3D7P,EAAiBD,EAAO,QAAQ,WAAW8P,EAAc,WAI1D,QAASO,KACR,QAAS3C,GAAKzB,EAAQqE,GACrB,QAASC,KACR,IAAID,EAAQE,QACX,KAAM,IAAIxG,WAAU,oCACd,IAAI,WAAasG,EAAQE,QAAQC,cACvC,KAAM,IAAIzG,WAAU,iCAAiCsG,EAAQE,QAAQ,KAIpEF,IACFC,IACAvE,EAAYsE,EAASrE,GACrByE,EAAQC,KAAKL,IAIf,GAAII,EAKJ,OAHAjQ,KACA2P,IAEO,SAAuBnE,EAAQrE,GAGrC,OAFA8I,WAEc,IACd,IAAK,YACL,IAAK,SACJrM,MAAM+I,UAAU/F,QAAQuJ,KACvBpL,SAASqL,iBAAkBjJ,GAAU,UACrC8F,EAAKL,KAAK1I,OAAWsH,GAEtB,MACD,KAAK,SACJyB,EAAKzB,EAAQrE,EACb,MACD,SACC,KAAM,IAAIoC,WAAU,+BAA+B,GAAS,KAG7D,MAAO0G,IAIT,QAASI,GAAyBC,GAC5BA,EAAE1C,GAGN0C,EAAE1C,GAAG2C,aAAe,SAAwB/E,GAC3C,QAASyB,GAAKuD,EAAOX,GACpBtE,EAAYsE,EAASrE,GAGtB,MAAOiF,MAAKC,OAAO,UAAUC,KAAK1D,GAAM2D,OAPzCxP,EAAK,GAAG,qDAr8BV,GACC2K,GAAwB,EACxB9K,GAAwB,EACxB6I,GAAwB,EACxB+G,EAAwB,UACxBvM,EAAwBuM,EAAU1Q,OAClCW,EAAwB,gBACxBuB,EAAwBvB,EAAMX,OAC9BmH,EAAwB,KACxBlH,EAAwBb,EAAOa,sBAC/BoM,GAAyB5J,IAAI,EAAEkO,OAAO,EAAEC,WAAW,EAAEC,sBAAsB,GAC3EhQ,KACA8M,EAAwB,KACxBzE,EAAwB,YAExByC,GACChB,YAA4B,EAC5BG,eAA4B,KAC5BF,WAA4B,KAC5BJ,aAA4B,EAC5BO,YAA4B,KAC5B3H,aAA4B,EAC5B6H,aAA4B,EAC5BP,qBAA4B,EAC5BG,wBAA4B,aAC5BxI,GAA4B,gBAC5BoI,SAA4B,GAC5BvK,KAA4B,EAC5B4Q,UAA4BtF,EAAAA,EAC5BuF,SAA4BvF,EAAAA,EAC5BwF,UAA4B,EAC5BC,SAA4B,EAC5B/F,WAA4B,SAC5Bc,WAA4B,EAC5BlC,YAA4B,EAC5BC,WAA4B,EAC5BiB,UAA4B,EAC5BG,uBAA4B,SAC5B+F,eAA4B,aAC5BC,aAA4B,aAC5BC,gBAA4B,WAAWlQ,EAAK,yCAC5CmQ,gBAA4B,aAC5BC,eAA4B,WAAW,OAAO,GAu6B5ClS,GAAOmS,QAAUrB,EAAyBqB,QAExB,kBAAXC,SAAyBA,OAAOC,IAC1CD,UAAU/B,GACkB,gBAAXiC,SAAiD,gBAAnBA,QAAOC,QACtDD,OAAOC,QAAUlC,IAEjBrQ,EAAOgR,aAAehR,EAAOgR,cAAgBX,KAG5CrQ","sourcesContent":["/*\n * File: iframeResizer.js\n * Desc: Force iframes to size to content.\n * Requires: iframeResizer.contentWindow.js to be loaded into the target frame.\n * Doc: https://github.com/davidjbradshaw/iframe-resizer\n * Author: David J. Bradshaw - dave@bradshaw.net\n * Contributor: Jure Mav - jure.mav@gmail.com\n * Contributor: Reed Dadoune - reed@dadoune.com\n */\n\n\n;(function(window) {\n\t'use strict';\n\n\tvar\n\t\tcount = 0,\n\t\tlogEnabled = false,\n\t\thiddenCheckEnabled = false,\n\t\tmsgHeader = 'message',\n\t\tmsgHeaderLen = msgHeader.length,\n\t\tmsgId = '[iFrameSizer]', //Must match iframe msg ID\n\t\tmsgIdLen = msgId.length,\n\t\tpagePosition = null,\n\t\trequestAnimationFrame = window.requestAnimationFrame,\n\t\tresetRequiredMethods = {max:1,scroll:1,bodyScroll:1,documentElementScroll:1},\n\t\tsettings = {},\n\t\ttimer = null,\n\t\tlogId = 'Host Page',\n\n\t\tdefaults = {\n\t\t\tautoResize : true,\n\t\t\tbodyBackground : null,\n\t\t\tbodyMargin : null,\n\t\t\tbodyMarginV1 : 8,\n\t\t\tbodyPadding : null,\n\t\t\tcheckOrigin : true,\n\t\t\tinPageLinks : false,\n\t\t\tenablePublicMethods : true,\n\t\t\theightCalculationMethod : 'bodyOffset',\n\t\t\tid : 'iFrameResizer',\n\t\t\tinterval : 32,\n\t\t\tlog : false,\n\t\t\tmaxHeight : Infinity,\n\t\t\tmaxWidth : Infinity,\n\t\t\tminHeight : 0,\n\t\t\tminWidth : 0,\n\t\t\tresizeFrom : 'parent',\n\t\t\tscrolling : false,\n\t\t\tsizeHeight : true,\n\t\t\tsizeWidth : false,\n\t\t\ttolerance : 0,\n\t\t\twidthCalculationMethod : 'scroll',\n\t\t\tclosedCallback : function(){},\n\t\t\tinitCallback : function(){},\n\t\t\tmessageCallback : function(){warn('MessageCallback function not defined');},\n\t\t\tresizedCallback : function(){},\n\t\t\tscrollCallback : function(){return true;}\n\t\t};\n\n\tfunction addEventListener(obj,evt,func){\n\t\t/* istanbul ignore else */ // Not testable in PhantonJS\n\t\tif ('addEventListener' in window){\n\t\t\tobj.addEventListener(evt,func, false);\n\t\t} else if ('attachEvent' in window){//IE\n\t\t\tobj.attachEvent('on'+evt,func);\n\t\t}\n\t}\n\n\tfunction removeEventListener(el,evt,func){\n\t\t/* istanbul ignore else */ // Not testable in phantonJS\n\t\tif ('removeEventListener' in window){\n\t\t\tel.removeEventListener(evt,func, false);\n\t\t} else if ('detachEvent' in window){ //IE\n\t\t\tel.detachEvent('on'+evt,func);\n\t\t}\n\t}\n\n\tfunction setupRequestAnimationFrame(){\n\t\tvar\n\t\t\tvendors = ['moz', 'webkit', 'o', 'ms'],\n\t\t\tx;\n\n\t\t// Remove vendor prefixing if prefixed and break early if not\n\t\tfor (x = 0; x < vendors.length && !requestAnimationFrame; x += 1) {\n\t\t\trequestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];\n\t\t}\n\n\t\tif (!(requestAnimationFrame)){\n\t\t\tlog('setup','RequestAnimationFrame not supported');\n\t\t}\n\t}\n\n\tfunction getMyID(iframeId){\n\t\tvar retStr = 'Host page: '+iframeId;\n\n\t\tif (window.top!==window.self){\n\t\t\tif (window.parentIFrame && window.parentIFrame.getId){\n\t\t\t\tretStr = window.parentIFrame.getId()+': '+iframeId;\n\t\t\t} else {\n\t\t\t\tretStr = 'Nested host page: '+iframeId;\n\t\t\t}\n\t\t}\n\n\t\treturn retStr;\n\t}\n\n\tfunction formatLogHeader(iframeId){\n\t\treturn msgId + '[' + getMyID(iframeId) + ']';\n\t}\n\n\tfunction isLogEnabled(iframeId){\n\t\treturn settings[iframeId] ? settings[iframeId].log : logEnabled;\n\t}\n\n\tfunction log(iframeId,msg){\n\t\toutput('log',iframeId,msg,isLogEnabled(iframeId));\n\t}\n\n\tfunction info(iframeId,msg){\n\t\toutput('info',iframeId,msg,isLogEnabled(iframeId));\n\t}\n\n\tfunction warn(iframeId,msg){\n\t\toutput('warn',iframeId,msg,true);\n\t}\n\n\tfunction output(type,iframeId,msg,enabled){\n\t\tif (true === enabled && 'object' === typeof window.console){\n\t\t\tconsole[type](formatLogHeader(iframeId),msg);\n\t\t}\n\t}\n\n\tfunction iFrameListener(event){\n\t\tfunction resizeIFrame(){\n\t\t\tfunction resize(){\n\t\t\t\tsetSize(messageData);\n\t\t\t\tsetPagePosition(iframeId);\n\t\t\t}\n\n\t\t\tensureInRange('Height');\n\t\t\tensureInRange('Width');\n\n\t\t\tsyncResize(resize,messageData,'init');\n\t\t}\n\n\t\tfunction processMsg(){\n\t\t\tvar data = msg.substr(msgIdLen).split(':');\n\n\t\t\treturn {\n\t\t\t\tiframe: settings[data[0]].iframe,\n\t\t\t\tid: data[0],\n\t\t\t\theight: data[1],\n\t\t\t\twidth: data[2],\n\t\t\t\ttype: data[3]\n\t\t\t};\n\t\t}\n\n\t\tfunction ensureInRange(Dimension){\n\t\t\tvar\n\t\t\t\tmax = Number(settings[iframeId]['max' + Dimension]),\n\t\t\t\tmin = Number(settings[iframeId]['min' + Dimension]),\n\t\t\t\tdimension = Dimension.toLowerCase(),\n\t\t\t\tsize = Number(messageData[dimension]);\n\n\t\t\tlog(iframeId,'Checking ' + dimension + ' is in range ' + min + '-' + max);\n\n\t\t\tif (size<min) {\n\t\t\t\tsize=min;\n\t\t\t\tlog(iframeId,'Set ' + dimension + ' to min value');\n\t\t\t}\n\n\t\t\tif (size>max) {\n\t\t\t\tsize=max;\n\t\t\t\tlog(iframeId,'Set ' + dimension + ' to max value');\n\t\t\t}\n\n\t\t\tmessageData[dimension] = '' + size;\n\t\t}\n\n\n\t\tfunction isMessageFromIFrame(){\n\t\t\tfunction checkAllowedOrigin(){\n\t\t\t\tfunction checkList(){\n\t\t\t\t\tvar\n\t\t\t\t\t\ti = 0,\n\t\t\t\t\t\tretCode = false;\n\n\t\t\t\t\tlog(iframeId,'Checking connection is from allowed list of origins: ' + checkOrigin);\n\n\t\t\t\t\tfor (; i < checkOrigin.length; i++) {\n\t\t\t\t\t\tif (checkOrigin[i] === origin) {\n\t\t\t\t\t\t\tretCode = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn retCode;\n\t\t\t\t}\n\n\t\t\t\tfunction checkSingle(){\n\t\t\t\t\tvar remoteHost = settings[iframeId].remoteHost;\n\t\t\t\t\tlog(iframeId,'Checking connection is from: '+remoteHost);\n\t\t\t\t\treturn origin === remoteHost;\n\t\t\t\t}\n\n\t\t\t\treturn checkOrigin.constructor === Array ? checkList() : checkSingle();\n\t\t\t}\n\n\t\t\tvar\n\t\t\t\torigin = event.origin,\n\t\t\t\tcheckOrigin = settings[iframeId].checkOrigin;\n\n\t\t\tif (checkOrigin && (''+origin !== 'null') && !checkAllowedOrigin()) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'Unexpected message received from: ' + origin +\n\t\t\t\t\t' for ' + messageData.iframe.id +\n\t\t\t\t\t'. Message was: ' + event.data +\n\t\t\t\t\t'. This error can be disabled by setting the checkOrigin: false option or by providing of array of trusted domains.'\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn true;\n\t\t}\n\n\t\tfunction isMessageForUs(){\n\t\t\treturn msgId === (('' + msg).substr(0,msgIdLen)) && (msg.substr(msgIdLen).split(':')[0] in settings); //''+Protects against non-string msg\n\t\t}\n\n\t\tfunction isMessageFromMetaParent(){\n\t\t\t//Test if this message is from a parent above us. This is an ugly test, however, updating\n\t\t\t//the message format would break backwards compatibity.\n\t\t\tvar retCode = messageData.type in {'true':1,'false':1,'undefined':1};\n\n\t\t\tif (retCode){\n\t\t\t\tlog(iframeId,'Ignoring init message from meta parent page');\n\t\t\t}\n\n\t\t\treturn retCode;\n\t\t}\n\n\t\tfunction getMsgBody(offset){\n\t\t\treturn msg.substr(msg.indexOf(':')+msgHeaderLen+offset);\n\t\t}\n\n\t\tfunction forwardMsgFromIFrame(msgBody){\n\t\t\tlog(iframeId,'MessageCallback passed: {iframe: '+ messageData.iframe.id + ', message: ' + msgBody + '}');\n\t\t\tcallback('messageCallback',{\n\t\t\t\tiframe: messageData.iframe,\n\t\t\t\tmessage: JSON.parse(msgBody)\n\t\t\t});\n\t\t\tlog(iframeId,'--');\n\t\t}\n\n\t\tfunction getPageInfo(){\n\t\t\tvar\n\t\t\t\tbodyPosition = document.body.getBoundingClientRect(),\n\t\t\t\tiFramePosition = messageData.iframe.getBoundingClientRect();\n\n\t\t\treturn JSON.stringify({\n\t\t\t\tiframeHeight: iFramePosition.height,\n\t\t\t\tiframeWidth: iFramePosition.width,\n\t\t\t\tclientHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0),\n\t\t\t\tclientWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0),\n\t\t\t\toffsetTop: parseInt(iFramePosition.top - bodyPosition.top, 10),\n\t\t\t\toffsetLeft: parseInt(iFramePosition.left - bodyPosition.left, 10),\n\t\t\t\tscrollTop: window.pageYOffset,\n\t\t\t\tscrollLeft: window.pageXOffset\n\t\t\t});\n\t\t}\n\n\t\tfunction sendPageInfoToIframe(iframe,iframeId){\n\t\t\tfunction debouncedTrigger(){\n\t\t\t\ttrigger(\n\t\t\t\t\t'Send Page Info',\n\t\t\t\t\t'pageInfo:' + getPageInfo(), \n\t\t\t\t\tiframe, \n\t\t\t\t\tiframeId\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tdebouce(debouncedTrigger,32);\n\t\t}\n\n\n\t\tfunction startPageInfoMonitor(){\n\t\t\tfunction setListener(type,func){\n\t\t\t\tfunction sendPageInfo(){\n\t\t\t\t\tif (settings[id]){\n\t\t\t\t\t\tsendPageInfoToIframe(settings[id].iframe,id);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tstop();\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t['scroll','resize'].forEach(function(evt){\n\t\t\t\t\tlog(id, type + evt + ' listener for sendPageInfo');\n\t\t\t\t\tfunc(window,evt,sendPageInfo);\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tfunction stop(){\n\t\t\t\tsetListener('Remove ', removeEventListener);\n\t\t\t}\n\n\t\t\tfunction start(){\n\t\t\t\tsetListener('Add ', addEventListener);\n\t\t\t}\n\t\t\t\n\t\t\tvar id = iframeId; //Create locally scoped copy of iFrame ID\n\n\t\t\tstart();\n\n\t\t\tsettings[id].stopPageInfo = stop;\n\t\t}\n\n\t\tfunction stopPageInfoMonitor(){\n\t\t\tif (settings[iframeId] && settings[iframeId].stopPageInfo){\n\t\t\t\tsettings[iframeId].stopPageInfo();\n\t\t\t\tdelete settings[iframeId].stopPageInfo;\n\t\t\t}\n\t\t}\n\n\t\tfunction checkIFrameExists(){\n\t\t\tvar retBool = true;\n\n\t\t\tif (null === messageData.iframe) {\n\t\t\t\twarn(iframeId,'IFrame ('+messageData.id+') not found');\n\t\t\t\tretBool = false;\n\t\t\t}\n\t\t\treturn retBool;\n\t\t}\n\n\t\tfunction getElementPosition(target){\n\t\t\tvar iFramePosition = target.getBoundingClientRect();\n\n\t\t\tgetPagePosition(iframeId);\n\n\t\t\treturn {\n\t\t\t\tx: Math.floor( Number(iFramePosition.left) + Number(pagePosition.x) ),\n\t\t\t\ty: Math.floor( Number(iFramePosition.top) + Number(pagePosition.y) )\n\t\t\t};\n\t\t}\n\n\t\tfunction scrollRequestFromChild(addOffset){\n\t\t\t/* istanbul ignore next */ //Not testable in Karma\n\t\t\tfunction reposition(){\n\t\t\t\tpagePosition = newPosition;\n\t\t\t\tscrollTo();\n\t\t\t\tlog(iframeId,'--');\n\t\t\t}\n\n\t\t\tfunction calcOffset(){\n\t\t\t\treturn {\n\t\t\t\t\tx: Number(messageData.width) + offset.x,\n\t\t\t\t\ty: Number(messageData.height) + offset.y\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tfunction scrollParent(){\n\t\t\t\tif (window.parentIFrame){\n\t\t\t\t\twindow.parentIFrame['scrollTo'+(addOffset?'Offset':'')](newPosition.x,newPosition.y);\n\t\t\t\t} else {\n\t\t\t\t\twarn(iframeId,'Unable to scroll to requested position, window.parentIFrame not found');\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar\n\t\t\t\toffset = addOffset ? getElementPosition(messageData.iframe) : {x:0,y:0},\n\t\t\t\tnewPosition = calcOffset();\n\n\t\t\tlog(iframeId,'Reposition requested from iFrame (offset x:'+offset.x+' y:'+offset.y+')');\n\n\t\t\tif(window.top!==window.self){\n\t\t\t\tscrollParent();\n\t\t\t} else {\n\t\t\t\treposition();\n\t\t\t}\n\t\t}\n\n\t\tfunction scrollTo(){\n\t\t\tif (false !== callback('scrollCallback',pagePosition)){\n\t\t\t\tsetPagePosition(iframeId);\n\t\t\t} else {\n\t\t\t\tunsetPagePosition();\n\t\t\t}\n\t\t}\n\n\t\tfunction findTarget(location){\n\t\t\tfunction jumpToTarget(){\n\t\t\t\tvar jumpPosition = getElementPosition(target);\n\n\t\t\t\tlog(iframeId,'Moving to in page link (#'+hash+') at x: '+jumpPosition.x+' y: '+jumpPosition.y);\n\t\t\t\tpagePosition = {\n\t\t\t\t\tx: jumpPosition.x,\n\t\t\t\t\ty: jumpPosition.y\n\t\t\t\t};\n\n\t\t\t\tscrollTo();\n\t\t\t\tlog(iframeId,'--');\n\t\t\t}\n\n\t\t\tfunction jumpToParent(){\n\t\t\t\tif (window.parentIFrame){\n\t\t\t\t\twindow.parentIFrame.moveToAnchor(hash);\n\t\t\t\t} else {\n\t\t\t\t\tlog(iframeId,'In page link #'+hash+' not found and window.parentIFrame not found');\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar\n\t\t\t\thash = location.split('#')[1] || '',\n\t\t\t\thashData = decodeURIComponent(hash),\n\t\t\t\ttarget = document.getElementById(hashData) || document.getElementsByName(hashData)[0];\n\n\t\t\tif (target){\n\t\t\t\tjumpToTarget();\n\t\t\t} else if(window.top!==window.self){\n\t\t\t\tjumpToParent();\n\t\t\t} else {\n\t\t\t\tlog(iframeId,'In page link #'+hash+' not found');\n\t\t\t}\n\t\t}\n\n\t\tfunction callback(funcName,val){\n\t\t\treturn chkCallback(iframeId,funcName,val);\n\t\t}\n\n\t\tfunction actionMsg(){\n\n\t\t\tif(settings[iframeId].firstRun) firstRun();\n\n\t\t\tswitch(messageData.type){\n\t\t\tcase 'close':\n\t\t\t\tcloseIFrame(messageData.iframe);\n\t\t\t\tbreak;\n\t\t\tcase 'message':\n\t\t\t\tforwardMsgFromIFrame(getMsgBody(6));\n\t\t\t\tbreak;\n\t\t\tcase 'scrollTo':\n\t\t\t\tscrollRequestFromChild(false);\n\t\t\t\tbreak;\n\t\t\tcase 'scrollToOffset':\n\t\t\t\tscrollRequestFromChild(true);\n\t\t\t\tbreak;\n\t\t\tcase 'pageInfo':\n\t\t\t\tsendPageInfoToIframe(settings[iframeId].iframe,iframeId);\n\t\t\t\tstartPageInfoMonitor();\n\t\t\t\tbreak;\n\t\t\tcase 'pageInfoStop':\n\t\t\t\tstopPageInfoMonitor();\n\t\t\t\tbreak;\n\t\t\tcase 'inPageLink':\n\t\t\t\tfindTarget(getMsgBody(9));\n\t\t\t\tbreak;\n\t\t\tcase 'reset':\n\t\t\t\tresetIFrame(messageData);\n\t\t\t\tbreak;\n\t\t\tcase 'init':\n\t\t\t\tresizeIFrame();\n\t\t\t\tcallback('initCallback',messageData.iframe);\n\t\t\t\tcallback('resizedCallback',messageData);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tresizeIFrame();\n\t\t\t\tcallback('resizedCallback',messageData);\n\t\t\t}\n\t\t}\n\n\t\tfunction hasSettings(iframeId){\n\t\t\tvar retBool = true;\n\n\t\t\tif (!settings[iframeId]){\n\t\t\t\tretBool = false;\n\t\t\t\twarn(messageData.type + ' No settings for ' + iframeId + '. Message was: ' + msg);\n\t\t\t}\n\n\t\t\treturn retBool;\n\t\t}\n\n\t\tfunction iFrameReadyMsgReceived(){\n\t\t\tfor (var iframeId in settings){\n\t\t\t\ttrigger('iFrame requested init',createOutgoingMsg(iframeId),document.getElementById(iframeId),iframeId);\n\t\t\t}\n\t\t}\n\n\t\tfunction firstRun() {\n\t\t\tsettings[iframeId].firstRun = false;\n\t\t}\n\n\t\tvar\n\t\t\tmsg = event.data,\n\t\t\tmessageData = {},\n\t\t\tiframeId = null;\n\n\t\tif('[iFrameResizerChild]Ready' === msg){\n\t\t\tiFrameReadyMsgReceived();\n\t\t} else if (isMessageForUs()){\n\t\t\tmessageData = processMsg();\n\t\t\tiframeId = logId = messageData.id;\n\n\t\t\tif (!isMessageFromMetaParent() && hasSettings(iframeId)){\n\t\t\t\tlog(iframeId,'Received: '+msg);\n\n\t\t\t\tif ( checkIFrameExists() && isMessageFromIFrame() ){\n\t\t\t\t\tactionMsg();\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tinfo(iframeId,'Ignored: '+msg);\n\t\t}\n\n\t}\n\n\n\tfunction chkCallback(iframeId,funcName,val){\n\t\tvar\n\t\t\tfunc = null,\n\t\t\tretVal = null;\n\n\t\tif(settings[iframeId]){\n\t\t\tfunc = settings[iframeId][funcName];\n\n\t\t\tif( 'function' === typeof func){\n\t\t\t\tretVal = func(val);\n\t\t\t} else {\n\t\t\t\tthrow new TypeError(funcName+' on iFrame['+iframeId+'] is not a function');\n\t\t\t}\n\t\t}\n\n\t\treturn retVal;\n\t}\n\n\tfunction closeIFrame(iframe){\n\t\tvar iframeId = iframe.id;\n\n\t\tlog(iframeId,'Removing iFrame: '+iframeId);\n\t\tiframe.parentNode.removeChild(iframe);\n\t\tchkCallback(iframeId,'closedCallback',iframeId);\n\t\tlog(iframeId,'--');\n\t\tdelete settings[iframeId];\n\t}\n\n\tfunction getPagePosition(iframeId){\n\t\tif(null === pagePosition){\n\t\t\tpagePosition = {\n\t\t\t\tx: (window.pageXOffset !== undefined) ? window.pageXOffset : document.documentElement.scrollLeft,\n\t\t\t\ty: (window.pageYOffset !== undefined) ? window.pageYOffset : document.documentElement.scrollTop\n\t\t\t};\n\t\t\tlog(iframeId,'Get page position: '+pagePosition.x+','+pagePosition.y);\n\t\t}\n\t}\n\n\tfunction setPagePosition(iframeId){\n\t\tif(null !== pagePosition){\n\t\t\twindow.scrollTo(pagePosition.x,pagePosition.y);\n\t\t\tlog(iframeId,'Set page position: '+pagePosition.x+','+pagePosition.y);\n\t\t\tunsetPagePosition();\n\t\t}\n\t}\n\n\tfunction unsetPagePosition(){\n\t\tpagePosition = null;\n\t}\n\n\tfunction resetIFrame(messageData){\n\t\tfunction reset(){\n\t\t\tsetSize(messageData);\n\t\t\ttrigger('reset','reset',messageData.iframe,messageData.id);\n\t\t}\n\n\t\tlog(messageData.id,'Size reset requested by '+('init'===messageData.type?'host page':'iFrame'));\n\t\tgetPagePosition(messageData.id);\n\t\tsyncResize(reset,messageData,'reset');\n\t}\n\n\tfunction setSize(messageData){\n\t\tfunction setDimension(dimension){\n\t\t\tmessageData.iframe.style[dimension] = messageData[dimension] + 'px';\n\t\t\tlog(\n\t\t\t\tmessageData.id,\n\t\t\t\t'IFrame (' + iframeId +\n\t\t\t\t') ' + dimension +\n\t\t\t\t' set to ' + messageData[dimension] + 'px'\n\t\t\t);\n\t\t}\n\n\t\tfunction chkZero(dimension){\n\t\t\t//FireFox sets dimension of hidden iFrames to zero.\n\t\t\t//So if we detect that set up an event to check for\n\t\t\t//when iFrame becomes visible.\n\n\t\t\t/* istanbul ignore next */ //Not testable in PhantomJS\n\t\t\tif (!hiddenCheckEnabled && '0' === messageData[dimension]){\n\t\t\t\thiddenCheckEnabled = true;\n\t\t\t\tlog(iframeId,'Hidden iFrame detected, creating visibility listener');\n\t\t\t\tfixHiddenIFrames();\n\t\t\t}\n\t\t}\n\n\t\tfunction processDimension(dimension){\n\t\t\tsetDimension(dimension);\n\t\t\tchkZero(dimension);\n\t\t}\n\n\t\tvar iframeId = messageData.iframe.id;\n\n\t\tif(settings[iframeId]){\n\t\t\tif( settings[iframeId].sizeHeight) { processDimension('height'); }\n\t\t\tif( settings[iframeId].sizeWidth ) { processDimension('width'); }\n\t\t}\n\t}\n\n\tfunction syncResize(func,messageData,doNotSync){\n\t\t/* istanbul ignore if */ //Not testable in PhantomJS\n\t\tif(doNotSync!==messageData.type && requestAnimationFrame){\n\t\t\tlog(messageData.id,'Requesting animation frame');\n\t\t\trequestAnimationFrame(func);\n\t\t} else {\n\t\t\tfunc();\n\t\t}\n\t}\n\n\tfunction trigger(calleeMsg,msg,iframe,id){\n\t\tfunction postMessageToIFrame(){\n\t\t\tvar target = settings[id].targetOrigin;\n\t\t\tlog(id,'[' + calleeMsg + '] Sending msg to iframe['+id+'] ('+msg+') targetOrigin: '+target);\n\t\t\tiframe.contentWindow.postMessage( msgId + msg, target );\n\t\t}\n\n\t\tfunction iFrameNotFound(){\n\t\t\tinfo(id,'[' + calleeMsg + '] IFrame('+id+') not found');\n\t\t\tif(settings[id]) {\n\t\t\t\tdelete settings[id];\n\t\t\t}\n\t\t}\n\n\t\tfunction chkAndSend(){\n\t\t\tif(iframe && 'contentWindow' in iframe && (null !== iframe.contentWindow)){ //Null test for PhantomJS\n\t\t\t\tpostMessageToIFrame();\n\t\t\t} else {\n\t\t\t\tiFrameNotFound();\n\t\t\t}\n\t\t}\n\n\t\tid = id || iframe.id;\n\n\t\tif(settings[id]) {\n\t\t\tchkAndSend();\n\t\t}\n\n\t}\n\n\tfunction createOutgoingMsg(iframeId){\n\t\treturn iframeId +\n\t\t\t':' + settings[iframeId].bodyMarginV1 +\n\t\t\t':' + settings[iframeId].sizeWidth +\n\t\t\t':' + settings[iframeId].log +\n\t\t\t':' + settings[iframeId].interval +\n\t\t\t':' + settings[iframeId].enablePublicMethods +\n\t\t\t':' + settings[iframeId].autoResize +\n\t\t\t':' + settings[iframeId].bodyMargin +\n\t\t\t':' + settings[iframeId].heightCalculationMethod +\n\t\t\t':' + settings[iframeId].bodyBackground +\n\t\t\t':' + settings[iframeId].bodyPadding +\n\t\t\t':' + settings[iframeId].tolerance +\n\t\t\t':' + settings[iframeId].inPageLinks +\n\t\t\t':' + settings[iframeId].resizeFrom +\n\t\t\t':' + settings[iframeId].widthCalculationMethod;\n\t}\n\n\tfunction setupIFrame(iframe,options){\n\t\tfunction setLimits(){\n\t\t\tfunction addStyle(style){\n\t\t\t\tif ((Infinity !== settings[iframeId][style]) && (0 !== settings[iframeId][style])){\n\t\t\t\t\tiframe.style[style] = settings[iframeId][style] + 'px';\n\t\t\t\t\tlog(iframeId,'Set '+style+' = '+settings[iframeId][style]+'px');\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfunction chkMinMax(dimension){\n\t\t\t\tif (settings[iframeId]['min'+dimension]>settings[iframeId]['max'+dimension]){\n\t\t\t\t\tthrow new Error('Value for min'+dimension+' can not be greater than max'+dimension);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tchkMinMax('Height');\n\t\t\tchkMinMax('Width');\n\n\t\t\taddStyle('maxHeight');\n\t\t\taddStyle('minHeight');\n\t\t\taddStyle('maxWidth');\n\t\t\taddStyle('minWidth');\n\t\t}\n\n\t\tfunction newId(){\n\t\t\tvar id = ((options && options.id) || defaults.id + count++);\n\t\t\tif (null!==document.getElementById(id)){\n\t\t\t\tid = id + count++;\n\t\t\t}\n\t\t\treturn id;\n\t\t}\n\n\t\tfunction ensureHasId(iframeId){\n\t\t\tlogId=iframeId;\n\t\t\tif (''===iframeId){\n\t\t\t\tiframe.id = iframeId = newId();\n\t\t\t\tlogEnabled = (options || {}).log;\n\t\t\t\tlogId=iframeId;\n\t\t\t\tlog(iframeId,'Added missing iframe ID: '+ iframeId +' (' + iframe.src + ')');\n\t\t\t}\n\n\n\t\t\treturn iframeId;\n\t\t}\n\n\t\tfunction setScrolling(){\n\t\t\tlog(iframeId,'IFrame scrolling ' + (settings[iframeId].scrolling ? 'enabled' : 'disabled') + ' for ' + iframeId);\n\t\t\tiframe.style.overflow = false === settings[iframeId].scrolling ? 'hidden' : 'auto';\n\t\t\tiframe.scrolling = false === settings[iframeId].scrolling ? 'no' : 'yes';\n\t\t}\n\n\t\t//The V1 iFrame script expects an int, where as in V2 expects a CSS\n\t\t//string value such as '1px 3em', so if we have an int for V2, set V1=V2\n\t\t//and then convert V2 to a string PX value.\n\t\tfunction setupBodyMarginValues(){\n\t\t\tif (('number'===typeof(settings[iframeId].bodyMargin)) || ('0'===settings[iframeId].bodyMargin)){\n\t\t\t\tsettings[iframeId].bodyMarginV1 = settings[iframeId].bodyMargin;\n\t\t\t\tsettings[iframeId].bodyMargin = '' + settings[iframeId].bodyMargin + 'px';\n\t\t\t}\n\t\t}\n\n\t\tfunction checkReset(){\n\t\t\t// Reduce scope of firstRun to function, because IE8's JS execution\n\t\t\t// context stack is borked and this value gets externally\n\t\t\t// changed midway through running this function!!!\n\t\t\tvar\n\t\t\t\tfirstRun = settings[iframeId].firstRun,\n\t\t\t\tresetRequertMethod = settings[iframeId].heightCalculationMethod in resetRequiredMethods;\n\n\t\t\tif (!firstRun && resetRequertMethod){\n\t\t\t\tresetIFrame({iframe:iframe, height:0, width:0, type:'init'});\n\t\t\t}\n\t\t}\n\n\t\tfunction setupIFrameObject(){\n\t\t\tif(Function.prototype.bind){ //Ignore unpolyfilled IE8.\n\t\t\t\tsettings[iframeId].iframe.iFrameResizer = {\n\n\t\t\t\t\tclose : closeIFrame.bind(null,settings[iframeId].iframe),\n\n\t\t\t\t\tresize : trigger.bind(null,'Window resize', 'resize', settings[iframeId].iframe),\n\n\t\t\t\t\tmoveToAnchor : function(anchor){\n\t\t\t\t\t\ttrigger('Move to anchor','moveToAnchor:'+anchor, settings[iframeId].iframe,iframeId);\n\t\t\t\t\t},\n\n\t\t\t\t\tsendMessage : function(message){\n\t\t\t\t\t\tmessage = JSON.stringify(message);\n\t\t\t\t\t\ttrigger('Send Message','message:'+message, settings[iframeId].iframe,iframeId);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t//We have to call trigger twice, as we can not be sure if all\n\t\t//iframes have completed loading when this code runs. The\n\t\t//event listener also catches the page changing in the iFrame.\n\t\tfunction init(msg){\n\t\t\tfunction iFrameLoaded(){\n\t\t\t\ttrigger('iFrame.onload',msg,iframe);\n\t\t\t\tcheckReset();\n\t\t\t}\n\n\t\t\taddEventListener(iframe,'load',iFrameLoaded);\n\t\t\ttrigger('init',msg,iframe);\n\t\t}\n\n\t\tfunction checkOptions(options){\n\t\t\tif ('object' !== typeof options){\n\t\t\t\tthrow new TypeError('Options is not an object');\n\t\t\t}\n\t\t}\n\n\t\tfunction copyOptions(options){\n\t\t\tfor (var option in defaults) {\n\t\t\t\tif (defaults.hasOwnProperty(option)){\n\t\t\t\t\tsettings[iframeId][option] = options.hasOwnProperty(option) ? options[option] : defaults[option];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfunction getTargetOrigin (remoteHost){\n\t\t\treturn ('' === remoteHost || 'file://' === remoteHost) ? '*' : remoteHost;\n\t\t}\n\n\t\tfunction processOptions(options){\n\t\t\toptions = options || {};\n\t\t\tsettings[iframeId] = {\n\t\t\t\tfirstRun\t: true,\n\t\t\t\tiframe\t\t: iframe,\n\t\t\t\tremoteHost\t: iframe.src.split('/').slice(0,3).join('/')\n\t\t\t};\n\n\t\t\tcheckOptions(options);\n\t\t\tcopyOptions(options);\n\n\t\t\tsettings[iframeId].targetOrigin = true === settings[iframeId].checkOrigin ? getTargetOrigin(settings[iframeId].remoteHost) : '*';\n\t\t}\n\n\t\tfunction beenHere(){\n\t\t\treturn (iframeId in settings && 'iFrameResizer' in iframe);\n\t\t}\n\n\t\tvar iframeId = ensureHasId(iframe.id);\n\n\t\tif (!beenHere()){\n\t\t\tprocessOptions(options);\n\t\t\tsetScrolling();\n\t\t\tsetLimits();\n\t\t\tsetupBodyMarginValues();\n\t\t\tinit(createOutgoingMsg(iframeId));\n\t\t\tsetupIFrameObject();\n\t\t} else {\n\t\t\twarn(iframeId,'Ignored iFrame, already setup.');\n\t\t}\n\t}\n\n\tfunction debouce(fn,time){\n\t\tif (null === timer){\n\t\t\ttimer = setTimeout(function(){\n\t\t\t\ttimer = null;\n\t\t\t\tfn();\n\t\t\t}, time);\n\t\t}\n\t}\n\n\t/* istanbul ignore next */ //Not testable in PhantomJS\n\tfunction fixHiddenIFrames(){\n\t\tfunction checkIFrames(){\n\t\t\tfunction checkIFrame(settingId){\n\t\t\t\tfunction chkDimension(dimension){\n\t\t\t\t\treturn '0px' === settings[settingId].iframe.style[dimension];\n\t\t\t\t}\n\n\t\t\t\tfunction isVisible(el) {\n\t\t\t\t\treturn (null !== el.offsetParent);\n\t\t\t\t}\n\n\t\t\t\tif (isVisible(settings[settingId].iframe) && (chkDimension('height') || chkDimension('width'))){\n\t\t\t\t\ttrigger('Visibility change', 'resize', settings[settingId].iframe,settingId);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (var settingId in settings){\n\t\t\t\tcheckIFrame(settingId);\n\t\t\t}\n\t\t}\n\n\t\tfunction mutationObserved(mutations){\n\t\t\tlog('window','Mutation observed: ' + mutations[0].target + ' ' + mutations[0].type);\n\t\t\tdebouce(checkIFrames,16);\n\t\t}\n\n\t\tfunction createMutationObserver(){\n\t\t\tvar\n\t\t\t\ttarget = document.querySelector('body'),\n\n\t\t\t\tconfig = {\n\t\t\t\t\tattributes : true,\n\t\t\t\t\tattributeOldValue : false,\n\t\t\t\t\tcharacterData : true,\n\t\t\t\t\tcharacterDataOldValue : false,\n\t\t\t\t\tchildList : true,\n\t\t\t\t\tsubtree : true\n\t\t\t\t},\n\n\t\t\t\tobserver = new MutationObserver(mutationObserved);\n\n\t\t\tobserver.observe(target, config);\n\t\t}\n\n\t\tvar MutationObserver = window.MutationObserver || window.WebKitMutationObserver;\n\n\t\tif (MutationObserver) createMutationObserver();\n\t}\n\n\n\tfunction resizeIFrames(event){\n\t\tfunction resize(){\n\t\t\tsendTriggerMsg('Window '+event,'resize');\n\t\t}\n\n\t\tlog('window','Trigger event: '+event);\n\t\tdebouce(resize,16);\n\t}\n\n\t/* istanbul ignore next */ //Not testable in PhantomJS\n\tfunction tabVisible() {\n\t\tfunction resize(){\n\t\t\tsendTriggerMsg('Tab Visable','resize');\n\t\t}\n\n\t\tif('hidden' !== document.visibilityState) {\n\t\t\tlog('document','Trigger event: Visiblity change');\n\t\t\tdebouce(resize,16);\n\t\t}\n\t}\n\n\tfunction sendTriggerMsg(eventName,event){\n\t\tfunction isIFrameResizeEnabled(iframeId) {\n\t\t\treturn\t'parent' === settings[iframeId].resizeFrom &&\n\t\t\t\t\tsettings[iframeId].autoResize &&\n\t\t\t\t\t!settings[iframeId].firstRun;\n\t\t}\n\n\t\tfor (var iframeId in settings){\n\t\t\tif(isIFrameResizeEnabled(iframeId)){\n\t\t\t\ttrigger(eventName,event,document.getElementById(iframeId),iframeId);\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction setupEventListeners(){\n\t\taddEventListener(window,'message',iFrameListener);\n\n\t\taddEventListener(window,'resize', function(){resizeIFrames('resize');});\n\n\t\taddEventListener(document,'visibilitychange',tabVisible);\n\t\taddEventListener(document,'-webkit-visibilitychange',tabVisible); //Andriod 4.4\n\t\taddEventListener(window,'focusin',function(){resizeIFrames('focus');}); //IE8-9\n\t\taddEventListener(window,'focus',function(){resizeIFrames('focus');});\n\t}\n\n\n\tfunction factory(){\n\t\tfunction init(options,element){\n\t\t\tfunction chkType(){\n\t\t\t\tif(!element.tagName) {\n\t\t\t\t\tthrow new TypeError('Object is not a valid DOM element');\n\t\t\t\t} else if ('IFRAME' !== element.tagName.toUpperCase()) {\n\t\t\t\t\tthrow new TypeError('Expected <IFRAME> tag, found <'+element.tagName+'>');\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif(element) {\n\t\t\t\tchkType();\n\t\t\t\tsetupIFrame(element, options);\n\t\t\t\tiFrames.push(element);\n\t\t\t}\n\t\t}\n\n\t\tvar iFrames;\n\n\t\tsetupRequestAnimationFrame();\n\t\tsetupEventListeners();\n\n\t\treturn function iFrameResizeF(options,target){\n\t\t\tiFrames = []; //Only return iFrames past in on this call\n\n\t\t\tswitch (typeof(target)){\n\t\t\tcase 'undefined':\n\t\t\tcase 'string':\n\t\t\t\tArray.prototype.forEach.call(\n\t\t\t\t\tdocument.querySelectorAll( target || 'iframe' ),\n\t\t\t\t\tinit.bind(undefined, options)\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase 'object':\n\t\t\t\tinit(options,target);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new TypeError('Unexpected data type ('+typeof(target)+')');\n\t\t\t}\n\n\t\t\treturn iFrames;\n\t\t};\n\t}\n\n\tfunction createJQueryPublicMethod($){\n\t\tif (!$.fn) {\n\t\t\tinfo('','Unable to bind to jQuery, it is not fully loaded.');\n\t\t} else {\n\t\t\t$.fn.iFrameResize = function $iFrameResizeF(options) {\n\t\t\t\tfunction init(index, element) {\n\t\t\t\t\tsetupIFrame(element, options);\n\t\t\t\t}\n\n\t\t\t\treturn this.filter('iframe').each(init).end();\n\t\t\t};\n\t\t}\n\t}\n\n\tif (window.jQuery) { createJQueryPublicMethod(jQuery); }\n\n\tif (typeof define === 'function' && define.amd) {\n\t\tdefine([],factory);\n\t} else if (typeof module === 'object' && typeof module.exports === 'object') { //Node for browserfy\n\t\tmodule.exports = factory();\n\t} else {\n\t\twindow.iFrameResize = window.iFrameResize || factory();\n\t}\n\n})(window || {});\n"]} \ No newline at end of file
diff --git a/libs/bower_components/iframe-resizer/js/iframeResizer.min.js b/libs/bower_components/iframe-resizer/js/iframeResizer.min.js
new file mode 100644
index 0000000000..40e2ec4798
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/js/iframeResizer.min.js
@@ -0,0 +1,9 @@
+/*! iFrame Resizer (iframeSizer.min.js ) - v3.5.5 - 2016-06-16
+ * Desc: Force cross domain iframes to size to content.
+ * Requires: iframeResizer.contentWindow.min.js to be loaded into the target frame.
+ * Copyright: (c) 2016 David J. Bradshaw - dave@bradshaw.net
+ * License: MIT
+ */
+
+!function(a){"use strict";function b(b,c,d){"addEventListener"in a?b.addEventListener(c,d,!1):"attachEvent"in a&&b.attachEvent("on"+c,d)}function c(b,c,d){"removeEventListener"in a?b.removeEventListener(c,d,!1):"detachEvent"in a&&b.detachEvent("on"+c,d)}function d(){var b,c=["moz","webkit","o","ms"];for(b=0;b<c.length&&!N;b+=1)N=a[c[b]+"RequestAnimationFrame"];N||h("setup","RequestAnimationFrame not supported")}function e(b){var c="Host page: "+b;return a.top!==a.self&&(c=a.parentIFrame&&a.parentIFrame.getId?a.parentIFrame.getId()+": "+b:"Nested host page: "+b),c}function f(a){return K+"["+e(a)+"]"}function g(a){return P[a]?P[a].log:G}function h(a,b){k("log",a,b,g(a))}function i(a,b){k("info",a,b,g(a))}function j(a,b){k("warn",a,b,!0)}function k(b,c,d,e){!0===e&&"object"==typeof a.console&&console[b](f(c),d)}function l(d){function e(){function a(){s(V),p(W)}g("Height"),g("Width"),t(a,V,"init")}function f(){var a=U.substr(L).split(":");return{iframe:P[a[0]].iframe,id:a[0],height:a[1],width:a[2],type:a[3]}}function g(a){var b=Number(P[W]["max"+a]),c=Number(P[W]["min"+a]),d=a.toLowerCase(),e=Number(V[d]);h(W,"Checking "+d+" is in range "+c+"-"+b),c>e&&(e=c,h(W,"Set "+d+" to min value")),e>b&&(e=b,h(W,"Set "+d+" to max value")),V[d]=""+e}function k(){function a(){function a(){var a=0,d=!1;for(h(W,"Checking connection is from allowed list of origins: "+c);a<c.length;a++)if(c[a]===b){d=!0;break}return d}function d(){var a=P[W].remoteHost;return h(W,"Checking connection is from: "+a),b===a}return c.constructor===Array?a():d()}var b=d.origin,c=P[W].checkOrigin;if(c&&""+b!="null"&&!a())throw new Error("Unexpected message received from: "+b+" for "+V.iframe.id+". Message was: "+d.data+". This error can be disabled by setting the checkOrigin: false option or by providing of array of trusted domains.");return!0}function l(){return K===(""+U).substr(0,L)&&U.substr(L).split(":")[0]in P}function w(){var a=V.type in{"true":1,"false":1,undefined:1};return a&&h(W,"Ignoring init message from meta parent page"),a}function y(a){return U.substr(U.indexOf(":")+J+a)}function z(a){h(W,"MessageCallback passed: {iframe: "+V.iframe.id+", message: "+a+"}"),N("messageCallback",{iframe:V.iframe,message:JSON.parse(a)}),h(W,"--")}function A(){var b=document.body.getBoundingClientRect(),c=V.iframe.getBoundingClientRect();return JSON.stringify({iframeHeight:c.height,iframeWidth:c.width,clientHeight:Math.max(document.documentElement.clientHeight,a.innerHeight||0),clientWidth:Math.max(document.documentElement.clientWidth,a.innerWidth||0),offsetTop:parseInt(c.top-b.top,10),offsetLeft:parseInt(c.left-b.left,10),scrollTop:a.pageYOffset,scrollLeft:a.pageXOffset})}function B(a,b){function c(){u("Send Page Info","pageInfo:"+A(),a,b)}x(c,32)}function C(){function d(b,c){function d(){P[g]?B(P[g].iframe,g):e()}["scroll","resize"].forEach(function(e){h(g,b+e+" listener for sendPageInfo"),c(a,e,d)})}function e(){d("Remove ",c)}function f(){d("Add ",b)}var g=W;f(),P[g].stopPageInfo=e}function D(){P[W]&&P[W].stopPageInfo&&(P[W].stopPageInfo(),delete P[W].stopPageInfo)}function E(){var a=!0;return null===V.iframe&&(j(W,"IFrame ("+V.id+") not found"),a=!1),a}function F(a){var b=a.getBoundingClientRect();return o(W),{x:Math.floor(Number(b.left)+Number(M.x)),y:Math.floor(Number(b.top)+Number(M.y))}}function G(b){function c(){M=g,H(),h(W,"--")}function d(){return{x:Number(V.width)+f.x,y:Number(V.height)+f.y}}function e(){a.parentIFrame?a.parentIFrame["scrollTo"+(b?"Offset":"")](g.x,g.y):j(W,"Unable to scroll to requested position, window.parentIFrame not found")}var f=b?F(V.iframe):{x:0,y:0},g=d();h(W,"Reposition requested from iFrame (offset x:"+f.x+" y:"+f.y+")"),a.top!==a.self?e():c()}function H(){!1!==N("scrollCallback",M)?p(W):q()}function I(b){function c(){var a=F(g);h(W,"Moving to in page link (#"+e+") at x: "+a.x+" y: "+a.y),M={x:a.x,y:a.y},H(),h(W,"--")}function d(){a.parentIFrame?a.parentIFrame.moveToAnchor(e):h(W,"In page link #"+e+" not found and window.parentIFrame not found")}var e=b.split("#")[1]||"",f=decodeURIComponent(e),g=document.getElementById(f)||document.getElementsByName(f)[0];g?c():a.top!==a.self?d():h(W,"In page link #"+e+" not found")}function N(a,b){return m(W,a,b)}function O(){switch(P[W].firstRun&&T(),V.type){case"close":n(V.iframe);break;case"message":z(y(6));break;case"scrollTo":G(!1);break;case"scrollToOffset":G(!0);break;case"pageInfo":B(P[W].iframe,W),C();break;case"pageInfoStop":D();break;case"inPageLink":I(y(9));break;case"reset":r(V);break;case"init":e(),N("initCallback",V.iframe),N("resizedCallback",V);break;default:e(),N("resizedCallback",V)}}function Q(a){var b=!0;return P[a]||(b=!1,j(V.type+" No settings for "+a+". Message was: "+U)),b}function S(){for(var a in P)u("iFrame requested init",v(a),document.getElementById(a),a)}function T(){P[W].firstRun=!1}var U=d.data,V={},W=null;"[iFrameResizerChild]Ready"===U?S():l()?(V=f(),W=R=V.id,!w()&&Q(W)&&(h(W,"Received: "+U),E()&&k()&&O())):i(W,"Ignored: "+U)}function m(a,b,c){var d=null,e=null;if(P[a]){if(d=P[a][b],"function"!=typeof d)throw new TypeError(b+" on iFrame["+a+"] is not a function");e=d(c)}return e}function n(a){var b=a.id;h(b,"Removing iFrame: "+b),a.parentNode.removeChild(a),m(b,"closedCallback",b),h(b,"--"),delete P[b]}function o(b){null===M&&(M={x:void 0!==a.pageXOffset?a.pageXOffset:document.documentElement.scrollLeft,y:void 0!==a.pageYOffset?a.pageYOffset:document.documentElement.scrollTop},h(b,"Get page position: "+M.x+","+M.y))}function p(b){null!==M&&(a.scrollTo(M.x,M.y),h(b,"Set page position: "+M.x+","+M.y),q())}function q(){M=null}function r(a){function b(){s(a),u("reset","reset",a.iframe,a.id)}h(a.id,"Size reset requested by "+("init"===a.type?"host page":"iFrame")),o(a.id),t(b,a,"reset")}function s(a){function b(b){a.iframe.style[b]=a[b]+"px",h(a.id,"IFrame ("+e+") "+b+" set to "+a[b]+"px")}function c(b){H||"0"!==a[b]||(H=!0,h(e,"Hidden iFrame detected, creating visibility listener"),y())}function d(a){b(a),c(a)}var e=a.iframe.id;P[e]&&(P[e].sizeHeight&&d("height"),P[e].sizeWidth&&d("width"))}function t(a,b,c){c!==b.type&&N?(h(b.id,"Requesting animation frame"),N(a)):a()}function u(a,b,c,d){function e(){var e=P[d].targetOrigin;h(d,"["+a+"] Sending msg to iframe["+d+"] ("+b+") targetOrigin: "+e),c.contentWindow.postMessage(K+b,e)}function f(){i(d,"["+a+"] IFrame("+d+") not found"),P[d]&&delete P[d]}function g(){c&&"contentWindow"in c&&null!==c.contentWindow?e():f()}d=d||c.id,P[d]&&g()}function v(a){return a+":"+P[a].bodyMarginV1+":"+P[a].sizeWidth+":"+P[a].log+":"+P[a].interval+":"+P[a].enablePublicMethods+":"+P[a].autoResize+":"+P[a].bodyMargin+":"+P[a].heightCalculationMethod+":"+P[a].bodyBackground+":"+P[a].bodyPadding+":"+P[a].tolerance+":"+P[a].inPageLinks+":"+P[a].resizeFrom+":"+P[a].widthCalculationMethod}function w(a,c){function d(){function b(b){1/0!==P[w][b]&&0!==P[w][b]&&(a.style[b]=P[w][b]+"px",h(w,"Set "+b+" = "+P[w][b]+"px"))}function c(a){if(P[w]["min"+a]>P[w]["max"+a])throw new Error("Value for min"+a+" can not be greater than max"+a)}c("Height"),c("Width"),b("maxHeight"),b("minHeight"),b("maxWidth"),b("minWidth")}function e(){var a=c&&c.id||S.id+F++;return null!==document.getElementById(a)&&(a+=F++),a}function f(b){return R=b,""===b&&(a.id=b=e(),G=(c||{}).log,R=b,h(b,"Added missing iframe ID: "+b+" ("+a.src+")")),b}function g(){h(w,"IFrame scrolling "+(P[w].scrolling?"enabled":"disabled")+" for "+w),a.style.overflow=!1===P[w].scrolling?"hidden":"auto",a.scrolling=!1===P[w].scrolling?"no":"yes"}function i(){("number"==typeof P[w].bodyMargin||"0"===P[w].bodyMargin)&&(P[w].bodyMarginV1=P[w].bodyMargin,P[w].bodyMargin=""+P[w].bodyMargin+"px")}function k(){var b=P[w].firstRun,c=P[w].heightCalculationMethod in O;!b&&c&&r({iframe:a,height:0,width:0,type:"init"})}function l(){Function.prototype.bind&&(P[w].iframe.iFrameResizer={close:n.bind(null,P[w].iframe),resize:u.bind(null,"Window resize","resize",P[w].iframe),moveToAnchor:function(a){u("Move to anchor","moveToAnchor:"+a,P[w].iframe,w)},sendMessage:function(a){a=JSON.stringify(a),u("Send Message","message:"+a,P[w].iframe,w)}})}function m(c){function d(){u("iFrame.onload",c,a),k()}b(a,"load",d),u("init",c,a)}function o(a){if("object"!=typeof a)throw new TypeError("Options is not an object")}function p(a){for(var b in S)S.hasOwnProperty(b)&&(P[w][b]=a.hasOwnProperty(b)?a[b]:S[b])}function q(a){return""===a||"file://"===a?"*":a}function s(b){b=b||{},P[w]={firstRun:!0,iframe:a,remoteHost:a.src.split("/").slice(0,3).join("/")},o(b),p(b),P[w].targetOrigin=!0===P[w].checkOrigin?q(P[w].remoteHost):"*"}function t(){return w in P&&"iFrameResizer"in a}var w=f(a.id);t()?j(w,"Ignored iFrame, already setup."):(s(c),g(),d(),i(),m(v(w)),l())}function x(a,b){null===Q&&(Q=setTimeout(function(){Q=null,a()},b))}function y(){function b(){function a(a){function b(b){return"0px"===P[a].iframe.style[b]}function c(a){return null!==a.offsetParent}c(P[a].iframe)&&(b("height")||b("width"))&&u("Visibility change","resize",P[a].iframe,a)}for(var b in P)a(b)}function c(a){h("window","Mutation observed: "+a[0].target+" "+a[0].type),x(b,16)}function d(){var a=document.querySelector("body"),b={attributes:!0,attributeOldValue:!1,characterData:!0,characterDataOldValue:!1,childList:!0,subtree:!0},d=new e(c);d.observe(a,b)}var e=a.MutationObserver||a.WebKitMutationObserver;e&&d()}function z(a){function b(){B("Window "+a,"resize")}h("window","Trigger event: "+a),x(b,16)}function A(){function a(){B("Tab Visable","resize")}"hidden"!==document.visibilityState&&(h("document","Trigger event: Visiblity change"),x(a,16))}function B(a,b){function c(a){return"parent"===P[a].resizeFrom&&P[a].autoResize&&!P[a].firstRun}for(var d in P)c(d)&&u(a,b,document.getElementById(d),d)}function C(){b(a,"message",l),b(a,"resize",function(){z("resize")}),b(document,"visibilitychange",A),b(document,"-webkit-visibilitychange",A),b(a,"focusin",function(){z("focus")}),b(a,"focus",function(){z("focus")})}function D(){function a(a,c){function d(){if(!c.tagName)throw new TypeError("Object is not a valid DOM element");if("IFRAME"!==c.tagName.toUpperCase())throw new TypeError("Expected <IFRAME> tag, found <"+c.tagName+">")}c&&(d(),w(c,a),b.push(c))}var b;return d(),C(),function(c,d){switch(b=[],typeof d){case"undefined":case"string":Array.prototype.forEach.call(document.querySelectorAll(d||"iframe"),a.bind(void 0,c));break;case"object":a(c,d);break;default:throw new TypeError("Unexpected data type ("+typeof d+")")}return b}}function E(a){a.fn?a.fn.iFrameResize=function(a){function b(b,c){w(c,a)}return this.filter("iframe").each(b).end()}:i("","Unable to bind to jQuery, it is not fully loaded.")}var F=0,G=!1,H=!1,I="message",J=I.length,K="[iFrameSizer]",L=K.length,M=null,N=a.requestAnimationFrame,O={max:1,scroll:1,bodyScroll:1,documentElementScroll:1},P={},Q=null,R="Host Page",S={autoResize:!0,bodyBackground:null,bodyMargin:null,bodyMarginV1:8,bodyPadding:null,checkOrigin:!0,inPageLinks:!1,enablePublicMethods:!0,heightCalculationMethod:"bodyOffset",id:"iFrameResizer",interval:32,log:!1,maxHeight:1/0,maxWidth:1/0,minHeight:0,minWidth:0,resizeFrom:"parent",scrolling:!1,sizeHeight:!0,sizeWidth:!1,tolerance:0,widthCalculationMethod:"scroll",closedCallback:function(){},initCallback:function(){},messageCallback:function(){j("MessageCallback function not defined")},resizedCallback:function(){},scrollCallback:function(){return!0}};a.jQuery&&E(jQuery),"function"==typeof define&&define.amd?define([],D):"object"==typeof module&&"object"==typeof module.exports?module.exports=D():a.iFrameResize=a.iFrameResize||D()}(window||{});
+//# sourceMappingURL=iframeResizer.map \ No newline at end of file
diff --git a/libs/bower_components/iframe-resizer/js/index.js b/libs/bower_components/iframe-resizer/js/index.js
new file mode 100644
index 0000000000..a9cb2845e2
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/js/index.js
@@ -0,0 +1,2 @@
+exports.iframeResizer = require('./iframeResizer');
+exports.iframeResizerContentWindow = require('./iframeResizer.contentWindow');
diff --git a/libs/bower_components/iframe-resizer/karma.conf.js b/libs/bower_components/iframe-resizer/karma.conf.js
new file mode 100644
index 0000000000..3e558511e6
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/karma.conf.js
@@ -0,0 +1,92 @@
+// Karma configuration
+// Generated on Tue Aug 25 2015 12:11:48 GMT+0100 (BST)
+
+module.exports = function(config) {
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: '',
+
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['jasmine-jquery', 'jasmine', 'requirejs'],
+
+ // Karma will require() these plugins
+ /*
+ plugins: [
+ 'logcapture',
+ 'karma-verbose-summary-reporter',
+ 'karma-jasmine',
+ 'karma-chrome-launcher'
+ ],
+ */
+
+ // list of files / patterns to load in the browser
+ files: [
+ 'test-main.js',
+ 'spec/lib/*.js',
+ 'js/ie8.polyfils.min.js',
+ {pattern: 'js/*.js', included: false},
+ {pattern: 'src/*.js', included: false},
+ {pattern: 'example/*.html', included: false},
+ {pattern: 'spec/*Spec.js', included: false},
+ {pattern: 'spec/resources/*', included: false},
+ {pattern: 'spec/javascripts/fixtures/*.html', included: false}
+ ],
+
+
+ // list of files to exclude
+ exclude: [
+ ],
+
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ 'src/*.js': ['coverage']
+ },
+
+ coverageReporter: {
+ type : 'html',
+ dir : 'coverage/'
+ },
+
+ client: {
+ captureConsole: true
+ },
+
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['logcapture', 'progress', 'verbose-summary', 'coverage'],
+
+
+ // web server port
+ port: 9876,
+
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: true,
+
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: [ ], //'Chrome', 'Firefox', 'Safari', 'PhantomJS'
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: true
+
+ })
+}
diff --git a/libs/bower_components/iframe-resizer/src/ie8.polyfils.js b/libs/bower_components/iframe-resizer/src/ie8.polyfils.js
new file mode 100644
index 0000000000..43dbb13d21
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/src/ie8.polyfils.js
@@ -0,0 +1,64 @@
+/*
+ * IE8 Polyfils for iframeResizer.js
+ *
+ * Public domain code - Mozilla Contributors
+ * https://developer.mozilla.org/
+ */
+
+ if (!Array.prototype.forEach){
+ Array.prototype.forEach = function(fun /*, thisArg */){
+ "use strict";
+ if (this === void 0 || this === null || typeof fun !== "function") throw new TypeError();
+
+ var
+ t = Object(this),
+ len = t.length >>> 0,
+ thisArg = arguments.length >= 2 ? arguments[1] : void 0;
+
+ for (var i = 0; i < len; i++)
+ if (i in t)
+ fun.call(thisArg, t[i], i, t);
+ };
+}
+
+
+if (!Function.prototype.bind) {
+ Function.prototype.bind = function(oThis) {
+ if (typeof this !== 'function') {
+ // closest thing possible to the ECMAScript 5
+ // internal IsCallable function
+ throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
+ }
+
+ var aArgs = Array.prototype.slice.call(arguments, 1),
+ fToBind = this,
+ fNOP = function() {},
+ fBound = function() {
+ return fToBind.apply(this instanceof fNOP ? this : oThis,
+ aArgs.concat(Array.prototype.slice.call(arguments)));
+ };
+
+ fNOP.prototype = this.prototype;
+ fBound.prototype = new fNOP();
+
+ return fBound;
+ };
+}
+
+if (!Array.prototype.forEach) {
+ Array.prototype.forEach = function(callback, thisArg) {
+ if (this === null) throw new TypeError(' this is null or not defined');
+ if (typeof callback !== 'function') throw new TypeError(callback + ' is not a function');
+
+ var
+ O = Object(this),
+ len = O.length >>> 0;
+
+ for (var k=0 ; k < len ; k++) {
+ if (k in O)
+ callback.call(thisArg, O[k], k, O);
+ }
+ };
+}
+
+
diff --git a/libs/bower_components/iframe-resizer/src/iframeResizer.contentWindow.js b/libs/bower_components/iframe-resizer/src/iframeResizer.contentWindow.js
new file mode 100644
index 0000000000..02d2602d8d
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/src/iframeResizer.contentWindow.js
@@ -0,0 +1,1123 @@
+/*
+ * File: iframeResizer.contentWindow.js
+ * Desc: Include this file in any page being loaded into an iframe
+ * to force the iframe to resize to the content size.
+ * Requires: iframeResizer.js on host page.
+ * Doc: https://github.com/davidjbradshaw/iframe-resizer
+ * Author: David J. Bradshaw - dave@bradshaw.net
+ * Contributor: Jure Mav - jure.mav@gmail.com
+ * Contributor: Ian Caunce - ian@hallnet.co.uk
+ */
+
+
+;(function(window, undefined) {
+ 'use strict';
+
+ var
+ autoResize = true,
+ base = 10,
+ bodyBackground = '',
+ bodyMargin = 0,
+ bodyMarginStr = '',
+ bodyObserver = null,
+ bodyPadding = '',
+ calculateWidth = false,
+ doubleEventList = {'resize':1,'click':1},
+ eventCancelTimer = 128,
+ firstRun = true,
+ height = 1,
+ heightCalcModeDefault = 'bodyOffset',
+ heightCalcMode = heightCalcModeDefault,
+ initLock = true,
+ initMsg = '',
+ inPageLinks = {},
+ interval = 32,
+ intervalTimer = null,
+ logging = false,
+ msgID = '[iFrameSizer]', //Must match host page msg ID
+ msgIdLen = msgID.length,
+ myID = '',
+ observer = null,
+ resetRequiredMethods = {max:1,min:1,bodyScroll:1,documentElementScroll:1},
+ resizeFrom = 'child',
+ sendPermit = true,
+ target = window.parent,
+ targetOriginDefault = '*',
+ tolerance = 0,
+ triggerLocked = false,
+ triggerLockedTimer = null,
+ throttledTimer = 16,
+ width = 1,
+ widthCalcModeDefault = 'scroll',
+ widthCalcMode = widthCalcModeDefault,
+ win = window,
+ messageCallback = function(){ warn('MessageCallback function not defined'); },
+ readyCallback = function(){},
+ pageInfoCallback = function(){},
+ customCalcMethods = {
+ height: function(){
+ warn('Custom height calculation function not defined');
+ return document.documentElement.offsetHeight;
+ },
+ width: function(){
+ warn('Custom width calculation function not defined');
+ return document.body.scrollWidth;
+ }
+ };
+
+
+ function addEventListener(el,evt,func){
+ /* istanbul ignore else */ // Not testable in phantonJS
+ if ('addEventListener' in window){
+ el.addEventListener(evt,func, false);
+ } else if ('attachEvent' in window){ //IE
+ el.attachEvent('on'+evt,func);
+ }
+ }
+
+ function removeEventListener(el,evt,func){
+ /* istanbul ignore else */ // Not testable in phantonJS
+ if ('removeEventListener' in window){
+ el.removeEventListener(evt,func, false);
+ } else if ('detachEvent' in window){ //IE
+ el.detachEvent('on'+evt,func);
+ }
+ }
+
+ function capitalizeFirstLetter(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+ }
+
+ //Based on underscore.js
+ function throttle(func) {
+ var
+ context, args, result,
+ timeout = null,
+ previous = 0,
+ later = function() {
+ previous = getNow();
+ timeout = null;
+ result = func.apply(context, args);
+ if (!timeout) {
+ context = args = null;
+ }
+ };
+
+ return function() {
+ var now = getNow();
+
+ if (!previous) {
+ previous = now;
+ }
+
+ var remaining = throttledTimer - (now - previous);
+
+ context = this;
+ args = arguments;
+
+ if (remaining <= 0 || remaining > throttledTimer) {
+ if (timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+
+ previous = now;
+ result = func.apply(context, args);
+
+ if (!timeout) {
+ context = args = null;
+ }
+
+ } else if (!timeout) {
+ timeout = setTimeout(later, remaining);
+ }
+
+ return result;
+ };
+ }
+
+ var getNow = Date.now || function() {
+ /* istanbul ignore next */ // Not testable in PhantonJS
+ return new Date().getTime();
+ };
+
+ function formatLogMsg(msg){
+ return msgID + '[' + myID + ']' + ' ' + msg;
+ }
+
+ function log(msg){
+ if (logging && ('object' === typeof window.console)){
+ console.log(formatLogMsg(msg));
+ }
+ }
+
+ function warn(msg){
+ if ('object' === typeof window.console){
+ console.warn(formatLogMsg(msg));
+ }
+ }
+
+
+ function init(){
+ readDataFromParent();
+ log('Initialising iFrame ('+location.href+')');
+ readDataFromPage();
+ setMargin();
+ setBodyStyle('background',bodyBackground);
+ setBodyStyle('padding',bodyPadding);
+ injectClearFixIntoBodyElement();
+ checkHeightMode();
+ checkWidthMode();
+ stopInfiniteResizingOfIFrame();
+ setupPublicMethods();
+ startEventListeners();
+ inPageLinks = setupInPageLinks();
+ sendSize('init','Init message from host page');
+ readyCallback();
+ }
+
+ function readDataFromParent(){
+
+ function strBool(str){
+ return 'true' === str ? true : false;
+ }
+
+ var data = initMsg.substr(msgIdLen).split(':');
+
+ myID = data[0];
+ bodyMargin = (undefined !== data[1]) ? Number(data[1]) : bodyMargin; //For V1 compatibility
+ calculateWidth = (undefined !== data[2]) ? strBool(data[2]) : calculateWidth;
+ logging = (undefined !== data[3]) ? strBool(data[3]) : logging;
+ interval = (undefined !== data[4]) ? Number(data[4]) : interval;
+ autoResize = (undefined !== data[6]) ? strBool(data[6]) : autoResize;
+ bodyMarginStr = data[7];
+ heightCalcMode = (undefined !== data[8]) ? data[8] : heightCalcMode;
+ bodyBackground = data[9];
+ bodyPadding = data[10];
+ tolerance = (undefined !== data[11]) ? Number(data[11]) : tolerance;
+ inPageLinks.enable = (undefined !== data[12]) ? strBool(data[12]): false;
+ resizeFrom = (undefined !== data[13]) ? data[13] : resizeFrom;
+ widthCalcMode = (undefined !== data[14]) ? data[14] : widthCalcMode;
+ }
+
+ function readDataFromPage(){
+ function readData(){
+ var data = window.iFrameResizer;
+
+ log('Reading data from page: ' + JSON.stringify(data));
+
+ messageCallback = ('messageCallback' in data) ? data.messageCallback : messageCallback;
+ readyCallback = ('readyCallback' in data) ? data.readyCallback : readyCallback;
+ targetOriginDefault = ('targetOrigin' in data) ? data.targetOrigin : targetOriginDefault;
+ heightCalcMode = ('heightCalculationMethod' in data) ? data.heightCalculationMethod : heightCalcMode;
+ widthCalcMode = ('widthCalculationMethod' in data) ? data.widthCalculationMethod : widthCalcMode;
+ }
+
+ function setupCustomCalcMethods(calcMode, calcFunc){
+ if ('function' === typeof calcMode) {
+ log('Setup custom ' + calcFunc + 'CalcMethod');
+ customCalcMethods[calcFunc] = calcMode;
+ calcMode = 'custom';
+ }
+
+ return calcMode;
+ }
+
+ if(('iFrameResizer' in window) && (Object === window.iFrameResizer.constructor)) {
+ readData();
+ heightCalcMode = setupCustomCalcMethods(heightCalcMode, 'height');
+ widthCalcMode = setupCustomCalcMethods(widthCalcMode, 'width');
+ }
+
+ log('TargetOrigin for parent set to: ' + targetOriginDefault);
+ }
+
+
+ function chkCSS(attr,value){
+ if (-1 !== value.indexOf('-')){
+ warn('Negative CSS value ignored for '+attr);
+ value='';
+ }
+ return value;
+ }
+
+ function setBodyStyle(attr,value){
+ if ((undefined !== value) && ('' !== value) && ('null' !== value)){
+ document.body.style[attr] = value;
+ log('Body '+attr+' set to "'+value+'"');
+ }
+ }
+
+ function setMargin(){
+ //If called via V1 script, convert bodyMargin from int to str
+ if (undefined === bodyMarginStr){
+ bodyMarginStr = bodyMargin+'px';
+ }
+
+ setBodyStyle('margin',chkCSS('margin',bodyMarginStr));
+ }
+
+ function stopInfiniteResizingOfIFrame(){
+ document.documentElement.style.height = '';
+ document.body.style.height = '';
+ log('HTML & body height set to "auto"');
+ }
+
+
+ function manageTriggerEvent(options){
+ function handleEvent(){
+ sendSize(options.eventName,options.eventType);
+ }
+
+ var listener = {
+ add: function(eventName){
+ addEventListener(window,eventName,handleEvent);
+ },
+ remove: function(eventName){
+ removeEventListener(window,eventName,handleEvent);
+ }
+ };
+
+ if(options.eventNames && Array.prototype.map){
+ options.eventName = options.eventNames[0];
+ options.eventNames.map(listener[options.method]);
+ } else {
+ listener[options.method](options.eventName);
+ }
+
+ log(capitalizeFirstLetter(options.method) + ' event listener: ' + options.eventType);
+ }
+
+ function manageEventListeners(method){
+ manageTriggerEvent({method:method, eventType: 'Animation Start', eventNames: ['animationstart','webkitAnimationStart'] });
+ manageTriggerEvent({method:method, eventType: 'Animation Iteration', eventNames: ['animationiteration','webkitAnimationIteration'] });
+ manageTriggerEvent({method:method, eventType: 'Animation End', eventNames: ['animationend','webkitAnimationEnd'] });
+ manageTriggerEvent({method:method, eventType: 'Input', eventName: 'input' });
+ manageTriggerEvent({method:method, eventType: 'Mouse Up', eventName: 'mouseup' });
+ manageTriggerEvent({method:method, eventType: 'Mouse Down', eventName: 'mousedown' });
+ manageTriggerEvent({method:method, eventType: 'Orientation Change', eventName: 'orientationchange' });
+ manageTriggerEvent({method:method, eventType: 'Print', eventName: ['afterprint', 'beforeprint'] });
+ manageTriggerEvent({method:method, eventType: 'Ready State Change', eventName: 'readystatechange' });
+ manageTriggerEvent({method:method, eventType: 'Touch Start', eventName: 'touchstart' });
+ manageTriggerEvent({method:method, eventType: 'Touch End', eventName: 'touchend' });
+ manageTriggerEvent({method:method, eventType: 'Touch Cancel', eventName: 'touchcancel' });
+ manageTriggerEvent({method:method, eventType: 'Transition Start', eventNames: ['transitionstart','webkitTransitionStart','MSTransitionStart','oTransitionStart','otransitionstart'] });
+ manageTriggerEvent({method:method, eventType: 'Transition Iteration', eventNames: ['transitioniteration','webkitTransitionIteration','MSTransitionIteration','oTransitionIteration','otransitioniteration'] });
+ manageTriggerEvent({method:method, eventType: 'Transition End', eventNames: ['transitionend','webkitTransitionEnd','MSTransitionEnd','oTransitionEnd','otransitionend'] });
+ if('child' === resizeFrom){
+ manageTriggerEvent({method:method, eventType: 'IFrame Resized', eventName: 'resize' });
+ }
+ }
+
+ function checkCalcMode(calcMode,calcModeDefault,modes,type){
+ if (calcModeDefault !== calcMode){
+ if (!(calcMode in modes)){
+ warn(calcMode + ' is not a valid option for '+type+'CalculationMethod.');
+ calcMode=calcModeDefault;
+ }
+ log(type+' calculation method set to "'+calcMode+'"');
+ }
+
+ return calcMode;
+ }
+
+ function checkHeightMode(){
+ heightCalcMode = checkCalcMode(heightCalcMode,heightCalcModeDefault,getHeight,'height');
+ }
+
+ function checkWidthMode(){
+ widthCalcMode = checkCalcMode(widthCalcMode,widthCalcModeDefault,getWidth,'width');
+ }
+
+ function startEventListeners(){
+ if ( true === autoResize ) {
+ manageEventListeners('add');
+ setupMutationObserver();
+ }
+ else {
+ log('Auto Resize disabled');
+ }
+ }
+
+ function stopMsgsToParent(){
+ log('Disable outgoing messages');
+ sendPermit = false;
+ }
+
+ function removeMsgListener(){
+ log('Remove event listener: Message');
+ removeEventListener(window, 'message', receiver);
+ }
+
+ function disconnectMutationObserver(){
+ if (null !== bodyObserver){
+ /* istanbul ignore next */ // Not testable in PhantonJS
+ bodyObserver.disconnect();
+ }
+ }
+
+ function stopEventListeners(){
+ manageEventListeners('remove');
+ disconnectMutationObserver();
+ clearInterval(intervalTimer);
+ }
+
+ function teardown(){
+ stopMsgsToParent();
+ removeMsgListener();
+ if (true === autoResize) stopEventListeners();
+ }
+
+ function injectClearFixIntoBodyElement(){
+ var clearFix = document.createElement('div');
+ clearFix.style.clear = 'both';
+ clearFix.style.display = 'block'; //Guard against this having been globally redefined in CSS.
+ document.body.appendChild(clearFix);
+ }
+
+ function setupInPageLinks(){
+
+ function getPagePosition (){
+ return {
+ x: (window.pageXOffset !== undefined) ? window.pageXOffset : document.documentElement.scrollLeft,
+ y: (window.pageYOffset !== undefined) ? window.pageYOffset : document.documentElement.scrollTop
+ };
+ }
+
+ function getElementPosition(el){
+ var
+ elPosition = el.getBoundingClientRect(),
+ pagePosition = getPagePosition();
+
+ return {
+ x: parseInt(elPosition.left,10) + parseInt(pagePosition.x,10),
+ y: parseInt(elPosition.top,10) + parseInt(pagePosition.y,10)
+ };
+ }
+
+ function findTarget(location){
+ function jumpToTarget(target){
+ var jumpPosition = getElementPosition(target);
+
+ log('Moving to in page link (#'+hash+') at x: '+jumpPosition.x+' y: '+jumpPosition.y);
+ sendMsg(jumpPosition.y, jumpPosition.x, 'scrollToOffset'); // X&Y reversed at sendMsg uses height/width
+ }
+
+ var
+ hash = location.split('#')[1] || location, //Remove # if present
+ hashData = decodeURIComponent(hash),
+ target = document.getElementById(hashData) || document.getElementsByName(hashData)[0];
+
+ if (undefined !== target){
+ jumpToTarget(target);
+ } else {
+ log('In page link (#' + hash + ') not found in iFrame, so sending to parent');
+ sendMsg(0,0,'inPageLink','#'+hash);
+ }
+ }
+
+ function checkLocationHash(){
+ if ('' !== location.hash && '#' !== location.hash){
+ findTarget(location.href);
+ }
+ }
+
+ function bindAnchors(){
+ function setupLink(el){
+ function linkClicked(e){
+ e.preventDefault();
+
+ /*jshint validthis:true */
+ findTarget(this.getAttribute('href'));
+ }
+
+ if ('#' !== el.getAttribute('href')){
+ addEventListener(el,'click',linkClicked);
+ }
+ }
+
+ Array.prototype.forEach.call( document.querySelectorAll( 'a[href^="#"]' ), setupLink );
+ }
+
+ function bindLocationHash(){
+ addEventListener(window,'hashchange',checkLocationHash);
+ }
+
+ function initCheck(){ //check if page loaded with location hash after init resize
+ setTimeout(checkLocationHash,eventCancelTimer);
+ }
+
+ function enableInPageLinks(){
+ /* istanbul ignore else */ // Not testable in phantonJS
+ if(Array.prototype.forEach && document.querySelectorAll){
+ log('Setting up location.hash handlers');
+ bindAnchors();
+ bindLocationHash();
+ initCheck();
+ } else {
+ warn('In page linking not fully supported in this browser! (See README.md for IE8 workaround)');
+ }
+ }
+
+ if(inPageLinks.enable){
+ enableInPageLinks();
+ } else {
+ log('In page linking not enabled');
+ }
+
+ return {
+ findTarget:findTarget
+ };
+ }
+
+ function setupPublicMethods(){
+ log('Enable public methods');
+
+ win.parentIFrame = {
+
+ autoResize: function autoResizeF(resize){
+ if (true === resize && false === autoResize) {
+ autoResize=true;
+ startEventListeners();
+ //sendSize('autoResize','Auto Resize enabled');
+ } else if (false === resize && true === autoResize) {
+ autoResize=false;
+ stopEventListeners();
+ }
+
+ return autoResize;
+ },
+
+ close: function closeF(){
+ sendMsg(0,0,'close');
+ teardown();
+ },
+
+ getId: function getIdF(){
+ return myID;
+ },
+
+ getPageInfo: function getPageInfoF(callback){
+ if ('function' === typeof callback){
+ pageInfoCallback = callback;
+ sendMsg(0,0,'pageInfo');
+ } else {
+ pageInfoCallback = function(){};
+ sendMsg(0,0,'pageInfoStop');
+ }
+ },
+
+ moveToAnchor: function moveToAnchorF(hash){
+ inPageLinks.findTarget(hash);
+ },
+
+ reset: function resetF(){
+ resetIFrame('parentIFrame.reset');
+ },
+
+ scrollTo: function scrollToF(x,y){
+ sendMsg(y,x,'scrollTo'); // X&Y reversed at sendMsg uses height/width
+ },
+
+ scrollToOffset: function scrollToF(x,y){
+ sendMsg(y,x,'scrollToOffset'); // X&Y reversed at sendMsg uses height/width
+ },
+
+ sendMessage: function sendMessageF(msg,targetOrigin){
+ sendMsg(0,0,'message',JSON.stringify(msg),targetOrigin);
+ },
+
+ setHeightCalculationMethod: function setHeightCalculationMethodF(heightCalculationMethod){
+ heightCalcMode = heightCalculationMethod;
+ checkHeightMode();
+ },
+
+ setWidthCalculationMethod: function setWidthCalculationMethodF(widthCalculationMethod){
+ widthCalcMode = widthCalculationMethod;
+ checkWidthMode();
+ },
+
+ setTargetOrigin: function setTargetOriginF(targetOrigin){
+ log('Set targetOrigin: '+targetOrigin);
+ targetOriginDefault = targetOrigin;
+ },
+
+ size: function sizeF(customHeight, customWidth){
+ var valString = ''+(customHeight?customHeight:'')+(customWidth?','+customWidth:'');
+ //lockTrigger();
+ sendSize('size','parentIFrame.size('+valString+')', customHeight, customWidth);
+ }
+ };
+ }
+
+ function initInterval(){
+ if ( 0 !== interval ){
+ log('setInterval: '+interval+'ms');
+ intervalTimer = setInterval(function(){
+ sendSize('interval','setInterval: '+interval);
+ },Math.abs(interval));
+ }
+ }
+
+ /* istanbul ignore next */ //Not testable in PhantomJS
+ function setupBodyMutationObserver(){
+ function addImageLoadListners(mutation) {
+ function addImageLoadListener(element){
+ if (false === element.complete) {
+ log('Attach listeners to ' + element.src);
+ element.addEventListener('load', imageLoaded, false);
+ element.addEventListener('error', imageError, false);
+ elements.push(element);
+ }
+ }
+
+ if (mutation.type === 'attributes' && mutation.attributeName === 'src'){
+ addImageLoadListener(mutation.target);
+ } else if (mutation.type === 'childList'){
+ Array.prototype.forEach.call(
+ mutation.target.querySelectorAll('img'),
+ addImageLoadListener
+ );
+ }
+ }
+
+ function removeFromArray(element){
+ elements.splice(elements.indexOf(element),1);
+ }
+
+ function removeImageLoadListener(element){
+ log('Remove listeners from ' + element.src);
+ element.removeEventListener('load', imageLoaded, false);
+ element.removeEventListener('error', imageError, false);
+ removeFromArray(element);
+ }
+
+ function imageEventTriggered(event,type,typeDesc){
+ removeImageLoadListener(event.target);
+ sendSize(type, typeDesc + ': ' + event.target.src, undefined, undefined);
+ }
+
+ function imageLoaded(event) {
+ imageEventTriggered(event,'imageLoad','Image loaded');
+ }
+
+ function imageError(event) {
+ imageEventTriggered(event,'imageLoadFailed','Image load failed');
+ }
+
+ function mutationObserved(mutations) {
+ sendSize('mutationObserver','mutationObserver: ' + mutations[0].target + ' ' + mutations[0].type);
+
+ //Deal with WebKit asyncing image loading when tags are injected into the page
+ mutations.forEach(addImageLoadListners);
+ }
+
+ function createMutationObserver(){
+ var
+ target = document.querySelector('body'),
+
+ config = {
+ attributes : true,
+ attributeOldValue : false,
+ characterData : true,
+ characterDataOldValue : false,
+ childList : true,
+ subtree : true
+ };
+
+ observer = new MutationObserver(mutationObserved);
+
+ log('Create body MutationObserver');
+ observer.observe(target, config);
+
+ return observer;
+ }
+
+ var
+ elements = [],
+ MutationObserver = window.MutationObserver || window.WebKitMutationObserver,
+ observer = createMutationObserver();
+
+ return {
+ disconnect: function (){
+ if ('disconnect' in observer){
+ log('Disconnect body MutationObserver');
+ observer.disconnect();
+ elements.forEach(removeImageLoadListener);
+ }
+ }
+ };
+ }
+
+ function setupMutationObserver(){
+ var forceIntervalTimer = 0 > interval;
+
+ /* istanbul ignore if */ // Not testable in PhantomJS
+ if (window.MutationObserver || window.WebKitMutationObserver){
+ if (forceIntervalTimer) {
+ initInterval();
+ } else {
+ bodyObserver = setupBodyMutationObserver();
+ }
+ } else {
+ log('MutationObserver not supported in this browser!');
+ initInterval();
+ }
+ }
+
+
+ // document.documentElement.offsetHeight is not reliable, so
+ // we have to jump through hoops to get a better value.
+ function getComputedStyle(prop,el) {
+ /* istanbul ignore next */ //Not testable in PhantomJS
+ function convertUnitsToPxForIE8(value) {
+ var PIXEL = /^\d+(px)?$/i;
+
+ if (PIXEL.test(value)) {
+ return parseInt(value,base);
+ }
+
+ var
+ style = el.style.left,
+ runtimeStyle = el.runtimeStyle.left;
+
+ el.runtimeStyle.left = el.currentStyle.left;
+ el.style.left = value || 0;
+ value = el.style.pixelLeft;
+ el.style.left = style;
+ el.runtimeStyle.left = runtimeStyle;
+
+ return value;
+ }
+
+ var retVal = 0;
+ el = el || document.body;
+
+ /* istanbul ignore else */ // Not testable in phantonJS
+ if (('defaultView' in document) && ('getComputedStyle' in document.defaultView)) {
+ retVal = document.defaultView.getComputedStyle(el, null);
+ retVal = (null !== retVal) ? retVal[prop] : 0;
+ } else {//IE8
+ retVal = convertUnitsToPxForIE8(el.currentStyle[prop]);
+ }
+
+ return parseInt(retVal,base);
+ }
+
+ function chkEventThottle(timer){
+ if(timer > throttledTimer/2){
+ throttledTimer = 2*timer;
+ log('Event throttle increased to ' + throttledTimer + 'ms');
+ }
+ }
+
+ //Idea from https://github.com/guardian/iframe-messenger
+ function getMaxElement(side,elements) {
+ var
+ elementsLength = elements.length,
+ elVal = 0,
+ maxVal = 0,
+ Side = capitalizeFirstLetter(side),
+ timer = getNow();
+
+ for (var i = 0; i < elementsLength; i++) {
+ elVal = elements[i].getBoundingClientRect()[side] + getComputedStyle('margin'+Side,elements[i]);
+ if (elVal > maxVal) {
+ maxVal = elVal;
+ }
+ }
+
+ timer = getNow() - timer;
+
+ log('Parsed '+elementsLength+' HTML elements');
+ log('Element position calculated in ' + timer + 'ms');
+
+ chkEventThottle(timer);
+
+ return maxVal;
+ }
+
+ function getAllMeasurements(dimention){
+ return [
+ dimention.bodyOffset(),
+ dimention.bodyScroll(),
+ dimention.documentElementOffset(),
+ dimention.documentElementScroll()
+ ];
+ }
+
+ function getTaggedElements(side,tag){
+ function noTaggedElementsFound(){
+ warn('No tagged elements ('+tag+') found on page');
+ return height; //current height
+ }
+
+ var elements = document.querySelectorAll('['+tag+']');
+
+ return 0 === elements.length ? noTaggedElementsFound() : getMaxElement(side,elements);
+ }
+
+ function getAllElements(){
+ return document.querySelectorAll('body *');
+ }
+
+ var
+ getHeight = {
+ bodyOffset: function getBodyOffsetHeight(){
+ return document.body.offsetHeight + getComputedStyle('marginTop') + getComputedStyle('marginBottom');
+ },
+
+ offset: function(){
+ return getHeight.bodyOffset(); //Backwards compatability
+ },
+
+ bodyScroll: function getBodyScrollHeight(){
+ return document.body.scrollHeight;
+ },
+
+ custom: function getCustomWidth(){
+ return customCalcMethods.height();
+ },
+
+ documentElementOffset: function getDEOffsetHeight(){
+ return document.documentElement.offsetHeight;
+ },
+
+ documentElementScroll: function getDEScrollHeight(){
+ return document.documentElement.scrollHeight;
+ },
+
+ max: function getMaxHeight(){
+ return Math.max.apply(null,getAllMeasurements(getHeight));
+ },
+
+ min: function getMinHeight(){
+ return Math.min.apply(null,getAllMeasurements(getHeight));
+ },
+
+ grow: function growHeight(){
+ return getHeight.max(); //Run max without the forced downsizing
+ },
+
+ lowestElement: function getBestHeight(){
+ return Math.max(getHeight.bodyOffset(), getMaxElement('bottom',getAllElements()));
+ },
+
+ taggedElement: function getTaggedElementsHeight(){
+ return getTaggedElements('bottom','data-iframe-height');
+ }
+ },
+
+ getWidth = {
+ bodyScroll: function getBodyScrollWidth(){
+ return document.body.scrollWidth;
+ },
+
+ bodyOffset: function getBodyOffsetWidth(){
+ return document.body.offsetWidth;
+ },
+
+ custom: function getCustomWidth(){
+ return customCalcMethods.width();
+ },
+
+ documentElementScroll: function getDEScrollWidth(){
+ return document.documentElement.scrollWidth;
+ },
+
+ documentElementOffset: function getDEOffsetWidth(){
+ return document.documentElement.offsetWidth;
+ },
+
+ scroll: function getMaxWidth(){
+ return Math.max(getWidth.bodyScroll(), getWidth.documentElementScroll());
+ },
+
+ max: function getMaxWidth(){
+ return Math.max.apply(null,getAllMeasurements(getWidth));
+ },
+
+ min: function getMinWidth(){
+ return Math.min.apply(null,getAllMeasurements(getWidth));
+ },
+
+ rightMostElement: function rightMostElement(){
+ return getMaxElement('right', getAllElements());
+ },
+
+ taggedElement: function getTaggedElementsWidth(){
+ return getTaggedElements('right', 'data-iframe-width');
+ }
+ };
+
+
+ function sizeIFrame(triggerEvent, triggerEventDesc, customHeight, customWidth){
+
+ function resizeIFrame(){
+ height = currentHeight;
+ width = currentWidth;
+
+ sendMsg(height,width,triggerEvent);
+ }
+
+ function isSizeChangeDetected(){
+ function checkTolarance(a,b){
+ var retVal = Math.abs(a-b) <= tolerance;
+ return !retVal;
+ }
+
+ currentHeight = (undefined !== customHeight) ? customHeight : getHeight[heightCalcMode]();
+ currentWidth = (undefined !== customWidth ) ? customWidth : getWidth[widthCalcMode]();
+
+ return checkTolarance(height,currentHeight) || (calculateWidth && checkTolarance(width,currentWidth));
+ }
+
+ function isForceResizableEvent(){
+ return !(triggerEvent in {'init':1,'interval':1,'size':1});
+ }
+
+ function isForceResizableCalcMode(){
+ return (heightCalcMode in resetRequiredMethods) || (calculateWidth && widthCalcMode in resetRequiredMethods);
+ }
+
+ function logIgnored(){
+ log('No change in size detected');
+ }
+
+ function checkDownSizing(){
+ if (isForceResizableEvent() && isForceResizableCalcMode()){
+ resetIFrame(triggerEventDesc);
+ } else if (!(triggerEvent in {'interval':1})){
+ logIgnored();
+ }
+ }
+
+ var currentHeight,currentWidth;
+
+ if (isSizeChangeDetected() || 'init' === triggerEvent){
+ lockTrigger();
+ resizeIFrame();
+ } else {
+ checkDownSizing();
+ }
+ }
+
+ var sizeIFrameThrottled = throttle(sizeIFrame);
+
+ function sendSize(triggerEvent, triggerEventDesc, customHeight, customWidth){
+ function recordTrigger(){
+ if (!(triggerEvent in {'reset':1,'resetPage':1,'init':1})){
+ log( 'Trigger event: ' + triggerEventDesc );
+ }
+ }
+
+ function isDoubleFiredEvent(){
+ return triggerLocked && (triggerEvent in doubleEventList);
+ }
+
+ if (!isDoubleFiredEvent()){
+ recordTrigger();
+ sizeIFrameThrottled(triggerEvent, triggerEventDesc, customHeight, customWidth);
+ } else {
+ log('Trigger event cancelled: '+triggerEvent);
+ }
+ }
+
+ function lockTrigger(){
+ if (!triggerLocked){
+ triggerLocked = true;
+ log('Trigger event lock on');
+ }
+ clearTimeout(triggerLockedTimer);
+ triggerLockedTimer = setTimeout(function(){
+ triggerLocked = false;
+ log('Trigger event lock off');
+ log('--');
+ },eventCancelTimer);
+ }
+
+ function triggerReset(triggerEvent){
+ height = getHeight[heightCalcMode]();
+ width = getWidth[widthCalcMode]();
+
+ sendMsg(height,width,triggerEvent);
+ }
+
+ function resetIFrame(triggerEventDesc){
+ var hcm = heightCalcMode;
+ heightCalcMode = heightCalcModeDefault;
+
+ log('Reset trigger event: ' + triggerEventDesc);
+ lockTrigger();
+ triggerReset('reset');
+
+ heightCalcMode = hcm;
+ }
+
+ function sendMsg(height,width,triggerEvent,msg,targetOrigin){
+ function setTargetOrigin(){
+ if (undefined === targetOrigin){
+ targetOrigin = targetOriginDefault;
+ } else {
+ log('Message targetOrigin: '+targetOrigin);
+ }
+ }
+
+ function sendToParent(){
+ var
+ size = height + ':' + width,
+ message = myID + ':' + size + ':' + triggerEvent + (undefined !== msg ? ':' + msg : '');
+
+ log('Sending message to host page (' + message + ')');
+ target.postMessage( msgID + message, targetOrigin);
+ }
+
+ if(true === sendPermit){
+ setTargetOrigin();
+ sendToParent();
+ }
+ }
+
+ function receiver(event) {
+ function isMessageForUs(){
+ return msgID === (''+event.data).substr(0,msgIdLen); //''+ Protects against non-string messages
+ }
+
+ function initFromParent(){
+ function fireInit(){
+ initMsg = event.data;
+ target = event.source;
+
+ init();
+ firstRun = false;
+ setTimeout(function(){ initLock = false;},eventCancelTimer);
+ }
+
+ if (document.body){
+ fireInit();
+ } else {
+ log('Waiting for page ready');
+ addEventListener(window,'readystatechange',initFromParent);
+ }
+ }
+
+ function resetFromParent(){
+ if (!initLock){
+ log('Page size reset by host page');
+ triggerReset('resetPage');
+ } else {
+ log('Page reset ignored by init');
+ }
+ }
+
+ function resizeFromParent(){
+ sendSize('resizeParent','Parent window requested size check');
+ }
+
+ function moveToAnchor(){
+ var anchor = getData();
+ inPageLinks.findTarget(anchor);
+ }
+
+ function getMessageType(){
+ return event.data.split(']')[1].split(':')[0];
+ }
+
+ function getData(){
+ return event.data.substr(event.data.indexOf(':')+1);
+ }
+
+ function isMiddleTier(){
+ return ('iFrameResize' in window);
+ }
+
+ function messageFromParent(){
+ var msgBody = getData();
+
+ log('MessageCallback called from parent: ' + msgBody );
+ messageCallback(JSON.parse(msgBody));
+ log(' --');
+ }
+
+ function pageInfoFromParent(){
+ var msgBody = getData();
+ log('PageInfoFromParent called from parent: ' + msgBody );
+ pageInfoCallback(JSON.parse(msgBody));
+ log(' --');
+ }
+
+ function isInitMsg(){
+ //Test if this message is from a child below us. This is an ugly test, however, updating
+ //the message format would break backwards compatibity.
+ return event.data.split(':')[2] in {'true':1,'false':1};
+ }
+
+ function callFromParent(){
+ switch (getMessageType()){
+ case 'reset':
+ resetFromParent();
+ break;
+ case 'resize':
+ resizeFromParent();
+ break;
+ case 'inPageLink':
+ case 'moveToAnchor':
+ moveToAnchor();
+ break;
+ case 'message':
+ messageFromParent();
+ break;
+ case 'pageInfo':
+ pageInfoFromParent();
+ break;
+ default:
+ if (!isMiddleTier() && !isInitMsg()){
+ warn('Unexpected message ('+event.data+')');
+ }
+ }
+ }
+
+ function processMessage(){
+ if (false === firstRun) {
+ callFromParent();
+ } else if (isInitMsg()) {
+ initFromParent();
+ } else {
+ log('Ignored message of type "' + getMessageType() + '". Received before initialization.');
+ }
+ }
+
+ if (isMessageForUs()){
+ processMessage();
+ }
+ }
+
+ //Normally the parent kicks things off when it detects the iFrame has loaded.
+ //If this script is async-loaded, then tell parent page to retry init.
+ function chkLateLoaded(){
+ if('loading' !== document.readyState){
+ window.parent.postMessage('[iFrameResizerChild]Ready','*');
+ }
+ }
+
+ addEventListener(window, 'message', receiver);
+ chkLateLoaded();
+
+ // TEST CODE START //
+
+ //Create test hooks
+
+ function mockMsgListener(msgObject){
+ receiver(msgObject);
+ return win;
+ }
+
+ win={};
+
+ removeEventListener(window, 'message', receiver);
+
+ define([], function() {return mockMsgListener;});
+
+ // TEST CODE END //
+
+})(window || {});
diff --git a/libs/bower_components/iframe-resizer/src/iframeResizer.js b/libs/bower_components/iframe-resizer/src/iframeResizer.js
new file mode 100644
index 0000000000..897a632ce4
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/src/iframeResizer.js
@@ -0,0 +1,1002 @@
+/*
+ * File: iframeResizer.js
+ * Desc: Force iframes to size to content.
+ * Requires: iframeResizer.contentWindow.js to be loaded into the target frame.
+ * Doc: https://github.com/davidjbradshaw/iframe-resizer
+ * Author: David J. Bradshaw - dave@bradshaw.net
+ * Contributor: Jure Mav - jure.mav@gmail.com
+ * Contributor: Reed Dadoune - reed@dadoune.com
+ */
+
+
+;(function(window) {
+ 'use strict';
+
+ var
+ count = 0,
+ logEnabled = false,
+ hiddenCheckEnabled = false,
+ msgHeader = 'message',
+ msgHeaderLen = msgHeader.length,
+ msgId = '[iFrameSizer]', //Must match iframe msg ID
+ msgIdLen = msgId.length,
+ pagePosition = null,
+ requestAnimationFrame = window.requestAnimationFrame,
+ resetRequiredMethods = {max:1,scroll:1,bodyScroll:1,documentElementScroll:1},
+ settings = {},
+ timer = null,
+ logId = 'Host Page',
+
+ defaults = {
+ autoResize : true,
+ bodyBackground : null,
+ bodyMargin : null,
+ bodyMarginV1 : 8,
+ bodyPadding : null,
+ checkOrigin : true,
+ inPageLinks : false,
+ enablePublicMethods : true,
+ heightCalculationMethod : 'bodyOffset',
+ id : 'iFrameResizer',
+ interval : 32,
+ log : false,
+ maxHeight : Infinity,
+ maxWidth : Infinity,
+ minHeight : 0,
+ minWidth : 0,
+ resizeFrom : 'parent',
+ scrolling : false,
+ sizeHeight : true,
+ sizeWidth : false,
+ tolerance : 0,
+ widthCalculationMethod : 'scroll',
+ closedCallback : function(){},
+ initCallback : function(){},
+ messageCallback : function(){warn('MessageCallback function not defined');},
+ resizedCallback : function(){},
+ scrollCallback : function(){return true;}
+ };
+
+ function addEventListener(obj,evt,func){
+ /* istanbul ignore else */ // Not testable in PhantonJS
+ if ('addEventListener' in window){
+ obj.addEventListener(evt,func, false);
+ } else if ('attachEvent' in window){//IE
+ obj.attachEvent('on'+evt,func);
+ }
+ }
+
+ function removeEventListener(el,evt,func){
+ /* istanbul ignore else */ // Not testable in phantonJS
+ if ('removeEventListener' in window){
+ el.removeEventListener(evt,func, false);
+ } else if ('detachEvent' in window){ //IE
+ el.detachEvent('on'+evt,func);
+ }
+ }
+
+ function setupRequestAnimationFrame(){
+ var
+ vendors = ['moz', 'webkit', 'o', 'ms'],
+ x;
+
+ // Remove vendor prefixing if prefixed and break early if not
+ for (x = 0; x < vendors.length && !requestAnimationFrame; x += 1) {
+ requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
+ }
+
+ if (!(requestAnimationFrame)){
+ log('setup','RequestAnimationFrame not supported');
+ }
+ }
+
+ function getMyID(iframeId){
+ var retStr = 'Host page: '+iframeId;
+
+ if (window.top!==window.self){
+ if (window.parentIFrame && window.parentIFrame.getId){
+ retStr = window.parentIFrame.getId()+': '+iframeId;
+ } else {
+ retStr = 'Nested host page: '+iframeId;
+ }
+ }
+
+ return retStr;
+ }
+
+ function formatLogHeader(iframeId){
+ return msgId + '[' + getMyID(iframeId) + ']';
+ }
+
+ function isLogEnabled(iframeId){
+ return settings[iframeId] ? settings[iframeId].log : logEnabled;
+ }
+
+ function log(iframeId,msg){
+ output('log',iframeId,msg,isLogEnabled(iframeId));
+ }
+
+ function info(iframeId,msg){
+ output('info',iframeId,msg,isLogEnabled(iframeId));
+ }
+
+ function warn(iframeId,msg){
+ output('warn',iframeId,msg,true);
+ }
+
+ function output(type,iframeId,msg,enabled){
+ if (true === enabled && 'object' === typeof window.console){
+ console[type](formatLogHeader(iframeId),msg);
+ }
+ }
+
+ function iFrameListener(event){
+ function resizeIFrame(){
+ function resize(){
+ setSize(messageData);
+ setPagePosition(iframeId);
+ }
+
+ ensureInRange('Height');
+ ensureInRange('Width');
+
+ syncResize(resize,messageData,'init');
+ }
+
+ function processMsg(){
+ var data = msg.substr(msgIdLen).split(':');
+
+ return {
+ iframe: settings[data[0]].iframe,
+ id: data[0],
+ height: data[1],
+ width: data[2],
+ type: data[3]
+ };
+ }
+
+ function ensureInRange(Dimension){
+ var
+ max = Number(settings[iframeId]['max' + Dimension]),
+ min = Number(settings[iframeId]['min' + Dimension]),
+ dimension = Dimension.toLowerCase(),
+ size = Number(messageData[dimension]);
+
+ log(iframeId,'Checking ' + dimension + ' is in range ' + min + '-' + max);
+
+ if (size<min) {
+ size=min;
+ log(iframeId,'Set ' + dimension + ' to min value');
+ }
+
+ if (size>max) {
+ size=max;
+ log(iframeId,'Set ' + dimension + ' to max value');
+ }
+
+ messageData[dimension] = '' + size;
+ }
+
+
+ function isMessageFromIFrame(){
+ function checkAllowedOrigin(){
+ function checkList(){
+ var
+ i = 0,
+ retCode = false;
+
+ log(iframeId,'Checking connection is from allowed list of origins: ' + checkOrigin);
+
+ for (; i < checkOrigin.length; i++) {
+ if (checkOrigin[i] === origin) {
+ retCode = true;
+ break;
+ }
+ }
+ return retCode;
+ }
+
+ function checkSingle(){
+ var remoteHost = settings[iframeId].remoteHost;
+ log(iframeId,'Checking connection is from: '+remoteHost);
+ return origin === remoteHost;
+ }
+
+ return checkOrigin.constructor === Array ? checkList() : checkSingle();
+ }
+
+ var
+ origin = event.origin,
+ checkOrigin = settings[iframeId].checkOrigin;
+
+ if (checkOrigin && (''+origin !== 'null') && !checkAllowedOrigin()) {
+ throw new Error(
+ 'Unexpected message received from: ' + origin +
+ ' for ' + messageData.iframe.id +
+ '. Message was: ' + event.data +
+ '. This error can be disabled by setting the checkOrigin: false option or by providing of array of trusted domains.'
+ );
+ }
+
+ return true;
+ }
+
+ function isMessageForUs(){
+ return msgId === (('' + msg).substr(0,msgIdLen)) && (msg.substr(msgIdLen).split(':')[0] in settings); //''+Protects against non-string msg
+ }
+
+ function isMessageFromMetaParent(){
+ //Test if this message is from a parent above us. This is an ugly test, however, updating
+ //the message format would break backwards compatibity.
+ var retCode = messageData.type in {'true':1,'false':1,'undefined':1};
+
+ if (retCode){
+ log(iframeId,'Ignoring init message from meta parent page');
+ }
+
+ return retCode;
+ }
+
+ function getMsgBody(offset){
+ return msg.substr(msg.indexOf(':')+msgHeaderLen+offset);
+ }
+
+ function forwardMsgFromIFrame(msgBody){
+ log(iframeId,'MessageCallback passed: {iframe: '+ messageData.iframe.id + ', message: ' + msgBody + '}');
+ callback('messageCallback',{
+ iframe: messageData.iframe,
+ message: JSON.parse(msgBody)
+ });
+ log(iframeId,'--');
+ }
+
+ function getPageInfo(){
+ var
+ bodyPosition = document.body.getBoundingClientRect(),
+ iFramePosition = messageData.iframe.getBoundingClientRect();
+
+ return JSON.stringify({
+ iframeHeight: iFramePosition.height,
+ iframeWidth: iFramePosition.width,
+ clientHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
+ clientWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
+ offsetTop: parseInt(iFramePosition.top - bodyPosition.top, 10),
+ offsetLeft: parseInt(iFramePosition.left - bodyPosition.left, 10),
+ scrollTop: window.pageYOffset,
+ scrollLeft: window.pageXOffset
+ });
+ }
+
+ function sendPageInfoToIframe(iframe,iframeId){
+ function debouncedTrigger(){
+ trigger(
+ 'Send Page Info',
+ 'pageInfo:' + getPageInfo(),
+ iframe,
+ iframeId
+ );
+ }
+
+ debouce(debouncedTrigger,32);
+ }
+
+
+ function startPageInfoMonitor(){
+ function setListener(type,func){
+ function sendPageInfo(){
+ if (settings[id]){
+ sendPageInfoToIframe(settings[id].iframe,id);
+ } else {
+ stop();
+ }
+ }
+
+ ['scroll','resize'].forEach(function(evt){
+ log(id, type + evt + ' listener for sendPageInfo');
+ func(window,evt,sendPageInfo);
+ });
+ }
+
+ function stop(){
+ setListener('Remove ', removeEventListener);
+ }
+
+ function start(){
+ setListener('Add ', addEventListener);
+ }
+
+ var id = iframeId; //Create locally scoped copy of iFrame ID
+
+ start();
+
+ settings[id].stopPageInfo = stop;
+ }
+
+ function stopPageInfoMonitor(){
+ if (settings[iframeId] && settings[iframeId].stopPageInfo){
+ settings[iframeId].stopPageInfo();
+ delete settings[iframeId].stopPageInfo;
+ }
+ }
+
+ function checkIFrameExists(){
+ var retBool = true;
+
+ if (null === messageData.iframe) {
+ warn(iframeId,'IFrame ('+messageData.id+') not found');
+ retBool = false;
+ }
+ return retBool;
+ }
+
+ function getElementPosition(target){
+ var iFramePosition = target.getBoundingClientRect();
+
+ getPagePosition(iframeId);
+
+ return {
+ x: Math.floor( Number(iFramePosition.left) + Number(pagePosition.x) ),
+ y: Math.floor( Number(iFramePosition.top) + Number(pagePosition.y) )
+ };
+ }
+
+ function scrollRequestFromChild(addOffset){
+ /* istanbul ignore next */ //Not testable in Karma
+ function reposition(){
+ pagePosition = newPosition;
+ scrollTo();
+ log(iframeId,'--');
+ }
+
+ function calcOffset(){
+ return {
+ x: Number(messageData.width) + offset.x,
+ y: Number(messageData.height) + offset.y
+ };
+ }
+
+ function scrollParent(){
+ if (window.parentIFrame){
+ window.parentIFrame['scrollTo'+(addOffset?'Offset':'')](newPosition.x,newPosition.y);
+ } else {
+ warn(iframeId,'Unable to scroll to requested position, window.parentIFrame not found');
+ }
+ }
+
+ var
+ offset = addOffset ? getElementPosition(messageData.iframe) : {x:0,y:0},
+ newPosition = calcOffset();
+
+ log(iframeId,'Reposition requested from iFrame (offset x:'+offset.x+' y:'+offset.y+')');
+
+ if(window.top!==window.self){
+ scrollParent();
+ } else {
+ reposition();
+ }
+ }
+
+ function scrollTo(){
+ if (false !== callback('scrollCallback',pagePosition)){
+ setPagePosition(iframeId);
+ } else {
+ unsetPagePosition();
+ }
+ }
+
+ function findTarget(location){
+ function jumpToTarget(){
+ var jumpPosition = getElementPosition(target);
+
+ log(iframeId,'Moving to in page link (#'+hash+') at x: '+jumpPosition.x+' y: '+jumpPosition.y);
+ pagePosition = {
+ x: jumpPosition.x,
+ y: jumpPosition.y
+ };
+
+ scrollTo();
+ log(iframeId,'--');
+ }
+
+ function jumpToParent(){
+ if (window.parentIFrame){
+ window.parentIFrame.moveToAnchor(hash);
+ } else {
+ log(iframeId,'In page link #'+hash+' not found and window.parentIFrame not found');
+ }
+ }
+
+ var
+ hash = location.split('#')[1] || '',
+ hashData = decodeURIComponent(hash),
+ target = document.getElementById(hashData) || document.getElementsByName(hashData)[0];
+
+ if (target){
+ jumpToTarget();
+ } else if(window.top!==window.self){
+ jumpToParent();
+ } else {
+ log(iframeId,'In page link #'+hash+' not found');
+ }
+ }
+
+ function callback(funcName,val){
+ return chkCallback(iframeId,funcName,val);
+ }
+
+ function actionMsg(){
+
+ if(settings[iframeId].firstRun) firstRun();
+
+ switch(messageData.type){
+ case 'close':
+ closeIFrame(messageData.iframe);
+ break;
+ case 'message':
+ forwardMsgFromIFrame(getMsgBody(6));
+ break;
+ case 'scrollTo':
+ scrollRequestFromChild(false);
+ break;
+ case 'scrollToOffset':
+ scrollRequestFromChild(true);
+ break;
+ case 'pageInfo':
+ sendPageInfoToIframe(settings[iframeId].iframe,iframeId);
+ startPageInfoMonitor();
+ break;
+ case 'pageInfoStop':
+ stopPageInfoMonitor();
+ break;
+ case 'inPageLink':
+ findTarget(getMsgBody(9));
+ break;
+ case 'reset':
+ resetIFrame(messageData);
+ break;
+ case 'init':
+ resizeIFrame();
+ callback('initCallback',messageData.iframe);
+ callback('resizedCallback',messageData);
+ break;
+ default:
+ resizeIFrame();
+ callback('resizedCallback',messageData);
+ }
+ }
+
+ function hasSettings(iframeId){
+ var retBool = true;
+
+ if (!settings[iframeId]){
+ retBool = false;
+ warn(messageData.type + ' No settings for ' + iframeId + '. Message was: ' + msg);
+ }
+
+ return retBool;
+ }
+
+ function iFrameReadyMsgReceived(){
+ for (var iframeId in settings){
+ trigger('iFrame requested init',createOutgoingMsg(iframeId),document.getElementById(iframeId),iframeId);
+ }
+ }
+
+ function firstRun() {
+ settings[iframeId].firstRun = false;
+ }
+
+ var
+ msg = event.data,
+ messageData = {},
+ iframeId = null;
+
+ if('[iFrameResizerChild]Ready' === msg){
+ iFrameReadyMsgReceived();
+ } else if (isMessageForUs()){
+ messageData = processMsg();
+ iframeId = logId = messageData.id;
+
+ if (!isMessageFromMetaParent() && hasSettings(iframeId)){
+ log(iframeId,'Received: '+msg);
+
+ if ( checkIFrameExists() && isMessageFromIFrame() ){
+ actionMsg();
+ }
+ }
+ } else {
+ info(iframeId,'Ignored: '+msg);
+ }
+
+ }
+
+
+ function chkCallback(iframeId,funcName,val){
+ var
+ func = null,
+ retVal = null;
+
+ if(settings[iframeId]){
+ func = settings[iframeId][funcName];
+
+ if( 'function' === typeof func){
+ retVal = func(val);
+ } else {
+ throw new TypeError(funcName+' on iFrame['+iframeId+'] is not a function');
+ }
+ }
+
+ return retVal;
+ }
+
+ function closeIFrame(iframe){
+ var iframeId = iframe.id;
+
+ log(iframeId,'Removing iFrame: '+iframeId);
+ iframe.parentNode.removeChild(iframe);
+ chkCallback(iframeId,'closedCallback',iframeId);
+ log(iframeId,'--');
+ delete settings[iframeId];
+ }
+
+ function getPagePosition(iframeId){
+ if(null === pagePosition){
+ pagePosition = {
+ x: (window.pageXOffset !== undefined) ? window.pageXOffset : document.documentElement.scrollLeft,
+ y: (window.pageYOffset !== undefined) ? window.pageYOffset : document.documentElement.scrollTop
+ };
+ log(iframeId,'Get page position: '+pagePosition.x+','+pagePosition.y);
+ }
+ }
+
+ function setPagePosition(iframeId){
+ if(null !== pagePosition){
+ window.scrollTo(pagePosition.x,pagePosition.y);
+ log(iframeId,'Set page position: '+pagePosition.x+','+pagePosition.y);
+ unsetPagePosition();
+ }
+ }
+
+ function unsetPagePosition(){
+ pagePosition = null;
+ }
+
+ function resetIFrame(messageData){
+ function reset(){
+ setSize(messageData);
+ trigger('reset','reset',messageData.iframe,messageData.id);
+ }
+
+ log(messageData.id,'Size reset requested by '+('init'===messageData.type?'host page':'iFrame'));
+ getPagePosition(messageData.id);
+ syncResize(reset,messageData,'reset');
+ }
+
+ function setSize(messageData){
+ function setDimension(dimension){
+ messageData.iframe.style[dimension] = messageData[dimension] + 'px';
+ log(
+ messageData.id,
+ 'IFrame (' + iframeId +
+ ') ' + dimension +
+ ' set to ' + messageData[dimension] + 'px'
+ );
+ }
+
+ function chkZero(dimension){
+ //FireFox sets dimension of hidden iFrames to zero.
+ //So if we detect that set up an event to check for
+ //when iFrame becomes visible.
+
+ /* istanbul ignore next */ //Not testable in PhantomJS
+ if (!hiddenCheckEnabled && '0' === messageData[dimension]){
+ hiddenCheckEnabled = true;
+ log(iframeId,'Hidden iFrame detected, creating visibility listener');
+ fixHiddenIFrames();
+ }
+ }
+
+ function processDimension(dimension){
+ setDimension(dimension);
+ chkZero(dimension);
+ }
+
+ var iframeId = messageData.iframe.id;
+
+ if(settings[iframeId]){
+ if( settings[iframeId].sizeHeight) { processDimension('height'); }
+ if( settings[iframeId].sizeWidth ) { processDimension('width'); }
+ }
+ }
+
+ function syncResize(func,messageData,doNotSync){
+ /* istanbul ignore if */ //Not testable in PhantomJS
+ if(doNotSync!==messageData.type && requestAnimationFrame){
+ log(messageData.id,'Requesting animation frame');
+ requestAnimationFrame(func);
+ } else {
+ func();
+ }
+ }
+
+ function trigger(calleeMsg,msg,iframe,id){
+ function postMessageToIFrame(){
+ var target = settings[id].targetOrigin;
+ log(id,'[' + calleeMsg + '] Sending msg to iframe['+id+'] ('+msg+') targetOrigin: '+target);
+ iframe.contentWindow.postMessage( msgId + msg, target );
+ }
+
+ function iFrameNotFound(){
+ info(id,'[' + calleeMsg + '] IFrame('+id+') not found');
+ if(settings[id]) {
+ delete settings[id];
+ }
+ }
+
+ function chkAndSend(){
+ if(iframe && 'contentWindow' in iframe && (null !== iframe.contentWindow)){ //Null test for PhantomJS
+ postMessageToIFrame();
+ } else {
+ iFrameNotFound();
+ }
+ }
+
+ id = id || iframe.id;
+
+ if(settings[id]) {
+ chkAndSend();
+ }
+
+ }
+
+ function createOutgoingMsg(iframeId){
+ return iframeId +
+ ':' + settings[iframeId].bodyMarginV1 +
+ ':' + settings[iframeId].sizeWidth +
+ ':' + settings[iframeId].log +
+ ':' + settings[iframeId].interval +
+ ':' + settings[iframeId].enablePublicMethods +
+ ':' + settings[iframeId].autoResize +
+ ':' + settings[iframeId].bodyMargin +
+ ':' + settings[iframeId].heightCalculationMethod +
+ ':' + settings[iframeId].bodyBackground +
+ ':' + settings[iframeId].bodyPadding +
+ ':' + settings[iframeId].tolerance +
+ ':' + settings[iframeId].inPageLinks +
+ ':' + settings[iframeId].resizeFrom +
+ ':' + settings[iframeId].widthCalculationMethod;
+ }
+
+ function setupIFrame(iframe,options){
+ function setLimits(){
+ function addStyle(style){
+ if ((Infinity !== settings[iframeId][style]) && (0 !== settings[iframeId][style])){
+ iframe.style[style] = settings[iframeId][style] + 'px';
+ log(iframeId,'Set '+style+' = '+settings[iframeId][style]+'px');
+ }
+ }
+
+ function chkMinMax(dimension){
+ if (settings[iframeId]['min'+dimension]>settings[iframeId]['max'+dimension]){
+ throw new Error('Value for min'+dimension+' can not be greater than max'+dimension);
+ }
+ }
+
+ chkMinMax('Height');
+ chkMinMax('Width');
+
+ addStyle('maxHeight');
+ addStyle('minHeight');
+ addStyle('maxWidth');
+ addStyle('minWidth');
+ }
+
+ function newId(){
+ var id = ((options && options.id) || defaults.id + count++);
+ if (null!==document.getElementById(id)){
+ id = id + count++;
+ }
+ return id;
+ }
+
+ function ensureHasId(iframeId){
+ logId=iframeId;
+ if (''===iframeId){
+ iframe.id = iframeId = newId();
+ logEnabled = (options || {}).log;
+ logId=iframeId;
+ log(iframeId,'Added missing iframe ID: '+ iframeId +' (' + iframe.src + ')');
+ }
+
+
+ return iframeId;
+ }
+
+ function setScrolling(){
+ log(iframeId,'IFrame scrolling ' + (settings[iframeId].scrolling ? 'enabled' : 'disabled') + ' for ' + iframeId);
+ iframe.style.overflow = false === settings[iframeId].scrolling ? 'hidden' : 'auto';
+ iframe.scrolling = false === settings[iframeId].scrolling ? 'no' : 'yes';
+ }
+
+ //The V1 iFrame script expects an int, where as in V2 expects a CSS
+ //string value such as '1px 3em', so if we have an int for V2, set V1=V2
+ //and then convert V2 to a string PX value.
+ function setupBodyMarginValues(){
+ if (('number'===typeof(settings[iframeId].bodyMargin)) || ('0'===settings[iframeId].bodyMargin)){
+ settings[iframeId].bodyMarginV1 = settings[iframeId].bodyMargin;
+ settings[iframeId].bodyMargin = '' + settings[iframeId].bodyMargin + 'px';
+ }
+ }
+
+ function checkReset(){
+ // Reduce scope of firstRun to function, because IE8's JS execution
+ // context stack is borked and this value gets externally
+ // changed midway through running this function!!!
+ var
+ firstRun = settings[iframeId].firstRun,
+ resetRequertMethod = settings[iframeId].heightCalculationMethod in resetRequiredMethods;
+
+ if (!firstRun && resetRequertMethod){
+ resetIFrame({iframe:iframe, height:0, width:0, type:'init'});
+ }
+ }
+
+ function setupIFrameObject(){
+ if(Function.prototype.bind){ //Ignore unpolyfilled IE8.
+ settings[iframeId].iframe.iFrameResizer = {
+
+ close : closeIFrame.bind(null,settings[iframeId].iframe),
+
+ resize : trigger.bind(null,'Window resize', 'resize', settings[iframeId].iframe),
+
+ moveToAnchor : function(anchor){
+ trigger('Move to anchor','moveToAnchor:'+anchor, settings[iframeId].iframe,iframeId);
+ },
+
+ sendMessage : function(message){
+ message = JSON.stringify(message);
+ trigger('Send Message','message:'+message, settings[iframeId].iframe,iframeId);
+ }
+ };
+ }
+ }
+
+ //We have to call trigger twice, as we can not be sure if all
+ //iframes have completed loading when this code runs. The
+ //event listener also catches the page changing in the iFrame.
+ function init(msg){
+ function iFrameLoaded(){
+ trigger('iFrame.onload',msg,iframe);
+ checkReset();
+ }
+
+ addEventListener(iframe,'load',iFrameLoaded);
+ trigger('init',msg,iframe);
+ }
+
+ function checkOptions(options){
+ if ('object' !== typeof options){
+ throw new TypeError('Options is not an object');
+ }
+ }
+
+ function copyOptions(options){
+ for (var option in defaults) {
+ if (defaults.hasOwnProperty(option)){
+ settings[iframeId][option] = options.hasOwnProperty(option) ? options[option] : defaults[option];
+ }
+ }
+ }
+
+ function getTargetOrigin (remoteHost){
+ return ('' === remoteHost || 'file://' === remoteHost) ? '*' : remoteHost;
+ }
+
+ function processOptions(options){
+ options = options || {};
+ settings[iframeId] = {
+ firstRun : true,
+ iframe : iframe,
+ remoteHost : iframe.src.split('/').slice(0,3).join('/')
+ };
+
+ checkOptions(options);
+ copyOptions(options);
+
+ settings[iframeId].targetOrigin = true === settings[iframeId].checkOrigin ? getTargetOrigin(settings[iframeId].remoteHost) : '*';
+ }
+
+ function beenHere(){
+ return (iframeId in settings && 'iFrameResizer' in iframe);
+ }
+
+ var iframeId = ensureHasId(iframe.id);
+
+ if (!beenHere()){
+ processOptions(options);
+ setScrolling();
+ setLimits();
+ setupBodyMarginValues();
+ init(createOutgoingMsg(iframeId));
+ setupIFrameObject();
+ } else {
+ warn(iframeId,'Ignored iFrame, already setup.');
+ }
+ }
+
+ function debouce(fn,time){
+ if (null === timer){
+ timer = setTimeout(function(){
+ timer = null;
+ fn();
+ }, time);
+ }
+ }
+
+ /* istanbul ignore next */ //Not testable in PhantomJS
+ function fixHiddenIFrames(){
+ function checkIFrames(){
+ function checkIFrame(settingId){
+ function chkDimension(dimension){
+ return '0px' === settings[settingId].iframe.style[dimension];
+ }
+
+ function isVisible(el) {
+ return (null !== el.offsetParent);
+ }
+
+ if (isVisible(settings[settingId].iframe) && (chkDimension('height') || chkDimension('width'))){
+ trigger('Visibility change', 'resize', settings[settingId].iframe,settingId);
+ }
+ }
+
+ for (var settingId in settings){
+ checkIFrame(settingId);
+ }
+ }
+
+ function mutationObserved(mutations){
+ log('window','Mutation observed: ' + mutations[0].target + ' ' + mutations[0].type);
+ debouce(checkIFrames,16);
+ }
+
+ function createMutationObserver(){
+ var
+ target = document.querySelector('body'),
+
+ config = {
+ attributes : true,
+ attributeOldValue : false,
+ characterData : true,
+ characterDataOldValue : false,
+ childList : true,
+ subtree : true
+ },
+
+ observer = new MutationObserver(mutationObserved);
+
+ observer.observe(target, config);
+ }
+
+ var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
+
+ if (MutationObserver) createMutationObserver();
+ }
+
+
+ function resizeIFrames(event){
+ function resize(){
+ sendTriggerMsg('Window '+event,'resize');
+ }
+
+ log('window','Trigger event: '+event);
+ debouce(resize,16);
+ }
+
+ /* istanbul ignore next */ //Not testable in PhantomJS
+ function tabVisible() {
+ function resize(){
+ sendTriggerMsg('Tab Visable','resize');
+ }
+
+ if('hidden' !== document.visibilityState) {
+ log('document','Trigger event: Visiblity change');
+ debouce(resize,16);
+ }
+ }
+
+ function sendTriggerMsg(eventName,event){
+ function isIFrameResizeEnabled(iframeId) {
+ return 'parent' === settings[iframeId].resizeFrom &&
+ settings[iframeId].autoResize &&
+ !settings[iframeId].firstRun;
+ }
+
+ for (var iframeId in settings){
+ if(isIFrameResizeEnabled(iframeId)){
+ trigger(eventName,event,document.getElementById(iframeId),iframeId);
+ }
+ }
+ }
+
+ function setupEventListeners(){
+ addEventListener(window,'message',iFrameListener);
+
+ addEventListener(window,'resize', function(){resizeIFrames('resize');});
+
+ addEventListener(document,'visibilitychange',tabVisible);
+ addEventListener(document,'-webkit-visibilitychange',tabVisible); //Andriod 4.4
+ addEventListener(window,'focusin',function(){resizeIFrames('focus');}); //IE8-9
+ addEventListener(window,'focus',function(){resizeIFrames('focus');});
+ }
+
+
+ function factory(){
+ function init(options,element){
+ function chkType(){
+ if(!element.tagName) {
+ throw new TypeError('Object is not a valid DOM element');
+ } else if ('IFRAME' !== element.tagName.toUpperCase()) {
+ throw new TypeError('Expected <IFRAME> tag, found <'+element.tagName+'>');
+ }
+ }
+
+ if(element) {
+ chkType();
+ setupIFrame(element, options);
+ iFrames.push(element);
+ }
+ }
+
+ var iFrames;
+
+ setupRequestAnimationFrame();
+ setupEventListeners();
+
+ return function iFrameResizeF(options,target){
+ iFrames = []; //Only return iFrames past in on this call
+
+ switch (typeof(target)){
+ case 'undefined':
+ case 'string':
+ Array.prototype.forEach.call(
+ document.querySelectorAll( target || 'iframe' ),
+ init.bind(undefined, options)
+ );
+ break;
+ case 'object':
+ init(options,target);
+ break;
+ default:
+ throw new TypeError('Unexpected data type ('+typeof(target)+')');
+ }
+
+ return iFrames;
+ };
+ }
+
+ function createJQueryPublicMethod($){
+ if (!$.fn) {
+ info('','Unable to bind to jQuery, it is not fully loaded.');
+ } else {
+ $.fn.iFrameResize = function $iFrameResizeF(options) {
+ function init(index, element) {
+ setupIFrame(element, options);
+ }
+
+ return this.filter('iframe').each(init).end();
+ };
+ }
+ }
+
+ if (window.jQuery) { createJQueryPublicMethod(jQuery); }
+
+ if (typeof define === 'function' && define.amd) {
+ define([],factory);
+ } else if (typeof module === 'object' && typeof module.exports === 'object') { //Node for browserfy
+ module.exports = factory();
+ } else {
+ window.iFrameResize = window.iFrameResize || factory();
+ }
+
+})(window || {});
diff --git a/libs/bower_components/iframe-resizer/test-main.js b/libs/bower_components/iframe-resizer/test-main.js
new file mode 100644
index 0000000000..1abfe13131
--- /dev/null
+++ b/libs/bower_components/iframe-resizer/test-main.js
@@ -0,0 +1,33 @@
+var allTestFiles = [];
+
+var TEST_REGEXP = /(spec|test)\.js$/i;
+
+// Get a list of all the test files to include
+Object.keys(window.__karma__.files).forEach(function(file) {
+ if (TEST_REGEXP.test(file)) {
+ // Normalize paths to RequireJS module names.
+ // If you require sub-dependencies of test files to be loaded as-is (requiring file extension)
+ // then do not normalize the paths
+ var normalizedTestModule = file.replace(/^\/base\/|\.js$/g, '');
+ allTestFiles.push(normalizedTestModule);
+ }
+});
+
+require.config({
+ // Karma serves files under /base, which is the basePath from your config file
+ baseUrl: '/base',
+
+ paths: {
+ jquery : 'node_modules/jquery/dist/jquery',
+ iframeResizerMin : 'js/iframeResizer.min',
+ iframeResizer : 'src/iframeResizer',
+ iframeResizerContentMin : 'js/iframeResizer.contentWindow.min',
+ iframeResizerContent : 'src/iframeResizer.contentWindow'
+ },
+
+ // dynamically load all test files
+ deps: allTestFiles,
+
+ // we have to kickoff jasmine, as it is asynchronous
+ callback: window.__karma__.start
+});
diff --git a/plugins/API/Menu.php b/plugins/API/Menu.php
index 12995fcb99..312f03a2ef 100644
--- a/plugins/API/Menu.php
+++ b/plugins/API/Menu.php
@@ -28,7 +28,7 @@ class Menu extends \Piwik\Plugin\Menu
{
$menu->addPlatformItem('General_API',
$this->urlForAction('listAllAPI', array('segment' => false)),
- 6,
+ 7,
Piwik::translate('API_TopLinkTooltip')
);
diff --git a/plugins/CoreAdminHome/Controller.php b/plugins/CoreAdminHome/Controller.php
index dcda3b0378..7621aa1f78 100644
--- a/plugins/CoreAdminHome/Controller.php
+++ b/plugins/CoreAdminHome/Controller.php
@@ -17,14 +17,13 @@ use Piwik\Menu\MenuTop;
use Piwik\Piwik;
use Piwik\Plugin;
use Piwik\Plugin\ControllerAdmin;
-use Piwik\Plugins\CorePluginsAdmin\CorePluginsAdmin;
+use Piwik\Plugins\Marketplace\Marketplace;
use Piwik\Plugins\CustomVariables\CustomVariables;
use Piwik\Plugins\LanguagesManager\LanguagesManager;
use Piwik\Plugins\PrivacyManager\DoNotTrackHeaderChecker;
use Piwik\Plugins\SitesManager\API as APISitesManager;
use Piwik\Site;
use Piwik\Translation\Translator;
-use Piwik\UpdateCheck;
use Piwik\Url;
use Piwik\View;
use Piwik\Widget\WidgetsList;
@@ -49,7 +48,7 @@ class Controller extends ControllerAdmin
public function home()
{
- $isMarketplaceEnabled = CorePluginsAdmin::isMarketplaceEnabled();
+ $isMarketplaceEnabled = Marketplace::isMarketplaceEnabled();
$isFeedbackEnabled = Plugin\Manager::getInstance()->isPluginLoaded('Feedback');
$widgetsList = WidgetsList::get();
diff --git a/plugins/CoreAdminHome/templates/home.twig b/plugins/CoreAdminHome/templates/home.twig
index b090ae0465..aaf5afc5ba 100644
--- a/plugins/CoreAdminHome/templates/home.twig
+++ b/plugins/CoreAdminHome/templates/home.twig
@@ -35,7 +35,7 @@
{% endif %}
{% if isMarketplaceEnabled %}
- <div piwik-widget-loader='{"module":"CorePluginsAdmin","action":"getNewPlugins", "isAdminPage": "1"}'></div>
+ <div piwik-widget-loader='{"module":"Marketplace","action":"getNewPlugins", "isAdminPage": "1"}'></div>
{% endif %}
{{ postEvent('Template.adminHome') }}
diff --git a/plugins/CoreConsole/Commands/GeneratePlugin.php b/plugins/CoreConsole/Commands/GeneratePlugin.php
index cfdaee35f5..611a177fbb 100644
--- a/plugins/CoreConsole/Commands/GeneratePlugin.php
+++ b/plugins/CoreConsole/Commands/GeneratePlugin.php
@@ -11,11 +11,11 @@ namespace Piwik\Plugins\CoreConsole\Commands;
use Piwik\Filesystem;
use Piwik\Plugins\ExamplePlugin\ExamplePlugin;
-use Piwik\Version;
use Piwik\Plugin;
-use Symfony\Component\Console\Input\ArrayInput;
+use Piwik\Version;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
/**
@@ -79,6 +79,7 @@ class GeneratePlugin extends GeneratePluginBase
}
$this->copyTemplateToPlugin($exampleFolder, $pluginName, $replace, $whitelistFiles);
+ $this->checkAndUpdateRequiredPiwikVersion($pluginName, new NullOutput());
if ($isTheme) {
$this->writeSuccessMessage($output, array(
diff --git a/plugins/CoreConsole/Commands/GeneratePluginBase.php b/plugins/CoreConsole/Commands/GeneratePluginBase.php
index f5ca71a405..38f53b4d1b 100644
--- a/plugins/CoreConsole/Commands/GeneratePluginBase.php
+++ b/plugins/CoreConsole/Commands/GeneratePluginBase.php
@@ -123,10 +123,18 @@ abstract class GeneratePluginBase extends ConsoleCommand
$pluginJson['require'] = array();
}
- $piwikVersion = Version::VERSION;
- $nextMajorVersion = (int) $piwikVersion + 1;
+ $piwikVersion = Version::VERSION;
+ $nextMajorVersion = (int) substr($piwikVersion, 0, strpos($piwikVersion, '.')) + 1;
$secondPartPiwikVersionRequire = ',<' . $nextMajorVersion . '.0.0-b1';
- $newRequiredVersion = '>=' . $piwikVersion . $secondPartPiwikVersionRequire;
+ if (false === strpos($piwikVersion, '-')) {
+ // see https://github.com/composer/composer/issues/4080 we need to specify -stable otherwise it would match
+ // $piwikVersion-dev meaning it would also match all pre-released. However, we only want to match a stable
+ // release
+ $piwikVersion.= '-stable';
+ }
+
+ $newRequiredVersion = sprintf('>=%s,<%d.0.0', $piwikVersion, $nextMajorVersion);
+
if (!empty($pluginJson['require']['piwik'])) {
$requiredVersion = trim($pluginJson['require']['piwik']);
diff --git a/plugins/CoreHome/Controller.php b/plugins/CoreHome/Controller.php
index 9a554fb2b8..60f0aa22f9 100644
--- a/plugins/CoreHome/Controller.php
+++ b/plugins/CoreHome/Controller.php
@@ -11,6 +11,7 @@ namespace Piwik\Plugins\CoreHome;
use Exception;
use Piwik\API\Request;
use Piwik\Common;
+use Piwik\Container\StaticContainer;
use Piwik\Date;
use Piwik\FrontController;
use Piwik\Notification\Manager as NotificationManager;
@@ -19,7 +20,6 @@ use Piwik\Plugin\Report;
use Piwik\Widget\Widget;
use Piwik\Plugins\CoreHome\DataTableRowAction\MultiRowEvolution;
use Piwik\Plugins\CoreHome\DataTableRowAction\RowEvolution;
-use Piwik\Plugins\CorePluginsAdmin\MarketplaceApiClient;
use Piwik\Plugins\Dashboard\DashboardManagerControl;
use Piwik\Plugins\UsersManager\API;
use Piwik\Site;
@@ -264,7 +264,8 @@ class Controller extends \Piwik\Plugin\Controller
// perform check (but only once every 10s)
UpdateCheck::check($force = false, UpdateCheck::UI_CLICK_CHECK_INTERVAL);
- MarketplaceApiClient::clearAllCacheEntries();
+ $marketplace = StaticContainer::get('Piwik\Plugins\Marketplace\Api\Client');
+ $marketplace->clearAllCacheEntries();
$view = new View('@CoreHome/checkForUpdates');
$this->setGeneralVariablesView($view);
diff --git a/plugins/CoreHome/CoreHome.php b/plugins/CoreHome/CoreHome.php
index 4c5abc8071..f5a1a594bc 100644
--- a/plugins/CoreHome/CoreHome.php
+++ b/plugins/CoreHome/CoreHome.php
@@ -239,10 +239,7 @@ class CoreHome extends \Piwik\Plugin
$jsFiles[] = "plugins/CorePluginsAdmin/angularjs/field/field.directive.js";
$jsFiles[] = "plugins/CorePluginsAdmin/angularjs/save-button/save-button.directive.js";
$jsFiles[] = "plugins/CorePluginsAdmin/angularjs/plugins/plugin-filter.directive.js";
- $jsFiles[] = "plugins/CorePluginsAdmin/angularjs/plugins/plugin-name.directive.js";
$jsFiles[] = "plugins/CorePluginsAdmin/angularjs/plugins/plugin-management.directive.js";
- $jsFiles[] = "plugins/CorePluginsAdmin/angularjs/marketplace/marketplace.controller.js";
- $jsFiles[] = "plugins/CorePluginsAdmin/angularjs/marketplace/marketplace.directive.js";
$jsFiles[] = "plugins/CoreHome/javascripts/iframeResizer.min.js";
}
diff --git a/plugins/CoreHome/lang/en.json b/plugins/CoreHome/lang/en.json
index 3f58bbb406..808250348e 100644
--- a/plugins/CoreHome/lang/en.json
+++ b/plugins/CoreHome/lang/en.json
@@ -65,6 +65,7 @@
"QuickAccessTitle": "Search for %s. Use the arrow keys to navigate through search results. Shortcut: Press 'f' to search.",
"MenuEntries": "Menu entries",
"Segments": "Segments",
+ "OneClickUpdateNotPossibleAsMultiServerEnvironment": "The one-click update is not available as you are using Piwik with multiple servers. Please download the latest version from %1$s to proceed.",
"AdblockIsMaybeUsed": "In case you are using an ad blocker, please disable it for this site to make sure Piwik works without any issues.",
"ChangeCurrentWebsite": "Choose a website, currently selected website: %s"
}
diff --git a/plugins/CoreHome/templates/_headerMessage.twig b/plugins/CoreHome/templates/_headerMessage.twig
index 051e10f74d..97ed8271a7 100644
--- a/plugins/CoreHome/templates/_headerMessage.twig
+++ b/plugins/CoreHome/templates/_headerMessage.twig
@@ -29,14 +29,18 @@
<div class="dropdown positionInViewport">
{% if isPiwikDemo %}
- {{ 'General_DownloadFullVersion'|translate("<a href='http://piwik.org/'>","</a>","<a href='http://piwik.org'>piwik.org</a>")|raw }}
+ {{ 'General_DownloadFullVersion'|translate("<a rel='noreferrer' href='https://piwik.org/'>","</a>","<a rel='noreferrer' href='https://piwik.org'>piwik.org</a>")|raw }}
<br/>
{% if isSuperUser and isAdminArea is defined and isAdminArea %}
<br/>
{% endif %}
{% endif %}
{% if latest_version_available and isSuperUser %}
- {{ 'General_PiwikXIsAvailablePleaseUpdateNow'|translate(latest_version_available,"<br /><a href='index.php?module=CoreUpdater&amp;action=newVersionAvailable'>","</a>","<a href='?module=Proxy&amp;action=redirect&amp;url=http://piwik.org/changelog/' target='_blank'>","</a>")|raw }}
+ {% if isMultiServerEnvironment %}
+ {{ 'CoreHome_OneClickUpdateNotPossibleAsMultiServerEnvironment'|translate("<a rel='noreferrer' href='https://builds.piwik.org/piwik-" ~ latest_version_available ~ ".zip'>","</a>")|raw }}
+ {% else %}
+ {{ 'General_PiwikXIsAvailablePleaseUpdateNow'|translate(latest_version_available,"<br /><a href='index.php?module=CoreUpdater&amp;action=newVersionAvailable'>","</a>","<a href='?module=Proxy&amp;action=redirect&amp;url=http://piwik.org/changelog/' target='_blank'>","</a>")|raw }}
+ {% endif %}
<br />
{% elseif latest_version_available and not isPiwikDemo and hasSomeViewAccess and not isUserIsAnonymous %}
{% set updateSubject = 'General_NewUpdatePiwikX'|translate(latest_version_available)|e('url') %}
diff --git a/plugins/CorePluginsAdmin/Controller.php b/plugins/CorePluginsAdmin/Controller.php
index f4d08fc12a..860cee7404 100644
--- a/plugins/CorePluginsAdmin/Controller.php
+++ b/plugins/CorePluginsAdmin/Controller.php
@@ -11,6 +11,7 @@ namespace Piwik\Plugins\CorePluginsAdmin;
use Exception;
use Piwik\API\Request;
use Piwik\Common;
+use Piwik\Container\StaticContainer;
use Piwik\Exception\MissingFilePermissionException;
use Piwik\Filechecks;
use Piwik\Filesystem;
@@ -18,6 +19,9 @@ use Piwik\Nonce;
use Piwik\Notification;
use Piwik\Piwik;
use Piwik\Plugin;
+use Piwik\Plugins\Marketplace\Marketplace;
+use Piwik\Plugins\Marketplace\Controller as MarketplaceController;
+use Piwik\Plugins\Marketplace\Plugins;
use Piwik\Translation\Translator;
use Piwik\Url;
use Piwik\Version;
@@ -25,15 +29,10 @@ use Piwik\View;
class Controller extends Plugin\ControllerAdmin
{
- const UPDATE_NONCE = 'CorePluginsAdmin.updatePlugin';
- const INSTALL_NONCE = 'CorePluginsAdmin.installPlugin';
const ACTIVATE_NONCE = 'CorePluginsAdmin.activatePlugin';
const DEACTIVATE_NONCE = 'CorePluginsAdmin.deactivatePlugin';
const UNINSTALL_NONCE = 'CorePluginsAdmin.uninstallPlugin';
- private $validSortMethods = array('popular', 'newest', 'alpha');
- private $defaultSortMethod = 'popular';
-
/**
* @var Translator
*/
@@ -44,94 +43,42 @@ class Controller extends Plugin\ControllerAdmin
*/
private $settingsProvider;
- public function __construct(Translator $translator, Plugin\SettingsProvider $settingsProvider)
- {
- $this->translator = $translator;
- $this->settingsProvider = $settingsProvider;
-
- parent::__construct();
- }
-
- public function marketplace()
- {
- self::dieIfMarketplaceIsDisabled();
-
- $show = Common::getRequestVar('show', 'plugins', 'string');
- $query = Common::getRequestVar('query', '', 'string', $_POST);
- $sort = Common::getRequestVar('sort', $this->defaultSortMethod, 'string');
- if (!in_array($sort, $this->validSortMethods)) {
- $sort = $this->defaultSortMethod;
- }
-
- $view = $this->configureView('@CorePluginsAdmin/marketplace');
-
- $marketplace = new Marketplace();
-
- $showThemes = ($show === 'themes');
- $view->plugins = $marketplace->searchPlugins($query, $sort, $showThemes);
- $view->showThemes = $showThemes;
- $view->query = $query;
- $view->sort = $sort;
- $view->pluginType = $show;
- $view->pluginTypeOptions = array(
- 'plugins' => Piwik::translate('General_Plugins'),
- 'themes' => Piwik::translate('CorePluginsAdmin_Themes')
- );
- $view->pluginSortOptions = array(
- 'popular' => Piwik::translate('CorePluginsAdmin_SortByPopular'),
- 'newest' => Piwik::translate('CorePluginsAdmin_SortByNewest'),
- 'alpha' => Piwik::translate('CorePluginsAdmin_SortByAlpha'),
- );
- $view->installNonce = Nonce::getNonce(static::INSTALL_NONCE);
- $view->updateNonce = Nonce::getNonce(static::UPDATE_NONCE);
- $view->isSuperUser = Piwik::hasUserSuperUserAccess();
+ /**
+ * @var PluginInstaller
+ */
+ private $pluginInstaller;
+ /**
+ * @var Plugin\Manager
+ */
+ private $pluginManager;
- return $view->render();
- }
+ /**
+ * @var Plugins
+ */
+ private $marketplacePlugins;
- private function createUpdateOrInstallView($template, $nonceName)
+ /**
+ * Controller constructor.
+ * @param Translator $translator
+ * @param Plugin\SettingsProvider $settingsProvider
+ * @param PluginInstaller $pluginInstaller
+ * @param Plugins $marketplacePlugins
+ */
+ public function __construct(Translator $translator, Plugin\SettingsProvider $settingsProvider, PluginInstaller $pluginInstaller, $marketplacePlugins = null)
{
- static::dieIfMarketplaceIsDisabled();
-
- $pluginName = $this->initPluginModification($nonceName);
- $this->dieIfPluginsAdminIsDisabled();
-
- $view = $this->configureView('@CorePluginsAdmin/' . $template);
-
- $view->plugin = array('name' => $pluginName);
-
- try {
- $pluginInstaller = new PluginInstaller($pluginName);
- $pluginInstaller->installOrUpdatePluginFromMarketplace();
-
- } catch (\Exception $e) {
-
- $notification = new Notification($e->getMessage());
- $notification->context = Notification::CONTEXT_ERROR;
- Notification\Manager::notify('CorePluginsAdmin_InstallPlugin', $notification);
-
- $this->redirectAfterModification(true);
- return;
+ $this->translator = $translator;
+ $this->settingsProvider = $settingsProvider;
+ $this->pluginInstaller = $pluginInstaller;
+ $this->pluginManager = Plugin\Manager::getInstance();
+
+ if (!empty($marketplacePlugins)) {
+ $this->marketplacePlugins = $marketplacePlugins;
+ } elseif (Marketplace::isMarketplaceEnabled()) {
+ // we load it manually as marketplace might not be loaded
+ $this->marketplacePlugins = StaticContainer::get('Piwik\Plugins\Marketplace\Plugins');
}
- $marketplace = new Marketplace();
- $view->plugin = $marketplace->getPluginInfo($pluginName);
-
- return $view;
- }
-
- public function updatePlugin()
- {
- $view = $this->createUpdateOrInstallView('updatePlugin', static::UPDATE_NONCE);
- return $view->render();
- }
-
- public function installPlugin()
- {
- $view = $this->createUpdateOrInstallView('installPlugin', static::INSTALL_NONCE);
- $view->nonce = Nonce::getNonce(static::ACTIVATE_NONCE);
-
- return $view->render();
+ parent::__construct();
}
public function uploadPlugin()
@@ -141,11 +88,11 @@ class Controller extends Plugin\ControllerAdmin
$nonce = Common::getRequestVar('nonce', null, 'string');
- if (!Nonce::verifyNonce(static::INSTALL_NONCE, $nonce)) {
+ if (!Nonce::verifyNonce(MarketplaceController::INSTALL_NONCE, $nonce)) {
throw new \Exception($this->translator->translate('General_ExceptionNonceMismatch'));
}
- Nonce::discardNonce(static::INSTALL_NONCE);
+ Nonce::discardNonce(MarketplaceController::INSTALL_NONCE);
if (empty($_FILES['pluginZip'])) {
throw new \Exception('You did not specify a ZIP file.');
@@ -162,52 +109,25 @@ class Controller extends Plugin\ControllerAdmin
$view = $this->configureView('@CorePluginsAdmin/uploadPlugin');
- $pluginInstaller = new PluginInstaller('uploaded');
- $pluginMetadata = $pluginInstaller->installOrUpdatePluginFromFile($file);
+ $pluginMetadata = $this->pluginInstaller->installOrUpdatePluginFromFile($file);
$view->nonce = Nonce::getNonce(static::ACTIVATE_NONCE);
$view->plugin = array(
'name' => $pluginMetadata->name,
'version' => $pluginMetadata->version,
'isTheme' => !empty($pluginMetadata->theme),
- 'isActivated' => \Piwik\Plugin\Manager::getInstance()->isPluginActivated($pluginMetadata->name)
+ 'isActivated' => $this->pluginManager->isPluginActivated($pluginMetadata->name)
);
return $view->render();
}
- public function pluginDetails()
- {
- static::dieIfMarketplaceIsDisabled();
-
- $pluginName = Common::getRequestVar('pluginName', null, 'string');
- $activeTab = Common::getRequestVar('activeTab', '', 'string');
- if ('changelog' !== $activeTab) {
- $activeTab = '';
- }
-
- $view = $this->configureView('@CorePluginsAdmin/pluginDetails');
-
- try {
- $marketplace = new Marketplace();
- $view->plugin = $marketplace->getPluginInfo($pluginName);
- $view->isSuperUser = Piwik::hasUserSuperUserAccess();
- $view->installNonce = Nonce::getNonce(static::INSTALL_NONCE);
- $view->updateNonce = Nonce::getNonce(static::UPDATE_NONCE);
- $view->activeTab = $activeTab;
- } catch (\Exception $e) {
- $view->errorMessage = $e->getMessage();
- }
-
- return $view->render();
- }
-
/**
* @deprecated
*/
public function browsePlugins()
{
- $this->redirectToIndex('CorePluginsAdmin', 'marketplace');
+ $this->redirectToIndex('Marketplace', 'overview');
}
/**
@@ -215,26 +135,7 @@ class Controller extends Plugin\ControllerAdmin
*/
public function browseThemes()
{
- $this->redirectToIndex('CorePluginsAdmin', 'marketplace', null, null, null, array('show' => 'themes'));
- }
-
- /**
- * @deprecated
- */
- public function userBrowsePlugins()
- {
- $this->redirectToIndex('CorePluginsAdmin', 'marketplace', null, null, null, array('mode' => 'user'));
- }
-
- private function dieIfMarketplaceIsDisabled()
- {
- if (!CorePluginsAdmin::isMarketplaceEnabled()) {
- throw new \Exception('The Marketplace feature has been disabled.
- You may enable the Marketplace by changing the config entry "enable_marketplace" to 1.
- Please contact your Piwik admins with your request so they can assist.');
- }
-
- $this->dieIfPluginsAdminIsDisabled();
+ $this->redirectToIndex('Marketplace', 'overview', null, null, null, array('show' => 'themes'));
}
private function dieIfPluginsAdminIsDisabled()
@@ -251,7 +152,7 @@ class Controller extends Plugin\ControllerAdmin
$view = $this->configureView('@CorePluginsAdmin/' . $template);
- $view->updateNonce = Nonce::getNonce(static::UPDATE_NONCE);
+ $view->updateNonce = Nonce::getNonce(MarketplaceController::UPDATE_NONCE);
$view->activateNonce = Nonce::getNonce(static::ACTIVATE_NONCE);
$view->uninstallNonce = Nonce::getNonce(static::UNINSTALL_NONCE);
$view->deactivateNonce = Nonce::getNonce(static::DEACTIVATE_NONCE);
@@ -259,23 +160,19 @@ class Controller extends Plugin\ControllerAdmin
$users = Request::processRequest('UsersManager.getUsers');
$view->otherUsersCount = count($users) - 1;
- $view->themeEnabled = \Piwik\Plugin\Manager::getInstance()->getThemeEnabled()->getPluginName();
+ $view->themeEnabled = $this->pluginManager->getThemeEnabled()->getPluginName();
$view->pluginNamesHavingSettings = array_keys($this->settingsProvider->getAllSystemSettings());
- $view->isMarketplaceEnabled = CorePluginsAdmin::isMarketplaceEnabled();
+ $view->isMarketplaceEnabled = Marketplace::isMarketplaceEnabled();
$view->isPluginsAdminEnabled = CorePluginsAdmin::isPluginsAdminEnabled();
$view->pluginsHavingUpdate = array();
$view->marketplacePluginNames = array();
- if (CorePluginsAdmin::isMarketplaceEnabled()) {
+ if (Marketplace::isMarketplaceEnabled() && $this->marketplacePlugins) {
try {
- $marketplace = new Marketplace();
- $view->marketplacePluginNames = $marketplace->getAvailablePluginNames($themesOnly);
-
- $pluginsHavingUpdate = $marketplace->getPluginsHavingUpdate(true);
- $themesHavingUpdate = $marketplace->getPluginsHavingUpdate(false);
- $view->pluginsHavingUpdate = $pluginsHavingUpdate + $themesHavingUpdate;
+ $view->marketplacePluginNames = $this->marketplacePlugins->getAvailablePluginNames($themesOnly);
+ $view->pluginsHavingUpdate = $this->marketplacePlugins->getPluginsHavingUpdate();
} catch(Exception $e) {
// curl exec connection error (ie. server not connected to internet)
}
@@ -315,12 +212,11 @@ class Controller extends Plugin\ControllerAdmin
protected function getPluginsInfo($themesOnly = false)
{
- $pluginManager = \Piwik\Plugin\Manager::getInstance();
- $plugins = $pluginManager->loadAllPluginsAndGetTheirInfo();
+ $plugins = $this->pluginManager->loadAllPluginsAndGetTheirInfo();
foreach ($plugins as $pluginName => &$plugin) {
- $plugin['isCorePlugin'] = $pluginManager->isPluginBundledWithCore($pluginName);
+ $plugin['isCorePlugin'] = $this->pluginManager->isPluginBundledWithCore($pluginName);
if (!empty($plugin['info']['description'])) {
$plugin['info']['description'] = $this->translator->translate($plugin['info']['description']);
@@ -408,7 +304,7 @@ class Controller extends Plugin\ControllerAdmin
$view->lastError = $lastError;
$view->isSuperUser = Piwik::hasUserSuperUserAccess();
$view->isAnonymousUser = Piwik::isUserIsAnonymous();
- $view->plugins = Plugin\Manager::getInstance()->loadAllPluginsAndGetTheirInfo();
+ $view->plugins = $this->pluginManager->loadAllPluginsAndGetTheirInfo();
$view->deactivateNonce = Nonce::getNonce(static::DEACTIVATE_NONCE);
$view->uninstallNonce = Nonce::getNonce(static::UNINSTALL_NONCE);
$view->emailSuperUser = implode(',', Piwik::getAllSuperUserAccessEmailAddresses());
@@ -432,16 +328,9 @@ class Controller extends Plugin\ControllerAdmin
$pluginName = $this->initPluginModification(static::ACTIVATE_NONCE);
$this->dieIfPluginsAdminIsDisabled();
- \Piwik\Plugin\Manager::getInstance()->activatePlugin($pluginName);
+ $this->pluginManager->activatePlugin($pluginName);
if ($redirectAfter) {
- $plugin = \Piwik\Plugin\Manager::getInstance()->loadPlugin($pluginName);
-
- $actionToRedirect = 'plugins';
- if ($plugin->isTheme()) {
- $actionToRedirect = 'themes';
- }
-
$message = $this->translator->translate('CorePluginsAdmin_SuccessfullyActicated', array($pluginName));
if ($this->settingsProvider->getSystemSettings($pluginName)) {
@@ -457,7 +346,22 @@ class Controller extends Plugin\ControllerAdmin
$notification->context = Notification::CONTEXT_SUCCESS;
Notification\Manager::notify('CorePluginsAdmin_PluginActivated', $notification);
- $this->redirectToIndex('CorePluginsAdmin', $actionToRedirect);
+ $redirectTo = Common::getRequestVar('redirectTo', '', 'string');
+ if (!empty($redirectTo) && $redirectTo === 'marketplace') {
+ $this->redirectToIndex('Marketplace', 'overview');
+ } elseif (!empty($redirectTo) && $redirectTo === 'referrer') {
+ $this->redirectAfterModification($redirectAfter);
+ } else {
+ $plugin = $this->pluginManager->loadPlugin($pluginName);
+
+ $actionToRedirect = 'plugins';
+ if ($plugin->isTheme()) {
+ $actionToRedirect = 'themes';
+ }
+
+ $this->redirectToIndex('CorePluginsAdmin', $actionToRedirect);
+ }
+
}
}
@@ -466,7 +370,7 @@ class Controller extends Plugin\ControllerAdmin
$pluginName = $this->initPluginModification(static::DEACTIVATE_NONCE);
$this->dieIfPluginsAdminIsDisabled();
- \Piwik\Plugin\Manager::getInstance()->deactivatePlugin($pluginName);
+ $this->pluginManager->deactivatePlugin($pluginName);
$this->redirectAfterModification($redirectAfter);
}
@@ -475,7 +379,7 @@ class Controller extends Plugin\ControllerAdmin
$pluginName = $this->initPluginModification(static::UNINSTALL_NONCE);
$this->dieIfPluginsAdminIsDisabled();
- $uninstalled = \Piwik\Plugin\Manager::getInstance()->uninstallPlugin($pluginName);
+ $uninstalled = $this->pluginManager->uninstallPlugin($pluginName);
if (!$uninstalled) {
$path = Filesystem::getPathToPiwikRoot() . '/plugins/' . $pluginName . '/';
@@ -495,6 +399,25 @@ class Controller extends Plugin\ControllerAdmin
$this->redirectAfterModification($redirectAfter);
}
+ public function showLicense()
+ {
+ $pluginName = Common::getRequestVar('pluginName', null, 'string');
+
+ $metadata = new Plugin\MetadataLoader($pluginName);
+ $license_file = $metadata->getPathToLicenseFile();
+
+ $license = 'No license file found for this plugin.';
+ if(!empty($license_file)) {
+ $license = file_get_contents($license_file);
+ $license = nl2br($license);
+ }
+
+ $view = $this->configureView('@CorePluginsAdmin/license');
+ $view->pluginName = $pluginName;
+ $view->license = $license;
+ return $view->render();
+ }
+
protected function initPluginModification($nonceName)
{
Piwik::checkUserHasSuperUserAccess();
@@ -509,6 +432,10 @@ class Controller extends Plugin\ControllerAdmin
$pluginName = Common::getRequestVar('pluginName', null, 'string');
+ if (!$this->pluginManager->isValidPluginName($pluginName)) {
+ throw new Exception('Invalid plugin name');
+ }
+
return $pluginName;
}
diff --git a/plugins/CorePluginsAdmin/CorePluginsAdmin.php b/plugins/CorePluginsAdmin/CorePluginsAdmin.php
index 21217f7743..df8934519b 100644
--- a/plugins/CorePluginsAdmin/CorePluginsAdmin.php
+++ b/plugins/CorePluginsAdmin/CorePluginsAdmin.php
@@ -11,10 +11,10 @@ namespace Piwik\Plugins\CorePluginsAdmin;
use Piwik\Config;
use Piwik\Plugin;
-class CorePluginsAdmin extends \Piwik\Plugin
+class CorePluginsAdmin extends Plugin
{
/**
- * @see Piwik\Plugin::registerEvents
+ * @see Plugin::registerEvents
*/
public function registerEvents()
{
@@ -27,18 +27,10 @@ class CorePluginsAdmin extends \Piwik\Plugin
public function getStylesheetFiles(&$stylesheets)
{
- $stylesheets[] = "plugins/CorePluginsAdmin/stylesheets/marketplace.less";
- $stylesheets[] = "plugins/CorePluginsAdmin/stylesheets/marketplace-widget.less";
$stylesheets[] = "plugins/CorePluginsAdmin/stylesheets/plugins_admin.less";
- $stylesheets[] = "plugins/CorePluginsAdmin/stylesheets/plugin-details.less";
$stylesheets[] = "plugins/CorePluginsAdmin/angularjs/plugin-settings/plugin-settings.directive.less";
}
- public static function isMarketplaceEnabled()
- {
- return (bool) Config::getInstance()->General['enable_marketplace'];
- }
-
public static function isPluginsAdminEnabled()
{
return (bool) Config::getInstance()->General['enable_plugins_admin'];
diff --git a/plugins/CorePluginsAdmin/Marketplace.php b/plugins/CorePluginsAdmin/Marketplace.php
deleted file mode 100644
index c8cfc114e8..0000000000
--- a/plugins/CorePluginsAdmin/Marketplace.php
+++ /dev/null
@@ -1,199 +0,0 @@
-<?php
-/**
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- *
- */
-namespace Piwik\Plugins\CorePluginsAdmin;
-
-use Piwik\Date;
-use Piwik\Plugin\Dependency as PluginDependency;
-
-/**
- *
- */
-class Marketplace
-{
- /**
- * @var MarketplaceApiClient
- */
- private $client;
-
- public function __construct()
- {
- $this->client = new MarketplaceApiClient();
- }
-
- public function getPluginInfo($pluginName)
- {
- $marketplace = new MarketplaceApiClient();
-
- $plugin = $marketplace->getPluginInfo($pluginName);
- $plugin = $this->enrichPluginInformation($plugin);
-
- return $plugin;
- }
-
- public function getAvailablePluginNames($themesOnly)
- {
- if ($themesOnly) {
- $plugins = $this->client->searchForThemes('', '', '');
- } else {
- $plugins = $this->client->searchForPlugins('', '', '');
- }
-
- $names = array();
- foreach ($plugins as $plugin) {
- $names[] = $plugin['name'];
- }
-
- return $names;
- }
-
- public function getAllAvailablePluginNames()
- {
- return array_merge(
- $this->getAvailablePluginNames(true),
- $this->getAvailablePluginNames(false)
- );
- }
-
- public function searchPlugins($query, $sort, $themesOnly)
- {
- if ($themesOnly) {
- $plugins = $this->client->searchForThemes('', $query, $sort);
- } else {
- $plugins = $this->client->searchForPlugins('', $query, $sort);
- }
-
- foreach ($plugins as $key => $plugin) {
- $plugins[$key] = $this->enrichPluginInformation($plugin);
- }
-
- return $plugins;
- }
-
- private function getPluginUpdateInformation($plugin)
- {
- if (empty($plugin['name'])) {
- return;
- }
-
- $pluginsHavingUpdate = $this->getPluginsHavingUpdate($plugin['isTheme']);
-
- foreach ($pluginsHavingUpdate as $pluginHavingUpdate) {
- if ($plugin['name'] == $pluginHavingUpdate['name']) {
- return $pluginHavingUpdate;
- }
- }
- }
-
- private function hasPluginUpdate($plugin)
- {
- $update = $this->getPluginUpdateInformation($plugin);
-
- return !empty($update);
- }
-
- /**
- * @param bool $themesOnly
- * @return array
- */
- public function getPluginsHavingUpdate($themesOnly)
- {
- $pluginManager = \Piwik\Plugin\Manager::getInstance();
- $pluginManager->loadAllPluginsAndGetTheirInfo();
- $loadedPlugins = $pluginManager->getLoadedPlugins();
-
- try {
- $pluginsHavingUpdate = $this->client->getInfoOfPluginsHavingUpdate($loadedPlugins, $themesOnly);
- } catch (\Exception $e) {
- $pluginsHavingUpdate = array();
- }
-
- foreach ($pluginsHavingUpdate as $key => $updatePlugin) {
- foreach ($loadedPlugins as $loadedPlugin) {
- if (!empty($updatePlugin['name'])
- && $loadedPlugin->getPluginName() == $updatePlugin['name']
- ) {
- $updatePlugin['currentVersion'] = $loadedPlugin->getVersion();
- $updatePlugin['isActivated'] = $pluginManager->isPluginActivated($updatePlugin['name']);
- $pluginsHavingUpdate[$key] = $this->addMissingRequirements($updatePlugin);
- break;
- }
- }
- }
-
- // remove plugins that have updates but for some reason are not loaded
- foreach ($pluginsHavingUpdate as $key => $updatePlugin) {
- if (empty($updatePlugin['currentVersion'])) {
- unset($pluginsHavingUpdate[$key]);
- }
- }
-
- return $pluginsHavingUpdate;
- }
-
- private function enrichPluginInformation($plugin)
- {
- $plugin['isInstalled'] = \Piwik\Plugin\Manager::getInstance()->isPluginLoaded($plugin['name']);
- $plugin['canBeUpdated'] = $plugin['isInstalled'] && $this->hasPluginUpdate($plugin);
- if (!empty($plugin['lastUpdated'])) {
- $plugin['lastUpdated'] = Date::factory($plugin['lastUpdated'])->getLocalized(Date::DATE_FORMAT_SHORT);
- }
-
- if ($plugin['canBeUpdated']) {
- $pluginUpdate = $this->getPluginUpdateInformation($plugin);
- $plugin['repositoryChangelogUrl'] = $pluginUpdate['repositoryChangelogUrl'];
- $plugin['currentVersion'] = $pluginUpdate['currentVersion'];
- }
-
- if (!empty($plugin['activity']['lastCommitDate'])
- && false === strpos($plugin['activity']['lastCommitDate'], '0000')) {
-
- $plugin['activity']['lastCommitDate'] = Date::factory($plugin['activity']['lastCommitDate'])->getLocalized(Date::DATE_FORMAT_LONG);
- } else {
- $plugin['activity']['lastCommitDate'] = null;
- }
-
- if (!empty($plugin['versions'])) {
-
- foreach ($plugin['versions'] as $index => $version) {
- if (!empty($version['release'])) {
- $plugin['versions'][$index]['release'] = Date::factory($version['release'])->getLocalized(Date::DATE_FORMAT_LONG);
- }
- }
- }
-
- $plugin = $this->addMissingRequirements($plugin);
-
- return $plugin;
- }
-
- /**
- * @param $plugin
- */
- private function addMissingRequirements($plugin)
- {
- $plugin['missingRequirements'] = array();
-
- if (empty($plugin['versions']) || !is_array($plugin['versions'])) {
- return $plugin;
- }
-
- $latestVersion = $plugin['versions'][count($plugin['versions']) - 1];
-
- if (empty($latestVersion['requires'])) {
- return $plugin;
- }
-
- $requires = $latestVersion['requires'];
-
- $dependency = new PluginDependency();
- $plugin['missingRequirements'] = $dependency->getMissingDependencies($requires);
-
- return $plugin;
- }
-}
diff --git a/plugins/CorePluginsAdmin/MarketplaceApiClient.php b/plugins/CorePluginsAdmin/MarketplaceApiClient.php
deleted file mode 100644
index eebc3d843b..0000000000
--- a/plugins/CorePluginsAdmin/MarketplaceApiClient.php
+++ /dev/null
@@ -1,200 +0,0 @@
-<?php
-/**
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- *
- */
-namespace Piwik\Plugins\CorePluginsAdmin;
-
-use Piwik\Cache;
-use Piwik\Container\StaticContainer;
-use Piwik\Http;
-use Piwik\Version;
-
-/**
- *
- */
-class MarketplaceApiClient
-{
- const CACHE_TIMEOUT_IN_SECONDS = 1200;
- const HTTP_REQUEST_TIMEOUT = 60;
-
- private $domain = 'http://plugins.piwik.org';
-
- public static function clearAllCacheEntries()
- {
- $cache = Cache::getLazyCache();
- $cache->flushAll();
- }
-
- public function getPluginInfo($name)
- {
- $action = sprintf('plugins/%s/info', $name);
-
- return $this->fetch($action, array());
- }
-
- public function download($pluginOrThemeName, $target)
- {
- $downloadUrl = $this->getDownloadUrl($pluginOrThemeName);
-
- if (empty($downloadUrl)) {
- return false;
- }
-
- $success = Http::fetchRemoteFile($downloadUrl, $target, 0, static::HTTP_REQUEST_TIMEOUT);
-
- return $success;
- }
-
- /**
- * @param \Piwik\Plugin[] $plugins
- * @return array|mixed
- */
- public function checkUpdates($plugins)
- {
- $params = array();
-
- foreach ($plugins as $plugin) {
- $pluginName = $plugin->getPluginName();
- if (!\Piwik\Plugin\Manager::getInstance()->isPluginBundledWithCore($pluginName)) {
- $params[] = array('name' => $plugin->getPluginName(), 'version' => $plugin->getVersion());
- }
- }
-
- if (empty($params)) {
- return array();
- }
-
- $params = array('plugins' => $params);
-
- $hasUpdates = $this->fetch('plugins/checkUpdates', array('plugins' => json_encode($params)));
-
- if (empty($hasUpdates)) {
- return array();
- }
-
- return $hasUpdates;
- }
-
- /**
- * @param \Piwik\Plugin[] $plugins
- * @param bool $themesOnly
- * @return array
- */
- public function getInfoOfPluginsHavingUpdate($plugins, $themesOnly)
- {
- $hasUpdates = $this->checkUpdates($plugins);
-
- $pluginDetails = array();
-
- foreach ($hasUpdates as $pluginHavingUpdate) {
- $plugin = $this->getPluginInfo($pluginHavingUpdate['name']);
- $plugin['repositoryChangelogUrl'] = $pluginHavingUpdate['repositoryChangelogUrl'];
-
- if (!empty($plugin['isTheme']) == $themesOnly) {
- $pluginDetails[] = $plugin;
- }
- }
-
- return $pluginDetails;
- }
-
- public function searchForPlugins($keywords, $query, $sort)
- {
- $response = $this->fetch('plugins', array('keywords' => $keywords, 'query' => $query, 'sort' => $sort));
-
- if (!empty($response['plugins'])) {
- return $response['plugins'];
- }
-
- return array();
- }
-
- public function searchForThemes($keywords, $query, $sort)
- {
- $response = $this->fetch('themes', array('keywords' => $keywords, 'query' => $query, 'sort' => $sort));
-
- if (!empty($response['plugins'])) {
- return $response['plugins'];
- }
-
- return array();
- }
-
- private function getPhpVersion()
- {
- return PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION;
- }
-
- public static function getPiwikVersion()
- {
- return StaticContainer::get('marketplacePiwikVersion');
- }
-
- private function fetch($action, $params)
- {
- $params['php'] = $this->getPhpVersion();
- $params['piwik'] = self::getPiwikVersion();
- $params['prefer_stable'] = '1';
- ksort($params);
- $query = http_build_query($params);
-
- $cacheId = $this->getCacheKey($action, $query);
- $cache = $this->buildCache();
- $result = $cache->fetch($cacheId);
-
- if (false === $result) {
- $endpoint = $this->domain . '/api/2.0/';
- $url = sprintf('%s%s?%s', $endpoint, $action, $query);
- $response = Http::sendHttpRequest($url, static::HTTP_REQUEST_TIMEOUT);
- $result = json_decode($response, true);
-
- if (is_null($result)) {
- $message = sprintf('There was an error reading the response from the Marketplace: %s. Please try again later.',
- substr($response, 0, 50));
- throw new MarketplaceApiException($message);
- }
-
- if (!empty($result['error'])) {
- throw new MarketplaceApiException($result['error']);
- }
-
- $cache->save($cacheId, $result, self::CACHE_TIMEOUT_IN_SECONDS);
- }
-
- return $result;
- }
-
- private function buildCache()
- {
- return Cache::getLazyCache();
- }
-
- private function getCacheKey($action, $query)
- {
- return sprintf('marketplace.api.2.0.%s.%s', str_replace('/', '.', $action), md5($query));
- }
-
- /**
- * @param $pluginOrThemeName
- * @throws MarketplaceApiException
- * @return string
- */
- public function getDownloadUrl($pluginOrThemeName)
- {
- $plugin = $this->getPluginInfo($pluginOrThemeName);
-
- if (empty($plugin['versions'])) {
- throw new MarketplaceApiException('Plugin has no versions.');
- }
-
- $latestVersion = array_pop($plugin['versions']);
- $downloadUrl = $latestVersion['download'];
-
- return $this->domain . $downloadUrl . '?coreVersion=' . self::getPiwikVersion();
- }
-
-}
diff --git a/plugins/CorePluginsAdmin/Menu.php b/plugins/CorePluginsAdmin/Menu.php
index 69a69b9e7f..8e1b57ef40 100644
--- a/plugins/CorePluginsAdmin/Menu.php
+++ b/plugins/CorePluginsAdmin/Menu.php
@@ -8,30 +8,43 @@
*/
namespace Piwik\Plugins\CorePluginsAdmin;
-use Piwik\Db;
+use Piwik\Container\StaticContainer;
use Piwik\Menu\MenuAdmin;
use Piwik\Piwik;
+use Piwik\Plugins\Marketplace\Marketplace;
+use Piwik\Plugins\Marketplace\Plugins;
-/**
- */
class Menu extends \Piwik\Plugin\Menu
{
+ private $marketplacePlugins;
+
+ /**
+ * Menu constructor.
+ * @param Plugins $marketplacePlugins
+ */
+ public function __construct($marketplacePlugins = null)
+ {
+ if (!empty($marketplacePlugins)) {
+ $this->marketplacePlugins = $marketplacePlugins;
+ } elseif (Marketplace::isMarketplaceEnabled()) {
+ // we load it manually as marketplace plugin might not be loaded
+ $this->marketplacePlugins = StaticContainer::get('Piwik\Plugins\Marketplace\Plugins');
+ }
+ }
public function configureAdminMenu(MenuAdmin $menu)
{
$hasSuperUserAcess = Piwik::hasUserSuperUserAccess();
$isAnonymous = Piwik::isUserIsAnonymous();
- $isMarketplaceEnabled = CorePluginsAdmin::isMarketplaceEnabled();
+ $isMarketplaceEnabled = Marketplace::isMarketplaceEnabled();
$pluginsUpdateMessage = '';
- if ($hasSuperUserAcess && $isMarketplaceEnabled) {
- $marketplace = new Marketplace();
- $pluginsHavingUpdate = $marketplace->getPluginsHavingUpdate($themesOnly = false);
- $themesHavingUpdate = $marketplace->getPluginsHavingUpdate($themesOnly = true);
+ if ($hasSuperUserAcess && $isMarketplaceEnabled && $this->marketplacePlugins) {
+ $pluginsHavingUpdate = $this->marketplacePlugins->getPluginsHavingUpdate();
if (!empty($pluginsHavingUpdate)) {
- $pluginsUpdateMessage = sprintf(' (%d)', count($pluginsHavingUpdate) + count($themesHavingUpdate));
+ $pluginsUpdateMessage = sprintf(' (%d)', count($pluginsHavingUpdate));
}
}
@@ -44,18 +57,6 @@ class Menu extends \Piwik\Plugin\Menu
$this->urlForAction('plugins', array('activated' => '')),
$order = 20);
}
-
- if ($this->isAllowedToSeeMarketPlace()) {
- $menu->addPlatformItem('CorePluginsAdmin_Marketplace',
- $this->urlForAction('marketplace', array('activated' => '', 'mode' => 'user')),
- $order = 5);
- }
}
- private function isAllowedToSeeMarketPlace()
- {
- $isAnonymous = Piwik::isUserIsAnonymous();
- $isMarketplaceEnabled = CorePluginsAdmin::isMarketplaceEnabled();
- return $isMarketplaceEnabled && !$isAnonymous;
- }
}
diff --git a/plugins/CorePluginsAdmin/PluginInstaller.php b/plugins/CorePluginsAdmin/PluginInstaller.php
index ab4f7b8622..d2fad35c76 100644
--- a/plugins/CorePluginsAdmin/PluginInstaller.php
+++ b/plugins/CorePluginsAdmin/PluginInstaller.php
@@ -8,13 +8,16 @@
*/
namespace Piwik\Plugins\CorePluginsAdmin;
+use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\Filechecks;
use Piwik\Filesystem;
use Piwik\Piwik;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Plugin\Dependency as PluginDependency;
+use Piwik\Plugins\Marketplace\Marketplace;
use Piwik\Unzip;
+use Piwik\Plugins\Marketplace\Api\Client;
/**
*
@@ -26,22 +29,38 @@ class PluginInstaller
private $pluginName;
- public function __construct($pluginName)
+ /**
+ * Null if Marketplace Plugin is not installed
+ * @var Client|null
+ */
+ private $marketplaceClient;
+
+ /**
+ * PluginInstaller constructor.
+ * @param Client|null $client
+ */
+ public function __construct($client = null)
{
- $this->pluginName = $pluginName;
+ if (!empty($client)) {
+ $this->marketplaceClient = $client;
+ } elseif (Marketplace::isMarketplaceEnabled()) {
+ // we load it manually as marketplace might not be loaded
+ $this->marketplaceClient = StaticContainer::get('Piwik\Plugins\Marketplace\Api\Client');
+ }
}
- public function installOrUpdatePluginFromMarketplace()
+ public function installOrUpdatePluginFromMarketplace($pluginName)
{
- $tmpPluginPath = StaticContainer::get('path.tmp') . '/latest/plugins/';
+ $this->checkMarketplaceIsEnabled();
- $tmpPluginZip = $tmpPluginPath . $this->pluginName . '.zip';
- $tmpPluginFolder = $tmpPluginPath . $this->pluginName;
+ $this->pluginName = $pluginName;
try {
$this->makeSureFoldersAreWritable();
$this->makeSurePluginNameIsValid();
- $this->downloadPluginFromMarketplace($tmpPluginZip);
+
+ $tmpPluginZip = $this->downloadPluginFromMarketplace();
+ $tmpPluginFolder = dirname($tmpPluginZip) . '/' . basename($tmpPluginZip, '.zip') . '/';
$this->extractPluginFiles($tmpPluginZip, $tmpPluginFolder);
$this->makeSurePluginJsonExists($tmpPluginFolder);
$metadata = $this->getPluginMetadataIfValid($tmpPluginFolder);
@@ -50,15 +69,22 @@ class PluginInstaller
Filesystem::deleteAllCacheOnUpdate($this->pluginName);
- $plugin = PluginManager::getInstance()->getLoadedPlugin($this->pluginName);
- if (!empty($plugin)) {
- $plugin->reloadPluginInformation();
+ $pluginManager = PluginManager::getInstance();
+ if ($pluginManager->isPluginLoaded($this->pluginName)) {
+ $plugin = PluginManager::getInstance()->getLoadedPlugin($this->pluginName);
+ if (!empty($plugin)) {
+ $plugin->reloadPluginInformation();
+ }
}
} catch (\Exception $e) {
- $this->removeFileIfExists($tmpPluginZip);
- $this->removeFolderIfExists($tmpPluginFolder);
+ if (!empty($tmpPluginZip)) {
+ Filesystem::deleteFileIfExists($tmpPluginZip);
+ }
+ if (!empty($tmpPluginFolder)) {
+ $this->removeFolderIfExists($tmpPluginFolder);
+ }
throw $e;
}
@@ -69,7 +95,8 @@ class PluginInstaller
public function installOrUpdatePluginFromFile($pathToZip)
{
- $tmpPluginFolder = StaticContainer::get('path.tmp') . self::PATH_TO_DOWNLOAD . $this->pluginName;
+ $tmpPluginName = 'uploaded' . Common::generateUniqId();
+ $tmpPluginFolder = StaticContainer::get('path.tmp') . self::PATH_TO_DOWNLOAD . $tmpPluginName;
try {
$this->makeSureFoldersAreWritable();
@@ -108,18 +135,18 @@ class PluginInstaller
));
}
- private function downloadPluginFromMarketplace($pluginZipTargetFile)
+ /**
+ * @return false|string false on failed download, or a path to the downloaded zip file
+ * @throws PluginInstallerException
+ */
+ private function downloadPluginFromMarketplace()
{
- $this->removeFileIfExists($pluginZipTargetFile);
-
- $marketplace = new MarketplaceApiClient();
-
try {
- $marketplace->download($this->pluginName, $pluginZipTargetFile);
+ return $this->marketplaceClient->download($this->pluginName);
} catch (\Exception $e) {
try {
- $downloadUrl = $marketplace->getDownloadUrl($this->pluginName);
+ $downloadUrl = $this->marketplaceClient->getDownloadUrl($this->pluginName);
$errorMessage = sprintf('Failed to download plugin from %s: %s', $downloadUrl, $e->getMessage());
} catch (\Exception $ex) {
@@ -166,10 +193,8 @@ class PluginInstaller
$requires = (array) $metadata->require;
}
- $piwikVersion = MarketplaceApiClient::getPiwikVersion();
-
$dependency = new PluginDependency();
- $dependency->setPiwikVersion($piwikVersion);
+ $dependency->setEnvironment($this->marketplaceClient->getEnvironment());
$missingDependencies = $dependency->getMissingDependencies($requires);
if (!empty($missingDependencies)) {
@@ -289,9 +314,7 @@ class PluginInstaller
*/
private function removeFileIfExists($targetTmpFile)
{
- if (file_exists($targetTmpFile)) {
- unlink($targetTmpFile);
- }
+ Filesystem::deleteFileIfExists($targetTmpFile);
}
/**
@@ -300,8 +323,7 @@ class PluginInstaller
private function makeSurePluginNameIsValid()
{
try {
- $marketplace = new MarketplaceApiClient();
- $pluginDetails = $marketplace->getPluginInfo($this->pluginName);
+ $pluginDetails = $this->marketplaceClient->getPluginInfo($this->pluginName);
} catch (\Exception $e) {
throw new PluginInstallerException($e->getMessage());
}
@@ -311,4 +333,11 @@ class PluginInstaller
}
}
+ private function checkMarketplaceIsEnabled()
+ {
+ if (!isset($this->marketplaceClient)) {
+ throw new PluginInstallerException('Marketplace plugin needs to be enabled to perform this action.');
+ }
+ }
+
}
diff --git a/plugins/CorePluginsAdmin/angularjs/marketplace/marketplace.directive.js b/plugins/CorePluginsAdmin/angularjs/marketplace/marketplace.directive.js
deleted file mode 100644
index e528c03bd0..0000000000
--- a/plugins/CorePluginsAdmin/angularjs/marketplace/marketplace.directive.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/*!
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- */
-
-/**
- * Usage:
- * <div piwik-marketplace>
- */
-(function () {
-
- angular.module('piwikApp').directive('piwikMarketplace', piwikMarketplace);
-
- piwikMarketplace.$inject = ['piwik', '$timeout'];
-
- function piwikMarketplace(piwik, $timeout){
-
- return {
- restrict: 'A',
- compile: function (element, attrs) {
-
- return function (scope, element, attrs) {
-
- $timeout(function () {
-
- $('.uploadPlugin').click(function (event) {
- event.preventDefault();
-
- piwikHelper.modalConfirm('#installPluginByUpload', {
- yes: function () {
- window.location = link;
- }
- });
- });
-
- $('#uploadPluginForm').submit(function (event) {
-
- var $zipFile = $('[name=pluginZip]');
- var fileName = $zipFile.val();
-
- if (!fileName || '.zip' != fileName.slice(-4)) {
- event.preventDefault();
- alert(_pk_translate('CorePluginsAdmin_NoZipFileSelected'));
- }
- });
-
- // Keeps the plugin descriptions the same height
- $('.marketplace .plugin .description').dotdotdot({
- after: 'a.more',
- watch: 'window'
- });
- });
- };
- }
- };
- }
-})(); \ No newline at end of file
diff --git a/plugins/CorePluginsAdmin/angularjs/plugins/plugin-management.directive.js b/plugins/CorePluginsAdmin/angularjs/plugins/plugin-management.directive.js
index e09c1e3d28..404d9a40a5 100644
--- a/plugins/CorePluginsAdmin/angularjs/plugins/plugin-management.directive.js
+++ b/plugins/CorePluginsAdmin/angularjs/plugins/plugin-management.directive.js
@@ -58,26 +58,6 @@
piwikHelper.modalConfirm('#'+overlayId, {});
});
- $('.uploadPlugin').click(function (event) {
- event.preventDefault();
-
- piwikHelper.modalConfirm('#installPluginByUpload', {
- yes: function () {
- window.location = link;
- }
- });
- });
-
- $('#uploadPluginForm').submit(function (event) {
-
- var $zipFile = $('[name=pluginZip]');
- var fileName = $zipFile.val();
-
- if (!fileName || '.zip' != fileName.slice(-4)) {
- event.preventDefault();
- alert(_pk_translate('CorePluginsAdmin_NoZipFileSelected'));
- }
- });
};
}
};
diff --git a/plugins/CorePluginsAdmin/config/config.php b/plugins/CorePluginsAdmin/config/config.php
deleted file mode 100644
index 5d30748a94..0000000000
--- a/plugins/CorePluginsAdmin/config/config.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-return array(
- 'marketplacePiwikVersion' => function () {
- return \Piwik\Version::VERSION;
- }
-);
diff --git a/plugins/CorePluginsAdmin/lang/en.json b/plugins/CorePluginsAdmin/lang/en.json
index 0519947747..1fe5a78fa9 100644
--- a/plugins/CorePluginsAdmin/lang/en.json
+++ b/plugins/CorePluginsAdmin/lang/en.json
@@ -1,43 +1,27 @@
{
"CorePluginsAdmin": {
- "ActionActivatePlugin": "Activate plugin",
- "ActionActivateTheme": "Activate theme",
- "ActionInstall": "Install",
"ActionUninstall": "Uninstall",
"Activate": "Activate",
"Activated": "Activated",
"Active": "Active",
"Activity": "Activity",
- "AllowedUploadFormats": "You may upload a plugin or theme in .zip format via this page.",
+ "AlwaysActivatedPluginsList": "The following plugins are always activated and cannot be disabled: %s",
"AuthorHomepage": "Author Homepage",
- "Authors": "Authors",
- "BackToExtendPiwik": "Back to Marketplace",
- "BeCarefulUsingPlugins": "Plugins that are not authored by Piwik team must be used with care: we did not review them.",
- "BeCarefulUsingThemes": "Themes that are not authored by Piwik team must be used with care: we did not review them.",
- "ByXDevelopers": "by %s developers",
- "CannotInstall": "Cannot install (help)",
"Changelog": "Changelog",
"ChangeSettingsPossible": "You can change %1$ssettings%2$s for this plugin.",
"CorePluginTooltip": "Core plugins have no version since they are distributed with Piwik.",
"Deactivate": "Deactivate",
- "Developer": "Developer",
- "DevelopersLearnHowToDevelopPlugins": "Developers: Learn how you can extend and customize Piwik by %1$sdeveloping plugins or themes%2$s.",
"DoMoreContactPiwikAdmins": "To install a new plugin or a new theme, please get in touch with your Piwik admins.",
- "FeaturedPlugin": "Featured plugin",
"ChangeLookByManageThemes": "You can change the appearance of Piwik by %1$sManaging Themes%2$s.",
- "GetEarlyAccessForPaidPlugins": "Note: all plugins are available for free at present; in the future we will enable Paid Plugins in the Marketplace (%1$scontact us%2$s for early access).",
"History": "History",
"Inactive": "Inactive",
+ "InstalledPlugins": "Installed plugins",
+ "InstalledThemes": "Installed themes",
"InfoPluginUpdateIsRecommended": "Update your plugins now to benefit from the latest improvements.",
"InfoThemeIsUsedByOtherUsersAsWell": "Note: the other %1$s users registered in this Piwik are also using the theme %2$s.",
"InfoThemeUpdateIsRecommended": "Update your themes to enjoy the latest version.",
- "InstallingPlugin": "Installing %s",
"InstallNewPlugins": "Install new plugins",
"InstallNewThemes": "Install new themes",
- "InstalledPlugins": "Installed plugins",
- "InstalledThemes": "Installed themes",
- "LastCommitTime": "(last commit %s)",
- "LastUpdated": "Last Updated",
"LicenseHomepage": "License Homepage",
"LikeThisPlugin": "Like this plugin?",
"ConsiderDonating": "Consider donating",
@@ -45,66 +29,38 @@
"ConsiderDonatingCreatorOf": "Please consider donating to the creator of %s",
"PluginsExtendPiwik": "Plugins extend and expand the functionality of Piwik.",
"OncePluginIsInstalledYouMayActivateHere": "Once a plugin is installed, you may activate it or deactivate it here.",
- "Marketplace": "Marketplace",
- "MarketplaceSellPluginSubject": "Marketplace - Sell Plugin",
"MenuPlatform": "Platform",
"MissingRequirementsNotice": "Please update %1$s %2$s to a newer version, %1$s %3$s is required.",
"MissingRequirementsPleaseInstallNotice": "Please install %1$s %2$s as it is required by %3$s.",
- "NewVersion": "new version",
- "NoPluginsFound": "No plugins found",
- "NoPluginSettings": "No plugin settings that can be configured",
- "NotAllowedToBrowseMarketplacePlugins": "You can browse the list of plugins that can be installed to customize or extend your Piwik platform. Please contact your administrator if you need any of these installed.",
- "NotAllowedToBrowseMarketplaceThemes": "You can browse the list of themes that can be installed to customize the appearance of the Piwik platform. Please contact your administrator to get any of these installed for you.",
- "NoThemesFound": "No themes found",
"NoZipFileSelected": "Please select a ZIP file.",
- "NumDownloadsLatestVersion": "Latest version: %s Downloads",
"NumUpdatesAvailable": "%s Update(s) available",
+ "NoPluginSettings": "No plugin settings that can be configured",
"Origin": "Origin",
"OriginCore": "Core",
"OriginThirdParty": "Third-party",
"PluginHomepage": "Plugin Homepage",
- "PluginKeywords": "Keywords",
"PluginNotCompatibleWith": "%1$s plugin is not compatible with %2$s.",
"PluginNotWorkingAlternative": "If you've been using this plugin, maybe you can find a more recent version in the Marketplace. If not, you may want to uninstall it.",
"PluginRequirement": "%1$s requires %2$s.",
"PluginsManagement": "Manage Plugins",
- "PluginUpdateAvailable": "You are using version %1$s and a new version %2$s is available.",
- "PluginVersionInfo": "%1$s from %2$s",
- "PluginWebsite": "Plugin Website",
- "Screenshots": "Screenshots",
- "SortByAlpha": "Alpha",
- "SortByNewest": "Newest",
- "SortByPopular": "Popular",
+ "NotDownloadable": "Not downloadable",
+ "PluginNotDownloadable": "The plugin is not downloadable.",
+ "PluginNotDownloadablePaidReason": "Possible reasons are an expired or exceeded license.",
+ "PluginActivated": "Plugin activated",
"Status": "Status",
- "StepDownloadingPluginFromMarketplace": "Downloading plugin from Marketplace",
- "StepDownloadingThemeFromMarketplace": "Downloading theme from Marketplace",
- "StepPluginSuccessfullyInstalled": "You have successfully installed the plugin %1$s %2$s.",
- "StepPluginSuccessfullyUpdated": "You have successfully updated the plugin %1$s %2$s.",
- "StepReplaceExistingPlugin": "Replacing existing plugin",
- "StepReplaceExistingTheme": "Replacing existing theme",
- "StepThemeSuccessfullyInstalled": "You have successfully installed the theme %1$s %2$s.",
- "StepThemeSuccessfullyUpdated": "You have successfully updated the theme %1$s %2$s.",
- "StepUnzippingPlugin": "Unzipping plugin",
- "StepUnzippingTheme": "Unzipping theme",
"SuccessfullyActicated": "You have successfully activated <strong>%s<\/strong>.",
- "Support": "Support",
"TeaserExtendPiwik": "Extend Piwik with Plugins and Themes",
"TeaserExtendPiwikByPlugin": "Extend Piwik by %1$sinstalling a new plugin%2$s.",
"TeaserExtendPiwikByTheme": "Enjoy another look & feel by %1$sinstalling a new theme%2$s.",
- "TeaserExtendPiwikByUpload": "Extend Piwik by uploading a ZIP file",
"InstallingNewPluginViaMarketplaceOrUpload": "You may automatically install plugins from the Marketplace or %1$supload a plugin%2$s in .zip format.",
"Theme": "Theme",
"Themes": "Themes",
"ThemesDescription": "Themes can change the appearance of Piwik user interface, and provide a completely new visual experience to enjoy your analytics reports.",
"ThemesManagement": "Manage Themes",
"UninstallConfirm": "You are about to uninstall a plugin %s. The plugin will be completely removed from your platform and it won't be recoverable. Are you sure you want to do this?",
- "Updated": "Updated",
- "UpdatingPlugin": "Updating %s",
- "UploadZipFile": "Upload ZIP file",
"Version": "Version",
- "ViewRepositoryChangelog": "View the changes",
"ViewAllMarketplacePlugins": "View all Marketplace plugins",
- "Websites": "Websites",
- "WeDeactivatedThePluginAsItHasMissingDependencies": "We disabled the plugin %s as it has missing dependencies:"
+ "WeDeactivatedThePluginAsItHasMissingDependencies": "We disabled the plugin %s as it has missing dependencies:",
+ "Websites": "Websites"
}
}
diff --git a/plugins/CorePluginsAdmin/lang/hr.json b/plugins/CorePluginsAdmin/lang/hr.json
deleted file mode 100644
index 3b6c746dd3..0000000000
--- a/plugins/CorePluginsAdmin/lang/hr.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "CorePluginsAdmin": {
- "ActionInstall": "Instaliraj"
- }
-} \ No newline at end of file
diff --git a/plugins/CorePluginsAdmin/stylesheets/plugins_admin.less b/plugins/CorePluginsAdmin/stylesheets/plugins_admin.less
index 467b9fe49b..0adcbc1216 100644
--- a/plugins/CorePluginsAdmin/stylesheets/plugins_admin.less
+++ b/plugins/CorePluginsAdmin/stylesheets/plugins_admin.less
@@ -1,6 +1,28 @@
+.pluginsManagement {
+ .footer-message {
+ margin-top: 32px;
+ .font-default(13px, 21px);
+ }
+}
+
+table.dataTable tr.active-plugin > td {
+ background-color:@theme-color-background-base !important;
+}
+
+table.dataTable tr.active-plugin:hover > td {
+ background-color:@theme-color-background-base !important;
+}
+
+table.dataTable tr.inactive-plugin > td {
+ background-color:#ddd !important;
+}
+
+table.dataTable tr.inactive-plugin:hover > td {
+ background-color:#ddd !important;
+}
.plugin-desc-text {
- margin-top:0em;
+ margin-top:0;
margin-bottom:1.5em;
}
@@ -44,12 +66,6 @@
.plugin-license {
float:right;
- font-style:italic;
-}
-
-.plugin-homepage {
- font-size:.8em;
- font-style:italic;
}
table.entityTable tr td .plugin-homepage a {
@@ -96,7 +112,6 @@ table.entityTable tr td a.uninstall {
.plugin-desc-missingrequirements {
font-weight:bold;
- font-style: italic;
a {
text-decoration: underline !important;
color: black;
@@ -107,7 +122,6 @@ table.entityTable tr td a.uninstall {
text-align: right;
width: 100%;
display: inline-block;
- font-style: italic;
}
}
diff --git a/plugins/CorePluginsAdmin/templates/installPlugin.twig b/plugins/CorePluginsAdmin/templates/installPlugin.twig
deleted file mode 100644
index 9f55777684..0000000000
--- a/plugins/CorePluginsAdmin/templates/installPlugin.twig
+++ /dev/null
@@ -1,41 +0,0 @@
-{% extends 'admin.twig' %}
-
-{% block content %}
-
- <div style="max-width:980px;">
-
- <h2>{{ 'CorePluginsAdmin_InstallingPlugin'|translate(plugin.name) }}</h2>
-
- <div>
-
- {% if plugin.isTheme %}
-
- <p>{{ 'CorePluginsAdmin_StepDownloadingThemeFromMarketplace'|translate }}</p>
-
- <p>{{ 'CorePluginsAdmin_StepUnzippingTheme'|translate }}</p>
-
- <p>{{ 'CorePluginsAdmin_StepThemeSuccessfullyInstalled'|translate(plugin.name, plugin.latestVersion) }}</p>
-
- <p><strong><a href="{{ linkTo({'action': 'activate', 'pluginName': plugin.name, 'nonce': nonce}) }}">{{ 'CorePluginsAdmin_ActionActivateTheme'|translate }}</a></strong>
-
- |
- <a href="{{ linkTo({'action': 'browseThemes'}) }}">{{ 'CorePluginsAdmin_BackToExtendPiwik'|translate }}</a></p>
-
- {% else %}
-
- <p>{{ 'CorePluginsAdmin_StepDownloadingPluginFromMarketplace'|translate }}</p>
-
- <p>{{ 'CorePluginsAdmin_StepUnzippingPlugin'|translate }}</p>
-
- <p>{{ 'CorePluginsAdmin_StepPluginSuccessfullyInstalled'|translate(plugin.name, plugin.latestVersion) }}</p>
-
- <p><strong><a href="{{ linkTo({'action': 'activate', 'pluginName': plugin.name, 'nonce': nonce}) }}">{{ 'CorePluginsAdmin_ActionActivatePlugin'|translate }}</a></strong>
-
- |
- <a href="{{ linkTo({'action': 'browsePlugins'}) }}">{{ 'CorePluginsAdmin_BackToExtendPiwik'|translate }}</a></p>
-
- {% endif %}
- </div>
- </div>
-
-{% endblock %}
diff --git a/plugins/CorePluginsAdmin/templates/license.twig b/plugins/CorePluginsAdmin/templates/license.twig
new file mode 100644
index 0000000000..9b2c9c0ae2
--- /dev/null
+++ b/plugins/CorePluginsAdmin/templates/license.twig
@@ -0,0 +1,4 @@
+
+<h1>License for {{ pluginName|raw }}</h1>
+
+{{ license|raw }}
diff --git a/plugins/CorePluginsAdmin/templates/macros.twig b/plugins/CorePluginsAdmin/templates/macros.twig
index eca8e609a6..78be51f24e 100644
--- a/plugins/CorePluginsAdmin/templates/macros.twig
+++ b/plugins/CorePluginsAdmin/templates/macros.twig
@@ -1,9 +1,11 @@
-{% macro tablePluginUpdates(pluginsHavingUpdate, nonce, isTheme) %}
+
+{% macro tablePluginUpdates(pluginsHavingUpdate, updateNonce, isMultiServerEnvironment) %}
+ {% import '@Marketplace/macros.twig' as marketplaceMacro %}
<table piwik-content-table>
<thead>
<tr>
- <th>{% if isTheme %}{{ 'CorePluginsAdmin_Theme'|translate }}{% else %}{{ 'General_Plugin'|translate }}{% endif %}</th>
+ <th>{{ 'General_Plugin'|translate }}</th>
<th class="num">{{ 'CorePluginsAdmin_Version'|translate }}</th>
<th>{{ 'General_Description'|translate }}</th>
<th class="status">{{ 'CorePluginsAdmin_Status'|translate }}</th>
@@ -27,7 +29,7 @@
</td>
<td class="desc">
{{ plugin.description }}
- {{ _self.missingRequirementsPleaseUpdateNotice(plugin) }}
+ {{ marketplaceMacro.missingRequirementsPleaseUpdateNotice(plugin) }}
</td>
<td class="status">
{% if plugin.isActivated %}
@@ -37,8 +39,13 @@
{% endif %}
</td>
<td class="togl action-links">
- {% if 0 == plugin.missingRequirements|length %}
- <a href="{{ linkTo({'action':'updatePlugin', 'pluginName': plugin.name, 'nonce': nonce}) }}">Update</a>
+ {% if plugin.isDownloadable is defined and not plugin.isDownloadable %}
+ <span title="{{ 'CorePluginsAdmin_PluginNotDownloadable'|translate|e('html_attr') }} {% if plugin.isPaid %}{{ 'CorePluginsAdmin_PluginNotDownloadablePaidReason'|translate|e('html_attr') }}{% endif %}"
+ >{{ 'CorePluginsAdmin_NotDownloadable'|translate|e('html_attr') }}</span>
+ {% elseif isMultiServerEnvironment %}
+ <a onclick="$(this).css('display', 'none')" href="{{ linkTo({'action':'download', 'module': 'Marketplace', 'pluginName': plugin.name, 'nonce': (plugin.name|nonce)}) }}">{{ 'General_Download'|translate }}</a>
+ {% elseif 0 == plugin.missingRequirements|length %}
+ <a href="{{ linkTo({'action':'updatePlugin', 'module': 'Marketplace', 'pluginName': plugin.name, 'nonce': updateNonce}) }}">{{ 'CoreUpdater_UpdateTitle'|translate }}</a>
{% else %}
-
{% endif %}
@@ -50,16 +57,15 @@
{% endmacro %}
-{% macro pluginDeveloper(owner) %}
- {% if 'piwik' == owner %}<img title="Piwik" alt="Piwik" style="padding-bottom:2px;height:11px;" src="plugins/Morpheus/images/logo-marketplace.png"/>{% else %}{{ owner }}{% endif %}
-{% endmacro %}
-
-{% macro featuredIcon(align='') %}
- <img class="featuredIcon"
- title="{{ 'CorePluginsAdmin_FeaturedPlugin'|translate }}"
- src="plugins/CorePluginsAdmin/images/rating_important.png"
- align="{{ align }}" />
-{% endmacro %}
+{% macro pluginActivateDeactivateAction(name, isActivated, missingRequirements, deactivateNonce, activateNonce) -%}
+ {%- if isActivated -%}
+ <a href='index.php?module=CorePluginsAdmin&action=deactivate&pluginName={{ name }}&nonce={{ deactivateNonce }}&redirectTo=referrer'>{{ 'CorePluginsAdmin_Deactivate'|translate }}</a>
+ {%- elseif missingRequirements %}
+ -
+ {% else -%}
+ <a href='index.php?module=CorePluginsAdmin&action=activate&pluginName={{ name }}&nonce={{ activateNonce }}&redirectTo=referrer'>{{ 'CorePluginsAdmin_Activate'|translate }}</a>
+ {%- endif -%}
+{%- endmacro %}
{% macro pluginsFilter() %}
@@ -81,175 +87,166 @@
{% endmacro %}
-{% macro missingRequirementsPleaseUpdateNotice(plugin) %}
- {% if plugin.missingRequirements and 0 < plugin.missingRequirements|length %}
- {% for req in plugin.missingRequirements -%}
- <div class="alert alert-danger">
- {% set requirement = req.requirement|capitalize %}
- {% if 'Php' == requirement %}
- {% set requirement = 'PHP' %}
- {% endif %}
- {{ 'CorePluginsAdmin_MissingRequirementsNotice'|translate(requirement, req.actualVersion, req.requiredVersion) }}
- </div>
- {%- endfor %}
- {% endif %}
-{% endmacro %}
-
{% macro tablePlugins(pluginsInfo, pluginNamesHavingSettings, activateNonce, deactivateNonce, uninstallNonce, isTheme, marketplacePluginNames, displayAdminLinks) %}
-<div id="confirmUninstallPlugin" class="ui-confirm">
+ <div id="confirmUninstallPlugin" class="ui-confirm">
- <h2 id="uninstallPluginConfirm">{{ 'CorePluginsAdmin_UninstallConfirm'|translate }}</h2>
- <input role="yes" type="button" value="{{ 'General_Yes'|translate }}"/>
- <input role="no" type="button" value="{{ 'General_No'|translate }}"/>
+ <h2 id="uninstallPluginConfirm">{{ 'CorePluginsAdmin_UninstallConfirm'|translate }}</h2>
+ <input role="yes" type="button" value="{{ 'General_Yes'|translate }}"/>
+ <input role="no" type="button" value="{{ 'General_No'|translate }}"/>
-</div>
+ </div>
-<table piwik-content-table>
- <thead>
- <tr>
- <th>{% if isTheme %}{{ 'CorePluginsAdmin_Theme'|translate }}{% else %}{{ 'General_Plugin'|translate }}{% endif %}</th>
- <th>{{ 'General_Description'|translate }}</th>
- <th class="status">{{ 'CorePluginsAdmin_Status'|translate }}</th>
- {% if (displayAdminLinks) %}
- <th class="action-links">{{ 'General_Action'|translate }}</th>
- {% endif %}
- </tr>
- </thead>
- <tbody id="plugins">
- {% for name,plugin in pluginsInfo %}
- {% set isDefaultTheme = isTheme and name == 'Morpheus' %}
- {% if (plugin.alwaysActivated is defined and not plugin.alwaysActivated) or isTheme %}
- <tr {% if plugin.activated %}class="active-plugin"{% else %}class="inactive-plugin"{% endif %} data-filter-status="{% if plugin.activated %}active{% else %}inactive{% endif %}" data-filter-origin="{% if plugin.isCorePlugin %}core{% else %}noncore{% endif %}">
- <td class="name">
- <a name="{{ name|e('html_attr') }}"></a>
- {% if not plugin.isCorePlugin and name in marketplacePluginNames -%}
- <a href="javascript:void(0);"
- piwik-plugin-name="{{ name|e('html_attr') }}"
- >{{ name }}</a>
- {%- else %}
- {{ name }}
- {% endif %}
- <span class="plugin-version" {% if plugin.isCorePlugin %}title="{{ 'CorePluginsAdmin_CorePluginTooltip'|translate }}"{% endif %}>({% if plugin.isCorePlugin %}{{ 'CorePluginsAdmin_OriginCore'|translate }}{% else %}v{{ plugin.info.version }}{% endif %})</span>
+ <table piwik-content-table>
+ <thead>
+ <tr>
+ <th>{% if isTheme %}{{ 'CorePluginsAdmin_Theme'|translate }}{% else %}{{ 'General_Plugin'|translate }}{% endif %}</th>
+ <th>{{ 'General_Description'|translate }}</th>
+ <th class="status">{{ 'CorePluginsAdmin_Status'|translate }}</th>
+ {% if (displayAdminLinks) %}
+ <th class="action-links">{{ 'General_Action'|translate }}</th>
+ {% endif %}
+ </tr>
+ </thead>
+ <tbody id="plugins">
+ {% for name,plugin in pluginsInfo %}
+ {% set isDefaultTheme = isTheme and name == 'Morpheus' %}
+ {% if (plugin.alwaysActivated is defined and not plugin.alwaysActivated) or isTheme %}
+ <tr {% if plugin.activated %}class="active-plugin"{% else %}class="inactive-plugin"{% endif %} data-filter-status="{% if plugin.activated %}active{% else %}inactive{% endif %}" data-filter-origin="{% if plugin.isCorePlugin %}core{% else %}noncore{% endif %}">
+ <td class="name">
+ <a name="{{ name|e('html_attr') }}"></a>
+ {% if not plugin.isCorePlugin and name in marketplacePluginNames -%}
+ <a href="javascript:void(0);"
+ piwik-plugin-name="{{ name|e('html_attr') }}"
+ >{{ name }}</a>
+ {%- else %}
+ {{ name }}
+ {% endif %}
+ <span class="plugin-version" {% if plugin.isCorePlugin %}title="{{ 'CorePluginsAdmin_CorePluginTooltip'|translate }}"{% endif %}>({% if plugin.isCorePlugin %}{{ 'CorePluginsAdmin_OriginCore'|translate }}{% else %}v{{ plugin.info.version }}{% endif %})</span>
- {% if name in pluginNamesHavingSettings %}
- <br /><br />
- <a href="{{ linkTo({'module':'CoreAdminHome', 'action': 'generalSettings'}) }}#{{ name|e('html_attr') }}" class="settingsLink">{{ 'General_Settings'|translate }}</a>
- {% endif %}
- </td>
- <td class="desc">
- <div class="plugin-desc-missingrequirements">
- {% if plugin.missingRequirements is defined and plugin.missingRequirements %}
- {{ plugin.missingRequirements }}
- <br />
+ {% if name in pluginNamesHavingSettings %}
+ <br /><br />
+ <a href="{{ linkTo({'module':'CoreAdminHome', 'action': 'generalSettings'}) }}#{{ name|e('html_attr') }}" class="settingsLink">{{ 'General_Settings'|translate }}</a>
{% endif %}
- </div>
- <div class="plugin-desc-text">
+ </td>
+ <td class="desc">
+ <div class="plugin-desc-missingrequirements">
+ {% if plugin.missingRequirements is defined and plugin.missingRequirements %}
+ {{ plugin.missingRequirements }}
+ <br />
+ {% endif %}
+ </div>
+ <div class="plugin-desc-text">
- {{ plugin.info.description|raw|nl2br }}
+ {{ plugin.info.description|raw|nl2br }}
- {% if plugin.info.homepage|default is not empty and plugin.info.homepage not in [
- 'http://piwik.org', 'http://www.piwik.org', 'http://piwik.org/', 'http://www.piwik.org/'
- ] %}
- <span class="plugin-homepage">
+ {% if plugin.info.homepage|default is not empty and plugin.info.homepage not in [
+ 'http://piwik.org', 'http://www.piwik.org', 'http://piwik.org/', 'http://www.piwik.org/'
+ ] %}
+ <span class="plugin-homepage">
<a target="_blank" href="{{ plugin.info.homepage }}">({{ 'CorePluginsAdmin_PluginHomepage'|translate|replace({' ': '&nbsp;'})|raw }})</a>
</span>
- {% endif %}
+ {% endif %}
- {% if plugin.info.donate is defined and plugin.info.donate|length %}
- <div class="plugin-donation">
- {{ 'CorePluginsAdmin_LikeThisPlugin'|translate }} <a href="javascript:;" class="plugin-donation-link" data-overlay-id="overlay-{{ name|escape('html_attr') }}">{{ 'CorePluginsAdmin_ConsiderDonating'|translate }}</a>
- <div id="overlay-{{ name|escape('html_attr') }}" class="donation-overlay ui-confirm" title="{{ 'CorePluginsAdmin_LikeThisPlugin'|translate }}">
- <p>{{ 'CorePluginsAdmin_CommunityContributedPlugin'|translate }}</p>
- <p>{{ 'CorePluginsAdmin_ConsiderDonatingCreatorOf'|translate("<b>" ~ name ~ "</b>")|raw }}</p>
- <div class="donation-links">
- {% if plugin.info.donate.paypal is defined and plugin.info.donate.paypal %}
- <a class="donation-link paypal" target="_blank" href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&item_name=Piwik%20Plugin%20{{ name|escape('url') }}&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted&business={{ plugin.info.donate.paypal|escape('url') }}"><img src="plugins/CorePluginsAdmin/images/paypal_donate.jpg" height="30"/></a>
- {% endif %}
- {% if plugin.info.donate.flattr is defined and plugin.info.donate.flattr %}
- <a class="donation-link flattr" target="_blank" href="{{ plugin.info.donate.flattr }}"><img class="alignnone" title="Flattr" alt="" src="plugins/CorePluginsAdmin/images/flattr.png" height="29" /></a>
- {% endif %}
- {% if plugin.info.donate.bitcoin is defined and plugin.info.donate.bitcoin %}
- <div class="donation-link bitcoin">
- <span>Donate Bitcoins to:</span>
- <a href="bitcoin:{{ plugin.info.donate.bitcoin|escape('url') }}">{{ plugin.info.donate.bitcoin }}</a>
+ {% if plugin.info.donate is defined and plugin.info.donate|length %}
+ <div class="plugin-donation">
+ {{ 'CorePluginsAdmin_LikeThisPlugin'|translate }} <a href="javascript:;" class="plugin-donation-link" data-overlay-id="overlay-{{ name|escape('html_attr') }}">{{ 'CorePluginsAdmin_ConsiderDonating'|translate }}</a>
+ <div id="overlay-{{ name|escape('html_attr') }}" class="donation-overlay ui-confirm" title="{{ 'CorePluginsAdmin_LikeThisPlugin'|translate }}">
+ <p>{{ 'CorePluginsAdmin_CommunityContributedPlugin'|translate }}</p>
+ <p>{{ 'CorePluginsAdmin_ConsiderDonatingCreatorOf'|translate("<b>" ~ name ~ "</b>")|raw }}</p>
+ <div class="donation-links">
+ {% if plugin.info.donate.paypal is defined and plugin.info.donate.paypal %}
+ <a class="donation-link paypal" target="_blank" href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&item_name=Piwik%20Plugin%20{{ name|escape('url') }}&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted&business={{ plugin.info.donate.paypal|escape('url') }}"><img src="plugins/CorePluginsAdmin/images/paypal_donate.jpg" height="30"/></a>
+ {% endif %}
+ {% if plugin.info.donate.flattr is defined and plugin.info.donate.flattr %}
+ <a class="donation-link flattr" target="_blank" href="{{ plugin.info.donate.flattr }}"><img class="alignnone" title="Flattr" alt="" src="plugins/CorePluginsAdmin/images/flattr.png" height="29" /></a>
+ {% endif %}
+ {% if plugin.info.donate.bitcoin is defined and plugin.info.donate.bitcoin %}
+ <div class="donation-link bitcoin">
+ <span>Donate Bitcoins to:</span>
+ <a href="bitcoin:{{ plugin.info.donate.bitcoin|escape('url') }}">{{ plugin.info.donate.bitcoin }}</a>
+ </div>
+ {% endif %}
</div>
- {% endif %}
+ <input role="no" type="button" value="{{ 'General_Close'|translate }}"/>
</div>
- <input role="no" type="button" value="{{ 'General_Close'|translate }}"/>
</div>
+ {% endif %}
+ </div>
+ {% if plugin.info.license is defined %}
+ <div class="plugin-license">
+ {% if plugin.info.license_file is defined %}<a title="{{ 'CorePluginsAdmin_LicenseHomepage'|translate }}" rel="noreferrer" target="_blank" href="index.php?module=CorePluginsAdmin&action=showLicense&pluginName={{ name }}">{% endif %}{{ plugin.info.license }}{% if plugin.info.license_file is defined %}</a>{% endif %}
</div>
{% endif %}
- </div>
- {% if plugin.info.license is defined %}
- <div class="plugin-license">
- {% if plugin.info.license_homepage is defined %}<a title="{{ 'CorePluginsAdmin_LicenseHomepage'|translate }}" rel="noreferrer" target="_blank" href="{{ plugin.info.license_homepage }}">{% endif %}{{ plugin.info.license }}{% if plugin.info.license_homepage is defined %}</a>{% endif %}
- </div>
- {% endif %}
- {% if plugin.info.authors is defined %}
- <div class="plugin-author">
- <cite>By
- {% if plugin.info.authors is defined -%}
- {% spaceless %}
- {% for author in plugin.info.authors if author.name %}
- {% if author.homepage is defined %}
- <a title="{{ 'CorePluginsAdmin_AuthorHomepage'|translate }}" href="{{ author.homepage }}" rel="noreferrer" target="_blank">{{ author.name }}</a>
+ {% if plugin.info.authors is defined %}
+ <div class="plugin-author">
+ By
+ {% if plugin.info.authors is defined -%}
+ {% spaceless %}
+ {% for author in plugin.info.authors if author.name %}
+ {% if author.homepage is defined %}
+ <a title="{{ 'CorePluginsAdmin_AuthorHomepage'|translate }}" href="{{ author.homepage }}" rel="noreferrer" target="_blank">{{ author.name }}</a>
+ {% else %}
+ {{ author.name }}
+ {% endif %}
+ {% if loop.index < plugin.info.authors|length %}
+ ,
+ {% endif %}
+ {% endfor %}
+ {% endspaceless %}
+ {%- endif %}.
+ </div>
+ {% endif %}
+ </td>
+ <td class="status" {% if isDefaultTheme %}style="border-left-width:0px;"{% endif %}>
+ {% if not isDefaultTheme -%}
+
+ {% if plugin.activated %}
+ {{ 'CorePluginsAdmin_Active'|translate }}
{% else %}
- {{ author.name }}
+ {{ 'CorePluginsAdmin_Inactive'|translate }}
+ {% if plugin.uninstallable and displayAdminLinks %} <br/> - <a data-plugin-name="{{ name|escape('html_attr') }}" class="uninstall" href='index.php?module=CorePluginsAdmin&action=uninstall&pluginName={{ name }}&nonce={{ uninstallNonce }}'>{{ 'CorePluginsAdmin_ActionUninstall'|translate }}</a>{% endif %}
{% endif %}
- {% if loop.index < plugin.info.authors|length %}
- ,
- {% endif %}
- {% endfor %}
- {% endspaceless %}
- {%- endif %}.</cite>
- </div>
- {% endif %}
- </td>
- <td class="status" {% if isDefaultTheme %}style="border-left-width:0px;"{% endif %}>
- {% if not isDefaultTheme -%}
- {% if plugin.activated %}
- {{ 'CorePluginsAdmin_Active'|translate }}
- {% else %}
- {{ 'CorePluginsAdmin_Inactive'|translate }}
- {% if plugin.uninstallable and displayAdminLinks %} <br/> - <a data-plugin-name="{{ name|escape('html_attr') }}" class="uninstall" href='index.php?module=CorePluginsAdmin&action=uninstall&pluginName={{ name }}&nonce={{ uninstallNonce }}'>{{ 'CorePluginsAdmin_ActionUninstall'|translate }}</a>{% endif %}
- {% endif %}
+ {%- endif %}
+ </td>
- {%- endif %}
- </td>
+ {% if displayAdminLinks %}
+ <td class="togl action-links" {% if isDefaultTheme %}style="border-left-width:0px;"{% endif %}>
+ {% if not isDefaultTheme -%}
- {% if displayAdminLinks %}
- <td class="togl action-links" {% if isDefaultTheme %}style="border-left-width:0px;"{% endif %}>
- {% if not isDefaultTheme -%}
+ {% if plugin.invalid is defined or plugin.alwaysActivated %}
+ -
+ {% else %}
+ {{ _self.pluginActivateDeactivateAction(name, plugin.activated, plugin.missingRequirements, deactivateNonce, activateNonce) }}
+ {% endif %}
- {% if plugin.invalid is defined or plugin.alwaysActivated %}
- -
- {% else %}
- {% if plugin.activated %}
- <a href='index.php?module=CorePluginsAdmin&action=deactivate&pluginName={{ name }}&nonce={{ deactivateNonce }}'>{{ 'CorePluginsAdmin_Deactivate'|translate }}</a>
- {% elseif plugin.missingRequirements %}
- -
- {% else %}
- <a href='index.php?module=CorePluginsAdmin&action=activate&pluginName={{ name }}&nonce={{ activateNonce }}'>{{ 'CorePluginsAdmin_Activate'|translate }}</a>
- {% endif %}
+ {%- endif %}
+ </td>
{% endif %}
+ </tr>
+ {% endif %}
+ {% endfor %}
+ </tbody>
+ </table>
- {%- endif %}
- </td>
- {% endif %}
- </tr>
+ <div class="tableActionBar">
+ {% if isTheme %}
+ <a href="{{ linkTo({'action':'browseThemes', 'sort': ''}) }}"><span class="icon-add"></span> {{ 'CorePluginsAdmin_InstallNewThemes'|translate }}</a>
+ {% else %}
+ <a href="{{ linkTo({'action':'browsePlugins', 'sort': ''}) }}"><span class="icon-add"></span> {{ 'CorePluginsAdmin_InstallNewPlugins'|translate }}</a>
{% endif %}
- {% endfor %}
- </tbody>
-</table>
-
-<div class="tableActionBar">
- {% if isTheme %}
- <a href="{{ linkTo({'action':'browseThemes', 'sort': ''}) }}"><span class="icon-add"></span> {{ 'CorePluginsAdmin_InstallNewThemes'|translate }}</a>
- {% else %}
- <a href="{{ linkTo({'action':'browsePlugins', 'sort': ''}) }}"><span class="icon-add"></span> {{ 'CorePluginsAdmin_InstallNewPlugins'|translate }}</a>
- {% endif %}
-</div>
+ </div>
+
+ <div class="footer-message">
+ {% set pluginsAlwaysActivated %}
+ {% for name,plugin in pluginsInfo %}
+ {% if plugin.alwaysActivated is defined and plugin.alwaysActivated %}
+ {{ name }}{% if not loop.last %}, {% endif %}
+ {% endif %}
+ {% endfor %}
+ {% endset %}
+ {{ 'CorePluginsAdmin_AlwaysActivatedPluginsList'|translate(pluginsAlwaysActivated) }}
+ </div>
{% endmacro %}
diff --git a/plugins/CorePluginsAdmin/templates/marketplace/plugin-list.twig b/plugins/CorePluginsAdmin/templates/marketplace/plugin-list.twig
deleted file mode 100644
index 8fe294eadb..0000000000
--- a/plugins/CorePluginsAdmin/templates/marketplace/plugin-list.twig
+++ /dev/null
@@ -1,82 +0,0 @@
-{% if plugins|length > 0 %}
- <div class="row">
- {% for plugin in plugins %}
- {% if plugin.isDownloadable %}
- <div class="col s12 m6 l4">
- {% embed 'contentBlock.twig' with {'title': ''} %}
- {% block content %}
- <div class="plugin">
- <h3 class="card-title" title="{{ 'General_MoreDetails'|translate }}">
- <a href="#" piwik-plugin-name="{{ plugin.name }}">{{ plugin.name }}</a>
- </h3>
-
- <p class="description">
- {{ plugin.description }}
- <a class="more" href="#" piwik-plugin-name="{{ plugin.name }}" title="{{ 'General_MoreDetails'|translate }}">
- &rsaquo; {{ 'General_MoreLowerCase'|translate }}</a>
- </p>
-
- {% if showThemes %}
- {# Screenshot for themes #}
- <a class="more" href="#" piwik-plugin-name="{{ plugin.name }}">
- <img title="{{ 'General_MoreDetails'|translate }}"
- class="preview" src="{{ plugin.screenshots|first }}?w=250&h=150"/></a>
- {% endif %}
-
- <ul class="metadata">
- <li>
- {{ 'CorePluginsAdmin_Version'|translate }}: {{ plugin.latestVersion }}
- {% if plugin.canBeUpdated %}
- <a class="update-available" href="#" piwik-plugin-name="{{ plugin.name }}" data-activePluginTab="changelog"
- title="{{ 'CorePluginsAdmin_PluginUpdateAvailable'|translate(plugin.currentVersion, plugin.latestVersion) }}">
- {{ 'CorePluginsAdmin_NewVersion'|translate }}</a>
- {% endif %}
- </li>
- <li>{{ 'CorePluginsAdmin_Updated'|translate }}: {{ plugin.lastUpdated }}</li>
- <li>{{ 'General_Downloads'|translate }}: {{ plugin.numDownloads }}</li>
- <li>{{ 'CorePluginsAdmin_Developer'|translate }}: {{ pluginsMacro.pluginDeveloper(plugin.owner) }}</li>
- </ul>
-
- {% if isSuperUser %}
- <div class="footer" piwik-plugin-name="{{ plugin.name }}">
- {% if plugin.canBeUpdated and 0 == plugin.missingRequirements|length %}
- <a class="btn"
- href="{{ linkTo({'action':'updatePlugin', 'pluginName': plugin.name, 'nonce': updateNonce}) }}">
- {{ 'CoreUpdater_UpdateTitle'|translate }}
- </a>
- {% elseif plugin.isInstalled %}
- <button class="btn btn-noop btn-block">
- {{ 'General_Installed'|translate }}
- </button>
- {% elseif plugin.missingRequirements|length > 0 %}
- <a class="btn btn-link btn-block" href="#" piwik-plugin-name="{{ plugin.name }}" title="{{ 'General_MoreDetails'|translate }}">
- {{ 'CorePluginsAdmin_CannotInstall'|translate }}
- </a>
- {% else %}
- <a href="{{ linkTo({'action': 'installPlugin', 'pluginName': plugin.name, 'nonce': installNonce}) }}"
- class="btn">
- {{ 'CorePluginsAdmin_ActionInstall'|translate }}
- </a>
- {% endif %}
- </div>
- {% endif %}
-
- </div>
- {% endblock %}
- {% endembed %}
- </div>
- {% endif %}
-{% endfor %}
- </div>
-{% endif %}
-
-{% if plugins|length == 0 %}
- <div piwik-content-block>
- {% if showThemes %}
- {{ 'CorePluginsAdmin_NoThemesFound'|translate }}
- {% else %}
- {{ 'CorePluginsAdmin_NoPluginsFound'|translate }}
- {% endif %}
- </div>
-{% endif %}
-
diff --git a/plugins/CorePluginsAdmin/templates/pluginDetails.twig b/plugins/CorePluginsAdmin/templates/pluginDetails.twig
deleted file mode 100644
index 33a73f3c80..0000000000
--- a/plugins/CorePluginsAdmin/templates/pluginDetails.twig
+++ /dev/null
@@ -1,209 +0,0 @@
-{% import '@CorePluginsAdmin/macros.twig' as pluginsMacro %}
-
-{% block content %}
-
- <div class="pluginDetails">
- {% if errorMessage %}
- {{ errorMessage }}
- {% elseif plugin and plugin.isDownloadable %}
-
- {% set latestVersion = plugin.versions[plugin.versions|length - 1] %}
-
- <div class="header">
- <div class="intro">
- <h2>{{ plugin.name }}</h2>
- <p class="description">
- {% if plugin.featured %}
- {{ pluginsMacro.featuredIcon('left') }}
- {% endif %}
- {{ plugin.description }}
- </p>
- </div>
- <div class="actionButton">
- {% if isSuperUser %}
- {% if plugin.canBeUpdated and 0 == plugin.missingRequirements|length %}
- <a class="install update"
- href="{{ linkTo({'action':'updatePlugin', 'pluginName': plugin.name, 'nonce': updateNonce}) }}"
- >{{ 'CoreUpdater_UpdateTitle'|translate }}</a>
- {% elseif plugin.isInstalled %}
- {% elseif 0 < plugin.missingRequirements|length %}
- {% else %}
- <a href="{{ linkTo({'action': 'installPlugin', 'pluginName': plugin.name, 'nonce': installNonce}) }}"
- class="install">{{ 'CorePluginsAdmin_ActionInstall'|translate }}</a>
- {% endif %}
- {% endif %}
- </div>
- </div>
-
- <div class="content">
- <div class="contentDetails">
- <div id="pluginDetailsTabs" class="row">
- <div class="col s12">
- <ul class="tabs">
- <li class="tab col s3"><a href="#tabs-description">{{ 'General_Description'|translate }}</a></li>
-
- {% if latestVersion.readmeHtml.faq %}
- <li class="tab col s3"><a href="#tabs-faq">{{ 'General_Faq'|translate }}</a></li>
- {% endif %}
-
- <li class="tab col s3"><a href="#tabs-changelog">{{ 'CorePluginsAdmin_Changelog'|translate }}</a></li>
-
- {% if plugin.screenshots|length %}
- <li class="tab col s3"><a href="#tabs-screenshots">{{ 'CorePluginsAdmin_Screenshots'|translate }}</a></li>
- {% endif %}
-
- {% if latestVersion.readmeHtml.support is defined and latestVersion.readmeHtml.support %}
- <li class="tab col s3"><a href="#tabs-support">{{ 'CorePluginsAdmin_Support'|translate }}</a></li>
- {% endif %}
- </ul>
- </div>
-
- <div id="tabs-description" class="tab-content col s12">
- {{ pluginsMacro.missingRequirementsPleaseUpdateNotice(plugin) }}
- {{ latestVersion.readmeHtml.description|raw }}
- </div>
-
- {% if latestVersion.readmeHtml.faq %}
- <div id="tabs-faq" class="tab-content col s12">
- {{ latestVersion.readmeHtml.faq|raw }}
- </div>
- {% endif %}
-
- <div id="tabs-changelog" class="tab-content col s12">
- {{ pluginsMacro.missingRequirementsPleaseUpdateNotice(plugin) }}
- {% if plugin.canBeUpdated %}
- <div class="alert alert-warning">
- {{ 'CorePluginsAdmin_PluginUpdateAvailable'|translate(plugin.currentVersion, plugin.latestVersion) }}
- {% if plugin.repositoryChangelogUrl %}<a rel="noreferrer" target="_blank" href="{{ plugin.repositoryChangelogUrl }}">{{ 'CorePluginsAdmin_ViewRepositoryChangelog'|translate }}</a>{% endif %}
- </div>
- {% endif %}
-
- {% if latestVersion.readmeHtml.changelog %}
- {{ latestVersion.readmeHtml.changelog|raw }}
- {% endif %}
-
- <h3>{{ 'CorePluginsAdmin_History'|translate }}</h3>
-
- <ul>
- {% for version in plugin.versions|reverse %}
- <li>
- {% set versionName %}
- <strong>
- {% if version.repositoryChangelogUrl %}
- <a target="_blank" title="{{ 'CorePluginsAdmin_Changelog'|translate }}" href="{{ version.repositoryChangelogUrl }}">{{ version.name }}</a>
- {% else %}
- {{ version.name }}
- {% endif %}
- </strong>
- {% endset %}
- {{ 'CorePluginsAdmin_PluginVersionInfo'|translate(versionName, version.release)|raw }}
- </li>
- {% endfor %}
- </ul>
- </div>
-
- {% if plugin.screenshots|length %}
- <div id="tabs-screenshots" class="tab-content col s12">
- <div class="thumbnails">
- {% for screenshot in plugin.screenshots %}
- <div class="thumbnail">
- <a href="{{ screenshot }}" target="_blank"><img src="{{ screenshot }}?w=400" width="400" alt=""></a>
- <p>
- {{ screenshot|split('/')|last|replace({'_': ' ', '.png': '', '.jpg': '', '.jpeg': ''}) }}
- </p>
- </div>
- {% endfor %}
- </div>
- </div>
- {% endif %}
-
- {% if latestVersion.readmeHtml.support is defined and latestVersion.readmeHtml.support %}
- <div id="tabs-support" class="tab-content col s12">
-
- {{ latestVersion.readmeHtml.support|raw }}
-
- </div>
- {% endif %}
- </div>
-
- </div>
- <div class="metadata">
- <dl>
- <dt>{{ 'CorePluginsAdmin_Version'|translate }}</dt>
- <dd>{{ plugin.latestVersion }}</dd>
- <dt>{{ 'CorePluginsAdmin_PluginKeywords'|translate }}</dt>
- <dd>{{ plugin.keywords|join(', ') }}</dd>
- <dt>{{ 'CorePluginsAdmin_LastUpdated'|translate }}</dt>
- <dd>{{ plugin.lastUpdated }}</dd>
- <dt>{{ 'General_Downloads'|translate }}</dt>
- <dd title="{{ 'CorePluginsAdmin_NumDownloadsLatestVersion'|translate(latestVersion.numDownloads|number_format) }}">{{ plugin.numDownloads }}</dd>
- <dt>{{ 'CorePluginsAdmin_Developer'|translate }}</dt>
- <dd>{{ pluginsMacro.pluginDeveloper(plugin.owner) }}</dd>
- <dt>{{ 'CorePluginsAdmin_Authors'|translate }}</dt>
- <dd>{% for author in plugin.authors if author.name %}
-
- {% spaceless %}
- {% if author.homepage %}
- <a target="_blank" href="{{ author.homepage }}">{{ author.name }}</a>
- {% elseif author.email %}
- <a href="mailto:{{ author.email|escape('url') }}">{{ author.name }}</a>
- {% else %}
- {{ author.name }}
- {% endif %}
-
- {% if loop.index < plugin.authors|length %}
- ,
- {% endif %}
- {% endspaceless %}
-
- {% endfor %}
- </dd>
- <dt>{{ 'CorePluginsAdmin_Websites'|translate }}</dt>
- <dd>
- {% if plugin.homepage %}
- <a target="_blank" href="{{ plugin.homepage }}">{{ 'CorePluginsAdmin_PluginWebsite'|translate }}</a>,
- {% endif %}
- <a target="_blank" href="{{ plugin.repositoryUrl }}">GitHub</a></dd>
- {% if plugin.activity %}
- <dt>{{ 'CorePluginsAdmin_Activity'|translate }}</dt>
- <dd>
- {{ plugin.activity.numCommits }} commits
-
- {% if plugin.activity.numContributors > 1 %}
- {{ 'CorePluginsAdmin_ByXDevelopers'|translate(plugin.activity.numContributors) }}
- {% endif %}
- {% if plugin.activity.lastCommitDate %}
- {{ 'CorePluginsAdmin_LastCommitTime'|translate(plugin.activity.lastCommitDate) }}
- {% endif %}</dd>
- {% endif %}
- </dl>
- <br />
- </div>
- </div>
- <script type="text/javascript">
- $(function() {
-
- var active = 0;
- {% if activeTab %}
- var $activeTab = $('#tabs-{{ activeTab|e('js') }}');
- if ($activeTab) {
- active = $activeTab.index() - 1;
- }
- {% endif %}
-
- $('#pluginDetailsTabs .tabs').tabs();
- $('#pluginDetailsTabs .tabs').tabs('select_tab', active >= 0 ? active : 0);
-
- $('.pluginDetails a').each(function (index, a) {
- var link = $(a).attr('href');
-
- if (link && 0 === link.indexOf('http')) {
- $(a).attr('target', '_blank');
- }
- });
- });
- </script>
- {% endif %}
- </div>
-
-{% endblock %}
diff --git a/plugins/CorePluginsAdmin/templates/plugins.twig b/plugins/CorePluginsAdmin/templates/plugins.twig
index 4a8649710b..4822c89d88 100644
--- a/plugins/CorePluginsAdmin/templates/plugins.twig
+++ b/plugins/CorePluginsAdmin/templates/plugins.twig
@@ -1,3 +1,4 @@
+
{% extends 'admin.twig' %}
{% import '@CorePluginsAdmin/macros.twig' as plugins %}
@@ -6,42 +7,44 @@
{% block content %}
-<div piwik-content-intro>
- <h2 piwik-enriched-headline>
- {{ title|e('html_attr') }}
- </h2>
+ <div piwik-content-intro>
+ <h2 piwik-enriched-headline>
+ {{ title|e('html_attr') }}
+ </h2>
- <p>{{ 'CorePluginsAdmin_PluginsExtendPiwik'|translate }}
- {{ 'CorePluginsAdmin_OncePluginIsInstalledYouMayActivateHere'|translate }}
+ <p>{{ 'CorePluginsAdmin_PluginsExtendPiwik'|translate }}
+ {{ 'CorePluginsAdmin_OncePluginIsInstalledYouMayActivateHere'|translate }}
- {% if isMarketplaceEnabled %}
- {{ 'CorePluginsAdmin_TeaserExtendPiwikByPlugin'|translate('<a href="' ~ linkTo({'action':'browsePlugins', 'sort': ''}) ~ '">', '</a>')|raw }}
- {% endif %}
+ {% if isMarketplaceEnabled %}
+ {{ 'CorePluginsAdmin_TeaserExtendPiwikByPlugin'|translate('<a href="' ~ linkTo({'action':'browsePlugins', 'sort': ''}) ~ '">', '</a>')|raw }}
+ {% endif %}
- {% if not isPluginsAdminEnabled %}
- <br/>{{ 'CorePluginsAdmin_DoMoreContactPiwikAdmins'|translate }}
- {% endif %}
+ {% if not isPluginsAdminEnabled %}
+ <br/>{{ 'CorePluginsAdmin_DoMoreContactPiwikAdmins'|translate }}
+ {% endif %}
- <br />
- {{ 'CorePluginsAdmin_ChangeLookByManageThemes'|translate('<a href="' ~ linkTo({'action': 'themes'}) ~'">', '</a>')|raw }}
- </p>
-</div>
+ <br />
+ {{ 'CorePluginsAdmin_ChangeLookByManageThemes'|translate('<a href="' ~ linkTo({'action': 'themes'}) ~'">', '</a>')|raw }}
+ </p>
+ </div>
-{% if pluginsHavingUpdate|length %}
- <div piwik-content-block content-title="{{ pluginsHavingUpdate|length }} Update(s) available">
+ {% if pluginsHavingUpdate|length %}
+ <div piwik-content-block content-title="{{ pluginsHavingUpdate|length }} Update(s) available">
- <p>{{ 'CorePluginsAdmin_InfoPluginUpdateIsRecommended'|translate }}</p>
+ <p>{{ 'CorePluginsAdmin_InfoPluginUpdateIsRecommended'|translate }}</p>
- {{ plugins.tablePluginUpdates(pluginsHavingUpdate, updateNonce, activateNonce, 0) }}
- </div>
-{% endif %}
+ {{ plugins.tablePluginUpdates(pluginsHavingUpdate, updateNonce, isMultiServerEnvironment) }}
+ </div>
+ {% endif %}
-<div piwik-content-block content-title="{{ 'CorePluginsAdmin_InstalledPlugins'|translate|e('html_attr') }}" piwik-plugin-management>
+ <div piwik-content-block content-title="{{ 'CorePluginsAdmin_InstalledPlugins'|translate|e('html_attr') }}"
+ class="pluginsManagement"
+ piwik-plugin-management>
- {{ plugins.pluginsFilter() }}
+ {{ plugins.pluginsFilter() }}
- {{ plugins.tablePlugins(pluginsInfo, pluginNamesHavingSettings, activateNonce, deactivateNonce, uninstallNonce, false, marketplacePluginNames, isPluginsAdminEnabled) }}
+ {{ plugins.tablePlugins(pluginsInfo, pluginNamesHavingSettings, activateNonce, deactivateNonce, uninstallNonce, false, marketplacePluginNames, isPluginsAdminEnabled) }}
-</div>
+ </div>
{% endblock %} \ No newline at end of file
diff --git a/plugins/CorePluginsAdmin/templates/updatePlugin.twig b/plugins/CorePluginsAdmin/templates/updatePlugin.twig
deleted file mode 100644
index 024dbc028f..0000000000
--- a/plugins/CorePluginsAdmin/templates/updatePlugin.twig
+++ /dev/null
@@ -1,41 +0,0 @@
-{% extends 'admin.twig' %}
-
-{% block content %}
-
- <div style="max-width:980px;">
-
- <h2>{{ 'CorePluginsAdmin_UpdatingPlugin'|translate(plugin.name) }}</h2>
-
- <div>
-
- {% if plugin.isTheme %}
-
- <p>{{ 'CorePluginsAdmin_StepDownloadingThemeFromMarketplace'|translate }}</p>
-
- <p>{{ 'CorePluginsAdmin_StepUnzippingTheme'|translate }}</p>
-
- <p>{{ 'CorePluginsAdmin_StepReplaceExistingTheme'|translate }}</p>
-
- <p>{{ 'CorePluginsAdmin_StepThemeSuccessfullyUpdated'|translate(plugin.name, plugin.latestVersion) }}</p>
-
- {% else %}
-
- <p>{{ 'CorePluginsAdmin_StepDownloadingPluginFromMarketplace'|translate }}</p>
-
- <p>{{ 'CorePluginsAdmin_StepUnzippingPlugin'|translate }}</p>
-
- <p>{{ 'CorePluginsAdmin_StepReplaceExistingPlugin'|translate }}</p>
-
- <p>{{ 'CorePluginsAdmin_StepPluginSuccessfullyUpdated'|translate(plugin.name, plugin.latestVersion) }}</p>
-
- {% endif %}
-
- <p><a href="{{ linkTo({'action': 'plugins'}) }}">{{ 'General_Plugins'|translate }}</a>
- |
- <a href="{{ linkTo({'action': 'themes'}) }}">{{ 'CorePluginsAdmin_Themes'|translate }}</a>
- |
- <a href="{{ linkTo({'action': 'marketplace'}) }}">{{ 'CorePluginsAdmin_Marketplace'|translate }}</a></p>
- </div>
- </div>
-
-{% endblock %}
diff --git a/plugins/CorePluginsAdmin/templates/uploadPlugin.twig b/plugins/CorePluginsAdmin/templates/uploadPlugin.twig
index ff42dd8b69..42825be41d 100644
--- a/plugins/CorePluginsAdmin/templates/uploadPlugin.twig
+++ b/plugins/CorePluginsAdmin/templates/uploadPlugin.twig
@@ -5,36 +5,36 @@
<div style="max-width:980px;">
<div>
- <h2>{{ 'CorePluginsAdmin_InstallingPlugin'|translate(plugin.name) }}</h2>
+ <h2>{{ 'Marketplace_InstallingPlugin'|translate(plugin.name) }}</h2>
{% if plugin.isTheme %}
- <p>{{ 'CorePluginsAdmin_StepUnzippingTheme'|translate }}</p>
+ <p>{{ 'Marketplace_StepUnzippingTheme'|translate }}</p>
- <p>{{ 'CorePluginsAdmin_StepThemeSuccessfullyInstalled'|translate(plugin.name, plugin.version) }}</p>
+ <p>{{ 'Marketplace_StepThemeSuccessfullyInstalled'|translate(plugin.name, plugin.version) }}</p>
<p>
{% if not plugin.isActivated %}
- <strong><a href="{{ linkTo({'action': 'activate', 'pluginName': plugin.name, 'nonce': nonce}) }}">{{ 'CorePluginsAdmin_ActionActivateTheme'|translate }}</a></strong>
+ <strong><a class="btn" href="{{ linkTo({'action': 'activate', 'module': 'CorePluginsAdmin', 'pluginName': plugin.name, 'nonce': nonce}) }}">{{ 'Marketplace_ActionActivateTheme'|translate }}</a></strong>
|
{% endif %}
- <a href="{{ linkTo({'action': 'marketplace'}) }}">{{ 'CorePluginsAdmin_BackToExtendPiwik'|translate }}</a>
+ <a href="{{ linkTo({'module': 'Marketplace', 'action': 'overview'}) }}">{{ 'Marketplace_BackToMarketplace'|translate }}</a>
</p>
{% else %}
- <p>{{ 'CorePluginsAdmin_StepUnzippingPlugin'|translate }}</p>
+ <p>{{ 'Marketplace_StepUnzippingPlugin'|translate }}</p>
- <p>{{ 'CorePluginsAdmin_StepPluginSuccessfullyInstalled'|translate(plugin.name, plugin.version) }}</p>
+ <p>{{ 'Marketplace_StepPluginSuccessfullyInstalled'|translate(plugin.name, plugin.version) }}</p>
<p>
{% if not plugin.isActivated %}
- <strong><a href="{{ linkTo({'action': 'activate', 'pluginName': plugin.name, 'nonce': nonce}) }}">{{ 'CorePluginsAdmin_ActionActivatePlugin'|translate }}</a></strong>
+ <strong><a class="btn" href="{{ linkTo({'action': 'activate', 'module': 'CorePluginsAdmin', 'pluginName': plugin.name, 'nonce': nonce}) }}">{{ 'Marketplace_ActionActivatePlugin'|translate }}</a></strong>
|
{% endif %}
- <a href="{{ linkTo({'action': 'marketplace'}) }}">{{ 'CorePluginsAdmin_BackToExtendPiwik'|translate }}</a>
+ <a href="{{ linkTo({'module': 'Marketplace', 'action': 'overview'}) }}">{{ 'Marketplace_BackToMarketplace'|translate }}</a>
</p>
{% endif %}
diff --git a/plugins/CoreUpdater/Controller.php b/plugins/CoreUpdater/Controller.php
index a39dc172dc..143c9c04b9 100644
--- a/plugins/CoreUpdater/Controller.php
+++ b/plugins/CoreUpdater/Controller.php
@@ -12,6 +12,7 @@ use Exception;
use Piwik\AssetManager;
use Piwik\Common;
use Piwik\Config;
+use Piwik\Container\StaticContainer;
use Piwik\DbHelper;
use Piwik\Filechecks;
use Piwik\Filesystem;
@@ -20,8 +21,8 @@ use Piwik\Option;
use Piwik\Piwik;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Plugin;
-use Piwik\Plugins\CorePluginsAdmin\Marketplace;
use Piwik\Plugins\LanguagesManager\LanguagesManager;
+use Piwik\Plugins\Marketplace\Plugins;
use Piwik\SettingsServer;
use Piwik\Updater as DbUpdater;
use Piwik\Version;
@@ -40,9 +41,15 @@ class Controller extends \Piwik\Plugin\Controller
*/
private $updater;
- public function __construct(Updater $updater)
+ /**
+ * @var Plugins
+ */
+ private $marketplacePlugins;
+
+ public function __construct(Updater $updater, Plugins $marketplacePlugins = null)
{
$this->updater = $updater;
+ $this->marketplacePlugins = $marketplacePlugins;
parent::__construct();
}
@@ -125,9 +132,8 @@ class Controller extends \Piwik\Plugin\Controller
$marketplacePlugins = array();
try {
- if (!empty($incompatiblePlugins)) {
- $marketplace = new Marketplace();
- $marketplacePlugins = $marketplace->getAllAvailablePluginNames();
+ if (!empty($incompatiblePlugins) && $this->marketplacePlugins) {
+ $marketplacePlugins = $this->marketplacePlugins->getAllAvailablePluginNames();
}
} catch (\Exception $e) {}
diff --git a/plugins/CoreUpdater/ReleaseChannel/Latest2XBeta.php b/plugins/CoreUpdater/ReleaseChannel/Latest2XBeta.php
index 6ff410e7e4..102cf7b0dd 100644
--- a/plugins/CoreUpdater/ReleaseChannel/Latest2XBeta.php
+++ b/plugins/CoreUpdater/ReleaseChannel/Latest2XBeta.php
@@ -28,6 +28,11 @@ class Latest2XBeta extends ReleaseChannel
return Piwik::translate('CoreUpdater_LtsSupportVersion');
}
+ public function doesPreferStable()
+ {
+ return false;
+ }
+
public function getOrder()
{
return 21;
diff --git a/plugins/CoreUpdater/ReleaseChannel/LatestBeta.php b/plugins/CoreUpdater/ReleaseChannel/LatestBeta.php
index 53b1c33975..572badac91 100644
--- a/plugins/CoreUpdater/ReleaseChannel/LatestBeta.php
+++ b/plugins/CoreUpdater/ReleaseChannel/LatestBeta.php
@@ -23,6 +23,11 @@ class LatestBeta extends ReleaseChannel
return Piwik::translate('CoreUpdater_LatestBetaRelease');
}
+ public function doesPreferStable()
+ {
+ return false;
+ }
+
public function getOrder()
{
return 11;
diff --git a/plugins/CoreUpdater/SystemSettings.php b/plugins/CoreUpdater/SystemSettings.php
index 256cf65f08..bc09bfaec5 100644
--- a/plugins/CoreUpdater/SystemSettings.php
+++ b/plugins/CoreUpdater/SystemSettings.php
@@ -11,7 +11,7 @@ namespace Piwik\Plugins\CoreUpdater;
use Piwik\Piwik;
use Piwik\Plugin\ReleaseChannels;
use Piwik\Plugins\CoreAdminHome\Controller as CoreAdminController;
-use Piwik\Plugins\CorePluginsAdmin\UpdateCommunication as PluginUpdateCommunication;
+use Piwik\Plugins\Marketplace\UpdateCommunication as PluginUpdateCommunication;
use Piwik\Settings\Setting;
use Piwik\Settings\FieldConfig;
diff --git a/plugins/CoreUpdater/Test/Integration/ReleaseChannelTest.php b/plugins/CoreUpdater/Test/Integration/ReleaseChannelTest.php
index 5d480ed18e..13b2422be5 100644
--- a/plugins/CoreUpdater/Test/Integration/ReleaseChannelTest.php
+++ b/plugins/CoreUpdater/Test/Integration/ReleaseChannelTest.php
@@ -65,4 +65,9 @@ class ReleaseChannelTest extends IntegrationTestCase
$this->assertStringStartsWith("http://api.piwik.org/1.0/getLatestVersion/?piwik_version=$version&php_version=$phpVersion&mysql_version=$mysqlVersion&release_channel=my_channel&url=$url&trigger=&timezone=", $urlToCheck);
}
+ public function test_doesPreferStable()
+ {
+ $this->assertTrue($this->channel->doesPreferStable());
+ }
+
}
diff --git a/plugins/CoreUpdater/Updater.php b/plugins/CoreUpdater/Updater.php
index 316f325690..347ea3f5ca 100644
--- a/plugins/CoreUpdater/Updater.php
+++ b/plugins/CoreUpdater/Updater.php
@@ -17,10 +17,9 @@ use Piwik\Http;
use Piwik\Option;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Plugin\ReleaseChannels;
-use Piwik\Plugins\CorePluginsAdmin\CorePluginsAdmin;
-use Piwik\Plugins\CorePluginsAdmin\MarketplaceApiClient;
-use Piwik\Plugins\CorePluginsAdmin\MarketplaceApiException;
use Piwik\Plugins\CorePluginsAdmin\PluginInstaller;
+use Piwik\Plugins\Marketplace\Api as MarketplaceApi;
+use Piwik\Plugins\Marketplace\Marketplace;
use Piwik\SettingsServer;
use Piwik\Translation\Translator;
use Piwik\Unzip;
@@ -115,10 +114,20 @@ class Updater
$this->verifyDecompressedArchive($extractedArchiveDirectory);
$messages[] = $this->translator->translate('CoreUpdater_VerifyingUnpackedFiles');
- // we need to load the marketplace already here, otherwise it will use the new, updated file in Piwik 3
- $marketplace = new MarketplaceApiClient();
- require_once PIWIK_DOCUMENT_ROOT . '/plugins/CorePluginsAdmin/PluginInstaller.php';
- require_once PIWIK_DOCUMENT_ROOT . '/plugins/CorePluginsAdmin/MarketplaceApiException.php';
+ if (Marketplace::isMarketplaceEnabled()) {
+ // we need to load the marketplace already here, otherwise it will use the new, updated file in Piwik 3
+
+ // we also need to make sure to create a new instance here as otherwise we would change the "global"
+ // environment, but we only want to change piwik version temporarily for this task here
+ $environment = StaticContainer::getContainer()->make('Piwik\Plugins\Marketplace\Environment');
+ $environment->setPiwikVersion($newVersion);
+ /** @var \Piwik\Plugins\Marketplace\Api\Client $marketplaceClient */
+ $marketplaceClient = StaticContainer::getContainer()->make('Piwik\Plugins\Marketplace\Api\Client', array(
+ 'environment' => $environment
+ ));
+ require_once PIWIK_DOCUMENT_ROOT . '/plugins/CorePluginsAdmin/PluginInstaller.php';
+ require_once PIWIK_DOCUMENT_ROOT . '/plugins/Marketplace/Api/Exception.php';
+ }
$this->installNewFiles($extractedArchiveDirectory);
$messages[] = $this->translator->translate('CoreUpdater_InstallingTheLatestVersion');
@@ -130,29 +139,25 @@ class Updater
}
try {
- if (CorePluginsAdmin::isMarketplaceEnabled()) {
- $messages[] = $this->translator->translate('CoreUpdater_CheckingForPluginUpdates');
+ if (Marketplace::isMarketplaceEnabled() && !empty($marketplaceClient)) {
+ $messages[] = $this->translator->translate('CoreUpdater_CheckingForPluginUpdates');
$pluginManager = PluginManager::getInstance();
$pluginManager->loadAllPluginsAndGetTheirInfo();
$loadedPlugins = $pluginManager->getLoadedPlugins();
- MarketplaceApiClient::clearAllCacheEntries();
- StaticContainer::getContainer()->set('marketplacePiwikVersion', $newVersion);
-
- $pluginsWithUpdate = $marketplace->checkUpdates($loadedPlugins);
+ $marketplaceClient->clearAllCacheEntries();
+ $pluginsWithUpdate = $marketplaceClient->checkUpdates($loadedPlugins);
foreach ($pluginsWithUpdate as $pluginWithUpdate) {
$pluginName = $pluginWithUpdate['name'];
-
$messages[] = $this->translator->translate('CoreUpdater_UpdatingPluginXToVersionY',
array($pluginName, $pluginWithUpdate['version']));
-
- $pluginInstaller = new PluginInstaller($pluginName);
- $pluginInstaller->installOrUpdatePluginFromMarketplace();
+ $pluginInstaller = new PluginInstaller($marketplaceClient);
+ $pluginInstaller->installOrUpdatePluginFromMarketplace($pluginName);
}
}
- } catch (MarketplaceApiException $e) {
+ } catch (MarketplaceApi\Exception $e) {
// there is a problem with the connection to the server, ignore for now
} catch (Exception $e) {
throw new UpdaterException($e, $messages);
diff --git a/plugins/CoreUpdater/templates/layout.twig b/plugins/CoreUpdater/templates/layout.twig
index 1af26ed70b..e49d4bdd3e 100644
--- a/plugins/CoreUpdater/templates/layout.twig
+++ b/plugins/CoreUpdater/templates/layout.twig
@@ -2,7 +2,7 @@
<html id="ng-app" ng-app="piwikApp">
<head>
<meta charset="utf-8">
- <title>Piwik &rsaquo; {{ 'CoreUpdater_UpdateTitle'|translate }}</title>
+ <title>Piwik &rsaquo; {{ pageTitle|default('CoreUpdater_UpdateTitle'|translate) }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=EDGE,chrome=1"/>
<meta name="viewport" content="initial-scale=1.0" />
<meta name="robots" content="noindex,nofollow">
diff --git a/plugins/ExamplePlugin/plugin.json b/plugins/ExamplePlugin/plugin.json
index c784750dec..a6cb365d6d 100644
--- a/plugins/ExamplePlugin/plugin.json
+++ b/plugins/ExamplePlugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "ExamplePlugin",
- "version": "0.1.0",
"description": "Piwik Platform showcase: how to create widgets, menus, scheduled tasks, a custom archiver, plugin tests, and a AngularJS component.",
+ "version": "0.1.0",
"theme": false,
"require": {
"piwik": ">=3.0.0-b1,<4.0.0-b1"
diff --git a/plugins/ExampleTheme/plugin.json b/plugins/ExampleTheme/plugin.json
index 6e99da3910..972af7f443 100644
--- a/plugins/ExampleTheme/plugin.json
+++ b/plugins/ExampleTheme/plugin.json
@@ -27,4 +27,4 @@
"homepage": ""
}
]
-}
+} \ No newline at end of file
diff --git a/plugins/Marketplace/API.php b/plugins/Marketplace/API.php
new file mode 100644
index 0000000000..0ebdaec905
--- /dev/null
+++ b/plugins/Marketplace/API.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace;
+
+use Exception;
+use Piwik\Piwik;
+use Piwik\Plugins\Marketplace\Api\Client;
+use Piwik\Plugins\Marketplace\Api\Service;
+use Piwik\Plugins\Marketplace\Plugins\InvalidLicenses;
+
+/**
+ * The Marketplace API lets you manage your license key so you can download & install in one-click <a target="_blank" rel="noreferrer" href="https://plugins.piwik.org/premium">paid premium plugins</a> you have subscribed to.
+ *
+ * @method static \Piwik\Plugins\Marketplace\API getInstance()
+ */
+class API extends \Piwik\Plugin\API
+{
+ /**
+ * @var Client
+ */
+ private $marketplaceClient;
+
+ /**
+ * @var Service
+ */
+ private $marketplaceService;
+
+ /**
+ * @var InvalidLicenses
+ */
+ private $expired;
+
+ public function __construct(Service $service, Client $client, InvalidLicenses $expired)
+ {
+ $this->marketplaceService = $service;
+ $this->marketplaceClient = $client;
+ $this->expired = $expired;
+ }
+
+ /**
+ * Deletes an existing license key if one is set.
+ *
+ * @return bool
+ */
+ public function deleteLicenseKey()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $this->setLicenseKey(null);
+ return true;
+ }
+
+ /**
+ * Saves the given license key in case the key is actually valid (exists on the Piwik Marketplace and is not
+ * yet expired).
+ *
+ * @param string $licenseKey
+ * @return bool
+ *
+ * @throws Exception In case of an invalid license key
+ * @throws Service\Exception In case of any network problems
+ */
+ public function saveLicenseKey($licenseKey)
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $licenseKey = trim($licenseKey);
+
+ // we are currently using the Marketplace service directly to 1) change LicenseKey and 2) not use any cache
+ $this->marketplaceService->authenticate($licenseKey);
+
+ try {
+ $consumer = $this->marketplaceService->fetch('consumer/validate', array());
+ } catch (Api\Service\Exception $e) {
+ if ($e->getCode() === Api\Service\Exception::HTTP_ERROR) {
+ throw $e;
+ }
+
+ $consumer = array();
+ }
+
+ if (empty($consumer['isValid'])) {
+ throw new Exception(Piwik::translate('Marketplace_ExceptionLinceseKeyIsNotValid'));
+ }
+
+ $this->setLicenseKey($licenseKey);
+
+ return true;
+ }
+
+ private function setLicenseKey($licenseKey)
+ {
+ $key = new LicenseKey();
+ $key->set($licenseKey);
+
+ $this->marketplaceClient->clearAllCacheEntries();
+ $this->expired->clearCache();
+ }
+
+}
diff --git a/plugins/Marketplace/Api/Client.php b/plugins/Marketplace/Api/Client.php
new file mode 100644
index 0000000000..0b4589f33a
--- /dev/null
+++ b/plugins/Marketplace/Api/Client.php
@@ -0,0 +1,326 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\Marketplace\Api;
+
+use Piwik\Cache;
+use Piwik\Common;
+use Piwik\Container\StaticContainer;
+use Piwik\Filesystem;
+use Piwik\Plugin;
+use Piwik\Plugins\Marketplace\Environment;
+use Piwik\Plugins\Marketplace\Api\Service;
+use Piwik\SettingsServer;
+use Exception as PhpException;
+use Psr\Log\LoggerInterface;
+
+/**
+ *
+ */
+class Client
+{
+ const CACHE_TIMEOUT_IN_SECONDS = 3600;
+ const HTTP_REQUEST_TIMEOUT = 60;
+
+ /**
+ * @var Service
+ */
+ private $service;
+
+ /**
+ * @var Cache\Lazy
+ */
+ private $cache;
+
+ /**
+ * @var Plugin\Manager
+ */
+ private $pluginManager;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * @var Environment
+ */
+ private $environment;
+
+ public function __construct(Service $service, Cache\Lazy $cache, LoggerInterface $logger, Environment $environment)
+ {
+ $this->service = $service;
+ $this->cache = $cache;
+ $this->logger = $logger;
+ $this->pluginManager = Plugin\Manager::getInstance();
+ $this->environment = $environment;
+ }
+
+ public function setEnvironment($environment)
+ {
+ $this->environment = $environment;
+ }
+
+ public function getEnvironment()
+ {
+ return $this->environment;
+ }
+
+ public function getPluginInfo($name)
+ {
+ $action = sprintf('plugins/%s/info', $name);
+
+ $plugin = $this->fetch($action, array());
+
+ if (!empty($plugin) && $this->shouldIgnorePlugin($plugin)) {
+ return;
+ }
+
+ return $plugin;
+ }
+
+ public function getInfo()
+ {
+ try {
+ $info = $this->fetch('info', array());
+ } catch (Exception $e) {
+ $info = null;
+ }
+
+ return $info;
+ }
+
+ public function getConsumer()
+ {
+ try {
+ $licenses = $this->fetch('consumer', array());
+ } catch (Exception $e) {
+ $licenses = null;
+ }
+
+ return $licenses;
+ }
+
+ public function isValidConsumer()
+ {
+ try {
+ $consumer = $this->fetch('consumer/validate', array());
+ } catch (Exception $e) {
+ $consumer = null;
+ }
+
+ return !empty($consumer['isValid']);
+ }
+
+ private function getRandomTmpPluginDownloadFilename()
+ {
+ $tmpPluginPath = StaticContainer::get('path.tmp') . '/latest/plugins/';
+
+ // we generate a random unique id as filename to prevent any user could possibly download zip directly by
+ // opening $piwikDomain/tmp/latest/plugins/$pluginName.zip in the browser. Instead we make it harder here
+ // and try to make sure to delete file in case of any error.
+ $tmpPluginFolder = Common::generateUniqId();
+
+ return $tmpPluginPath . $tmpPluginFolder . '.zip';
+ }
+
+ public function download($pluginOrThemeName)
+ {
+ @ignore_user_abort(true);
+ SettingsServer::setMaxExecutionTime(0);
+
+ $downloadUrl = $this->getDownloadUrl($pluginOrThemeName);
+
+ if (empty($downloadUrl)) {
+ return false;
+ }
+
+ // in the beginning we allowed to specify a download path but this way we make sure security is always taken
+ // care of and we always generate a random download filename.
+ $target = $this->getRandomTmpPluginDownloadFilename();
+
+ Filesystem::deleteFileIfExists($target);
+
+ $success = $this->service->download($downloadUrl, $target, static::HTTP_REQUEST_TIMEOUT);
+
+ if ($success) {
+ return $target;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param \Piwik\Plugin[] $plugins
+ * @return array|mixed
+ */
+ public function checkUpdates($plugins)
+ {
+ $params = array();
+
+ foreach ($plugins as $plugin) {
+ $pluginName = $plugin->getPluginName();
+ if (!$this->pluginManager->isPluginBundledWithCore($pluginName)) {
+ $params[] = array('name' => $plugin->getPluginName(), 'version' => $plugin->getVersion());
+ }
+ }
+
+ if (empty($params)) {
+ return array();
+ }
+
+ $params = array('plugins' => $params);
+
+ $hasUpdates = $this->fetch('plugins/checkUpdates', array('plugins' => json_encode($params)));
+
+ if (empty($hasUpdates)) {
+ return array();
+ }
+
+ return $hasUpdates;
+ }
+
+ /**
+ * @param \Piwik\Plugin[] $plugins
+ * @return array
+ */
+ public function getInfoOfPluginsHavingUpdate($plugins)
+ {
+ $hasUpdates = $this->checkUpdates($plugins);
+
+ $pluginDetails = array();
+
+ foreach ($hasUpdates as $pluginHavingUpdate) {
+ if (empty($pluginHavingUpdate)) {
+ continue;
+ }
+
+ try {
+ $plugin = $this->getPluginInfo($pluginHavingUpdate['name']);
+ } catch (PhpException $e) {
+ $this->logger->error($e->getMessage());
+ $plugin = null;
+ }
+
+ if (!empty($plugin)) {
+ $plugin['repositoryChangelogUrl'] = $pluginHavingUpdate['repositoryChangelogUrl'];
+ $pluginDetails[] = $plugin;
+ }
+
+ }
+
+ return $pluginDetails;
+ }
+
+ public function searchForPlugins($keywords, $query, $sort, $purchaseType)
+ {
+ $response = $this->fetch('plugins', array('keywords' => $keywords, 'query' => $query, 'sort' => $sort, 'purchase_type' => $purchaseType));
+
+ if (!empty($response['plugins'])) {
+ return $this->removeNotNeededPluginsFromResponse($response);
+ }
+
+ return array();
+ }
+
+ private function removeNotNeededPluginsFromResponse($response)
+ {
+ foreach ($response['plugins'] as $index => $plugin) {
+ if ($this->shouldIgnorePlugin($plugin)) {
+ unset($response['plugins'][$index]);
+ continue;
+ }
+ }
+ return array_values($response['plugins']);
+ }
+
+ private function shouldIgnorePlugin($plugin)
+ {
+ return !empty($plugin['isCustomPlugin']);
+ }
+
+ public function searchForThemes($keywords, $query, $sort, $purchaseType)
+ {
+ $response = $this->fetch('themes', array('keywords' => $keywords, 'query' => $query, 'sort' => $sort, 'purchase_type' => $purchaseType));
+
+ if (!empty($response['plugins'])) {
+ return $this->removeNotNeededPluginsFromResponse($response);
+ }
+
+ return array();
+ }
+
+ private function fetch($action, $params)
+ {
+ ksort($params); // sort params so cache is reused more often even if param order is different
+
+ $releaseChannel = $this->environment->getReleaseChannel();
+
+ if (!empty($releaseChannel)) {
+ $params['release_channel'] = $releaseChannel;
+ }
+
+ $params['prefer_stable'] = (int) $this->environment->doesPreferStable();
+ $params['piwik'] = $this->environment->getPiwikVersion();
+ $params['php'] = $this->environment->getPhpVersion();
+ $params['mysql'] = $this->environment->getMySQLVersion();
+ $params['num_users'] = $this->environment->getNumUsers();
+ $params['num_websites'] = $this->environment->getNumWebsites();
+
+ $query = http_build_query($params);
+ $cacheId = $this->getCacheKey($action, $query);
+
+ $result = $this->cache->fetch($cacheId);
+
+ if ($result !== false) {
+ return $result;
+ }
+
+ try {
+ $result = $this->service->fetch($action, $params);
+ } catch (Service\Exception $e) {
+ throw new Exception($e->getMessage(), $e->getCode());
+ }
+
+ $this->cache->save($cacheId, $result, self::CACHE_TIMEOUT_IN_SECONDS);
+
+ return $result;
+ }
+
+ public function clearAllCacheEntries()
+ {
+ $this->cache->flushAll();
+ }
+
+ private function getCacheKey($action, $query)
+ {
+ $version = $this->service->getVersion();
+
+ return sprintf('marketplace.api.%s.%s.%s', $version, str_replace('/', '.', $action), md5($query));
+ }
+
+ /**
+ * @param $pluginOrThemeName
+ * @throws Exception
+ * @return string
+ */
+ public function getDownloadUrl($pluginOrThemeName)
+ {
+ $plugin = $this->getPluginInfo($pluginOrThemeName);
+
+ if (empty($plugin['versions'])) {
+ throw new Exception('Plugin has no versions.');
+ }
+
+ $latestVersion = array_pop($plugin['versions']);
+ $downloadUrl = $latestVersion['download'];
+
+ return $this->service->getDomain() . $downloadUrl . '?coreVersion=' . $this->environment->getPiwikVersion();
+ }
+
+}
diff --git a/plugins/CorePluginsAdmin/MarketplaceApiException.php b/plugins/Marketplace/Api/Exception.php
index 616a9b89b1..7dce508bf9 100644
--- a/plugins/CorePluginsAdmin/MarketplaceApiException.php
+++ b/plugins/Marketplace/Api/Exception.php
@@ -7,11 +7,11 @@
*
*/
-namespace Piwik\Plugins\CorePluginsAdmin;
+namespace Piwik\Plugins\Marketplace\Api;
/**
*/
-class MarketplaceApiException extends \Exception
+class Exception extends \Exception
{
}
diff --git a/plugins/Marketplace/Api/Service.php b/plugins/Marketplace/Api/Service.php
new file mode 100644
index 0000000000..e493b69941
--- /dev/null
+++ b/plugins/Marketplace/Api/Service.php
@@ -0,0 +1,158 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\Marketplace\Api;
+
+use Piwik\Http;
+
+/**
+ *
+ */
+class Service
+{
+ const CACHE_TIMEOUT_IN_SECONDS = 1200;
+ const HTTP_REQUEST_TIMEOUT = 60;
+
+ /**
+ * @var string
+ */
+ private $domain;
+
+ /**
+ * @var null|string
+ */
+ private $accessToken;
+
+ /**
+ * API version to use on the Marketplace
+ * @var string
+ */
+ private $version = '2.0';
+
+ public function __construct($domain)
+ {
+ $this->domain = $domain;
+ }
+
+ public function authenticate($accessToken)
+ {
+ if (empty($accessToken)) {
+ $this->accessToken = null;
+ } elseif (ctype_alnum($accessToken)) {
+ $this->accessToken = $accessToken;
+ }
+ }
+
+ /**
+ * The API version that will be used on the Marketplace.
+ * @return string eg 2.0
+ */
+ public function getVersion()
+ {
+ return $this->version;
+ }
+
+ /**
+ * Returns the currently set access token
+ * @return null|string
+ */
+ public function getAccessToken()
+ {
+ return $this->accessToken;
+ }
+
+ public function hasAccessToken()
+ {
+ return !empty($this->accessToken);
+ }
+
+ /**
+ * Downloads data from the given URL via a POST request. If a destination path is given, the downloaded data
+ * will be stored in the given path and returned otherwise.
+ *
+ * Make sure to call {@link authenticate()} to download paid plugins.
+ *
+ * @param string $url An absolute URL to the marketplace including domain.
+ * @param null|string $destinationPath
+ * @param null|int $timeout Defaults to 60 seconds see {@link self::HTTP_REQUEST_METHOD}
+ * @return bool|string Returns the downloaded data or true if a destination path was given.
+ * @throws \Exception
+ */
+ public function download($url, $destinationPath = null, $timeout = null)
+ {
+ $method = Http::getTransportMethod();
+
+ if (!isset($timeout)) {
+ $timeout = static::HTTP_REQUEST_TIMEOUT;
+ }
+
+ $post = null;
+ if ($this->accessToken) {
+ $post = array('access_token' => $this->accessToken);
+ }
+
+ $file = Http::ensureDestinationDirectoryExists($destinationPath);
+
+ $response = Http::sendHttpRequestBy($method,
+ $url,
+ $timeout,
+ $userAgent = null,
+ $destinationPath,
+ $file,
+ $followDepth = 0,
+ $acceptLanguage = false,
+ $acceptInvalidSslCertificate = false,
+ $byteRange = false, $getExtendedInfo = false, $httpMethod = 'POST',
+ $httpUsername = null, $httpPassword = null, $post);
+
+ return $response;
+ }
+
+ /**
+ * Executes the given API action on the Marketplace using the given params and returns the result.
+ *
+ * Make sure to call {@link authenticate()} to download paid plugins.
+ *
+ * @param string $action eg 'plugins', 'plugins/$pluginName/info', ...
+ * @param array $params eg array('sort' => 'alpha')
+ * @return mixed
+ * @throws Service\Exception
+ */
+ public function fetch($action, $params)
+ {
+ $endpoint = sprintf('%s/api/%s/', $this->domain, $this->version);
+
+ $query = http_build_query($params);
+ $url = sprintf('%s%s?%s', $endpoint, $action, $query);
+
+ $response = $this->download($url);
+
+ $result = json_decode($response, true);
+
+ if (is_null($result)) {
+ $message = sprintf('There was an error reading the response from the Marketplace: Please try again later.');
+ throw new Service\Exception($message, Service\Exception::HTTP_ERROR);
+ }
+
+ if (!empty($result['error'])) {
+ throw new Service\Exception($result['error'], Service\Exception::API_ERROR);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get the domain that is used in order to access the Marketplace. Eg http://plugins.piwik.org
+ * @return string
+ */
+ public function getDomain()
+ {
+ return $this->domain;
+ }
+
+}
diff --git a/plugins/Marketplace/Api/Service/Exception.php b/plugins/Marketplace/Api/Service/Exception.php
new file mode 100644
index 0000000000..181c0be568
--- /dev/null
+++ b/plugins/Marketplace/Api/Service/Exception.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\Marketplace\Api\Service;
+
+/**
+ */
+class Exception extends \Exception
+{
+ const HTTP_ERROR = 100;
+ const API_ERROR = 101;
+
+}
diff --git a/plugins/Marketplace/Consumer.php b/plugins/Marketplace/Consumer.php
new file mode 100644
index 0000000000..1ae8ed490b
--- /dev/null
+++ b/plugins/Marketplace/Consumer.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\Marketplace;
+
+/**
+ * A consumer is a user having specified a license key in the Marketplace.
+ */
+class Consumer
+{
+ /**
+ * @var Api\Client
+ */
+ private $marketplaceClient;
+
+ private $consumer = false;
+ private $isValid = null;
+
+ public function __construct(Api\Client $marketplaceClient)
+ {
+ $this->marketplaceClient = $marketplaceClient;
+ }
+
+ /**
+ * For tests only.
+ * @internal
+ * @return Api\Client
+ */
+ public function getApiClient()
+ {
+ return $this->marketplaceClient;
+ }
+
+ public function clearCache()
+ {
+ $this->consumer = false;
+ $this->isValid = null;
+ }
+
+ public function getConsumer()
+ {
+ if ($this->consumer === false) {
+ $consumer = $this->marketplaceClient->getConsumer();
+ if (!empty($consumer)) {
+ $this->consumer = $consumer;
+ } else {
+ $this->consumer = array();
+ }
+ }
+
+ return $this->consumer;
+ }
+
+ public function isValidConsumer()
+ {
+ if (!isset($this->isValid)) {
+ $this->isValid = $this->marketplaceClient->isValidConsumer();
+ }
+
+ return $this->isValid;
+ }
+
+}
diff --git a/plugins/Marketplace/Controller.php b/plugins/Marketplace/Controller.php
new file mode 100644
index 0000000000..986ee5cf9a
--- /dev/null
+++ b/plugins/Marketplace/Controller.php
@@ -0,0 +1,459 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace;
+
+use Piwik\Common;
+use Piwik\Date;
+use Piwik\Filesystem;
+use Piwik\Log;
+use Piwik\Nonce;
+use Piwik\Notification;
+use Piwik\Piwik;
+use Piwik\Plugin;
+use Piwik\Plugins\CorePluginsAdmin\Controller as PluginsController;
+use Piwik\Plugins\CorePluginsAdmin\CorePluginsAdmin;
+use Piwik\Plugins\CorePluginsAdmin\PluginInstaller;
+use Piwik\Plugins\Marketplace\Input\Mode;
+use Piwik\Plugins\Marketplace\Input\PluginName;
+use Piwik\Plugins\Marketplace\Input\PurchaseType;
+use Piwik\Plugins\Marketplace\Input\Sort;
+use Piwik\ProxyHttp;
+use Piwik\SettingsPiwik;
+use Piwik\Url;
+use Piwik\View;
+use Exception;
+
+class Controller extends \Piwik\Plugin\ControllerAdmin
+{
+ const UPDATE_NONCE = 'Marketplace.updatePlugin';
+ const INSTALL_NONCE = 'Marketplace.installPlugin';
+
+ /**
+ * @var LicenseKey
+ */
+ private $licenseKey;
+ /**
+ * @var Plugins
+ */
+ private $plugins;
+
+ /**
+ * @var Api\Client
+ */
+ private $marketplaceApi;
+
+ /**
+ * @var Consumer
+ */
+ private $consumer;
+
+ /**
+ * @var PluginInstaller
+ */
+ private $pluginInstaller;
+
+ /**
+ * @var Plugin\Manager
+ */
+ private $pluginManager;
+
+ /**
+ * @var Environment
+ */
+ private $environment;
+
+ public function __construct(LicenseKey $licenseKey, Plugins $plugins, Api\Client $marketplaceApi, Consumer $consumer, PluginInstaller $pluginInstaller, Environment $environment)
+ {
+ $this->licenseKey = $licenseKey;
+ $this->plugins = $plugins;
+ $this->marketplaceApi = $marketplaceApi;
+ $this->consumer = $consumer;
+ $this->pluginInstaller = $pluginInstaller;
+ $this->pluginManager = Plugin\Manager::getInstance();
+ $this->environment = $environment;
+
+ parent::__construct();
+ }
+
+ public function subscriptionOverview()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ // we want to make sure to fetch the latest results, eg in case user has purchased a subscription meanwhile
+ // this is also like a self-repair to clear the caches :)
+ $this->marketplaceApi->clearAllCacheEntries();
+ $this->consumer->clearCache();
+
+ $hasLicenseKey = $this->licenseKey->has();
+
+ $consumer = $this->consumer->getConsumer();
+
+ $subscriptions = array();
+ $loginUrl = '';
+
+ if (!empty($consumer['loginUrl'])) {
+ $loginUrl = $consumer['loginUrl'];
+ }
+
+ if (!empty($consumer['licenses'])) {
+ foreach ($consumer['licenses'] as $subscription) {
+ $subscription['start'] = $this->getPrettyLongDate($subscription['startDate']);
+ $subscription['end'] = $this->getPrettyLongDate($subscription['endDate']);
+ $subscription['nextPayment'] = $this->getPrettyLongDate($subscription['nextPaymentDate']);
+ $subscriptions[] = $subscription;
+ }
+ }
+
+ return $this->renderTemplate('@Marketplace/subscription-overview', array(
+ 'hasLicenseKey' => $hasLicenseKey,
+ 'subscriptions' => $subscriptions,
+ 'loginUrl' => $loginUrl,
+ 'numUsers' => $this->environment->getNumUsers()
+ ));
+ }
+
+ private function getPrettyLongDate($date)
+ {
+ if (empty($date)) {
+ return '';
+ }
+
+ return Date::factory($date)->getLocalized(Date::DATE_FORMAT_LONG);
+ }
+
+ public function pluginDetails()
+ {
+ $view = $this->configureViewAndCheckPermission('@Marketplace/plugin-details');
+
+ $pluginName = new PluginName();
+ $pluginName = $pluginName->getPluginName();
+
+ $activeTab = Common::getRequestVar('activeTab', '', 'string');
+ if ('changelog' !== $activeTab) {
+ $activeTab = '';
+ }
+
+ try {
+ $plugin = $this->plugins->getPluginInfo($pluginName);
+
+ if (empty($plugin['name'])) {
+ throw new Exception('Plugin does not exist');
+ }
+ } catch (Exception $e) {
+ $plugin = null;
+ $view->errorMessage = $e->getMessage();
+ }
+
+ $view->plugin = $plugin;
+ $view->isSuperUser = Piwik::hasUserSuperUserAccess();
+ $view->installNonce = Nonce::getNonce(static::INSTALL_NONCE);
+ $view->updateNonce = Nonce::getNonce(static::UPDATE_NONCE);
+ $view->activeTab = $activeTab;
+ $view->isAutoUpdatePossible = SettingsPiwik::isAutoUpdatePossible();
+ $view->isAutoUpdateEnabled = SettingsPiwik::isAutoUpdateEnabled();
+ $view->numUsers = $this->environment->getNumUsers();
+
+ return $view->render();
+ }
+
+ public function download()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $this->dieIfPluginsAdminIsDisabled();
+
+ $pluginName = new PluginName();
+ $pluginName = $pluginName->getPluginName();
+
+ Nonce::checkNonce($pluginName);
+
+ $filename = $pluginName . '.zip';
+
+ try {
+ $pathToPlugin = $this->marketplaceApi->download($pluginName);
+ ProxyHttp::serverStaticFile($pathToPlugin, 'application/zip', $expire = 0, $start = false, $end = false, $filename);
+ } catch (Exception $e) {
+ Common::sendResponseCode(500);
+ Log::warning('Could not download file . ' . $e->getMessage());
+ }
+
+ if (!empty($pathToPlugin)) {
+ Filesystem::deleteFileIfExists($pathToPlugin);
+ }
+ }
+
+ public function overview()
+ {
+ $view = $this->configureViewAndCheckPermission('@Marketplace/overview');
+
+ $show = Common::getRequestVar('show', 'plugins', 'string');
+ $query = Common::getRequestVar('query', '', 'string', $_POST);
+
+ $sort = new Sort();
+ $sort = $sort->getSort();
+
+ $mode = new Mode();
+ $mode = $mode->getMode();
+
+ // we're fetching all available plugins to decide which tabs need to be shown in the UI and to know the number
+ // of total available plugins
+ $allPlugins = $this->plugins->getAllPlugins();
+ $allThemes = $this->plugins->getAllThemes();
+ $paidPlugins = $this->plugins->getAllPaidPlugins();
+
+ $showThemes = ($show === 'themes');
+ $showPlugins = !$showThemes;
+ $showPaid = ($show === 'premium');
+ $showAll = !$showPaid;
+
+ if ($showPlugins && $showPaid) {
+ $type = PurchaseType::TYPE_PAID;
+ $view->numAvailablePlugins = count($paidPlugins);
+ } elseif ($showPlugins && $showAll) {
+ $type = PurchaseType::TYPE_ALL;
+ $view->numAvailablePlugins = count($allPlugins);
+ } else {
+ $type = PurchaseType::TYPE_ALL;
+ $view->numAvailablePlugins = count($allThemes);
+ }
+
+ $pluginsToShow = $this->plugins->searchPlugins($query, $sort, $showThemes, $type);
+
+ $paidPluginsToInstallAtOnce = array();
+ if (SettingsPiwik::isAutoUpdatePossible()) {
+ foreach ($paidPlugins as $paidPlugin) {
+ if ($this->canPluginBeInstalled($paidPlugin)
+ || ($this->pluginManager->isPluginInstalled($paidPlugin['name'])
+ && !$this->pluginManager->isPluginActivated($paidPlugin['name']))) {
+ $paidPluginsToInstallAtOnce[] = $paidPlugin['name'];
+ }
+ }
+ }
+
+ $view->paidPluginsToInstallAtOnce = $paidPluginsToInstallAtOnce;
+ $view->pluginsToShow = $pluginsToShow;
+ $view->isValidConsumer = $this->consumer->isValidConsumer();
+ $view->paidPlugins = $paidPlugins;
+ $view->freePlugins = $allPlugins;
+ $view->themes = $allThemes;
+ $view->showThemes = $showThemes;
+ $view->showPlugins = $showPlugins;
+ $view->showFree = $showAll;
+ $view->showPaid = $showPaid;
+ $view->pluginType = $show;
+ $view->pluginTypeOptions = array(
+ 'plugins' => Piwik::translate('General_Plugins'),
+ 'premium' => Piwik::translate('Marketplace_PaidPlugins'),
+ 'themes' => Piwik::translate('CorePluginsAdmin_Themes')
+ );
+ $view->pluginSortOptions = array(
+ Sort::METHOD_LAST_UPDATED => Piwik::translate('Marketplace_SortByLastUpdated'),
+ Sort::METHOD_POPULAR => Piwik::translate('Marketplace_SortByPopular'),
+ Sort::METHOD_NEWEST => Piwik::translate('Marketplace_SortByNewest'),
+ Sort::METHOD_ALPHA => Piwik::translate('Marketplace_SortByAlpha'),
+ );
+ $view->mode = $mode;
+ $view->query = $query;
+ $view->sort = $sort;
+ $view->hasLicenseKey = $this->licenseKey->has();
+ $view->hasSomeAdminAccess = Piwik::isUserHasSomeAdminAccess();
+ $view->installNonce = Nonce::getNonce(static::INSTALL_NONCE);
+ $view->updateNonce = Nonce::getNonce(static::UPDATE_NONCE);
+ $view->deactivateNonce = Nonce::getNonce(PluginsController::DEACTIVATE_NONCE);
+ $view->activateNonce = Nonce::getNonce(PluginsController::ACTIVATE_NONCE);
+ $view->isSuperUser = Piwik::hasUserSuperUserAccess();
+ $view->isPluginsAdminEnabled = CorePluginsAdmin::isPluginsAdminEnabled();
+ $view->isAutoUpdatePossible = SettingsPiwik::isAutoUpdatePossible();
+ $view->isAutoUpdateEnabled = SettingsPiwik::isAutoUpdateEnabled();
+
+ return $view->render();
+ }
+
+ public function installAllPaidPlugins()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $this->dieIfPluginsAdminIsDisabled();
+ Plugin\ControllerAdmin::displayWarningIfConfigFileNotWritable();
+
+ Nonce::checkNonce(static::INSTALL_NONCE);
+
+ $paidPlugins = $this->plugins->getAllPaidPlugins();
+
+ $hasErrors = false;
+ foreach ($paidPlugins as $paidPlugin) {
+ if (!$this->canPluginBeInstalled($paidPlugin)) {
+ continue;
+ }
+
+ $pluginName = $paidPlugin['name'];
+
+ try {
+
+ $this->pluginInstaller->installOrUpdatePluginFromMarketplace($pluginName);
+
+ } catch (\Exception $e) {
+
+ $notification = new Notification($e->getMessage());
+ $notification->context = Notification::CONTEXT_ERROR;
+ Notification\Manager::notify('Marketplace_Install' . $pluginName, $notification);
+
+ $hasErrors = true;
+ }
+ }
+
+ if ($hasErrors) {
+ Url::redirectToReferrer();
+ return;
+ }
+
+ $dependency = new Plugin\Dependency();
+
+ for ($i = 0; $i <= 10; $i++) {
+ foreach ($paidPlugins as $index => $paidPlugin) {
+ $pluginName = $paidPlugin['name'];
+
+ if ($this->pluginManager->isPluginActivated($pluginName)) {
+ unset($paidPlugins[$index]);
+ continue;
+ }
+
+ if (empty($paidPlugin['require'])
+ || !$dependency->hasDependencyToDisabledPlugin($paidPlugin['require'])) {
+
+ unset($paidPlugins[$index]);
+
+ try {
+ $this->pluginManager->activatePlugin($pluginName);
+ } catch (Exception $e) {
+
+ $hasErrors = true;
+ $notification = new Notification($e->getMessage());
+ $notification->context = Notification::CONTEXT_ERROR;
+ Notification\Manager::notify('Marketplace_Install' . $pluginName, $notification);
+ }
+ }
+ }
+ }
+
+ if ($hasErrors) {
+ $notification = new Notification(Piwik::translate('Marketplace_OnlySomePaidPluginsInstalledAndActivated'));
+ $notification->context = Notification::CONTEXT_INFO;
+ } else {
+ $notification = new Notification(Piwik::translate('Marketplace_AllPaidPluginsInstalledAndActivated'));
+ $notification->context = Notification::CONTEXT_SUCCESS;
+ }
+
+ Notification\Manager::notify('Marketplace_InstallAll', $notification);
+
+ Url::redirectToReferrer();
+ }
+
+ public function updatePlugin()
+ {
+ $view = $this->createUpdateOrInstallView('updatePlugin', static::UPDATE_NONCE);
+ return $view->render();
+ }
+
+ public function installPlugin()
+ {
+ $view = $this->createUpdateOrInstallView('installPlugin', static::INSTALL_NONCE);
+ $view->nonce = Nonce::getNonce(PluginsController::ACTIVATE_NONCE);
+
+ return $view->render();
+ }
+
+ private function createUpdateOrInstallView($template, $nonceName)
+ {
+ Piwik::checkUserHasSuperUserAccess();
+ $this->dieIfPluginsAdminIsDisabled();
+ $this->displayWarningIfConfigFileNotWritable();
+
+ $pluginName = $this->getPluginNameIfNonceValid($nonceName);
+
+ $view = new View('@Marketplace/' . $template);
+ $this->setBasicVariablesView($view);
+ $view->errorMessage = '';
+ $view->plugin = array('name' => $pluginName);
+
+ try {
+ $this->pluginInstaller->installOrUpdatePluginFromMarketplace($pluginName);
+
+ } catch (\Exception $e) {
+
+ $notification = new Notification($e->getMessage());
+ $notification->context = Notification::CONTEXT_ERROR;
+ $notification->type = Notification::TYPE_PERSISTENT;
+ $notification->flags = Notification::FLAG_CLEAR;
+ Notification\Manager::notify('CorePluginsAdmin_InstallPlugin', $notification);
+
+ Url::redirectToReferrer();
+ return;
+ }
+
+ $view->plugin = $this->plugins->getPluginInfo($pluginName);
+
+ return $view;
+ }
+
+ private function getPluginNameIfNonceValid($nonceName)
+ {
+ $nonce = Common::getRequestVar('nonce', null, 'string');
+
+ if (!Nonce::verifyNonce($nonceName, $nonce)) {
+ throw new \Exception(Piwik::translate('General_ExceptionNonceMismatch'));
+ }
+
+ Nonce::discardNonce($nonceName);
+
+ $pluginName = Common::getRequestVar('pluginName', null, 'string');
+
+ if (!$this->pluginManager->isValidPluginName($pluginName)) {
+ throw new Exception('Invalid plugin name');
+ }
+
+ return $pluginName;
+ }
+
+ private function dieIfPluginsAdminIsDisabled()
+ {
+ if (!CorePluginsAdmin::isPluginsAdminEnabled()) {
+ throw new \Exception('Enabling, disabling and uninstalling plugins has been disabled by Piwik admins.
+ Please contact your Piwik admins with your request so they can assist you.');
+ }
+ }
+
+ private function canPluginBeInstalled($plugin)
+ {
+ if (empty($plugin['isDownloadable'])) {
+ return false;
+ }
+
+ $pluginName = $plugin['name'];
+
+ $isAlreadyInstalled = $this->pluginManager->isPluginInstalled($pluginName)
+ || $this->pluginManager->isPluginLoaded($pluginName)
+ || $this->pluginManager->isPluginActivated($pluginName);
+
+ return !$isAlreadyInstalled;
+ }
+
+ protected function configureViewAndCheckPermission($template)
+ {
+ Piwik::checkUserIsNotAnonymous();
+
+ $view = new View($template);
+ $this->setBasicVariablesView($view);
+ $this->displayWarningIfConfigFileNotWritable();
+
+ $view->errorMessage = '';
+
+ return $view;
+ }
+}
diff --git a/plugins/Marketplace/Environment.php b/plugins/Marketplace/Environment.php
new file mode 100644
index 0000000000..0cfa124d9e
--- /dev/null
+++ b/plugins/Marketplace/Environment.php
@@ -0,0 +1,104 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace;
+
+use Piwik\Common;
+use Piwik\Db;
+use Piwik\Plugin\ReleaseChannels;
+use Piwik\Plugins\CoreUpdater\ReleaseChannel;
+use Piwik\Version;
+
+class Environment
+{
+ /**
+ * @var ReleaseChannel
+ */
+ private $releaseChannel;
+
+ private $usersCache = null;
+ private $websitesCache = null;
+ private $mySqlCache = null;
+ private $piwikVersion = null;
+
+ public function __construct(ReleaseChannels $releaseChannels)
+ {
+ $this->releaseChannel = $releaseChannels->getActiveReleaseChannel();
+ }
+
+ public function setPiwikVersion($piwikVersion)
+ {
+ $this->piwikVersion = $piwikVersion;
+ }
+
+ public function getNumUsers()
+ {
+ if (!isset($this->usersCache)) {
+ $this->usersCache = (int) Db::get()->fetchOne('SELECT count(login) FROM ' . Common::prefixTable('user') . ' WHERE login <> "anonymous" ');
+ }
+
+ return $this->usersCache;
+ }
+
+ public function getNumWebsites()
+ {
+ if (!isset($this->websitesCache)) {
+ $this->websitesCache = (int) Db::get()->fetchOne('SELECT count(idsite) FROM ' . Common::prefixTable('site'));
+ }
+
+ return $this->websitesCache;
+ }
+
+ public function getPhpVersion()
+ {
+ return PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION;
+ }
+
+ public function getPiwikVersion()
+ {
+ if (!empty($this->piwikVersion)) {
+ return $this->piwikVersion;
+ }
+
+ return Version::VERSION;
+ }
+
+ public function doesPreferStable()
+ {
+ if (!empty($this->releaseChannel)) {
+ return $this->releaseChannel->doesPreferStable();
+ }
+
+ return true;
+ }
+
+ public function getReleaseChannel()
+ {
+ if (!empty($this->releaseChannel)) {
+ return $this->releaseChannel->getId();
+ }
+ }
+
+ public function getMySQLVersion()
+ {
+ if (isset($this->mySqlCache)) {
+ return $this->mySqlCache;
+ }
+
+ $this->mySqlCache = '';
+
+ $db = Db::get();
+ if (method_exists($db, 'getServerVersion')) {
+ $this->mySqlCache = $db->getServerVersion();
+ }
+
+ return $this->mySqlCache;
+ }
+
+
+}
diff --git a/plugins/Marketplace/Input/Mode.php b/plugins/Marketplace/Input/Mode.php
new file mode 100644
index 0000000000..12ec883b3f
--- /dev/null
+++ b/plugins/Marketplace/Input/Mode.php
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\Marketplace\Input;
+use Piwik\Common;
+
+/**
+ */
+class Mode
+{
+
+ public function getMode()
+ {
+ $mode = Common::getRequestVar('mode', 'admin', 'string');
+
+ if (!in_array($mode, array('user', 'admin'))) {
+ $mode = 'admin';
+ }
+
+ return $mode;
+ }
+
+}
diff --git a/plugins/Marketplace/Input/PluginName.php b/plugins/Marketplace/Input/PluginName.php
new file mode 100644
index 0000000000..354b7ff9d9
--- /dev/null
+++ b/plugins/Marketplace/Input/PluginName.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\Marketplace\Input;
+use Piwik\Common;
+use Piwik\Plugin;
+use Exception;
+
+/**
+ */
+class PluginName
+{
+ private $requestParam = '';
+
+ public function __construct($requestParam = 'pluginName')
+ {
+ $this->requestParam = $requestParam;
+ }
+
+ public function getPluginName()
+ {
+ $pluginName = Common::getRequestVar($this->requestParam, null, 'string');
+
+ $this->dieIfPluginNameIsInvalid($pluginName);
+
+ return $pluginName;
+ }
+
+ private function dieIfPluginNameIsInvalid($pluginName)
+ {
+ if (!Plugin\Manager::getInstance()->isValidPluginName($pluginName)){
+ throw new Exception('Invalid plugin name given');
+ }
+ }
+
+}
diff --git a/plugins/Marketplace/Input/PurchaseType.php b/plugins/Marketplace/Input/PurchaseType.php
new file mode 100644
index 0000000000..7a76450c56
--- /dev/null
+++ b/plugins/Marketplace/Input/PurchaseType.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\Marketplace\Input;
+
+/**
+ */
+class PurchaseType
+{
+ const TYPE_FREE = 'free';
+ const TYPE_PAID = 'paid';
+ const TYPE_ALL = '';
+
+}
diff --git a/plugins/Marketplace/Input/Sort.php b/plugins/Marketplace/Input/Sort.php
new file mode 100644
index 0000000000..ba9aec307e
--- /dev/null
+++ b/plugins/Marketplace/Input/Sort.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\Marketplace\Input;
+use Piwik\Common;
+
+/**
+ */
+class Sort
+{
+ const METHOD_POPULAR = 'popular';
+ const METHOD_ALPHA = 'alpha';
+ const METHOD_LAST_UPDATED = 'lastupdated';
+ const METHOD_NEWEST = 'newest';
+ const DEFAULT_SORT = self::METHOD_LAST_UPDATED;
+
+ public function getSort()
+ {
+ $sort = Common::getRequestVar('sort', self::DEFAULT_SORT, 'string');
+
+ if (!$this->isValidSortMethod($sort)) {
+ $sort = self::DEFAULT_SORT;
+ }
+
+ return $sort;
+ }
+
+ private function isValidSortMethod($sortMethod)
+ {
+ $valid = array(self::METHOD_POPULAR, self::METHOD_NEWEST, self::METHOD_ALPHA);
+
+ return in_array($sortMethod, $valid, $strict = true);
+ }
+
+}
diff --git a/plugins/Marketplace/LicenseKey.php b/plugins/Marketplace/LicenseKey.php
new file mode 100644
index 0000000000..cc53142b61
--- /dev/null
+++ b/plugins/Marketplace/LicenseKey.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace;
+
+use Piwik\Option;
+
+class LicenseKey
+{
+ public function get()
+ {
+ return Option::get('marketplace_license_key');
+ }
+
+ public function has()
+ {
+ $key = $this->get();
+
+ return isset($key) && $key !== false && $key !== '';
+ }
+
+ /**
+ * @param string|null|false $licenseKey `null` or `false` will delete an existing a license key
+ */
+ public function set($licenseKey)
+ {
+ if (!isset($licenseKey) || $licenseKey === false) {
+ $this->delete();
+ } else {
+ Option::set('marketplace_license_key', (string) $licenseKey);
+ }
+ }
+
+ private function delete()
+ {
+ Option::delete('marketplace_license_key');
+ }
+
+}
diff --git a/plugins/Marketplace/Marketplace.php b/plugins/Marketplace/Marketplace.php
new file mode 100644
index 0000000000..50f7682a05
--- /dev/null
+++ b/plugins/Marketplace/Marketplace.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace;
+
+use Piwik\Plugin;
+
+class Marketplace extends \Piwik\Plugin
+{
+ /**
+ * @see \Piwik\Plugin::registerEvents
+ */
+ public function registerEvents()
+ {
+ return array(
+ 'AssetManager.getJavaScriptFiles' => 'getJsFiles',
+ 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
+ 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
+ );
+ }
+
+ public function getStylesheetFiles(&$stylesheets)
+ {
+ $stylesheets[] = "plugins/Marketplace/stylesheets/marketplace.less";
+ $stylesheets[] = "plugins/Marketplace/stylesheets/plugin-details.less";
+ $stylesheets[] = "plugins/Marketplace/stylesheets/marketplace-widget.less";
+ }
+
+ public function getJsFiles(&$jsFiles)
+ {
+ $jsFiles[] = "libs/bower_components/iframe-resizer/js/iframeResizer.min.js";
+
+ $jsFiles[] = "plugins/Marketplace/angularjs/plugins/plugin-name.directive.js";
+ $jsFiles[] = "plugins/Marketplace/angularjs/licensekey/licensekey.controller.js";
+ $jsFiles[] = "plugins/Marketplace/angularjs/marketplace/marketplace.controller.js";
+ $jsFiles[] = "plugins/Marketplace/angularjs/marketplace/marketplace.directive.js";
+ }
+
+ public function getClientSideTranslationKeys(&$translationKeys)
+ {
+ $translationKeys[] = 'Marketplace_LicenseKeyActivatedSuccess';
+ $translationKeys[] = 'Marketplace_LicenseKeyDeletedSuccess';
+ }
+
+ public static function isMarketplaceEnabled()
+ {
+ return self::getPluginManager()->isPluginActivated('Marketplace');
+ }
+
+ /**
+ * @return Plugin\Manager
+ */
+ private static function getPluginManager()
+ {
+ return Plugin\Manager::getInstance();
+ }
+
+}
diff --git a/plugins/Marketplace/Menu.php b/plugins/Marketplace/Menu.php
new file mode 100644
index 0000000000..ae67515a9c
--- /dev/null
+++ b/plugins/Marketplace/Menu.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\Marketplace;
+
+use Piwik\Menu\MenuAdmin;
+use Piwik\Piwik;
+
+/**
+ */
+class Menu extends \Piwik\Plugin\Menu
+{
+
+ public function configureAdminMenu(MenuAdmin $menu)
+ {
+ if (!Piwik::isUserIsAnonymous()) {
+ $menu->addPlatformItem('Marketplace_Marketplace',
+ $this->urlForAction('overview', array('activated' => '', 'mode' => 'admin', 'type' => '', 'show' => '')),
+ $order = 5);
+ }
+ }
+
+}
diff --git a/plugins/Marketplace/Plugins.php b/plugins/Marketplace/Plugins.php
new file mode 100644
index 0000000000..558a83c4da
--- /dev/null
+++ b/plugins/Marketplace/Plugins.php
@@ -0,0 +1,318 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\Marketplace;
+
+use Piwik\Date;
+use Piwik\ProfessionalServices\Advertising;
+use Piwik\Plugin\Dependency as PluginDependency;
+use Piwik\Plugin;
+use Piwik\Plugins\Marketplace\Input\PurchaseType;
+use Piwik\Plugins\Marketplace\Input\Sort;
+
+/**
+ *
+ */
+class Plugins
+{
+ /**
+ * @var Api\Client
+ */
+ private $marketplaceClient;
+
+ /**
+ * @var Consumer
+ */
+ private $consumer;
+
+ /**
+ * @var Advertising
+ */
+ private $advertising;
+
+ /**
+ * @var Plugin\Manager
+ */
+ private $pluginManager;
+
+ /**
+ * @internal for tests only
+ * @var array
+ */
+ private $activatedPluginNames = array();
+
+ private $pluginsHavingUpdateCache = null;
+
+ public function __construct(Api\Client $marketplaceClient, Consumer $consumer, Advertising $advertising)
+ {
+ $this->marketplaceClient = $marketplaceClient;
+ $this->consumer = $consumer;
+ $this->advertising = $advertising;
+ $this->pluginManager = Plugin\Manager::getInstance();
+ }
+
+ public function getPluginInfo($pluginName)
+ {
+ $plugin = $this->marketplaceClient->getPluginInfo($pluginName);
+ $plugin = $this->enrichPluginInformation($plugin);
+
+ return $plugin;
+ }
+
+ public function getAvailablePluginNames($themesOnly)
+ {
+ if ($themesOnly) {
+ // we do not use getAllThemes() or getAllPlugins() since those methods would apply a whitelist
+ // github organization filter and here we actually want to get all plugin names.
+ $plugins = $this->marketplaceClient->searchForThemes('', '', Sort::DEFAULT_SORT, PurchaseType::TYPE_ALL);
+ } else {
+ $plugins = $this->marketplaceClient->searchForPlugins('', '', Sort::DEFAULT_SORT, PurchaseType::TYPE_ALL);
+ }
+
+ $names = array();
+ foreach ($plugins as $plugin) {
+ $names[] = $plugin['name'];
+ }
+
+ return $names;
+ }
+
+ public function getAllAvailablePluginNames()
+ {
+ return array_merge(
+ $this->getAvailablePluginNames(true),
+ $this->getAvailablePluginNames(false)
+ );
+ }
+
+ public function searchPlugins($query, $sort, $themesOnly, $purchaseType = '')
+ {
+ if ($themesOnly) {
+ $plugins = $this->marketplaceClient->searchForThemes('', $query, $sort, $purchaseType);
+ } else {
+ $plugins = $this->marketplaceClient->searchForPlugins('', $query, $sort, $purchaseType);
+ }
+
+ foreach ($plugins as $index => $plugin) {
+ $plugins[$index] = $this->enrichPluginInformation($plugin);
+ }
+
+ return array_values($plugins);
+ }
+
+ public function getAllPaidPlugins()
+ {
+ return $this->searchPlugins($query = '', Sort::DEFAULT_SORT, $themes = false, PurchaseType::TYPE_PAID);
+ }
+
+ public function getAllFreePlugins()
+ {
+ return $this->searchPlugins($query = '', Sort::DEFAULT_SORT, $themes = false, PurchaseType::TYPE_FREE);
+ }
+
+ public function getAllThemes()
+ {
+ return $this->searchPlugins($query = '', Sort::DEFAULT_SORT, $themes = true, PurchaseType::TYPE_ALL);
+ }
+
+ public function getAllPlugins()
+ {
+ return $this->searchPlugins($query = '', Sort::DEFAULT_SORT, $themes = false, PurchaseType::TYPE_ALL);
+ }
+
+ private function getPluginUpdateInformation($plugin)
+ {
+ if (empty($plugin['name'])) {
+ return;
+ }
+
+ if (!isset($this->pluginsHavingUpdateCache)) {
+ $this->pluginsHavingUpdateCache = $this->getPluginsHavingUpdate();
+ }
+
+ foreach ($this->pluginsHavingUpdateCache as $pluginHavingUpdate) {
+ if ($plugin['name'] == $pluginHavingUpdate['name']) {
+ return $pluginHavingUpdate;
+ }
+ }
+ }
+
+ /**
+ * for tests only
+ * @internal
+ * @ignore
+ * @param $plugins
+ */
+ public function setPluginsHavingUpdateCache($plugins)
+ {
+ $this->pluginsHavingUpdateCache = $plugins;
+ }
+
+ private function hasPluginUpdate($plugin)
+ {
+ $update = $this->getPluginUpdateInformation($plugin);
+
+ return !empty($update);
+ }
+
+ /**
+ * @param bool $themesOnly
+ * @return array
+ */
+ public function getPluginsHavingUpdate()
+ {
+ $this->pluginManager->loadAllPluginsAndGetTheirInfo();
+ $loadedPlugins = $this->pluginManager->getLoadedPlugins();
+
+ try {
+ $pluginsHavingUpdate = $this->marketplaceClient->getInfoOfPluginsHavingUpdate($loadedPlugins);
+ } catch (\Exception $e) {
+ $pluginsHavingUpdate = array();
+ }
+
+ foreach ($pluginsHavingUpdate as $key => $updatePlugin) {
+ foreach ($loadedPlugins as $loadedPlugin) {
+ if (!empty($updatePlugin['name'])
+ && $loadedPlugin->getPluginName() == $updatePlugin['name']
+ ) {
+ $updatePlugin['currentVersion'] = $loadedPlugin->getVersion();
+ $updatePlugin['isActivated'] = $this->pluginManager->isPluginActivated($updatePlugin['name']);
+ $pluginsHavingUpdate[$key] = $this->addMissingRequirements($updatePlugin);
+ break;
+ }
+ }
+ }
+
+ // remove plugins that have updates but for some reason are not loaded
+ foreach ($pluginsHavingUpdate as $key => $updatePlugin) {
+ if (empty($updatePlugin['currentVersion'])) {
+ unset($pluginsHavingUpdate[$key]);
+ }
+ }
+
+ return $pluginsHavingUpdate;
+ }
+
+ /**
+ * for tests only
+ * @param array $pluginNames
+ * @internal
+ * @ignore
+ */
+ public function setActivatedPluginNames($pluginNames)
+ {
+ $this->activatedPluginNames = $pluginNames;
+ }
+
+ private function isPluginActivated($pluginName)
+ {
+ if (in_array($pluginName, $this->activatedPluginNames)) {
+ return true;
+ }
+
+ return $this->pluginManager->isPluginActivated($pluginName);
+ }
+
+ private function isPluginInstalled($pluginName)
+ {
+ if (in_array($pluginName, $this->activatedPluginNames)) {
+ return true;
+ }
+
+ return $this->pluginManager->isPluginInstalled($pluginName);
+ }
+
+ private function enrichPluginInformation($plugin)
+ {
+ if (empty($plugin)) {
+ return $plugin;
+ }
+
+ $plugin['isInstalled'] = $this->isPluginInstalled($plugin['name']);
+ $plugin['isActivated'] = $this->isPluginActivated($plugin['name']);
+ $plugin['isInvalid'] = $this->pluginManager->isPluginThirdPartyAndBogus($plugin['name']);
+ $plugin['canBeUpdated'] = $plugin['isInstalled'] && $this->hasPluginUpdate($plugin);
+ $plugin['lastUpdated'] = $this->toShortDate($plugin['lastUpdated']);
+ $plugin['hasExceededLicense'] = !empty($plugin['isInstalled']) && !empty($plugin['shop']) && empty($plugin['isFree']) && empty($plugin['isDownloadable']) && !empty($plugin['consumer']['license']['isValid']) && !empty($plugin['consumer']['license']['isExceeded']);
+ $plugin['isMissingLicense'] = !empty($plugin['isInstalled']) && !empty($plugin['shop']) && empty($plugin['isFree']) && empty($plugin['isDownloadable']) && empty($plugin['consumer']['license']);
+
+ if (!empty($plugin['owner'])
+ && strtolower($plugin['owner']) === 'piwikpro'
+ && !empty($plugin['homepage'])
+ && strpos($plugin['homepage'], 'pk_campaign') === false) {
+ $plugin['homepage'] = $this->advertising->addPromoCampaignParametersToUrl($plugin['homepage'], Advertising::CAMPAIGN_NAME_PROFESSIONAL_SERVICES, 'Marketplace', $plugin['name']);
+ }
+
+ if ($plugin['canBeUpdated']) {
+ $pluginUpdate = $this->getPluginUpdateInformation($plugin);
+ $plugin['repositoryChangelogUrl'] = $pluginUpdate['repositoryChangelogUrl'];
+ $plugin['currentVersion'] = $pluginUpdate['currentVersion'];
+ }
+
+ if (!empty($plugin['activity']['lastCommitDate'])
+ && false === strpos($plugin['activity']['lastCommitDate'], '0000')
+ && false === strpos($plugin['activity']['lastCommitDate'], '1970')) {
+ $plugin['activity']['lastCommitDate'] = $this->toLongDate($plugin['activity']['lastCommitDate']);
+ } else {
+ $plugin['activity']['lastCommitDate'] = null;
+ }
+
+ if (!empty($plugin['versions'])) {
+ foreach ($plugin['versions'] as $index => $version) {
+ $plugin['versions'][$index]['release'] = $this->toLongDate($version['release']);
+ }
+ }
+
+ $plugin = $this->addMissingRequirements($plugin);
+
+ return $plugin;
+ }
+
+ private function toLongDate($date)
+ {
+ if (!empty($date)) {
+ $date = Date::factory($date)->getLocalized(Date::DATE_FORMAT_LONG);
+ }
+
+ return $date;
+ }
+
+ private function toShortDate($date)
+ {
+ if (!empty($date)) {
+ $date = Date::factory($date)->getLocalized(Date::DATE_FORMAT_SHORT);
+ }
+
+ return $date;
+ }
+
+ /**
+ * @param $plugin
+ */
+ private function addMissingRequirements($plugin)
+ {
+ $plugin['missingRequirements'] = array();
+
+ if (empty($plugin['versions']) || !is_array($plugin['versions'])) {
+ return $plugin;
+ }
+
+ $latestVersion = $plugin['versions'][count($plugin['versions']) - 1];
+
+ if (empty($latestVersion['requires'])) {
+ return $plugin;
+ }
+
+ $requires = $latestVersion['requires'];
+
+ $dependency = new PluginDependency();
+ $plugin['missingRequirements'] = $dependency->getMissingDependencies($requires);
+
+ return $plugin;
+ }
+}
diff --git a/plugins/Marketplace/Plugins/InvalidLicenses.php b/plugins/Marketplace/Plugins/InvalidLicenses.php
new file mode 100644
index 0000000000..1a0a198f67
--- /dev/null
+++ b/plugins/Marketplace/Plugins/InvalidLicenses.php
@@ -0,0 +1,238 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\Marketplace\Plugins;
+
+use Piwik\Cache;
+use Piwik\Piwik;
+use Piwik\Plugin;
+use Piwik\Plugins\Marketplace\Api\Client;
+use Piwik\Plugins\Marketplace\Plugins;
+use Piwik\Translation\Translator;
+use Piwik\Url;
+
+/**
+ *
+ */
+class InvalidLicenses
+{
+ /**
+ * @var Client
+ */
+ private $client;
+
+ /**
+ * @var Plugin\Manager
+ */
+ private $pluginManager;
+
+ /**
+ * @var Translator
+ */
+ private $translator;
+
+ /**
+ * @var Cache\Eager
+ */
+ private $cache;
+
+ /**
+ * @var array
+ */
+ private $activatedPluginNames = array();
+
+ private $plugins;
+
+ private $cacheKey = 'Marketplace_ExpiredPlugins';
+
+ public function __construct(Client $client, Cache\Eager $cache, Translator $translator, Plugins $plugins)
+ {
+ $this->client = $client;
+ $this->translator = $translator;
+ $this->pluginManager = Plugin\Manager::getInstance();
+ $this->cache = $cache;
+ $this->plugins = $plugins;
+ }
+
+ public function getPluginNamesOfInvalidLicenses()
+ {
+ // it is very important this is cached, otherwise performance may decrease a lot. Eager cache is currently
+ // cached for 12 hours. In case we lower ttl for eager cache it might be worth considering to change to another
+ // cache
+ if ($this->cache->contains($this->cacheKey)) {
+ $expiredPlugins = $this->cache->fetch($this->cacheKey);
+ } else {
+ $expiredPlugins = $this->getPluginNamesToExpireInCaseLicenseIsInvalid();
+ $this->cache->save($this->cacheKey, $expiredPlugins);
+ }
+
+ return $expiredPlugins;
+ }
+
+ public function clearCache()
+ {
+ $this->cache->delete($this->cacheKey);
+ }
+
+ public function getMessageExceededLicenses()
+ {
+ $plugins = $this->getPluginNamesOfInvalidLicenses();
+
+ if (empty($plugins['exceeded'])) {
+ return;
+ }
+
+ $plugins = '<strong>' . implode('</strong>, <strong>', $plugins['exceeded']) . '</strong>';
+ $loginUrl = $this->getLoginLink();
+ $loginUrlEnd = '';
+ if (!empty($loginUrl)) {
+ $loginUrlEnd = '</a>';
+ }
+
+ $message = $this->translator->translate('Marketplace_LicenseExceededDescription', array($plugins, '<br/>', "<strong>" . $loginUrl, $loginUrlEnd . "</strong>"));
+
+ if (Piwik::hasUserSuperUserAccess()) {
+ $message .= ' ' . $this->getSubscritionSummaryMessage();
+ }
+
+ return $message;
+ }
+
+ public function getMessageNoLicense()
+ {
+ $plugins = $this->getPluginNamesOfInvalidLicenses();
+
+ if (empty($plugins['noLicense'])) {
+ return;
+ }
+
+ $plugins = '<strong>' . implode('</strong>, <strong>', $plugins['noLicense']) . '</strong>';
+ $loginUrl = $this->getLoginLink();
+ $loginUrlEnd = '';
+ if (!empty($loginUrl)) {
+ $loginUrlEnd = '</a>';
+ }
+
+ $message = $this->translator->translate('Marketplace_LicenseMissingDescription', array($plugins, '<br/>', "<strong>" . $loginUrl, $loginUrlEnd. "</strong>"));
+
+ if (Piwik::hasUserSuperUserAccess()) {
+ $message .= ' ' . $this->getSubscritionSummaryMessage();
+ }
+
+ return $message;
+ }
+
+ public function getMessageExpiredLicenses()
+ {
+ $plugins = $this->getPluginNamesOfInvalidLicenses();
+
+ if (empty($plugins['expired'])) {
+ return;
+ }
+
+ $plugins = '<strong>' . implode('</strong>, <strong>', $plugins['expired']) . '</strong>';
+ $loginUrl = $this->getLoginLink();
+ $loginUrlEnd = '';
+ if (!empty($loginUrl)) {
+ $loginUrlEnd = '</a>';
+ }
+
+ $message = $this->translator->translate('Marketplace_LicenseExpiredDescription', array($plugins, '<br/>', "<strong>" . $loginUrl, $loginUrlEnd . "</strong>"));
+
+ if (Piwik::hasUserSuperUserAccess()) {
+ $message .= ' ' . $this->getSubscritionSummaryMessage();
+ }
+
+ return $message;
+ }
+
+ private function getLoginLink()
+ {
+ $info = $this->client->getInfo();
+
+ if (empty($info['loginUrl'])) {
+ return '';
+ }
+
+ return '<a href="' . $info['loginUrl'] . '" target="_blank" rel="noreferrer">';
+ }
+
+ private function getSubscritionSummaryMessage()
+ {
+ $url = Url::getCurrentQueryStringWithParametersModified(array(
+ 'module' => 'Marketplace', 'action' => 'subscriptionOverview'
+ ));
+
+ $link = '<a href="' . $url . '">';
+
+ return "<br/>" . $this->translator->translate('Marketplace_ViewSubscriptionsSummary', array($link, '</a>'));
+ }
+
+ private function getPluginNamesToExpireInCaseLicenseIsInvalid()
+ {
+ $pluginNames = array(
+ 'exceeded' => array(),
+ 'expired' => array(),
+ 'noLicense' => array()
+ );
+
+ try {
+ $paidPlugins = $this->plugins->getAllPaidPlugins();
+ } catch (\Exception $e) {
+ return $pluginNames;
+ }
+
+ if (!empty($paidPlugins)) {
+ foreach ($paidPlugins as $plugin) {
+ $pluginName = $plugin['name'];
+ if ($this->isPluginActivated($pluginName)) {
+ if (empty($plugin['consumer']['license'])) {
+ $pluginNames['noLicense'][] = $pluginName;
+ } elseif (!empty($plugin['consumer']['license']['isExceeded'])) {
+ $pluginNames['exceeded'][] = $pluginName;
+ } elseif (isset($plugin['consumer']['license']['isValid'])
+ && empty($plugin['consumer']['license']['isValid'])) {
+ $pluginNames['expired'][] = $pluginName;
+ }
+ }
+ }
+ }
+
+ return $pluginNames;
+ }
+
+ /**
+ * for tests only
+ * @param array $pluginNames
+ * @internal
+ * @ignore
+ */
+ public function setActivatedPluginNames($pluginNames)
+ {
+ $this->activatedPluginNames = $pluginNames;
+ }
+
+ protected function isPluginInstalled($pluginName)
+ {
+ if (in_array($pluginName, $this->activatedPluginNames)) {
+ return true;
+ }
+
+ return $this->pluginManager->isPluginInstalled($pluginName);
+ }
+
+ protected function isPluginActivated($pluginName)
+ {
+ if (in_array($pluginName, $this->activatedPluginNames)) {
+ return true;
+ }
+
+ return $this->pluginManager->isPluginActivated($pluginName);
+ }
+
+}
diff --git a/plugins/CorePluginsAdmin/Tasks.php b/plugins/Marketplace/Tasks.php
index 8354694e3b..81d27d4f62 100644
--- a/plugins/CorePluginsAdmin/Tasks.php
+++ b/plugins/Marketplace/Tasks.php
@@ -6,7 +6,7 @@
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
-namespace Piwik\Plugins\CorePluginsAdmin;
+namespace Piwik\Plugins\Marketplace;
class Tasks extends \Piwik\Plugin\Tasks
{
@@ -15,24 +15,26 @@ class Tasks extends \Piwik\Plugin\Tasks
*/
private $updateCommunication;
- public function __construct(UpdateCommunication $updateCommunication)
+ /**
+ * @var Api\Client
+ */
+ private $api;
+
+ public function __construct(UpdateCommunication $updateCommunication, Api\Client $api)
{
$this->updateCommunication = $updateCommunication;
+ $this->api = $api;
}
public function schedule()
{
$this->daily('clearAllCacheEntries', null, self::LOWEST_PRIORITY);
-
- if (CorePluginsAdmin::isMarketplaceEnabled()) {
- $this->daily('sendNotificationIfUpdatesAvailable', null, self::LOWEST_PRIORITY);
- }
+ $this->daily('sendNotificationIfUpdatesAvailable', null, self::LOWEST_PRIORITY);
}
public function clearAllCacheEntries()
{
- $marketplace = new MarketplaceApiClient();
- $marketplace->clearAllCacheEntries();
+ $this->api->clearAllCacheEntries();
}
public function sendNotificationIfUpdatesAvailable()
diff --git a/plugins/CorePluginsAdmin/UpdateCommunication.php b/plugins/Marketplace/UpdateCommunication.php
index da9f379324..efebfa899b 100644
--- a/plugins/CorePluginsAdmin/UpdateCommunication.php
+++ b/plugins/Marketplace/UpdateCommunication.php
@@ -6,9 +6,10 @@
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
-namespace Piwik\Plugins\CorePluginsAdmin;
+namespace Piwik\Plugins\Marketplace;
use Piwik\Config;
+use Piwik\Container\StaticContainer;
use Piwik\Mail;
use Piwik\Option;
use Piwik\Piwik;
@@ -56,7 +57,7 @@ class UpdateCommunication
{
$isEnabled = Config::getInstance()->General['enable_update_communication'];
- return CorePluginsAdmin::isMarketplaceEnabled() && !empty($isEnabled);
+ return Marketplace::isMarketplaceEnabled() && !empty($isEnabled);
}
/**
@@ -164,13 +165,10 @@ class UpdateCommunication
protected function getPluginsHavingUpdate()
{
- $marketplace = new Marketplace();
- $pluginsHavingUpdate = $marketplace->getPluginsHavingUpdate($themesOnly = false);
- $themesHavingUpdate = $marketplace->getPluginsHavingUpdate($themesOnly = true);
+ $marketplace = StaticContainer::get('Piwik\Plugins\Marketplace\Plugins');
+ $pluginsHavingUpdate = $marketplace->getPluginsHavingUpdate();
- $plugins = array_merge($pluginsHavingUpdate, $themesHavingUpdate);
-
- return $plugins;
+ return $pluginsHavingUpdate;
}
protected function buildNotificationMessage($pluginsToBeNotified, $hasThemeUpdate, $hasPluginUpdate)
diff --git a/plugins/CorePluginsAdmin/Widgets/GetNewPlugins.php b/plugins/Marketplace/Widgets/GetNewPlugins.php
index a45b35e0b6..6079c1a8e8 100644
--- a/plugins/CorePluginsAdmin/Widgets/GetNewPlugins.php
+++ b/plugins/Marketplace/Widgets/GetNewPlugins.php
@@ -6,22 +6,22 @@
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
-namespace Piwik\Plugins\CorePluginsAdmin\Widgets;
+namespace Piwik\Plugins\Marketplace\Widgets;
use Piwik\Common;
-use Piwik\Plugins\CorePluginsAdmin\MarketplaceApiClient;
+use Piwik\Plugins\Marketplace\Api\Client;
+use Piwik\Plugins\Marketplace\Input\Sort;
use Piwik\Widget\Widget;
use Piwik\Widget\WidgetConfig;
-use Piwik\View;
class GetNewPlugins extends Widget
{
/**
- * @var MarketplaceApiClient
+ * @var Client
*/
private $marketplaceApiClient;
- public function __construct(MarketplaceApiClient $marketplaceApiClient)
+ public function __construct(Client $marketplaceApiClient)
{
$this->marketplaceApiClient = $marketplaceApiClient;
}
@@ -29,9 +29,6 @@ class GetNewPlugins extends Widget
public static function configure(WidgetConfig $config)
{
$config->setCategoryId('About Piwik');
- // TODO it actually shows "recently updated plugins currently". Need a new sort filter in the Marketplace
- // TODO when decided whether to show new plugins or recently updated plugins add translation key
- // we want to show new plugins likely (when changed Marketplace to support actually "newest" plugins)
$config->setName('Latest Marketplace Updates');
$config->setOrder(19);
}
@@ -46,7 +43,7 @@ class GetNewPlugins extends Widget
$template = 'getNewPlugins';
}
- $plugins = $this->marketplaceApiClient->searchForPlugins('', '', 'newest');
+ $plugins = $this->marketplaceApiClient->searchForPlugins('', '', Sort::METHOD_LAST_UPDATED, '');
return $this->renderTemplate($template, array(
'plugins' => array_splice($plugins, 0, 3)
diff --git a/plugins/Marketplace/angularjs/licensekey/licensekey.controller.js b/plugins/Marketplace/angularjs/licensekey/licensekey.controller.js
new file mode 100644
index 0000000000..15a3cfa6cc
--- /dev/null
+++ b/plugins/Marketplace/angularjs/licensekey/licensekey.controller.js
@@ -0,0 +1,63 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+(function () {
+ angular.module('piwikApp').controller('PiwikMarketplaceLicenseController', PiwikMarketplaceLicenseController);
+
+ PiwikMarketplaceLicenseController.$inject = ['piwik', 'piwikApi'];
+
+ function PiwikMarketplaceLicenseController(piwik, piwikApi) {
+
+ this.licenseKey = '';
+ this.enableUpdate = false;
+ this.isUpdating = false;
+
+ var self = this;
+
+ function updateLicenseKey(action, licenseKey, onSuccessMessage)
+ {
+
+ piwikApi.withTokenInUrl();
+ piwikApi.post({
+ module: 'API',
+ method: 'Marketplace.' + action,
+ format: 'JSON'
+ }, {licenseKey: licenseKey}).then(function (response) {
+ self.isUpdating = false;
+
+ if (response && response.value) {
+ var UI = require('piwik/UI');
+ var notification = new UI.Notification();
+ notification.show(onSuccessMessage, {context: 'success'});
+
+ piwik.helper.redirect();
+ }
+ }, function () {
+ self.isUpdating = false;
+ });
+ }
+
+ this.updatedLicenseKey = function () {
+ this.enableUpdate = !!this.licenseKey;
+ };
+
+ this.updateLicense = function () {
+ this.enableUpdate = false;
+ this.isUpdating = true;
+
+ updateLicenseKey('saveLicenseKey', this.licenseKey, _pk_translate('Marketplace_LicenseKeyActivatedSuccess'));
+ };
+
+ this.removeLicense = function () {
+ piwik.helper.modalConfirm('#confirmRemoveLicense', {yes: function () {
+ self.enableUpdate = false;
+ self.isUpdating = true;
+ updateLicenseKey('deleteLicenseKey', '', _pk_translate('Marketplace_LicenseKeyDeletedSuccess'));
+ }});
+ };
+
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CorePluginsAdmin/angularjs/marketplace/marketplace.controller.js b/plugins/Marketplace/angularjs/marketplace/marketplace.controller.js
index 0f55e6da31..0f55e6da31 100644
--- a/plugins/CorePluginsAdmin/angularjs/marketplace/marketplace.controller.js
+++ b/plugins/Marketplace/angularjs/marketplace/marketplace.controller.js
diff --git a/plugins/Marketplace/angularjs/marketplace/marketplace.directive.js b/plugins/Marketplace/angularjs/marketplace/marketplace.directive.js
new file mode 100644
index 0000000000..96812865d6
--- /dev/null
+++ b/plugins/Marketplace/angularjs/marketplace/marketplace.directive.js
@@ -0,0 +1,132 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Usage:
+ * <div piwik-marketplace>
+ */
+(function () {
+
+ angular.module('piwikApp').directive('piwikMarketplace', piwikMarketplace);
+
+ piwikMarketplace.$inject = ['piwik', '$timeout'];
+
+ function piwikMarketplace(piwik, $timeout){
+
+ return {
+ restrict: 'A',
+ compile: function (element, attrs) {
+
+ return function (scope, element, attrs) {
+
+ $timeout(function () {
+
+
+ $('.installAllPaidPlugins').click(function (event) {
+ event.preventDefault();
+
+ piwikHelper.modalConfirm('#installAllPaidPluginsAtOnce');
+ });
+
+ $('.uploadPlugin').click(function (event) {
+ event.preventDefault();
+
+ piwikHelper.modalConfirm('#installPluginByUpload', {});
+ });
+
+
+ $('#uploadPluginForm').submit(function (event) {
+
+ var $zipFile = $('[name=pluginZip]');
+ var fileName = $zipFile.val();
+
+ if (!fileName || '.zip' != fileName.slice(-4)) {
+ event.preventDefault();
+ alert(_pk_translate('CorePluginsAdmin_NoZipFileSelected'));
+ }
+ });
+
+ // Keeps the plugin descriptions the same height
+ $('.marketplace .plugin .description').dotdotdot({
+ after: 'a.more',
+ watch: 'window'
+ });
+
+ function syncMaxHeight2 (selector) {
+
+ if (!selector) {
+ return;
+ }
+
+ var $nodes = $(selector);
+
+ if (!$nodes || !$nodes.length) {
+ return;
+ }
+
+ var maxh3 = null;
+ var maxMeta = null;
+ var maxFooter = null;
+ var nodesToUpdate = [];
+ var lastTop = 0;
+ $nodes.each(function (index, node) {
+ var $node = $(node);
+ var top = $node.offset().top;
+
+ if (lastTop !== top) {
+ nodesToUpdate = [];
+ lastTop = top;
+ maxh3 = null;
+ maxMeta = null;
+ maxFooter = null;
+ }
+
+ nodesToUpdate.push($node);
+
+ var heightH3 = $node.find('h3').height();
+ var heightMeta = $node.find('.metadata').height();
+ var heightFooter = $node.find('.footer').height();
+
+ if (!maxh3) {
+ maxh3 = heightH3;
+ } else if (maxh3 < heightH3) {
+ maxh3 = heightH3;
+ }
+
+ if (!maxMeta) {
+ maxMeta = heightMeta;
+ } else if (maxMeta < heightMeta) {
+ maxMeta = heightMeta;
+ }
+
+ if (!maxFooter) {
+ maxFooter = heightFooter;
+ } else if (maxFooter < heightFooter) {
+ maxFooter = heightFooter;
+ }
+
+ $.each(nodesToUpdate, function (index, $node) {
+ if (maxh3) {
+ $node.find('h3').height(maxh3 + 'px');
+ }
+ if (maxMeta) {
+ $node.find('.metadata').height(maxMeta + 'px');
+ }
+ if (maxFooter) {
+ $node.find('.footer').height(maxFooter + 'px');
+ }
+ });
+ });
+ }
+ syncMaxHeight2('.marketplace .plugin');
+
+ });
+ };
+ }
+ };
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CorePluginsAdmin/angularjs/plugins/plugin-name.directive.js b/plugins/Marketplace/angularjs/plugins/plugin-name.directive.js
index 26c38c6443..aeaacea5b7 100644
--- a/plugins/CorePluginsAdmin/angularjs/plugins/plugin-name.directive.js
+++ b/plugins/Marketplace/angularjs/plugins/plugin-name.directive.js
@@ -20,7 +20,7 @@
pluginName = value.substr(0, value.indexOf('!'));
}
- var url = 'module=CorePluginsAdmin&action=pluginDetails&pluginName=' + encodeURIComponent(pluginName);
+ var url = 'module=Marketplace&action=pluginDetails&pluginName=' + encodeURIComponent(pluginName);
if (activeTab) {
url += '&activeTab=' + encodeURIComponent(activeTab);
diff --git a/plugins/Marketplace/config/config.php b/plugins/Marketplace/config/config.php
new file mode 100644
index 0000000000..ffab9b07b4
--- /dev/null
+++ b/plugins/Marketplace/config/config.php
@@ -0,0 +1,32 @@
+<?php
+
+use Interop\Container\ContainerInterface;
+use Piwik\Plugins\Marketplace\Api\Service;
+use Piwik\Plugins\Marketplace\LicenseKey;
+
+return array(
+ 'MarketplaceEndpoint' => function (ContainerInterface $c) {
+ $domain = 'http://plugins.piwik.org';
+ $updater = $c->get('Piwik\Plugins\CoreUpdater\Updater');
+
+ if ($updater->isUpdatingOverHttps()) {
+ $domain = str_replace('http://', 'https://', $domain);
+ }
+
+ return $domain;
+ },
+ 'Piwik\Plugins\Marketplace\Api\Service' => function (ContainerInterface $c) {
+ /** @var \Piwik\Plugins\Marketplace\Api\Service $previous */
+
+ $domain = $c->get('MarketplaceEndpoint');
+
+ $service = new Service($domain);
+
+ $key = new LicenseKey();
+ $accessToken = $key->get();
+
+ $service->authenticate($accessToken);
+
+ return $service;
+ }
+); \ No newline at end of file
diff --git a/plugins/Marketplace/config/test.php b/plugins/Marketplace/config/test.php
new file mode 100644
index 0000000000..8c21be15f8
--- /dev/null
+++ b/plugins/Marketplace/config/test.php
@@ -0,0 +1,144 @@
+<?php
+
+use Interop\Container\ContainerInterface;
+use Piwik\Plugins\Marketplace\tests\Framework\Mock\Consumer as MockConsumer;
+use Piwik\Plugins\Marketplace\LicenseKey;
+use Piwik\Plugins\Marketplace\tests\Framework\Mock\Service as MockService;
+use Piwik\Plugins\Marketplace\Input\PurchaseType;
+
+return array(
+ 'MarketplaceEndpoint' => function (ContainerInterface $c) {
+ // if you wonder why this here is configured here again, and the same as in `config.php`,
+ // it is because someone might have overwritten MarketplaceEndpoit in local config.php and we want
+ // to make sure system tests of marketplace are ran against plugins.piwik.org
+ $domain = 'http://plugins.piwik.org';
+ $updater = $c->get('Piwik\Plugins\CoreUpdater\Updater');
+
+ if ($updater->isUpdatingOverHttps()) {
+ $domain = str_replace('http://', 'https://', $domain);
+ }
+
+ return $domain;
+ },
+ 'Piwik\Plugins\Marketplace\Consumer' => function (ContainerInterface $c) {
+ $consumerTest = $c->get('test.vars.consumer');
+ $licenseKey = new LicenseKey();
+
+ if ($consumerTest == 'validLicense') {
+ $consumer = MockConsumer::buildValidLicense();
+ $licenseKey->set('123456789');
+ } elseif ($consumerTest == 'exceededLicense') {
+ $consumer = MockConsumer::buildExceededLicense();
+ $licenseKey->set('1234567891');
+ } elseif ($consumerTest == 'expiredLicense') {
+ $consumer = MockConsumer::buildExpiredLicense();
+ $licenseKey->set('1234567892');
+ } else {
+ $consumer = MockConsumer::buildNoLicense();
+ $licenseKey->set(null);
+ }
+
+ return $consumer;
+ },
+ 'Piwik\Plugins\Marketplace\Plugins' => DI\decorate(function ($previous, ContainerInterface $c) {
+ /** @var \Piwik\Plugins\Marketplace\Plugins $previous */
+ $previous->setPluginsHavingUpdateCache(null);
+
+ $pluginNames = $c->get('test.vars.mockMarketplaceAssumePluginNamesActivated');
+
+ if (!empty($pluginNames)) {
+ /** @var \Piwik\Plugins\Marketplace\Plugins $previous */
+ $previous->setActivatedPluginNames($pluginNames);
+ }
+
+ return $previous;
+ }),
+ 'Piwik\Plugins\Marketplace\Api\Client' => DI\decorate(function ($previous) {
+ /** @var \Piwik\Plugins\Marketplace\Api\Client $previous */
+ $previous->clearAllCacheEntries();
+
+ return $previous;
+ }),
+ 'Piwik\Plugins\Marketplace\Plugins\InvalidLicenses' => DI\decorate(function ($previous, ContainerInterface $c) {
+
+ $pluginNames = $c->get('test.vars.mockMarketplaceAssumePluginNamesActivated');
+
+ if (!empty($pluginNames)) {
+ /** @var \Piwik\Plugins\Marketplace\Plugins\InvalidLicenses $previous */
+ $previous->setActivatedPluginNames($pluginNames);
+ $previous->clearCache();
+ }
+
+ return $previous;
+
+ }),
+ 'Piwik\Plugins\Marketplace\Api\Service' => DI\decorate(function ($previous, ContainerInterface $c) {
+ if (!$c->get('test.vars.mockMarketplaceApiService')) {
+ return $previous;
+ }
+
+ // for ui tests
+ $service = new MockService();
+
+ $key = new LicenseKey();
+ $accessToken = $key->get();
+
+ $service->authenticate($accessToken);
+
+ function removeReviewsUrl($content)
+ {
+ $content = json_decode($content, true);
+ if (!empty($content['shop']['reviews']['embedUrl'])) {
+ $content['shop']['reviews']['embedUrl'] = '';
+ }
+ return json_encode($content);
+ }
+
+ $isExceededUser = $c->get('test.vars.consumer') === 'exceededLicense';
+ $isExpiredUser = $c->get('test.vars.consumer') === 'expiredLicense';
+ $isValidUser = $c->get('test.vars.consumer') === 'validLicense';
+
+ $service->setOnDownloadCallback(function ($action, $params) use ($service, $isExceededUser, $isValidUser, $isExpiredUser) {
+ if ($action === 'info') {
+ return $service->getFixtureContent('v2.0_info.json');
+ } elseif ($action === 'consumer' && $service->getAccessToken() === 'valid') {
+ return $service->getFixtureContent('v2.0_consumer-access_token-consumer2_paid1.json');
+ } elseif ($action === 'consumer/validate' && $service->getAccessToken() === 'valid') {
+ return $service->getFixtureContent('v2.0_consumer_validate-access_token-consumer2_paid1.json');
+ } elseif ($action === 'consumer' && $service->getAccessToken() === 'invalid') {
+ return $service->getFixtureContent('v2.0_consumer-access_token-notexistingtoken.json');
+ } elseif ($action === 'consumer/validate' && $service->getAccessToken() === 'invalid') {
+ return $service->getFixtureContent('v2.0_consumer_validate-access_token-notexistingtoken.json');
+ } elseif ($action === 'plugins' && empty($params['purchase_type']) && empty($params['query'])) {
+ return $service->getFixtureContent('v2.0_plugins.json');
+ } elseif ($action === 'plugins' && $isExceededUser && !empty($params['purchase_type']) && $params['purchase_type'] === PurchaseType::TYPE_PAID && empty($params['query'])) {
+ return $service->getFixtureContent('v2.0_plugins-purchase_type-paid-num_users-201-access_token-consumer2_paid1.json');
+ } elseif ($action === 'plugins' && $isExpiredUser && !empty($params['purchase_type']) && $params['purchase_type'] === PurchaseType::TYPE_PAID && empty($params['query'])) {
+ return $service->getFixtureContent('v2.0_plugins-purchase_type-paid-access_token-consumer1_paid2_custom1.json');
+ } elseif ($action === 'plugins' && ($service->hasAccessToken() || $isValidUser) && !empty($params['purchase_type']) && $params['purchase_type'] === PurchaseType::TYPE_PAID && empty($params['query'])) {
+ return $service->getFixtureContent('v2.0_plugins-purchase_type-paid-access_token-consumer2_paid1.json');
+ } elseif ($action === 'plugins' && !$service->hasAccessToken() && !empty($params['purchase_type']) && $params['purchase_type'] === PurchaseType::TYPE_PAID && empty($params['query'])) {
+ return $service->getFixtureContent('v2.0_plugins-purchase_type-paid-access_token-notexistingtoken.json');
+ } elseif ($action === 'themes' && empty($params['purchase_type']) && empty($params['query'])) {
+ return $service->getFixtureContent('v2.0_themes.json');
+ } elseif ($action === 'plugins/Barometer/info') {
+ return $service->getFixtureContent('v2.0_plugins_Barometer_info.json');
+ } elseif ($action === 'plugins/TreemapVisualization/info') {
+ return $service->getFixtureContent('v2.0_plugins_TreemapVisualization_info.json');
+ } elseif ($action === 'plugins/PaidPlugin1/info' && $service->hasAccessToken() && $isExceededUser) {
+ $content = $service->getFixtureContent('v2.0_plugins_PaidPlugin1_info-purchase_type-paid-num_users-201-access_token-consumer2_paid1.json');
+ return removeReviewsUrl($content);
+ } elseif ($action === 'plugins/PaidPlugin1/info' && $service->hasAccessToken()) {
+ $content = $service->getFixtureContent('v2.0_plugins_PaidPlugin1_info-access_token-consumer3_paid1_custom2.json');
+ return removeReviewsUrl($content);
+ } elseif ($action === 'plugins/PaidPlugin1/info' && !$service->hasAccessToken()) {
+ $content = $service->getFixtureContent('v2.0_plugins_PaidPlugin1_info.json');
+ return removeReviewsUrl($content);
+ } elseif ($action === 'plugins/checkUpdates') {
+ return $service->getFixtureContent('v2.0_plugins_checkUpdates-pluginspluginsnameAnonymousPi.json');
+ }
+ });
+
+ return $service;
+ })
+); \ No newline at end of file
diff --git a/plugins/CorePluginsAdmin/images/rating_important.png b/plugins/Marketplace/images/rating_important.png
index 662df523a8..662df523a8 100755
--- a/plugins/CorePluginsAdmin/images/rating_important.png
+++ b/plugins/Marketplace/images/rating_important.png
Binary files differ
diff --git a/plugins/Marketplace/lang/en.json b/plugins/Marketplace/lang/en.json
new file mode 100644
index 0000000000..1278adf2c2
--- /dev/null
+++ b/plugins/Marketplace/lang/en.json
@@ -0,0 +1,112 @@
+{
+ "Marketplace": {
+ "ActivateLicenseKey": "Activate",
+ "ActionActivatePlugin": "Activate plugin",
+ "ActionActivateTheme": "Activate theme",
+ "ActionInstall": "Install",
+ "AddToCart": "Add to cart",
+ "AllowedUploadFormats": "You may upload a plugin or theme in .zip format via this page.",
+ "Authors": "Authors",
+ "BackToMarketplace": "Back to Marketplace",
+ "BrowseMarketplace": "Browse Marketplace",
+ "ByXDevelopers": "by %s developers",
+ "CannotInstall": "Cannot install",
+ "CannotUpdate": "Cannot update",
+ "ClickToCompletePurchase": "Click to complete purchase.",
+ "CurrentNumPiwikUsers": "Your Piwik currently has %1$s registered users.",
+ "ConfirmRemoveLicense": "Are you sure you want to remove your license key? You will no longer receive any updates for any of your purchased plugins.",
+ "Developer": "Developer",
+ "DevelopersLearnHowToDevelopPlugins": "Developers: Learn how you can extend and customize Piwik by %1$sdeveloping plugins or themes%2$s.",
+ "Marketplace": "Marketplace",
+ "PaidPlugins": "Premium Features",
+ "FeaturedPlugin": "Featured plugin",
+ "InstallingNewPluginViaMarketplaceOrUpload": "You may automatically install %1$s from the Marketplace or %2$supload a %3$s%4$s in .zip format.",
+ "InstallingPlugin": "Installing %s",
+ "InstallPurchasedPlugins": "Install purchased plugins",
+ "LastCommitTime": "(last commit %s)",
+ "LastUpdated": "Last Updated",
+ "License": "License",
+ "LicenseKey": "License key",
+ "LicenseKeyActivatedSuccess": "License key successfully activated!",
+ "LicenseKeyDeletedSuccess": "License key successfully deleted.",
+ "Exceeded": "Exceeded",
+ "LicenseMissing": "License missing",
+ "LicenseMissingDescription": "You are using the following plugins without a license: %1$s. %2$sTo resolve this issue either update your license key, %3$sget a subscription now%4$s or deactivate the plugin.",
+ "PluginLicenseMissingDescription": "You are not allowed to download this plugin because there is no license for this plugin. To resolve this issue either update your license key, get a subscription or uninstall the plugin.",
+ "LicenseExceeded": "License exceeded",
+ "LicenseExceededDescription": "The licenses for the following plugins are no longer valid as the number of authorized users for the license is exceeded: %1$s. %2$sYou will not be able to download updates for these plugins. To resolve this issue either delete some users or %3$supgrade the subscription now%4$s.",
+ "PluginLicenseExceededDescription": "You are not allowed to download this plugin. The license for this plugin is no longer valid as the number of authorized users for the license is exceeded. To resolve this issue either delete some users or upgrade your subscription now.",
+ "LicenseExpired": "License expired",
+ "LicenseExpiredDescription": "The licenses for the following plugins are expired: %1$s. %2$sYou will no longer receive any updates for these plugins. To resolve this issue either %3$srenew your subscription now%4$s, or deactivate the plugin if you no longer use it.",
+ "LicenseRenewsNextPaymentDate": "Renews at next payment date",
+ "UpgradeSubscription": "Upgrade Subscription",
+ "ViewSubscriptionsSummary": "%1$sView your plugin subscriptions.%2$s",
+ "ViewSubscriptions": "View subscriptions",
+ "ExceptionLinceseKeyIsExpired": "This license key is expired.",
+ "ExceptionLinceseKeyIsNotValid": "This license key is not valid.",
+ "LicenseKeyIsValidShort": "License key is valid!",
+ "RemoveLicenseKey": "Remove license key",
+ "InstallAllPurchasedPlugins": "Install all purchased plugins at once",
+ "InstallAllPurchasedPluginsAction": "Install and activate %d purchased plugins",
+ "InstallThesePlugins": "This will install and activate the following plugins:",
+ "AllPaidPluginsInstalledAndActivated": "All paid plugins were successfully installed and activated.",
+ "OnlySomePaidPluginsInstalledAndActivated": "Some paid plugins were not installed successfully.",
+ "NewVersion": "new version",
+ "NotAllowedToBrowseMarketplacePlugins": "You can browse the list of plugins that can be installed to customize or extend your Piwik platform. Please contact your administrator if you need any of these installed.",
+ "NotAllowedToBrowseMarketplaceThemes": "You can browse the list of themes that can be installed to customize the appearance of the Piwik platform. Please contact your administrator to get any of these installed for you.",
+ "NoPluginsFound": "No plugins found",
+ "NoThemesFound": "No themes found",
+ "NoSubscriptionsFound": "No subscriptions found",
+ "NumDownloadsLatestVersion": "Latest version: %s Downloads",
+ "OverviewPluginSubscriptions": "Overview of your plugin subscriptions",
+ "OverviewPluginSubscriptionsMissingLicense": "You do not have a license key set. If you have purchased a plugin subscription, go to the %1$sMarketplace%2$s and enter your license key.",
+ "OverviewPluginSubscriptionsAllDetails": "To see all details, or to change a subscription, log into your account.",
+ "OverviewPluginSubscriptionsMissingInfo": "It may be possible that a subscription is missing, for example if a payment is not through yet. In such a case try again in a few hours, or contact the Piwik team.",
+ "NoValidSubscriptionNoUpdates": "Once a subscription is expired you will no longer receive any updates for this plugin.",
+ "PluginSubscriptionsList": "This is a list of subscriptions associated with your license key.",
+ "PaidPluginsNoLicenseKeyIntro": "If you have purchased a %1$spremium paid plugin%2$s, please insert the received licence key below.",
+ "PaidPluginsWithLicenseKeyIntro": "A valid license key has been set up. For security reasons we do not show the license key here. If you have lost your license key please contact the Piwik team.",
+ "PaidPluginsNoLicenseKeyIntroNoSuperUserAccess": "In case you have purchased a %1$spremium paid plugin%2$s on the Marketplace please ask a user with Super User access to add the license key.",
+ "PluginDescription": "Extend and expand the functionality of Piwik via the Marketplace by downloading plugins and themes.",
+ "PluginKeywords": "Keywords",
+ "PluginUpdateAvailable": "You are using version %1$s and a new version %2$s is available.",
+ "PluginVersionInfo": "%1$s from %2$s",
+ "PluginWebsite": "Plugin Website",
+ "PriceExclTax": "%1$s %2$s excl. tax.",
+ "PriceFromPerPeriod": "From %1$s / %2$s",
+ "Reviews": "Reviews",
+ "ShownPriceIsExclTax": "Shown price is excl. tax.",
+ "Screenshots": "Screenshots",
+ "SortByNewest": "Newest",
+ "SortByAlpha": "Alpha",
+ "SortByLastUpdated": "Last updated",
+ "SortByPopular": "Popular",
+ "StepDownloadingPluginFromMarketplace": "Downloading plugin from Marketplace",
+ "StepDownloadingThemeFromMarketplace": "Downloading theme from Marketplace",
+ "StepUnzippingPlugin": "Unzipping plugin",
+ "StepUnzippingTheme": "Unzipping theme",
+ "StepThemeSuccessfullyInstalled": "You have successfully installed the theme %1$s %2$s.",
+ "StepPluginSuccessfullyInstalled": "You have successfully installed the plugin %1$s %2$s.",
+ "StepPluginSuccessfullyUpdated": "You have successfully updated the plugin %1$s %2$s.",
+ "StepReplaceExistingPlugin": "Replacing existing plugin",
+ "StepReplaceExistingTheme": "Replacing existing theme",
+ "StepThemeSuccessfullyUpdated": "You have successfully updated the theme %1$s %2$s.",
+ "SubscriptionType": "Type",
+ "SubscriptionStartDate": "Start date",
+ "SubscriptionEndDate": "End date",
+ "SubscriptionNextPaymentDate": "Next payment date",
+ "SubscriptionInvalid": "This subscription is invalid or expired",
+ "SubscriptionExpiresSoon": "This subscription expires soon",
+ "Support": "Support",
+ "TeaserExtendPiwikByUpload": "Extend Piwik by uploading a ZIP file",
+ "LicenseExceededPossibleCause": "The license is exceeded. There are possibly more users on this Piwik installation than the subscription authorizes.",
+ "Updated": "Updated",
+ "UpdatingPlugin": "Updating %1$s",
+ "UploadZipFile": "Upload ZIP file",
+ "LicenseKeyExpiresSoon": "Your license key expires soon, please contact %1$s.",
+ "LicenseKeyIsExpired": "Your license key is expired, please contact %1$s.",
+ "MultiServerEnvironmentWarning": "You cannot install or update the plugin directly as you are using Piwik on multiple servers. The plugin would be only installed on one server. Instead download the plugin and deploy it manually to all your servers.",
+ "AutoUpdateDisabledWarning": "You cannot install or update the plugin directly as automatic updates are disabled in the config. To enable automatic updates set %1$s in %2$s.",
+ "ViewRepositoryChangelog": "View the changes"
+ }
+} \ No newline at end of file
diff --git a/plugins/CorePluginsAdmin/stylesheets/marketplace-widget.less b/plugins/Marketplace/stylesheets/marketplace-widget.less
index 3438983825..3438983825 100644
--- a/plugins/CorePluginsAdmin/stylesheets/marketplace-widget.less
+++ b/plugins/Marketplace/stylesheets/marketplace-widget.less
diff --git a/plugins/CorePluginsAdmin/stylesheets/marketplace.less b/plugins/Marketplace/stylesheets/marketplace.less
index 3c1371ffb8..2a4115e60f 100644
--- a/plugins/CorePluginsAdmin/stylesheets/marketplace.less
+++ b/plugins/Marketplace/stylesheets/marketplace.less
@@ -1,8 +1,63 @@
+.subscriptionOverview {
+ .icon-error, .errorMessage {
+ color: #D4291F;
+ }
+
+ .icon-ok {
+ color: #009874;
+ }
+
+ .icon-warning {
+ color: #CA8100;
+ }
+ .subscriptionName,
+ .subscriptionType,
+ .subscriptionStatus {
+ white-space: nowrap;
+ }
+}
+
+.marketplace-paid-intro {
+ .licenseKeyText {
+ min-width: 210px;
+ .form-group {
+ margin-top: 0;
+ }
+ }
+
+ .licenseToolbar {
+ > a, > div:not(.licenseKeyText) {
+ margin-right: 16px;
+ white-space: nowrap;
+ }
+ }
+
+}
+
+#installAllPaidPluginsAtOnce {
+ ul {
+ li {
+ list-style-type: disc;
+ list-style-position: inside;
+ margin-top: 8px;
+ }
+ }
+
+ a.btn {
+ color: #fff;
+ }
+}
+
.marketplace {
- > .row {
+ .marketplaceActions.row,
+ .pluginListContainer.row {
margin: 0 -0.75rem;
}
+ input.btn[disabled] {
+ background-color: @theme-color-brand !important;
+ }
+
.marketplaceActions {
margin-bottom: 0;
}
@@ -22,7 +77,7 @@
.plugin {
h3 {
- word-break: break-all;
+ word-wrap: break-word;
}
text-align: center;
.description {
@@ -40,6 +95,12 @@
max-width: 250px;
width: 100%;
}
+ .footer {
+ .download.plugin-details {
+ padding-left: 0;
+ padding-right: 0;
+ }
+ }
.metadata {
color: @color-silver-l50;
font-size: 95%;
@@ -70,15 +131,24 @@
}
.footer {
padding: 12px 40px;
+
+ .btn-link.plugin-details {
+ padding-left: 0;
+ padding-right: 0;
+ }
+
+ .purchaseable {
+ background-color: #1e93d1;
+ }
}
}
.footer-message {
margin-top:30px;
- font-style: italic;
}
}
+
#installPluginByUpload {
.description {
margin-top: 30px;
@@ -88,4 +158,4 @@
margin-top: 20px;
margin-bottom: 20px;
}
-}
+} \ No newline at end of file
diff --git a/plugins/CorePluginsAdmin/stylesheets/plugin-details.less b/plugins/Marketplace/stylesheets/plugin-details.less
index 1dee1eb224..445dcc7d21 100644
--- a/plugins/CorePluginsAdmin/stylesheets/plugin-details.less
+++ b/plugins/Marketplace/stylesheets/plugin-details.less
@@ -3,45 +3,44 @@
text-align: left;
line-height: 20px;
- .header {
- .intro {
- width:75%;
- float:left;
- margin-bottom: 8px;
- }
- .actionButton {
- width:25%;
- float:left;
- }
+ > .row > .col {
+ padding-left: 0;
+ padding-right: 0;
}
#pluginDetailsTabs > .col {
padding: 0 16px 0 0;
}
- .content {
- .contentDetails {
- width:75%;
- float:left;
- }
- .metadata {
- width:25%;
- float:left;
- padding-top: 16px;
- }
- }
-
h3, h4, h5, h6 {
- margin: 20px 0px 10px 0px;
+ margin: 20px 0 10px 0;
color: #000000;
}
- p {
+ iframe{
+ width:100%;
+ border: 0;
+ background-image: url('plugins/Morpheus/images/loading-blue.gif');
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+
+ .tab-content ul, .tab-content ol {
+ list-style: initial;
+ padding-left: 20px;
+ }
+
+ p, .tab-content ul, .tab-content li {
text-align: left;
line-height: 20px;
+ list-style: initial;
+ }
+
+ #pluginDetailsTabs {
+ margin-top: 25px;
}
- .content p {
+ .contentDetails p {
margin: 0 0 10px;
}
@@ -49,6 +48,20 @@
padding-right: 25px;
}
+ .variationPicker {
+ margin-top: 0;
+ margin-bottom: 15px;
+ input.select-dropdown {
+ font-size: 15px;
+ }
+ }
+
+ .contentDetails a {
+ color: @theme-color-link;
+ text-decoration: none;
+ }
+
+ .download,
.install {
padding: 11px 19px;
font-size: 17.5px;
@@ -61,6 +74,7 @@
text-decoration: none;
}
+ .download:hover,
.install:hover {
text-decoration: underline;
}
diff --git a/plugins/CorePluginsAdmin/templates/getNewPlugins.twig b/plugins/Marketplace/templates/getNewPlugins.twig
index 0c68e552e3..0c68e552e3 100644
--- a/plugins/CorePluginsAdmin/templates/getNewPlugins.twig
+++ b/plugins/Marketplace/templates/getNewPlugins.twig
diff --git a/plugins/CorePluginsAdmin/templates/getNewPluginsAdmin.twig b/plugins/Marketplace/templates/getNewPluginsAdmin.twig
index b929a48430..b929a48430 100644
--- a/plugins/CorePluginsAdmin/templates/getNewPluginsAdmin.twig
+++ b/plugins/Marketplace/templates/getNewPluginsAdmin.twig
diff --git a/plugins/Marketplace/templates/installPlugin.twig b/plugins/Marketplace/templates/installPlugin.twig
new file mode 100644
index 0000000000..7182ed9d16
--- /dev/null
+++ b/plugins/Marketplace/templates/installPlugin.twig
@@ -0,0 +1,41 @@
+{% extends 'admin.twig' %}
+
+{% block content %}
+
+ <div style="max-width:980px;">
+
+ <h2>{{ 'Marketplace_InstallingPlugin'|translate(plugin.name) }}</h2>
+
+ <div>
+
+ {% if plugin.isTheme %}
+
+ <p>{{ 'Marketplace_StepDownloadingThemeFromMarketplace'|translate }}</p>
+
+ <p>{{ 'Marketplace_StepUnzippingTheme'|translate }}</p>
+
+ <p>{{ 'Marketplace_StepThemeSuccessfullyInstalled'|translate(plugin.name, plugin.latestVersion) }}</p>
+
+ <p><strong><a class="btn" href="{{ linkTo({'module': 'CorePluginsAdmin', 'action': 'activate', 'pluginName': plugin.name, 'nonce': nonce, 'redirectTo': 'marketplace'}) }}">{{ 'Marketplace_ActionActivateTheme'|translate }}</a></strong>
+
+ |
+ <a href="{{ linkTo({'module': 'Marketplace', 'action': 'overview', 'show': 'themes'}) }}">{{ 'Marketplace_BackToMarketplace'|translate }}</a></p>
+
+ {% else %}
+
+ <p>{{ 'Marketplace_StepDownloadingPluginFromMarketplace'|translate }}</p>
+
+ <p>{{ 'Marketplace_StepUnzippingPlugin'|translate }}</p>
+
+ <p>{{ 'Marketplace_StepPluginSuccessfullyInstalled'|translate(plugin.name, plugin.latestVersion) }}</p>
+
+ <p><strong><a class="btn" href="{{ linkTo({'module': 'CorePluginsAdmin', 'action': 'activate', 'pluginName': plugin.name, 'nonce': nonce, 'redirectTo': 'marketplace'}) }}">{{ 'Marketplace_ActionActivatePlugin'|translate }}</a></strong>
+
+ |
+ <a href="{{ linkTo({'module': 'Marketplace', 'action': 'overview', 'show': ''}) }}">{{ 'Marketplace_BackToMarketplace'|translate }}</a></p>
+
+ {% endif %}
+ </div>
+ </div>
+
+{% endblock %}
diff --git a/plugins/Marketplace/templates/licenseform.twig b/plugins/Marketplace/templates/licenseform.twig
new file mode 100644
index 0000000000..c43a4fd4df
--- /dev/null
+++ b/plugins/Marketplace/templates/licenseform.twig
@@ -0,0 +1,75 @@
+{% set defaultLicenseKeyFields %}
+ <div piwik-field uicontrol="text" name="license_key"
+ class="valign licenseKeyText"
+ full-width="true"
+ ng-model="licenseController.licenseKey"
+ ng-change="licenseController.updatedLicenseKey()"
+ placeholder="{% if isValidConsumer %}{{ 'Marketplace_LicenseKeyIsValidShort'|translate }}{% else %}{{ 'Marketplace_LicenseKey'|translate|e('html_attr') }}{% endif %}">
+ </div>
+ <div piwik-save-button
+ class="valign"
+ onconfirm="licenseController.updateLicense()"
+ disabled="!licenseController.enableUpdate"
+ value="{% if hasLicenseKey %}{{ 'CoreUpdater_UpdateTitle'|translate|e('html_attr') }}{% else %}{{ 'Marketplace_ActivateLicenseKey'|translate|e('html_attr') }}{% endif %}"
+ id="submit_license_key"></div>
+{% endset %}
+
+<div class="marketplace-max-width" ng-controller="PiwikMarketplaceLicenseController as licenseController">
+ <div class="marketplace-paid-intro">
+ {% if isValidConsumer %}
+ {% if isSuperUser %}
+ {{ 'Marketplace_PaidPluginsWithLicenseKeyIntro'|translate('')|raw }}
+ <br/>
+
+ <div class="licenseToolbar valign-wrapper">
+ {{ defaultLicenseKeyFields|raw }}
+
+ <div piwik-save-button
+ class="valign"
+ id="remove_license_key"
+ onconfirm="licenseController.removeLicense()"
+ value="{{ 'Marketplace_RemoveLicenseKey'|translate|e('html_attr') }}"
+ ></div>
+
+ <a href="{{ linkTo({'action': 'subscriptionOverview'}) }}" class="btn valign">
+ {{ 'Marketplace_ViewSubscriptions'|translate }}
+ </a>
+
+ {% if isAutoUpdatePossible and isPluginsAdminEnabled and paidPluginsToInstallAtOnce|length %}
+ <a href="javascript:;" class="btn installAllPaidPlugins valign">
+ {{ 'Marketplace_InstallPurchasedPlugins'|translate }}
+ </a>
+ {% include '@Marketplace/paid-plugins-install-list.twig' %}
+ {% endif %}
+
+ </div>
+
+ <div piwik-activity-indicator loading="licenseController.isUpdating"></div>
+ {% endif %}
+
+ {% else %}
+ {% if isSuperUser %}
+ {{ 'Marketplace_PaidPluginsNoLicenseKeyIntro'|translate("<a target='_blank' href='?module=Proxy&action=redirect&url=https://plugins.piwik.org/premium'>", "</a>")|raw }}
+
+ <br/>
+
+ <div class="licenseToolbar valign-wrapper">
+ {{ defaultLicenseKeyFields|raw }}
+ </div>
+
+ <div piwik-activity-indicator loading="licenseController.isUpdating"></div>
+
+ {% else %}
+ {{ 'Marketplace_PaidPluginsNoLicenseKeyIntroNoSuperUserAccess'|translate("<a target='_blank' href='?module=Proxy&action=redirect&url=https://plugins.piwik.org/premium'>", "</a>")|raw }}
+ {% endif %}
+
+ {% endif %}
+ </div>
+</div>
+
+
+<div class="ui-confirm" id="confirmRemoveLicense">
+ <h2>{{ 'Marketplace_ConfirmRemoveLicense'|translate }}</h2>
+ <input role="yes" type="button" value="{{ 'General_Yes'|translate }}"/>
+ <input role="no" type="button" value="{{ 'General_No'|translate }}"/>
+</div>
diff --git a/plugins/Marketplace/templates/macros.twig b/plugins/Marketplace/templates/macros.twig
new file mode 100644
index 0000000000..50aa06af8d
--- /dev/null
+++ b/plugins/Marketplace/templates/macros.twig
@@ -0,0 +1,25 @@
+
+{% macro pluginDeveloper(owner) %}
+ {% if 'piwik' == owner %}<img title="Piwik" alt="Piwik" style="padding-bottom:2px;height:11px;" src="plugins/Morpheus/images/logo-marketplace.png"/>{% else %}{{ owner }}{% endif %}
+{% endmacro %}
+
+{% macro featuredIcon(align='') %}
+ <img class="featuredIcon"
+ title="{{ 'Marketplace_FeaturedPlugin'|translate }}"
+ src="plugins/Marketplace/images/rating_important.png"
+ align="{{ align }}" />
+{% endmacro %}
+
+{% macro missingRequirementsPleaseUpdateNotice(plugin) %}
+ {% if plugin.missingRequirements and 0 < plugin.missingRequirements|length %}
+ {% for req in plugin.missingRequirements -%}
+ <div class="alert alert-danger">
+ {% set requirement = req.requirement|capitalize %}
+ {% if 'Php' == requirement %}
+ {% set requirement = 'PHP' %}
+ {% endif %}
+ {{ 'CorePluginsAdmin_MissingRequirementsNotice'|translate(requirement, req.actualVersion, req.requiredVersion) }}
+ </div>
+ {%- endfor %}
+ {% endif %}
+{% endmacro %}
diff --git a/plugins/CorePluginsAdmin/templates/marketplace.twig b/plugins/Marketplace/templates/overview.twig
index 61f46f8cd2..9541ab0a16 100644
--- a/plugins/CorePluginsAdmin/templates/marketplace.twig
+++ b/plugins/Marketplace/templates/overview.twig
@@ -1,7 +1,7 @@
{% extends "admin.twig" %}
{% import '@CorePluginsAdmin/macros.twig' as pluginsMacro %}
-{% set title %}{{ 'CorePluginsAdmin_Marketplace'|translate }}{% endset %}
+{% set title %}{{ 'Marketplace_Marketplace'|translate }}{% endset %}
{% block content %}
@@ -9,48 +9,41 @@
<div piwik-content-intro>
<h2 piwik-enriched-headline feature-name="{{ 'CorePluginsAdmin_Marketplace'|translate }}"
- >{{ title|e('html_attr') }}</h2>
+ >{{ title|e('html_attr') }}</h2>
<p>
- {% if showThemes %}
+ {% if not isSuperUser %}
+ {% if showThemes %}
+ {{ 'Marketplace_NotAllowedToBrowseMarketplaceThemes'|translate }}
+ {% else %}
+ {{ 'Marketplace_NotAllowedToBrowseMarketplacePlugins'|translate }}
+ {% endif %}
+ {% elseif showThemes %}
{{ 'CorePluginsAdmin_ThemesDescription'|translate }}
- {{ 'CorePluginsAdmin_InstallingNewPluginViaMarketplaceOrUpload'|translate('<a href="#" class="uploadPlugin">','</a>')|raw }}
- <br/>
- {{ 'CorePluginsAdmin_BeCarefulUsingThemes'|translate }}
+ {{ 'Marketplace_InstallingNewPluginViaMarketplaceOrUpload'|translate(('CorePluginsAdmin_Themes'|translate), '<a href="#" class="uploadPlugin">', ('CorePluginsAdmin_Theme'|translate), '</a>')|raw }}
{% else %}
{{ 'CorePluginsAdmin_PluginsExtendPiwik'|translate }}
- {{ 'CorePluginsAdmin_InstallingNewPluginViaMarketplaceOrUpload'|translate('<a href="#" class="uploadPlugin">','</a>')|raw }}
- <br/>
- {{ 'CorePluginsAdmin_BeCarefulUsingPlugins'|translate }}
+ {{ 'Marketplace_InstallingNewPluginViaMarketplaceOrUpload'|translate(('General_Plugins'|translate), '<a href="#" class="uploadPlugin">', ('General_Plugin'|translate), '</a>')|raw }}
{% endif %}
</p>
- {% if not isSuperUser %}
- <p>
- {% if showThemes %}
- {{ 'CorePluginsAdmin_NotAllowedToBrowseMarketplaceThemes'|translate }}
- {% else %}
- {{ 'CorePluginsAdmin_NotAllowedToBrowseMarketplacePlugins'|translate }}
- {% endif %}
- </p>
- {% endif %}
-
+ {% include '@Marketplace/licenseform.twig' %}
<div class="ui-confirm" id="installPluginByUpload">
- <h2>{{ 'CorePluginsAdmin_TeaserExtendPiwikByUpload'|translate }}</h2>
+ <h2>{{ 'Marketplace_TeaserExtendPiwikByUpload'|translate }}</h2>
- <p class="description"> {{ 'CorePluginsAdmin_AllowedUploadFormats'|translate }} </p>
+ <p class="description"> {{ 'Marketplace_AllowedUploadFormats'|translate }} </p>
<form enctype="multipart/form-data" method="post" id="uploadPluginForm"
- action="{{ linkTo({'action':'uploadPlugin', 'nonce': installNonce}) }}">
+ action="{{ linkTo({'module':'CorePluginsAdmin', 'action':'uploadPlugin', 'nonce': installNonce}) }}">
<input type="file" name="pluginZip">
<br />
- <input class="startUpload btn" type="submit" value="{{ 'CorePluginsAdmin_UploadZipFile'|translate }}">
+ <input class="startUpload btn" type="submit" value="{{ 'Marketplace_UploadZipFile'|translate }}">
</form>
</div>
<div class="row marketplaceActions" ng-controller="PiwikMarketplaceController as marketplace">
<div piwik-field uicontrol="select" name="plugin_type"
- class="col s12 m4"
+ class="col s12 m6 l4"
ng-model="marketplace.pluginType"
ng-change="marketplace.changePluginType()"
title="{{ 'Show'|translate|e('html_attr') }}"
@@ -64,17 +57,17 @@
value="{{ sort }}"
ng-model="marketplace.pluginSort"
ng-change="marketplace.changePluginSort()"
- class="col s12 m4"
+ class="col s12 m6 l4"
full-width="true"
options="{{ pluginSortOptions|json_encode }}">
</div>
{# Hide filters and search for themes because we don't have many of them #}
- {% if not showThemes %}
- <div class="col s12 m4 ">
+ {% if (pluginsToShow|length) > 20 or query %}
+ <div class="col s12 m12 l4 ">
<form action="{{ linkTo({'sort': ''}) }}" method="post" class="plugin-search">
<div piwik-field uicontrol="text" name="query"
- title="{{ 'General_Search'|translate }} {{ plugins|length }} {{ 'General_Plugins'|translate|lcfirst }}..."
+ title="{{ 'General_Search'|translate }} {{ numAvailablePlugins }} {{ 'General_Plugins'|translate|lcfirst }}..."
value="{{ query }}"
full-width="true">
</div>
@@ -85,15 +78,19 @@
</div>
</div>
- {% include '@CorePluginsAdmin/marketplace/plugin-list.twig' %}
-
- <div class="footer-message">
- {% set marketplaceSellPluginSubject = 'CorePluginsAdmin_MarketplaceSellPluginSubject'|translate %}
- {{ 'CorePluginsAdmin_GetEarlyAccessForPaidPlugins'|translate("<a href='mailto:hello@piwik.org?subject=" ~ marketplaceSellPluginSubject ~ "'>", "</a>")|raw }}
- <br/>
- {{ 'CorePluginsAdmin_DevelopersLearnHowToDevelopPlugins'|translate('<a href="?module=Proxy&action=redirect&url=http://developer.piwik.org/plugins" target="_blank">', '</a>')|raw }}
+ {% include '@Marketplace/plugin-list.twig' %}
+
+ <div class="footer-message center">
+ {{ 'Marketplace_DevelopersLearnHowToDevelopPlugins'|translate('<a href="?module=Proxy&action=redirect&url=http://developer.piwik.org/develop" target="_blank">', '</a>')|raw }}
+ <br />
+ <br />
+ <br />
+ <a rel="noreferrer" href="https://shop.piwik.org/faq/" target="_blank">FAQ</a> |
+ <a rel="noreferrer" href="https://shop.piwik.org/terms-conditions/" target="_blank">Terms</a> |
+ <a rel="noreferrer" href="https://piwik.org/privacy-policy/" target="_blank">Privacy</a> |
+ <a rel="noreferrer" href="https://piwik.org/contact/" target="_blank">Contact</a>
</div>
</div>
-{% endblock %}
+{% endblock %} \ No newline at end of file
diff --git a/plugins/Marketplace/templates/paid-plugins-install-list.twig b/plugins/Marketplace/templates/paid-plugins-install-list.twig
new file mode 100644
index 0000000000..9a322245d7
--- /dev/null
+++ b/plugins/Marketplace/templates/paid-plugins-install-list.twig
@@ -0,0 +1,22 @@
+<div class="ui-confirm" id="installAllPaidPluginsAtOnce">
+ <h2>{{ 'Marketplace_InstallAllPurchasedPlugins'|translate }}</h2>
+ <p>
+ {{ 'Marketplace_InstallThesePlugins'|translate }}
+ <br /><br />
+ </p>
+ <ul>
+ {% for pluginName in paidPluginsToInstallAtOnce %}
+ <li>{{ pluginName }}</li>
+ {% endfor %}
+ </ul>
+
+ <p>
+ <a href="{{ linkTo({'action': 'installAllPaidPlugins', 'nonce': installNonce}) }}" class="btn">
+ {{ 'Marketplace_InstallAllPurchasedPluginsAction'|translate(paidPluginsToInstallAtOnce|length) }}
+ </a>
+
+ <a href="{{ linkTo({'rand': random(999999)}) }}" class="btn-flat">
+ Cancel
+ </a>
+ </p>
+</div> \ No newline at end of file
diff --git a/plugins/Marketplace/templates/plugin-details.twig b/plugins/Marketplace/templates/plugin-details.twig
new file mode 100644
index 0000000000..bed0a19d14
--- /dev/null
+++ b/plugins/Marketplace/templates/plugin-details.twig
@@ -0,0 +1,361 @@
+{% import '@Marketplace/macros.twig' as marketplaceMacro %}
+
+{% block content %}
+
+ <div class="pluginDetails">
+ {% if errorMessage %}
+ {{ errorMessage }}
+ {% elseif plugin %}
+
+ {% if plugin.versions is not empty and plugin.versions[plugin.versions|length - 1] %}
+ {% set latestVersion = plugin.versions[plugin.versions|length - 1] %}
+ {% else %}
+ {% set latestVersion = '' %}
+ {% endif %}
+
+ {% set hasChangelog = plugin.isDownloadable and ((latestVersion and latestVersion.readmeHtml.changelog) or plugin.versions|length > 1) %}
+
+ <div class="row">
+ <div class="col s12 m9">
+ <h2>{{ plugin.displayName }}</h2>
+ <p class="description">
+ {% if plugin.featured %}
+ {{ marketplaceMacro.featuredIcon('left') }}
+ {% endif %}
+ {{ plugin.description }}
+ </p>
+ <div class="contentDetails">
+ <div id="pluginDetailsTabs" class="row">
+ <div class="col s12">
+ <ul class="tabs">
+ <li class="tab col s3"><a href="#tabs-description">{{ 'General_Description'|translate }}</a></li>
+
+ {% if latestVersion.readmeHtml.faq %}
+ <li class="tab col s3"><a href="#tabs-faq">{{ 'General_Faq'|translate }}</a></li>
+ {% endif %}
+
+ {% if latestVersion.readmeHtml.documentation %}
+ <li class="tab col s3"><a href="#tabs-documentation">{{ 'General_Documentation'|translate }}</a></li>
+ {% endif %}
+
+ {% if hasChangelog %}
+ <li class="tab col s3"><a href="#tabs-changelog">{{ 'CorePluginsAdmin_Changelog'|translate }}</a></li>
+ {% endif %}
+
+ {% if plugin.screenshots|length %}
+ <li class="tab col s3"><a href="#tabs-screenshots">{{ 'Marketplace_Screenshots'|translate }}</a></li>
+ {% endif %}
+
+ {% if plugin.support is not empty %}
+ <li class="tab col s3"><a href="#tabs-support">{{ 'Marketplace_Support'|translate }}</a></li>
+ {% endif %}
+
+ {% if plugin.shop is defined and plugin.shop and plugin.shop.reviews and plugin.shop.reviews.embedUrl is defined and plugin.shop.reviews.embedUrl %}
+ <li class="tab col s3"><a href="#tabs-reviews">{{ 'Marketplace_Reviews'|translate }}</a></li>
+ {% endif %}
+ </ul>
+ </div>
+
+ <div id="tabs-description" class="tab-content col s12">
+ {% if isSuperUser and (plugin.isDownloadable or plugin.isInstalled) %}
+ {{ marketplaceMacro.missingRequirementsPleaseUpdateNotice(plugin) }}
+
+ {% if isMultiServerEnvironment %}
+ <div class="alert alert-warning">{{ 'Marketplace_MultiServerEnvironmentWarning'|translate }}</div>
+ {% elseif not isAutoUpdateEnabled %}
+ <div class="alert alert-warning">{{ 'Marketplace_AutoUpdateDisabledWarning'|translate("'[General]enable_auto_updates=1'", "'config/config.ini.php'") }}</div>
+ {% endif %}
+ {% endif %}
+
+ {% if hasSomeAdminAccess and plugin.isMissingLicense is defined and plugin.isMissingLicense %}
+ <div class="alert alert-danger">{{ 'Marketplace_PluginLicenseMissingDescription'|translate }}</div>
+ {% elseif hasSomeAdminAccess and plugin.hasExceededLicense is defined and plugin.hasExceededLicense %}
+ <div class="alert alert-warning">{{ 'Marketplace_PluginLicenseExceededDescription'|translate }}</div>
+ {% endif %}
+
+ {{ latestVersion.readmeHtml.description|raw }}
+ </div>
+
+ {% if latestVersion.readmeHtml.faq %}
+ <div id="tabs-faq" class="tab-content col s12">
+ {{ latestVersion.readmeHtml.faq|raw }}
+ </div>
+ {% endif %}
+
+ {% if latestVersion.readmeHtml.documentation %}
+ <div id="tabs-documentation" class="tab-content col s12">
+ {{ latestVersion.readmeHtml.documentation|raw }}
+ </div>
+ {% endif %}
+
+ {% if hasChangelog %}
+ <div id="tabs-changelog" class="tab-content col s12">
+ {{ marketplaceMacro.missingRequirementsPleaseUpdateNotice(plugin) }}
+ {% if plugin.canBeUpdated %}
+ <div class="alert alert-warning">
+ {{ 'Marketplace_PluginUpdateAvailable'|translate(plugin.currentVersion, plugin.latestVersion) }}
+ {% if plugin.repositoryChangelogUrl %}<a rel="noreferrer" target="_blank" href="{{ plugin.repositoryChangelogUrl }}">{{ 'Marketplace_ViewRepositoryChangelog'|translate }}</a>{% endif %}
+ </div>
+ {% endif %}
+
+ {% if latestVersion.readmeHtml.changelog %}
+ {{ latestVersion.readmeHtml.changelog|raw }}
+ {% endif %}
+
+ <h3>{{ 'CorePluginsAdmin_History'|translate }}</h3>
+
+ <ul>
+ {% for version in plugin.versions|reverse %}
+ <li>
+ {% set versionName %}
+ <strong>
+ {% if version.repositoryChangelogUrl %}
+ <a target="_blank" title="{{ 'CorePluginsAdmin_Changelog'|translate }}" href="{{ version.repositoryChangelogUrl }}">{{ version.name }}</a>
+ {% else %}
+ {{ version.name }}
+ {% endif %}
+ </strong>
+ {% endset %}
+ {{ 'Marketplace_PluginVersionInfo'|translate(versionName, version.release)|raw }}
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+ {% endif %}
+
+ {% if plugin.screenshots|length %}
+ <div id="tabs-screenshots" class="tab-content col s12">
+ <div class="thumbnails">
+ {% for screenshot in plugin.screenshots %}
+ <div class="thumbnail">
+ <a href="{{ screenshot }}" target="_blank"><img src="{{ screenshot }}?w=400" width="400" alt=""></a>
+ <p>
+ {{ screenshot|split('/')|last|replace({'_': ' ', '.png': '', '.jpg': '', '.jpeg': ''}) }}
+ </p>
+ </div>
+ {% endfor %}
+ </div>
+ </div>
+ {% endif %}
+
+ {% if plugin.support is not empty %}
+ <div id="tabs-support" class="tab-content col s12">
+ <ul>
+ {% for entry in plugin.support %}
+ {% if entry.name and entry.value %}
+ <li>
+ {{ entry.name }}: {{ entry.value }}
+ </li>
+ {% endif %}
+ {% endfor %}
+ </ul>
+ </div>
+ {% endif %}
+
+ {% if plugin.shop is defined and plugin.shop and plugin.shop.reviews and plugin.shop.reviews.embedUrl is defined and plugin.shop.reviews.embedUrl %}
+ <div id="tabs-reviews" class="tab-content col s12">
+ <iframe class="reviewIframe"
+ style="{% if plugin.shop.reviews.height %}height:{{ plugin.shop.reviews.height }}px;{% endif %}"
+ id="{{ plugin.shop.reviews.embedUrl|md5 }}"
+ src="{{ plugin.shop.reviews.embedUrl|raw }}"></iframe>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+ <div class="col s12 m3">
+ <div class="metadata">
+ <div class="actionButton">
+ {% if not plugin.isDownloadable or not isSuperUser %}
+ {% if hasSomeAdminAccess and plugin.hasExceededLicense is defined and plugin.hasExceededLicense and plugin.consumer %}
+ {% if plugin.consumer.loginUrl is defined and plugin.consumer.loginUrl %}
+ <a class="install update"
+ target="_blank"
+ rel="noreferrer"
+ href="{{ plugin.consumer.loginUrl|default('')|e('html_attr') }}"
+ >{{ 'Marketplace_UpgradeSubscription'|translate }}</a>
+ {% endif %}
+
+ {% elseif not plugin.isDownloadable and plugin.isPaid and plugin.shop is defined and plugin.shop %}
+
+ {% if plugin.shop.variations|length %}
+
+ <div class="input-field variationPicker">
+ <select title="{{ 'Marketplace_ShownPriceIsExclTax'|translate|e('html_attr') }} {{ 'Marketplace_CurrentNumPiwikUsers'|translate(numUsers)|e('html_attr') }}">
+ {% for variation in plugin.shop.variations %}
+ <option value="{{ variation.addToCartUrl }}"
+ title="{{ 'Marketplace_PriceExclTax'|translate(variation.price, variation.currency)|e('html_attr') }} {{ 'Marketplace_CurrentNumPiwikUsers'|translate(numUsers)|e('html_attr') }}"
+ {% if variation.recommended is defined and variation.recommended %}selected{% endif %}
+ >{{ variation.name }} - {{ variation.prettyPrice }} / {{ variation.period }}</option>
+ {% endfor %}
+ </select>
+ </div>
+
+ <a class="install update addToCartLink" target="_blank"
+ title="{{ 'Marketplace_ClickToCompletePurchase'|translate|e('html_attr') }}"
+ rel="noreferrer"
+ href="{{ plugin.shop.url|default('')|e('html_attr') }}"
+ >{{ 'Marketplace_AddToCart'|translate }}</a>
+ {% else %}
+ <a class="install update" target="_blank"
+ rel="noreferrer"
+ href="{% if plugin.shop is defined and plugin.shop and plugin.shop.url %}{{ plugin.shop.url|e('html_attr') }}{% else %}{{ plugin.homepage|e('html_attr') }}{% endif %}"
+ >{{ 'General_MoreDetails'|translate }}</a>
+ {% endif %}
+ {% endif %}
+ {% elseif isSuperUser %}
+ {% if not isAutoUpdatePossible %}
+ <a onclick="$(this).css('display', 'none')" href="{{ linkTo({'action': 'download', 'pluginName': plugin.name, 'nonce': (plugin.name|nonce)}) }}"
+ class="download">{{ 'General_Download'|translate }}</a>
+ {% elseif plugin.canBeUpdated and 0 == plugin.missingRequirements|length %}
+ <a class="install update"
+ href="{{ linkTo({'module': 'Marketplace', 'action':'updatePlugin', 'pluginName': plugin.name, 'nonce': updateNonce}) }}"
+ >{{ 'CoreUpdater_UpdateTitle'|translate }}</a>
+ {% elseif plugin.isInstalled %}
+ <br />
+ <br />
+ <br />
+ <br />
+ {% elseif 0 < plugin.missingRequirements|length %}
+ <br />
+ <br />
+ <br />
+ <br />
+ {% else %}
+ <a href="{{ linkTo({'module': 'Marketplace', 'action': 'installPlugin', 'pluginName': plugin.name, 'nonce': installNonce}) }}"
+ class="install">{{ 'Marketplace_ActionInstall'|translate }}</a>
+ {% endif %}
+ {% else %}
+ <br />
+ <br />
+ <br />
+ <br />
+ {% endif %}
+ </div>
+
+ <p><br /></p>
+ <dl>
+ <dt>{{ 'CorePluginsAdmin_Version'|translate }}</dt>
+ <dd>{{ plugin.latestVersion }}</dd>
+ <dt>{{ 'Marketplace_PluginKeywords'|translate }}</dt>
+ <dd>{{ plugin.keywords|join(', ') }}</dd>
+ {% if plugin.lastUpdated %}
+ <dt>{{ 'Marketplace_LastUpdated'|translate }}</dt>
+ <dd>{{ plugin.lastUpdated }}</dd>
+ {% endif %}
+ {% if plugin.numDownloads %}
+ <dt>{{ 'General_Downloads'|translate }}</dt>
+ <dd title="{{ 'Marketplace_NumDownloadsLatestVersion'|translate(latestVersion.numDownloads|number_format) }}">{{ plugin.numDownloads }}</dd>
+ {% endif %}
+ <dt>{{ 'Marketplace_Developer'|translate }}</dt>
+ <dd>{{ marketplaceMacro.pluginDeveloper(plugin.owner) }}</dd>
+ {% if latestVersion and latestVersion.license is defined and latestVersion.license and latestVersion.license.name is defined and latestVersion.license.name %}
+ <dt>{{ 'Marketplace_License'|translate }}</dt>
+ <dd>
+ {% if latestVersion.license.url is defined and latestVersion.license.url %}
+ <a rel="noreferrer"
+ href="{{ latestVersion.license.url }}"
+ target="_blank">{{ latestVersion.license.name }}</a>
+ {% else %}
+ {{ latestVersion.license.name }}
+ {% endif %}
+ </dd>
+ {% endif %}
+ <dt>{{ 'Marketplace_Authors'|translate }}</dt>
+ <dd>{% for author in plugin.authors if author.name %}
+
+ {% spaceless %}
+ {% if author.homepage %}
+ <a target="_blank" rel="noreferrer" href="{{ author.homepage }}">{{ author.name }}</a>
+ {% elseif author.email %}
+ <a href="mailto:{{ author.email|escape('url') }}">{{ author.name }}</a>
+ {% else %}
+ {{ author.name }}
+ {% endif %}
+
+ {% if loop.index < plugin.authors|length %}
+ ,
+ {% endif %}
+ {% endspaceless %}
+
+ {% endfor %}
+ </dd>
+ <dt>{{ 'CorePluginsAdmin_Websites'|translate }}</dt>
+ <dd>
+ {% if plugin.homepage %}
+ <a target="_blank" rel="noreferrer" href="{{ plugin.homepage }}">{{ 'Marketplace_PluginWebsite'|translate }}</a>,
+ {% endif %}
+ <a target="_blank" href="{{ plugin.repositoryUrl }}">GitHub</a></dd>
+ {% if plugin.activity and plugin.activity.numCommits %}
+ <dt>{{ 'CorePluginsAdmin_Activity'|translate }}</dt>
+ <dd>
+ {{ plugin.activity.numCommits }} commits
+
+ {% if plugin.activity.numContributors > 1 %}
+ {{ 'Marketplace_ByXDevelopers'|translate(plugin.activity.numContributors) }}
+ {% endif %}
+ {% if plugin.activity.lastCommitDate %}
+ {{ 'Marketplace_LastCommitTime'|translate(plugin.activity.lastCommitDate) }}
+ {% endif %}</dd>
+ {% endif %}
+ </dl>
+ <br />
+ </div>
+ </div>
+ </div>
+
+ <script type="text/javascript">
+ $(function() {
+
+ var active = 0;
+ {% if activeTab %}
+ var $activeTab = $('#tabs-{{ activeTab|e('js') }}');
+ if ($activeTab) {
+ active = $activeTab.index() - 1;
+ }
+ {% endif %}
+
+ $('#pluginDetailsTabs .tabs').tabs();
+ $('#pluginDetailsTabs .tabs').tabs('select_tab', active >= 0 ? active : 0);
+
+ $('.pluginDetails a').each(function (index, a) {
+ var link = $(a).attr('href');
+
+ if (link && 0 === link.indexOf('http')) {
+ $(a).attr('target', '_blank');
+ }
+ });
+ });
+ </script>
+
+ {% if plugin.shop is defined and plugin.shop and plugin.shop.reviews and plugin.shop.reviews.embedUrl is defined and plugin.shop.reviews.embedUrl %}
+ <script type="text/javascript">
+ $(function() {
+ var $iFrames = $('.pluginDetails iframe.reviewIframe');
+ for (var i = 0; i < $iFrames.length; i++) {
+ iFrameResize({checkOrigin: ['{{ plugin.shop.reviews.embedUrl|domainOnly }}']}, $iFrames[i]);
+ }
+ });
+ </script>
+ {% endif %}
+
+ <script type="text/javascript">
+ $(function() {
+ var $variationPicker = $('.pluginDetails .variationPicker select');
+ if ($variationPicker.val()) {
+ $('.addToCartLink').attr('href', $variationPicker.val());
+ }
+ $variationPicker.on('change', function () {
+ $('.addToCartLink').attr('href', $variationPicker.val())
+ });
+
+ if ($variationPicker.length) {
+ $variationPicker.material_select();
+ }
+ });
+ </script>
+ {% endif %}
+ </div>
+
+
+{% endblock %}
diff --git a/plugins/Marketplace/templates/plugin-list.twig b/plugins/Marketplace/templates/plugin-list.twig
new file mode 100644
index 0000000000..d071fad3d8
--- /dev/null
+++ b/plugins/Marketplace/templates/plugin-list.twig
@@ -0,0 +1,159 @@
+{% import '@Marketplace/macros.twig' as marketplaceMacro %}
+
+{% if pluginsToShow|length > 0 %}
+ <div class="pluginListContainer row">
+ {% for plugin in pluginsToShow %}
+ <div class="col s12 m6 l4">
+ {% embed 'contentBlock.twig' with {'title': ''} %}
+ {% block content %}
+ <div class="plugin">
+ <h3 class="card-title" title="{{ 'General_MoreDetails'|translate }}">
+ <a href="#" piwik-plugin-name="{{ plugin.name }}">{{ plugin.displayName }}</a>
+ </h3>
+
+ <p class="description">
+ {{ plugin.description }}
+ <a class="more" href="#" piwik-plugin-name="{{ plugin.name }}" title="{{ 'General_MoreDetails'|translate }}">
+ &rsaquo; {{ 'General_MoreLowerCase'|translate }}</a>
+ </p>
+
+ {% if showThemes %}
+ {# Screenshot for themes #}
+ <a class="more" href="#" piwik-plugin-name="{{ plugin.name }}">
+ <img title="{{ 'General_MoreDetails'|translate }}"
+ class="preview" src="{{ plugin.screenshots|first }}?w=250&h=150"/></a>
+ {% endif %}
+
+ <ul class="metadata">
+ <li>
+ {% if plugin.latestVersion %}
+ {{ 'CorePluginsAdmin_Version'|translate }}: {{ plugin.latestVersion }}
+ {% endif %}
+
+ {% if plugin.canBeUpdated %}
+ <a class="update-available" href="#" piwik-plugin-name="{{ plugin.name }}" data-activePluginTab="changelog"
+ title="{{ 'Marketplace_PluginUpdateAvailable'|translate(plugin.currentVersion, plugin.latestVersion) }}">
+ {{ 'Marketplace_NewVersion'|translate }}</a>
+ {% endif %}
+ </li>
+ {% if plugin.lastUpdated %}
+ <li>{{ 'Marketplace_Updated'|translate }}: {{ plugin.lastUpdated }}</li>
+ {% endif %}
+ {% if plugin.numDownloads %}
+ <li>{{ 'General_Downloads'|translate }}: {{ plugin.numDownloads }}</li>
+ {% endif %}
+ <li>{{ 'Marketplace_Developer'|translate }}: {{ marketplaceMacro.pluginDeveloper(plugin.owner) }}</li>
+ </ul>
+
+ {% macro moreDetailsLink(plugin) %}
+ {% set canBePurchased = not plugin.isDownloadable and plugin.shop is defined and plugin.shop and plugin.shop.url %}
+ <a class="btn btn-block plugin-details {% if canBePurchased %}purchaseable{% endif %}" href="#" piwik-plugin-name="{{ plugin.name }}" title="{{ 'General_MoreDetails'|translate }}">
+
+ {% if canBePurchased and plugin.shop.variations %}
+ {% set foundCheapest = 0 %}
+ {% for variation in plugin.shop.variations %}
+ {% if not foundCheapest and variation.cheapest is defined and variation.cheapest %}
+ {% set foundCheapest = 1 %}
+ {{ 'Marketplace_PriceFromPerPeriod'|translate(variation.prettyPrice, variation.period) }}
+ {% endif %}
+ {% endfor %}
+ {% if not foundCheapest %}
+ {{ 'Marketplace_PriceFromPerPeriod'|translate(plugin.shop.variations.0.prettyPrice, plugin.shop.variations.0.period) }}
+ {% endif %}
+ {% else %}
+ {{ 'General_MoreDetails'|translate }}
+ {% endif %}
+
+ </a>
+ {% endmacro %}
+
+
+ {% if isSuperUser %}
+ <div class="footer">
+ {% if plugin.isMissingLicense is defined and plugin.isMissingLicense %}
+
+ <div class="alert alert-danger" >
+ {{ 'Marketplace_LicenseMissing'|translate }}
+
+ <span style="white-space:nowrap">(<a class="plugin-details" href="#" piwik-plugin-name="{{ plugin.name }}" title="{{ 'General_MoreDetails'|translate }}">{{ 'General_Help'|translate }}</a>)</span>
+ </div>
+
+ {% elseif plugin.hasExceededLicense is defined and plugin.hasExceededLicense %}
+
+ <div class="alert alert-danger">
+ {{ 'Marketplace_LicenseExceeded'|translate }}
+
+ <span style="white-space:nowrap">(<a class="plugin-details" href="#" piwik-plugin-name="{{ plugin.name }}" title="{{ 'General_MoreDetails'|translate }}">{{ 'General_Help'|translate }}</a>)</span>
+ </div>
+
+ {% elseif plugin.canBeUpdated and 0 == plugin.missingRequirements|length and isAutoUpdatePossible %}
+ <a class="btn btn-block"
+ href="{{ linkTo({'module': 'Marketplace', 'action':'updatePlugin', 'pluginName': plugin.name, 'nonce': updateNonce}) }}">
+ {{ 'CoreUpdater_UpdateTitle'|translate }}
+ </a>
+ {% elseif plugin.missingRequirements|length > 0 or not isAutoUpdatePossible %}
+
+ {% macro downloadButton(showOr, plugin, isAutoUpdatePossible, showBrackets = false) -%}
+ {%- if plugin.missingRequirements|length == 0 and plugin.isDownloadable and not isAutoUpdatePossible -%}
+ {% if showBrackets %}({% endif %}<span onclick="$(this).css('display', 'none')">
+ {%- if showOr %} {{ 'General_Or'|translate }} {% endif -%}
+ <a class="plugin-details download"
+ href="{{ linkTo({'module': 'Marketplace', 'action': 'download', 'pluginName': plugin.name, 'nonce': (plugin.name|nonce)}) }}"
+ >{{ 'General_Download'|translate }}</a></span>{% if showBrackets %}){% endif %}
+ {%- endif -%}
+ {%- endmacro %}
+
+ {% if plugin.canBeUpdated and 0 == plugin.missingRequirements|length %}
+ {{ 'Marketplace_CannotUpdate'|translate }}
+ <span style="white-space:nowrap">(<a class="plugin-details" href="#" piwik-plugin-name="{{ plugin.name }}" title="{{ 'General_MoreDetails'|translate }}">{{ 'General_Help'|translate }}</a>{{ _self.downloadButton(true, plugin, isAutoUpdatePossible)|raw }})</span>
+ {% elseif plugin.isInstalled %}
+ {{ 'General_Installed'|translate }}
+ {{ _self.downloadButton(false, plugin, isAutoUpdatePossible, true)|raw }}
+ {% elseif not plugin.isDownloadable %}
+ {{ _self.moreDetailsLink(plugin)|raw }}
+ {% else %}
+ {{ 'Marketplace_CannotInstall'|translate }}
+
+ <span style="white-space:nowrap">(<a class="plugin-details" href="#" piwik-plugin-name="{{ plugin.name }}" title="{{ 'General_MoreDetails'|translate }}">{{ 'General_Help'|translate }}</a>{{ _self.downloadButton(true, plugin, isAutoUpdatePossible)|raw }})</span>
+ {% endif %}
+
+ {% elseif plugin.isInstalled %}
+ {{ 'General_Installed'|translate }}
+
+ {% if not plugin.isInvalid and not isMultiServerEnvironment and isPluginsAdminEnabled %}
+ ({{ pluginsMacro.pluginActivateDeactivateAction(plugin.name, plugin.isActivated, plugin.missingRequirements, deactivateNonce, activateNonce) }})
+ {% endif %}
+
+ {% elseif plugin.isPaid and not plugin.isDownloadable %}
+ {{ _self.moreDetailsLink(plugin)|raw }}
+ {% else %}
+ <a href="{{ linkTo({'module': 'Marketplace', 'action': 'installPlugin', 'pluginName': plugin.name, 'nonce': installNonce}) }}"
+ class="btn">
+ {{ 'Marketplace_ActionInstall'|translate }}
+ </a>
+ {% endif %}
+ </div>
+ {% else %}
+ <div class="footer">
+ {{ _self.moreDetailsLink(plugin)|raw }}
+ </div>
+ {% endif %}
+
+ </div>
+ {% endblock %}
+ {% endembed %}
+ </div>
+ {% endfor %}
+ </div>
+{% endif %}
+
+{% if pluginsToShow|length == 0 %}
+ <div piwik-content-block>
+ {% if showThemes %}
+ {{ 'Marketplace_NoThemesFound'|translate }}
+ {% else %}
+ {{ 'Marketplace_NoPluginsFound'|translate }}
+ {% endif %}
+ </div>
+{% endif %}
+
diff --git a/plugins/Marketplace/templates/subscription-overview.twig b/plugins/Marketplace/templates/subscription-overview.twig
new file mode 100644
index 0000000000..0ace015545
--- /dev/null
+++ b/plugins/Marketplace/templates/subscription-overview.twig
@@ -0,0 +1,101 @@
+{% extends mode is defined and mode == 'user' ? "user.twig" : "admin.twig" %}
+{% import '@Marketplace/macros.twig' as marketplaceMacro %}
+
+{% set title %}{{ 'Marketplace_Marketplace'|translate }}{% endset %}
+
+{% block content %}
+
+ <div piwik-content-block
+ content-title="{{ 'Marketplace_OverviewPluginSubscriptions'|translate|e('html_attr') }}"
+ class="subscriptionOverview">
+
+ {% if hasLicenseKey %}
+ <p>
+ {{ 'Marketplace_PluginSubscriptionsList'|translate }}
+ {% if loginUrl %}
+ <a target="_blank" rel="noreferrer" href="{{ loginUrl }}">{{ 'Marketplace_OverviewPluginSubscriptionsAllDetails'|translate }}</a>
+ {% endif %}
+ <br/>
+ {{ 'Marketplace_OverviewPluginSubscriptionsMissingInfo'|translate }}
+ <br />
+
+ {{ 'Marketplace_NoValidSubscriptionNoUpdates'|translate }}
+ {{ 'Marketplace_CurrentNumPiwikUsers'|translate('<strong>' ~ numUsers ~ '</strong>')|raw }}
+ </p>
+
+ <br />
+
+ <table piwik-content-table>
+ <thead>
+ <tr>
+ <th>{{ 'General_Name'|translate }}</th>
+ <th>{{ 'Marketplace_SubscriptionType'|translate }}</th>
+ <th>{{ 'CorePluginsAdmin_Status'|translate }}</th>
+ <th>{{ 'Marketplace_SubscriptionStartDate'|translate }}</th>
+ <th>{{ 'Marketplace_SubscriptionEndDate'|translate }}</th>
+ <th>{{ 'Marketplace_SubscriptionNextPaymentDate'|translate }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% if subscriptions|length %}
+ {% for subscription in subscriptions %}
+ <tr>
+ <td class="subscriptionName">
+ {% if subscription.plugin.htmlUrl %}
+ <a href="{{ subscription.plugin.htmlUrl }}" rel="noreferrer" target="_blank">
+ {% endif %}
+
+ {{ subscription.plugin.displayName }}
+
+ {% if subscription.plugin.htmlUrl %}
+ </a>
+ {% endif %}
+ </td>
+ <td class="subscriptionType">{{ subscription.productType }}</td>
+ <td class="subscriptionStatus"
+ title="{% if not subscription.isValid %}{{ 'Marketplace_SubscriptionInvalid'|translate|e('html_attr') }}{% elseif subscription.isExpiredSoon %}{{ 'Marketplace_SubscriptionExpiresSoon'|translate|e('html_attr') }}{% endif %}">
+ {% if not subscription.isValid %}
+ <span class="icon-error"></span>
+ {% elseif subscription.isExpiredSoon %}
+ <span class="icon-warning"></span>
+ {% else %}
+ <span class="icon-ok"></span>
+ {% endif %}
+
+ {{ subscription.status }}
+
+ {% if subscription.isExceeded is defined and subscription.isExceeded %}
+ <span class="errorMessage" title="{{ 'Marketplace_LicenseExceededPossibleCause'|translate }}"><span class="icon-error"></span> {{ 'Marketplace_Exceeded'|translate }}</span>
+ {% endif %}
+ </td>
+ <td>{{ subscription.start }}</td>
+ <td>{% if subscription.isValid and subscription.nextPayment %}
+ {{ 'Marketplace_LicenseRenewsNextPaymentDate'|translate }}
+ {% else %}
+ {{ subscription.end }}
+ {% endif %}
+ </td>
+ <td>{{ subscription.nextPayment }}</td>
+
+ </tr>
+ {% endfor %}
+ {% else %}
+ <tr><td colspan="6">{{ 'Marketplace_NoSubscriptionsFound'|translate }}</td></tr>
+ {% endif %}
+ </tbody>
+ </table>
+
+ <div class="tableActionBar">
+ <a href="{{ linkTo({'module':"Marketplace", 'action':"overview"}) }}"
+ class="">
+ <span class="icon-table"></span>
+ {{ 'Marketplace_BrowseMarketplace'|translate }}
+ </a>
+ </div>
+
+ {% else %}
+ <p>{{ 'Marketplace_OverviewPluginSubscriptionsMissingLicense'|translate('<a href="'~ linkTo({'module':"Marketplace", 'action':"overview"}) ~'">', '</a>')|raw }}</p>
+ {% endif %}
+ </div>
+
+{% endblock %} \ No newline at end of file
diff --git a/plugins/Marketplace/templates/updatePlugin.twig b/plugins/Marketplace/templates/updatePlugin.twig
new file mode 100644
index 0000000000..ec920cda92
--- /dev/null
+++ b/plugins/Marketplace/templates/updatePlugin.twig
@@ -0,0 +1,41 @@
+{% extends 'admin.twig' %}
+
+{% block content %}
+
+ <div style="max-width:980px;">
+
+ <h2>{{ 'Marketplace_UpdatingPlugin'|translate(plugin.name) }}</h2>
+
+ <div>
+
+ {% if plugin.isTheme %}
+
+ <p>{{ 'Marketplace_StepDownloadingThemeFromMarketplace'|translate }}</p>
+
+ <p>{{ 'Marketplace_StepUnzippingTheme'|translate }}</p>
+
+ <p>{{ 'Marketplace_StepReplaceExistingTheme'|translate }}</p>
+
+ <p>{{ 'Marketplace_StepThemeSuccessfullyUpdated'|translate(plugin.name, plugin.latestVersion) }}</p>
+
+ {% else %}
+
+ <p>{{ 'Marketplace_StepDownloadingPluginFromMarketplace'|translate }}</p>
+
+ <p>{{ 'Marketplace_StepUnzippingPlugin'|translate }}</p>
+
+ <p>{{ 'Marketplace_StepReplaceExistingPlugin'|translate }}</p>
+
+ <p>{{ 'Marketplace_StepPluginSuccessfullyUpdated'|translate(plugin.name, plugin.latestVersion) }}</p>
+
+ {% endif %}
+
+ <p><a href="{{ linkTo({'module': 'CorePluginsAdmin', 'action': 'plugins'}) }}">{{ 'General_Plugins'|translate }}</a>
+ |
+ <a href="{{ linkTo({'module': 'CorePluginsAdmin', 'action': 'themes'}) }}">{{ 'CorePluginsAdmin_Themes'|translate }}</a>
+ |
+ <a href="{{ linkTo({'module': 'Marketplace', 'action': 'overview'}) }}">{{ 'Marketplace_Marketplace'|translate }}</a></p>
+ </div>
+ </div>
+
+{% endblock %}
diff --git a/plugins/Marketplace/tests/Fixtures/SimpleFixtureTrackFewVisits.php b/plugins/Marketplace/tests/Fixtures/SimpleFixtureTrackFewVisits.php
new file mode 100644
index 0000000000..1da808af4e
--- /dev/null
+++ b/plugins/Marketplace/tests/Fixtures/SimpleFixtureTrackFewVisits.php
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\Marketplace\tests\Fixtures;
+
+use Piwik\Date;
+use Piwik\Tests\Framework\Fixture;
+
+class SimpleFixtureTrackFewVisits extends Fixture
+{
+ public $dateTime = '2013-01-23 01:23:45';
+ public $idSite = 1;
+
+ public function setUp()
+ {
+ $this->setUpWebsite();
+ $this->trackVisits();
+ }
+
+ public function tearDown()
+ {
+ // empty
+ }
+
+ private function setUpWebsite()
+ {
+ if (!self::siteCreated($this->idSite)) {
+ $idSite = self::createWebsite($this->dateTime, $ecommerce = 1);
+ $this->assertSame($this->idSite, $idSite);
+ }
+ }
+
+ protected function trackVisits()
+ {
+ $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
+ $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.1)->getDatetime());
+ $t->setUrl('http://example.com/');
+ self::checkResponse($t->doTrackPageView('Viewing homepage'));
+
+ $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
+ $t->setIp('56.11.55.73');
+ $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.1)->getDatetime());
+ $t->setUrl('http://example.com/sub/page');
+ self::checkResponse($t->doTrackPageView('Viewing homepage'));
+ }
+} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/Framework/Mock/Client.php b/plugins/Marketplace/tests/Framework/Mock/Client.php
new file mode 100644
index 0000000000..fee4de1afe
--- /dev/null
+++ b/plugins/Marketplace/tests/Framework/Mock/Client.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\Framework\Mock;
+
+use Piwik\Cache\Backend\NullCache;
+use Piwik\Cache\Lazy;
+use Psr\Log\NullLogger;
+
+class Client {
+
+ public static function build($service)
+ {
+ $environment = new Environment();
+ return new \Piwik\Plugins\Marketplace\Api\Client($service, new Lazy(new NullCache()), new NullLogger(), $environment);
+ }
+}
diff --git a/plugins/Marketplace/tests/Framework/Mock/Consumer.php b/plugins/Marketplace/tests/Framework/Mock/Consumer.php
new file mode 100644
index 0000000000..9c72c20c51
--- /dev/null
+++ b/plugins/Marketplace/tests/Framework/Mock/Consumer.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\Framework\Mock;
+
+use \Piwik\Plugins\Marketplace\Consumer as ActualConsumer;
+use Piwik\Plugins\Marketplace\Input\PurchaseType;
+
+class Consumer {
+
+ public static function build($service)
+ {
+ $client = Client::build($service);
+ return new ActualConsumer($client);
+ }
+
+ public static function buildNoLicense()
+ {
+ $service = new Service();
+ $service->setOnDownloadCallback(function ($action, $params) use ($service) {
+ if ($action === 'info') {
+ return $service->getFixtureContent('v2.0_info.json');
+ } elseif ($action === 'consumer') {
+ return $service->getFixtureContent('v2.0_consumer-access_token-notexistingtoken.json');
+ } elseif ($action === 'consumer/validate') {
+ return $service->getFixtureContent('v2.0_consumer_validate-access_token-notexistingtoken.json');
+ } elseif ($action === 'plugins' && !empty($params['purchase_type']) && $params['purchase_type'] === PurchaseType::TYPE_PAID) {
+ return $service->getFixtureContent('v2.0_plugins-purchase_type-paid-access_token-notexistingtoken.json');
+ }
+ });
+ return static::build($service);
+ }
+
+ public static function buildValidLicense()
+ {
+ $service = new Service();
+ $service->setOnDownloadCallback(function ($action, $params) use ($service) {
+ if ($action === 'info') {
+ return $service->getFixtureContent('v2.0_info.json');
+ } elseif ($action === 'consumer') {
+ return $service->getFixtureContent('v2.0_consumer-access_token-consumer2_paid1.json');
+ } elseif ($action === 'consumer/validate') {
+ return $service->getFixtureContent('v2.0_consumer_validate-access_token-consumer2_paid1.json');
+ } elseif ($action === 'plugins' && !empty($params['purchase_type']) && $params['purchase_type'] === PurchaseType::TYPE_PAID) {
+ return $service->getFixtureContent('v2.0_plugins-purchase_type-paid-access_token-consumer2_paid1.json');
+ }
+ });
+ return static::build($service);
+ }
+
+ public static function buildExceededLicense()
+ {
+ $service = new Service();
+ $service->setOnDownloadCallback(function ($action, $params) use ($service) {
+ if ($action === 'info') {
+ return $service->getFixtureContent('v2.0_info.json');
+ } elseif ($action === 'consumer') {
+ return $service->getFixtureContent('v2.0_consumer-num_users-201-access_token-consumer1_paid2_custom1.json');
+ } elseif ($action === 'consumer/validate') {
+ return $service->getFixtureContent('v2.0_consumer_validate-access_token-consumer1_paid2_custom1.json');
+ } elseif ($action === 'plugins' && !empty($params['purchase_type']) && $params['purchase_type'] === PurchaseType::TYPE_PAID) {
+ return $service->getFixtureContent('v2.0_plugins-purchase_type-paid-num_users-201-access_token-consumer1_paid2_custom1.json');
+ }
+ });
+
+ return static::build($service);
+ }
+
+ public static function buildExpiredLicense()
+ {
+ $service = new Service();
+ $service->setOnDownloadCallback(function ($action, $params) use ($service) {
+ if ($action === 'info') {
+ return $service->getFixtureContent('v2.0_info.json');
+ } elseif ($action === 'consumer') {
+ return $service->getFixtureContent('v2.0_consumer-access_token-consumer3_paid1_custom2.json');
+ } elseif ($action === 'consumer/validate') {
+ return $service->getFixtureContent('v2.0_consumer_validate-access_token-consumer3_paid1_custom2.json');
+ } elseif ($action === 'plugins' && !empty($params['purchase_type']) && $params['purchase_type'] === PurchaseType::TYPE_PAID) {
+ return $service->getFixtureContent('v2.0_plugins-purchase_type-paid-access_token-consumer1_paid2_custom1.json');
+ }
+ });
+ return static::build($service);
+ }
+
+}
diff --git a/plugins/Marketplace/tests/Framework/Mock/Environment.php b/plugins/Marketplace/tests/Framework/Mock/Environment.php
new file mode 100644
index 0000000000..354766c9c2
--- /dev/null
+++ b/plugins/Marketplace/tests/Framework/Mock/Environment.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\Framework\Mock;
+
+class Environment extends \Piwik\Plugins\Marketplace\Environment {
+
+ public function __construct()
+ {
+ }
+
+ public function getNumUsers()
+ {
+ return 5;
+ }
+
+ public function getNumWebsites()
+ {
+ return 21;
+ }
+
+ public function getPhpVersion()
+ {
+ return '7.0.1';
+ }
+
+ public function getPiwikVersion()
+ {
+ return '2.16.3';
+ }
+
+ public function doesPreferStable()
+ {
+ return true;
+ }
+
+ public function getReleaseChannel()
+ {
+ return 'latest_stable';
+ }
+
+ public function getMySQLVersion()
+ {
+ return '5.7.1';
+ }
+
+
+}
diff --git a/plugins/Marketplace/tests/Framework/Mock/Service.php b/plugins/Marketplace/tests/Framework/Mock/Service.php
new file mode 100644
index 0000000000..b74c8ce12e
--- /dev/null
+++ b/plugins/Marketplace/tests/Framework/Mock/Service.php
@@ -0,0 +1,122 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\Framework\Mock;
+
+use Piwik\Filesystem;
+
+class Service extends \Piwik\Plugins\Marketplace\Api\Service {
+
+ public $action;
+ public $params;
+
+ private $fixtureToReturn;
+ private $exception;
+ private $onFetchCallback;
+ private $onDownloadCallback;
+
+ public function __construct()
+ {
+ parent::__construct('http://plugins.piwik.org');
+ }
+
+ /**
+ * Will cause the service to throw an exception when any data is fetched
+ * @param $exception
+ */
+ public function throwException($exception)
+ {
+ $this->exception = $exception;
+ $this->fixtureToReturn = null;
+ }
+
+ /**
+ * Will cause the service to use the content of the given fixture as a response of the plugins API.
+ * Should be either a filename of a file within the "plugins/Marketplace/tests/resources/" directory or
+ * an array of filenames. An array is useful if the service gets called multiple times and you want to return
+ * different results for each API call. If an array is given, first filename will be returned first, then next, ...
+ *
+ * @param string|array $fixtureName
+ */
+ public function returnFixture($fixtureName)
+ {
+ $this->fixtureToReturn = $fixtureName;
+ $this->exception = null;
+ }
+
+ public function download($url, $destinationPath = null, $timeout = null)
+ {
+ if ($this->onDownloadCallback && is_callable($this->onDownloadCallback)) {
+ $result = call_user_func($this->onDownloadCallback, $this->action, $this->params);
+ if (!empty($result)) {
+ return $result;
+ }
+ }
+
+ if ($destinationPath) {
+ Filesystem::mkdir(@dirname($destinationPath));
+ file_put_contents($destinationPath, $url);
+ return true;
+ }
+
+ if (!empty($this->fixtureToReturn)) {
+ if (is_array($this->fixtureToReturn)) {
+ $fixture = array_shift($this->fixtureToReturn);
+ } else {
+ $fixture = $this->fixtureToReturn;
+ $this->fixtureToReturn = null;
+ }
+
+ return $this->getFixtureContent($fixture);
+ }
+ }
+
+ public function getFixtureContent($fixture)
+ {
+ $path = PIWIK_INCLUDE_PATH . '/plugins/Marketplace/tests/resources/' . $fixture;
+
+ return file_get_contents($path);
+ }
+
+ // here you can set a custom callback and record all actions/ params and even return a custom result for each
+ // action / params if wanted
+ public function setOnFetchCallback($callback)
+ {
+ $this->onFetchCallback = $callback;
+ }
+
+ // here you can set a custom callback and record all actions/ params and even return a custom result for each
+ // action / params if wanted
+ public function setOnDownloadCallback($callback)
+ {
+ $this->onDownloadCallback = $callback;
+ }
+
+ public function fetch($action, $params)
+ {
+ $this->action = $action;
+ $this->params = $params;
+
+ if ($this->onFetchCallback && is_callable($this->onFetchCallback)) {
+ $result = call_user_func($this->onFetchCallback, $action, $params);
+ if (!empty($result)) {
+ return $result;
+ }
+ }
+
+ if (isset($this->exception)) {
+ throw $this->exception;
+ } elseif (!empty($this->fixtureToReturn) || $this->onDownloadCallback) {
+ // we want to make sure to test as much of the service class as possible.
+ // Therefore we only mock the HTTP request in download()
+ return parent::fetch($action, $params);
+ }
+
+ return array();
+ }
+}
diff --git a/plugins/Marketplace/tests/Integration/ApiTest.php b/plugins/Marketplace/tests/Integration/ApiTest.php
new file mode 100644
index 0000000000..8b64948fd7
--- /dev/null
+++ b/plugins/Marketplace/tests/Integration/ApiTest.php
@@ -0,0 +1,198 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\Integration;
+
+use Piwik\Plugins\Marketplace\API;
+use Piwik\Plugins\Marketplace\LicenseKey;
+use Piwik\Plugins\Marketplace\tests\Framework\Mock\Service;
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Tests\Framework\Mock\FakeAccess;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+use Piwik\Plugins\Marketplace\Api\Service\Exception as ServiceException;
+use Exception;
+
+/**
+ * @group Marketplace
+ * @group ApiTest
+ * @group Api
+ * @group Plugins
+ */
+class ApiTest extends IntegrationTestCase
+{
+ /**
+ * @var API
+ */
+ private $api;
+
+ /**
+ * @var Service
+ */
+ private $service;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ API::unsetInstance();
+ $this->api = API::getInstance();
+
+ Fixture::createSuperUser();
+ if (!Fixture::siteCreated(1)) {
+ Fixture::createWebsite('2012-01-01 00:00:00');
+ }
+
+ $this->setSuperUser();
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectedExceptionMessage checkUserHasSuperUserAccess
+ */
+ public function test_deleteLicenseKey_requiresSuperUserAccess_IfUser()
+ {
+ $this->setUser();
+ $this->api->deleteLicenseKey();
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectedExceptionMessage checkUserHasSuperUserAccess
+ */
+ public function test_deleteLicenseKey_requiresSuperUserAccess_IfAnonymous()
+ {
+ $this->setAnonymousUser();
+ $this->api->deleteLicenseKey();
+ }
+
+ public function test_deleteLicenseKey_shouldRemoveAnExistingKey()
+ {
+ $this->buildLicenseKey()->set('key');
+ $this->assertHasLicenseKey();
+
+ $this->api->deleteLicenseKey();
+
+ $this->assertNotHasLicenseKey();
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectedExceptionMessage checkUserHasSuperUserAccess
+ */
+ public function test_saveLicenseKey_requiresSuperUserAccess_IfUser()
+ {
+ $this->setUser();
+ $this->api->saveLicenseKey('key');
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectedExceptionMessage checkUserHasSuperUserAccess
+ */
+ public function test_saveLicenseKey_requiresSuperUserAccess_IfAnonymous()
+ {
+ $this->setAnonymousUser();
+ $this->api->saveLicenseKey('key');
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectedExceptionMessage Marketplace_ExceptionLinceseKeyIsNotValid
+ */
+ public function test_saveLicenseKey_shouldThrowException_IfTokenIsNotValid()
+ {
+ $this->service->returnFixture('v2.0_consumer_validate-access_token-notexistingtoken.json');
+ $this->api->saveLicenseKey('key');
+ }
+
+ public function test_saveLicenseKey_shouldCallTheApiTheCorrectWay()
+ {
+ $this->service->returnFixture('v2.0_consumer-access_token-valid_but_expired.json');
+
+ try {
+ $this->api->saveLicenseKey('key123');
+ } catch (Exception $e) {
+
+ }
+
+ // make sure calls API the correct way
+ $this->assertSame('consumer/validate', $this->service->action);
+ $this->assertSame(array(), $this->service->params);
+ $this->assertSame('key123', $this->service->getAccessToken());
+ $this->assertNotHasLicenseKey();
+ }
+
+ public function test_saveLicenseKey_shouldActuallySaveToken_IfValid()
+ {
+ $this->service->returnFixture('v2.0_consumer_validate-access_token-consumer1_paid2_custom1.json');
+ $success = $this->api->saveLicenseKey('123licensekey');
+ $this->assertTrue($success);
+
+ $this->assertHasLicenseKey();
+ $this->assertSame('123licensekey', $this->buildLicenseKey()->get());
+ }
+
+ /**
+ * @expectedExceptionMessage Host not reachable
+ * @expectedException \Piwik\Plugins\Marketplace\Api\Service\Exception
+ */
+ public function test_saveLicenseKey_shouldThrowException_IfConnectionToMarketplaceFailed()
+ {
+ $this->service->throwException(new ServiceException('Host not reachable', ServiceException::HTTP_ERROR));
+ $success = $this->api->saveLicenseKey('123licensekey');
+ $this->assertTrue($success);
+
+ $this->assertHasLicenseKey();
+ $this->assertSame('123licensekey', $this->buildLicenseKey()->get());
+ }
+
+ public function provideContainerConfig()
+ {
+ $this->service = new Service();
+
+ return array(
+ 'Piwik\Access' => new FakeAccess(),
+ 'Piwik\Plugins\Marketplace\Api\Service' => $this->service
+ );
+ }
+
+ protected function setSuperUser()
+ {
+ FakeAccess::clearAccess(true);
+ }
+
+ protected function setUser()
+ {
+ FakeAccess::clearAccess(false);
+ FakeAccess::$idSitesView = array(1);
+ FakeAccess::$idSitesAdmin = array();
+ FakeAccess::$identity = 'aUser';
+ }
+
+ protected function setAnonymousUser()
+ {
+ FakeAccess::clearAccess();
+ FakeAccess::$identity = 'anonymous';
+ }
+
+ protected function buildLicenseKey()
+ {
+ return new LicenseKey();
+ }
+
+ private function assertHasLicenseKey()
+ {
+ $this->assertTrue($this->buildLicenseKey()->has());
+ }
+
+ private function assertNotHasLicenseKey()
+ {
+ $this->assertFalse($this->buildLicenseKey()->has());
+ }
+
+}
diff --git a/plugins/Marketplace/tests/Integration/ClientTest.php b/plugins/Marketplace/tests/Integration/ClientTest.php
new file mode 100644
index 0000000000..22ab948151
--- /dev/null
+++ b/plugins/Marketplace/tests/Integration/ClientTest.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\Integration\Api;
+
+use Piwik\Filesystem;
+use Piwik\Plugins\Marketplace\Api\Client;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+use Piwik\Plugins\Marketplace\tests\Framework\Mock\Service as TestService;
+use Piwik\Plugins\Marketplace\tests\Framework\Mock\Client as ClientBuilder;
+
+/**
+ * @group Plugins
+ * @group Marketplace
+ * @group ClientTest
+ * @group Client
+ */
+class ClientTest extends IntegrationTestCase
+{
+ /**
+ * @var Client
+ */
+ private $client;
+
+ /**
+ * @var TestService
+ */
+ private $service;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->service = new TestService();
+ $this->client = $this->buildClient();
+ }
+
+ public function test_download()
+ {
+ $this->service->returnFixture('v2.0_plugins_TreemapVisualization_info.json');
+
+ $file = $this->client->download('AnyPluginName');
+
+ $this->assertFileExists($file);
+ $this->assertStringEqualsFile($file, 'http://plugins.piwik.org/api/2.0/plugins/TreemapVisualization/download/1.0.1?coreVersion=2.16.3');
+ Filesystem::deleteFileIfExists($file);
+
+ $this->assertStringStartsWith(PIWIK_INCLUDE_PATH . '/tmp/latest/plugins/', $file);
+ $this->assertStringEndsWith('.zip', $file);
+ }
+
+ /**
+ * @expectedException \Piwik\Plugins\Marketplace\Api\Exception
+ * @expectedExceptionMessage Requested plugin does not exist.
+ */
+ public function test_getPluginInfo_shouldThrowException_IfNotAllowedToRequestPlugin()
+ {
+ $this->service->returnFixture('v2.0_plugins_CustomPlugin1_info-access_token-notexistingtoken.json');
+ $this->client->getPluginInfo('CustomPlugin1');
+ }
+
+ private function buildClient()
+ {
+ return ClientBuilder::build($this->service);
+ }
+
+}
diff --git a/plugins/Marketplace/tests/Integration/EnvironmentTest.php b/plugins/Marketplace/tests/Integration/EnvironmentTest.php
new file mode 100644
index 0000000000..5c23b28145
--- /dev/null
+++ b/plugins/Marketplace/tests/Integration/EnvironmentTest.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\Integration\Api;
+
+use Piwik\Plugin;
+use Piwik\Plugin\ReleaseChannels;
+use Piwik\Plugins\Marketplace\Environment;
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+use Piwik\Version;
+
+/**
+ * @group Plugins
+ * @group Marketplace
+ * @group EnvironmentTest
+ * @group Environment
+ */
+class EnvironmentTest extends IntegrationTestCase
+{
+ /**
+ * @var Environment
+ */
+ private $environment;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ Fixture::createSuperUser();
+ Fixture::createWebsite('2014-01-01 02:02:02');
+ Fixture::createWebsite('2014-01-01 02:02:02');
+ Fixture::createWebsite('2014-01-01 02:02:02');
+
+ $releaseChannes = new ReleaseChannels(Plugin\Manager::getInstance());
+ $releaseChannes->setActiveReleaseChannelId('latest_stable');
+
+ $this->environment = new Environment($releaseChannes);
+ }
+
+ public function test_getPhpVersion()
+ {
+ $this->assertEquals(phpversion(), $this->environment->getPhpVersion());
+ }
+
+ public function test_getPiwikVersion()
+ {
+ $this->assertEquals(Version::VERSION, $this->environment->getPiwikVersion());
+ }
+
+ public function test_setPiwikVersion_OverwritesCurrentPiwikVersion()
+ {
+ $this->environment->setPiwikVersion('1.12.0');
+ $this->assertSame('1.12.0', $this->environment->getPiwikVersion());
+ }
+
+ public function test_getNumUsers()
+ {
+ $this->assertSame(1, $this->environment->getNumUsers());
+ }
+
+ public function test_getNumWebsites()
+ {
+ $this->assertSame(3, $this->environment->getNumWebsites());
+ }
+
+ public function test_getMySQLVersion()
+ {
+ $this->assertNotEmpty($this->environment->getMySQLVersion());
+ }
+
+ public function test_getReleaseChannel()
+ {
+ $this->assertEquals('latest_stable', $this->environment->getReleaseChannel());
+ }
+
+ public function test_doesPreferStable()
+ {
+ $this->assertTrue($this->environment->doesPreferStable());
+ }
+
+}
diff --git a/plugins/Marketplace/tests/Integration/Input/PluginNameTest.php b/plugins/Marketplace/tests/Integration/Input/PluginNameTest.php
new file mode 100644
index 0000000000..51e8ace8be
--- /dev/null
+++ b/plugins/Marketplace/tests/Integration/Input/PluginNameTest.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\Integration\Input;
+
+use Piwik\Plugins\Marketplace\Input\PluginName;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+
+/**
+ * @group Plugins
+ * @group Marketplace
+ * @group PluginNameTest
+ * @group PluginName
+ */
+class PluginNameTest extends IntegrationTestCase
+{
+ public function tearDown()
+ {
+ unset($_GET['pluginName']);
+ }
+
+ public function test_findsPluginName()
+ {
+ $this->setPluginName('CoreFooBar');
+
+ $pluginName = new PluginName();
+ $this->assertSame('CoreFooBar', $pluginName->getPluginName());
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectedExceptionMessage Invalid plugin name given
+ */
+ public function test_throws_exception_ifInvalidName()
+ {
+ $this->setPluginName('CoreFooBar-?4');
+
+ $pluginName = new PluginName();
+ $pluginName->getPluginName();
+ }
+
+ private function setPluginName($name)
+ {
+ $_GET['pluginName'] = $name;
+ }
+
+
+}
diff --git a/plugins/Marketplace/tests/Integration/LicenseKeyTest.php b/plugins/Marketplace/tests/Integration/LicenseKeyTest.php
new file mode 100644
index 0000000000..5f548b1c31
--- /dev/null
+++ b/plugins/Marketplace/tests/Integration/LicenseKeyTest.php
@@ -0,0 +1,126 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\Integration;
+
+use Piwik\Plugins\Marketplace\LicenseKey;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+
+/**
+ * @group Plugins
+ * @group Marketplace
+ * @group LicenseKeyTest
+ * @group LicenseKey
+ */
+class LicenseKeyTest extends IntegrationTestCase
+{
+ /**
+ * @var LicenseKey
+ */
+ private $licenseKey;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->licenseKey = $this->buildLicenseKey();
+ }
+
+ public function test_get_noLicenseKeyIsSetByDefault()
+ {
+ $this->assertFalse($this->licenseKey->get());
+ $this->assertFalse($this->licenseKey->has());
+ }
+
+ public function test_set_get_persistsALicenseKey()
+ {
+ $key = 'foobarBaz';
+ $this->licenseKey->set($key);
+ $this->assertSame($key, $this->licenseKey->get());
+
+ // verify it is saved across requests by creating a new instance
+ $this->assertPersistedLicenseKeyEquals($key);
+ }
+
+ public function test_set_shouldOverwriteAnExistingKey()
+ {
+ $this->setExampleLicenseKey();
+
+ $key = 'foobarBaz2Unique299';
+ $this->assertPersistedLicenseKeyNotEquals($key);
+ $this->licenseKey->set($key);
+ $this->assertPersistedLicenseKeyEquals($key);
+ }
+
+ public function test_set_deletesAnExistingLicenseKey_IfValueIsFalse()
+ {
+ $this->setExampleLicenseKey();
+
+ $this->licenseKey->set(false);
+ $this->assertFalse($this->licenseKey->has());
+ }
+
+ public function test_set_deletesAnExistingLicenseKey_IfValueIsNotSet()
+ {
+ $this->setExampleLicenseKey();
+
+ $this->licenseKey->set(null);
+ $this->assertFalse($this->licenseKey->has());
+ }
+
+ public function test_has_detectsWhetherANonEmptyKeyIsSet()
+ {
+ $this->assertNotHasPersistedLicenseKey();
+ $this->setExampleLicenseKey();
+ $this->assertHasPersistedLicenseKey();
+ $this->licenseKey->set('');
+ $this->assertNotHasPersistedLicenseKey();
+ $this->licenseKey->set('1');
+ $this->assertHasPersistedLicenseKey();
+ $this->licenseKey->set('0');
+ $this->assertHasPersistedLicenseKey();
+ $this->licenseKey->set(null);
+ $this->assertNotHasPersistedLicenseKey();
+ }
+
+ private function assertHasPersistedLicenseKey()
+ {
+ // we create a new instance so it's actually persisted and not hold in an object instance
+ $this->assertTrue($this->buildLicenseKey()->has());
+ }
+
+ private function assertNotHasPersistedLicenseKey()
+ {
+ // we create a new instance so it's actually persisted and not hold in an object instance
+ $this->assertFalse($this->buildLicenseKey()->has());
+ }
+
+ private function assertPersistedLicenseKeyEquals($expectedKey)
+ {
+ // we create a new instance so it's actually persisted and not hold in an object instance
+ $this->assertSame($expectedKey, $this->buildLicenseKey()->get());
+ }
+
+ private function assertPersistedLicenseKeyNotEquals($expectedKey)
+ {
+ // we create a new instance so it's actually persisted and not hold in an object instance
+ $this->assertNotSame($expectedKey, $this->buildLicenseKey()->get());
+ }
+
+ private function setExampleLicenseKey()
+ {
+ $this->licenseKey->set('foo');
+ $this->assertTrue($this->licenseKey->has());
+ }
+
+ private function buildLicenseKey()
+ {
+ return new LicenseKey();
+ }
+
+}
diff --git a/plugins/Marketplace/tests/Integration/Plugins/InvalidLicensesTest.php b/plugins/Marketplace/tests/Integration/Plugins/InvalidLicensesTest.php
new file mode 100644
index 0000000000..b31aea1e2e
--- /dev/null
+++ b/plugins/Marketplace/tests/Integration/Plugins/InvalidLicensesTest.php
@@ -0,0 +1,290 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\Integration\Plugins;
+
+use Piwik\Cache\Backend\ArrayCache;
+use Piwik\Cache\Eager;
+use Piwik\Container\StaticContainer;
+use Piwik\Plugins\Marketplace\Consumer;
+use Piwik\Plugins\Marketplace\Plugins;
+use Piwik\Plugins\Marketplace\Plugins\InvalidLicenses;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+use Piwik\Plugins\Marketplace\tests\Framework\Mock\Consumer as ConsumerBuilder;
+use Piwik\Translate;
+
+class CustomInvalidLicenses extends InvalidLicenses {
+ private $isActivated = true;
+
+ public function setPluginIsActivated($isActivated)
+ {
+ $this->isActivated = $isActivated;
+ }
+
+ public function isPluginActivated($pluginName)
+ {
+ return $this->isActivated;
+ }
+}
+
+/**
+ * @group Marketplace
+ * @group InvalidLicensesTest
+ * @group InvalidLicenses
+ * @group Plugins
+ */
+class InvalidLicensesTest extends IntegrationTestCase
+{
+ /**
+ * @var Eager
+ */
+ private $cache;
+
+ private $cacheKey = 'Marketplace_ExpiredPlugins';
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ Translate::loadEnglishTranslation();
+
+ $this->cache = new Eager(new ArrayCache(), 'test');
+ }
+
+ public function tearDown()
+ {
+ Translate::unloadEnglishTranslation();
+ parent::tearDown();
+ }
+
+ public function test_getNamesOfExpiredPaidPlugins_validLicenses_noPaidPluginActivated()
+ {
+ $expired = $this->buildWithValidLicense();
+ $expired->setPluginIsActivated(false);
+
+ $expected = array('exceeded' => array(), 'expired' => array(), 'noLicense' => array());
+
+ $this->assertSame($expected, $expired->getPluginNamesOfInvalidLicenses());
+ }
+
+ public function test_getNamesOfExpiredPaidPlugins_noLicenses_noPaidPluginActivated()
+ {
+ $expired = $this->buildWithNoLicense();
+ $expired->setPluginIsActivated(false);
+
+ $expected = array(
+ 'exceeded' => array(),
+ 'expired' => array(),
+ 'noLicense' => array());
+
+ $this->assertSame($expected, $expired->getPluginNamesOfInvalidLicenses());
+ }
+
+ public function test_getNamesOfExpiredPaidPlugins_invalidLicenses_noPaidPluginActivated()
+ {
+ $expired = $this->buildWithExpiredLicense();
+ $expired->setPluginIsActivated(false);
+
+ $expected = array(
+ 'exceeded' => array(),
+ 'expired' => array(),
+ 'noLicense' => array());
+
+ $this->assertSame($expected, $expired->getPluginNamesOfInvalidLicenses());
+ }
+
+ public function test_getNamesOfExpiredPaidPlugins_exceededLicenses_noPaidPluginActivated()
+ {
+ $expired = $this->buildWithExceededLicense();
+ $expired->setPluginIsActivated(false);
+
+ $expected = array('exceeded' => array(), 'expired' => array(), 'noLicense' => array());
+
+ $this->assertSame($expected, $expired->getPluginNamesOfInvalidLicenses());
+ }
+
+ public function test_getNamesOfExpiredPaidPlugins_validLicenses()
+ {
+ $expired = $this->buildWithValidLicense();
+
+ $expected = array('exceeded' => array(), 'expired' => array(), 'noLicense' => array());
+
+ $this->assertSame($expected, $expired->getPluginNamesOfInvalidLicenses());
+ }
+
+ public function test_getNamesOfExpiredPaidPlugins_noLicenses()
+ {
+ $expired = $this->buildWithNoLicense();
+
+ $expected = array(
+ 'exceeded' => array(),
+ 'expired' => array(),
+ 'noLicense' => array('PaidPlugin1'));
+
+ $this->assertSame($expected, $expired->getPluginNamesOfInvalidLicenses());
+ }
+
+ public function test_getNamesOfExpiredPaidPlugins_invalidLicenses()
+ {
+ $expired = $this->buildWithExpiredLicense();
+
+ $expected = array(
+ 'exceeded' => array(),
+ 'expired' => array('PaidPlugin1'),
+ 'noLicense' => array());
+
+ $this->assertSame($expected, $expired->getPluginNamesOfInvalidLicenses());
+ }
+
+ public function test_getNamesOfExpiredPaidPlugins_exceededLicenses()
+ {
+ $expired = $this->buildWithExceededLicense();
+
+ $expected = array(
+ 'exceeded' => array('PaidPlugin2'),
+ 'expired' => array('PaidPlugin1'),
+ 'noLicense' => array());
+ $this->assertEquals($expected, $expired->getPluginNamesOfInvalidLicenses());
+ }
+
+ public function test_getNamesOfExpiredPaidPlugins_shouldCacheAnyResult()
+ {
+ $this->assertFalse($this->cache->contains($this->cacheKey));
+
+ $this->buildWithValidLicense()->getPluginNamesOfInvalidLicenses();
+
+ $this->assertTrue($this->cache->contains($this->cacheKey));
+
+ $expected = array('exceeded' => array(), 'expired' => array(), 'noLicense' => array());
+
+ $this->assertSame($expected, $this->cache->fetch($this->cacheKey));
+ }
+
+ public function test_getNamesOfExpiredPaidPlugins_shouldCache_IfNotValidLicenseKeyButPaidPluginsInstalled()
+ {
+ $this->buildWithExpiredLicense()->getPluginNamesOfInvalidLicenses();
+
+ $expected = array(
+ 'exceeded' => array(),
+ 'expired' => array('PaidPlugin1'),
+ 'noLicense' => array());
+
+ $this->assertSame($expected, $this->cache->fetch($this->cacheKey));
+ }
+
+ public function test_getMessageExceededLicenses_getMessageExpiredLicenses_validLicenses_noPaidPluginActivated()
+ {
+ $expired = $this->buildWithValidLicense();
+ $expired->setPluginIsActivated(false);
+
+ $this->assertNull($expired->getMessageExceededLicenses());
+ $this->assertNull($expired->getMessageExpiredLicenses());
+ $this->assertNull($expired->getMessageNoLicense());
+ }
+
+ public function test_getMessageExceededLicenses_getMessageExpiredLicenses_invalidLicenses_noPaidPluginActivated()
+ {
+ $expired = $this->buildWithExpiredLicense();
+ $expired->setPluginIsActivated(false);
+
+ $this->assertNull($expired->getMessageExceededLicenses());
+ $this->assertNull($expired->getMessageExpiredLicenses());
+ $this->assertNull($expired->getMessageNoLicense());
+ }
+
+ public function test_getMessageExceededLicenses_getMessageExpiredLicenses_exceededLicenses_noPaidPluginActivated()
+ {
+ $expired = $this->buildWithExceededLicense();
+ $expired->setPluginIsActivated(false);
+ $this->assertNull($expired->getMessageExceededLicenses());
+ $this->assertNull($expired->getMessageExpiredLicenses());
+ $this->assertNull($expired->getMessageNoLicense());
+ }
+
+ public function test_getMessageExceededLicenses_getMessageExpiredLicenses_validLicenses_PaidPluginActivated()
+ {
+ $expired = $this->buildWithValidLicense();
+
+ $this->assertNull($expired->getMessageExceededLicenses());
+ $this->assertNull($expired->getMessageExpiredLicenses());
+ $this->assertNull($expired->getMessageNoLicense());
+ }
+
+ public function test_getMessageExceededLicenses_getMessageExpiredLicenses_noLicenses_PaidPluginActivated()
+ {
+ // in theory we would need to show a warning as there is no license, but this can also happen if there's some random
+ // error and the user actually has a license, eg if the request aborted when fetching consumer etc
+ $expired = $this->buildWithNoLicense();
+
+ $this->assertEquals('', $expired->getMessageExceededLicenses());
+ $this->assertEquals('', $expired->getMessageExpiredLicenses());
+ $this->assertEquals('You are using the following plugins without a license: <strong>PaidPlugin1</strong>. <br/>To resolve this issue either update your license key, <strong><a href="https://shop.piwik.org/my-account" target="_blank" rel="noreferrer">get a subscription now</a></strong> or deactivate the plugin. <br/><a href="?module=Marketplace&action=subscriptionOverview">View your plugin subscriptions.</a>', $expired->getMessageNoLicense());
+ }
+
+ public function test_getMessageExceededLicenses_getMessageExpiredLicenses_invalidLicenses_PaidPluginActivated()
+ {
+ $expired = $this->buildWithExpiredLicense();
+
+ $this->assertNull($expired->getMessageExceededLicenses());
+ $this->assertEquals('The licenses for the following plugins are expired: <strong>PaidPlugin1</strong>. <br/>You will no longer receive any updates for these plugins. To resolve this issue either <strong><a href="https://shop.piwik.org/my-account" target="_blank" rel="noreferrer">renew your subscription now</a></strong>, or deactivate the plugin if you no longer use it. <br/><a href="?module=Marketplace&action=subscriptionOverview">View your plugin subscriptions.</a>', $expired->getMessageExpiredLicenses());
+ }
+
+ public function test_getMessageExceededLicenses_getMessageExpiredLicenses_exceededLicenses_PaidPluginActivated()
+ {
+ $expired = $this->buildWithExceededLicense();
+ $this->assertEquals('The licenses for the following plugins are no longer valid as the number of authorized users for the license is exceeded: <strong>PaidPlugin2</strong>. <br/>You will not be able to download updates for these plugins. To resolve this issue either delete some users or <strong><a href="https://shop.piwik.org/my-account" target="_blank" rel="noreferrer">upgrade the subscription now</a></strong>. <br/><a href="?module=Marketplace&action=subscriptionOverview">View your plugin subscriptions.</a>', $expired->getMessageExceededLicenses());
+ $this->assertEquals('The licenses for the following plugins are expired: <strong>PaidPlugin1</strong>. <br/>You will no longer receive any updates for these plugins. To resolve this issue either <strong><a href="https://shop.piwik.org/my-account" target="_blank" rel="noreferrer">renew your subscription now</a></strong>, or deactivate the plugin if you no longer use it. <br/><a href="?module=Marketplace&action=subscriptionOverview">View your plugin subscriptions.</a>', $expired->getMessageExpiredLicenses());
+ }
+
+ public function test_getMessageMissingLicenses_getMessageMissingLicenses_PaidPluginActivated()
+ {
+ $expired = $this->buildWithNoLicense();
+ $this->assertEquals('You are using the following plugins without a license: <strong>PaidPlugin1</strong>. <br/>To resolve this issue either update your license key, <strong><a href="https://shop.piwik.org/my-account" target="_blank" rel="noreferrer">get a subscription now</a></strong> or deactivate the plugin. <br/><a href="?module=Marketplace&action=subscriptionOverview">View your plugin subscriptions.</a>', $expired->getMessageNoLicense());
+ }
+
+ private function buildWithValidLicense()
+ {
+ $consumer = ConsumerBuilder::buildValidLicense();
+ return $this->buildInvalidLicense($consumer);
+ }
+
+ private function buildWithExpiredLicense()
+ {
+ $consumer = ConsumerBuilder::buildExpiredLicense();
+ return $this->buildInvalidLicense($consumer);
+ }
+
+ private function buildWithNoLicense()
+ {
+ $consumer = ConsumerBuilder::buildNoLicense();
+ return $this->buildInvalidLicense($consumer);
+ }
+
+ private function buildWithExceededLicense()
+ {
+ $consumer = ConsumerBuilder::buildExceededLicense();
+ return $this->buildInvalidLicense($consumer);
+ }
+
+ /**
+ * @param Consumer $consumer
+ * @return CustomInvalidLicenses
+ */
+ private function buildInvalidLicense($consumer)
+ {
+ $translator = StaticContainer::get('Piwik\Translation\Translator');
+ $advertising = StaticContainer::get('Piwik\ProfessionalServices\Advertising');
+ $client = $consumer->getApiClient();
+ $plugins = new Plugins($client, $consumer, $advertising);
+
+ $licenses = new CustomInvalidLicenses($client, $this->cache, $translator, $plugins);
+ $licenses->clearCache();
+ return $licenses;
+ }
+
+}
diff --git a/plugins/Marketplace/tests/Integration/PluginsTest.php b/plugins/Marketplace/tests/Integration/PluginsTest.php
new file mode 100644
index 0000000000..815fd537a5
--- /dev/null
+++ b/plugins/Marketplace/tests/Integration/PluginsTest.php
@@ -0,0 +1,486 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\Integration;
+
+use Piwik\Plugins\Marketplace\API;
+use Piwik\Plugins\Marketplace\Consumer;
+use Piwik\Plugins\Marketplace\Input\PurchaseType;
+use Piwik\Plugins\Marketplace\Input\Sort;
+use Piwik\Plugins\Marketplace\Plugins;
+use Piwik\Plugins\Marketplace\tests\Framework\Mock\Client;
+use Piwik\Plugins\Marketplace\tests\Framework\Mock\Service;
+use Piwik\Tests\Framework\Mock\ProfessionalServices\Advertising;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+use Piwik\Plugin;
+
+/**
+ * @group Marketplace
+ * @group PluginsTest
+ * @group Plugins
+ */
+class PluginsTest extends IntegrationTestCase
+{
+ /**
+ * @var Plugins
+ */
+ private $plugins;
+
+ /**
+ * @var Service
+ */
+ private $service;
+
+ /**
+ * @var Service
+ */
+ private $consumerService;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ API::unsetInstance();
+
+ $this->service = new Service();
+ $this->consumerService = new Service();
+
+ $this->plugins = new Plugins(
+ Client::build($this->service),
+ new Consumer(Client::build($this->consumerService)),
+ new Advertising()
+ );
+ }
+
+ public function test_getAllAvailablePluginNames_noPluginsFound()
+ {
+ $pluginNames = $this->plugins->getAllAvailablePluginNames();
+ $this->assertSame(array(), $pluginNames);
+ }
+
+ public function test_getAllAvailablePluginNames()
+ {
+ $this->service->returnFixture(array(
+ 'v2.0_themes.json', 'v2.0_plugins.json'
+ ));
+ $pluginNames = $this->plugins->getAllAvailablePluginNames();
+ $expected = array (
+ 'AnotherBlackTheme',
+ 'Barometer',
+ 'Counter',
+ 'CustomAlerts',
+ 'CustomOptOut',
+ 'FeedAnnotation',
+ 'IPv6Usage',
+ 'LiveTab',
+ 'LoginHttpAuth',
+ 'page2images-visual-link',
+ 'PaidPlugin1',
+ 'ReferrersManager',
+ 'SecurityInfo',
+ 'TasksTimetable',
+ 'TreemapVisualization',
+ );
+ foreach ($expected as $name) {
+ $this->assertContains($name, $pluginNames);
+ }
+ }
+
+ public function test_getAvailablePluginNames_noPluginsFound()
+ {
+ $pluginNames = $this->plugins->getAvailablePluginNames($themesOnly = true);
+ $this->assertSame(array(), $pluginNames);
+
+ $pluginNames = $this->plugins->getAvailablePluginNames($themesOnly = false);
+ $this->assertSame(array(), $pluginNames);
+ }
+
+ public function test_getAvailablePluginNames_shouldReturnPluginNames()
+ {
+ $this->service->returnFixture('v2.0_themes.json');
+ $pluginNames = $this->plugins->getAvailablePluginNames($themesOnly = true);
+ $this->assertSame(array(
+ 'AnotherBlackTheme',
+ 'Darkness',
+ 'Proteus_Bold',
+ 'Terrano',
+ 'CoffeeCup',
+ 'Vale',
+ 'ModernBlue',
+ 'ModernGreen'), $pluginNames);
+
+ $this->service->returnFixture('v2.0_plugins.json');
+ $pluginNames = $this->plugins->getAvailablePluginNames($themesOnly = false);
+ $this->assertSame($this->getExpectedPluginNames(), $pluginNames);
+ }
+
+ public function test_getAvailablePluginNames_shouldCallCorrectApi()
+ {
+ $this->plugins->getAvailablePluginNames($themesOnly = true);
+ $this->assertSame('themes', $this->service->action);
+
+ $this->plugins->getAvailablePluginNames($themesOnly = false);
+ $this->assertSame('plugins', $this->service->action);
+ }
+
+ public function test_getPluginInfo_noSuchPluginExists()
+ {
+ $plugin = $this->plugins->getPluginInfo('fooBarBaz');
+ $this->assertSame(array(), $plugin);
+ }
+
+ public function test_getPluginInfo_notInstalledPlugin_shouldEnrichPluginInformation()
+ {
+ $this->service->returnFixture('v2.0_plugins_Barometer_info.json');
+ $plugin = $this->plugins->getPluginInfo('Barometer');
+
+ unset($plugin['versions']);
+
+ $expected = array (
+ 'name' => 'Barometer',
+ 'displayName' => 'Barometer',
+ 'owner' => 'halfdan',
+ 'description' => 'Live Plugin that shows the current number of visitors on the page.',
+ 'homepage' => 'http://github.com/halfdan/piwik-barometer-plugin',
+ 'createdDateTime' => '2014-12-23 00:38:20',
+ 'donate' =>
+ array (
+ 'flattr' => 'https://flattr.com/profile/test1',
+ 'bitcoin' => NULL,
+ ),
+ 'support' =>
+ array (
+ array (
+ 'name' => 'Documentation',
+ 'key' => 'docs',
+ 'value' => 'https://barometer.org/docs/',
+ 'type' => 'url',
+ ),
+ array (
+ 'name' => 'Wiki',
+ 'key' => 'wiki',
+ 'value' => 'https://github.com/barometer/piwik/wiki',
+ 'type' => 'url',
+ ),
+ array (
+ 'name' => 'Forum',
+ 'key' => 'forum',
+ 'value' => 'https://baromter.forum.org',
+ 'type' => 'url',
+ ),
+ array (
+ 'name' => 'Email',
+ 'key' => 'email',
+ 'value' => 'barometer@example.com',
+ 'type' => 'email',
+ ),
+ array (
+ 'name' => 'IRC',
+ 'key' => 'irc',
+ 'value' => 'irc://freenode/baromter',
+ 'type' => 'text',
+ ),
+ array (
+ 'name' => 'Issues / Bugs',
+ 'key' => 'issues',
+ 'value' => 'https://github.com/barometer/issues',
+ 'type' => 'url',
+ ),
+ array (
+ 'name' => 'Source',
+ 'key' => 'source',
+ 'value' => 'https://github.com/barometer/piwik/',
+ 'type' => 'url',
+ ),
+ array (
+ 'name' => 'RSS',
+ 'key' => 'rss',
+ 'value' => 'https://barometer.org/feed/',
+ 'type' => 'url',
+ ),
+ ),
+ 'isTheme' => false,
+ 'keywords' => array ('barometer','live',),
+ 'basePrice' => 0,
+ 'authors' =>
+ array (array (
+ 'name' => 'Fabian Becker',
+ 'email' => 'test8@example.com',
+ 'homepage' => 'http://geekproject.eu',
+ ),),
+ 'repositoryUrl' => 'https://github.com/halfdan/piwik-barometer-plugin',
+ 'lastUpdated' => 'Intl_4or41Intl_Time_AMt_357Intl_Time_AMt_S12ort',
+ 'latestVersion' => '0.5.0',
+ 'numDownloads' => 0,
+ 'screenshots' =>
+ array (
+ 'https://plugins.piwik.org/Barometer/images/0.5.0/piwik-barometer-01.png',
+ 'https://plugins.piwik.org/Barometer/images/0.5.0/piwik-barometer-02.png',
+ ),
+ 'previews' =>
+ array (array (
+ 'type' => 'demo',
+ 'provider' => 'link',
+ 'url' => 'https://demo.piwik.org',
+ ),),
+ 'activity' =>
+ array (
+ 'numCommits' => '31',
+ 'numContributors' => '3',
+ 'lastCommitDate' => NULL,
+ ),
+ 'featured' => false,
+ 'isFree' => true,
+ 'isPaid' => false,
+ 'isCustomPlugin' => false,
+ 'shop' => NULL,
+ 'isDownloadable' => true,
+ 'consumer' => array ('license' => NULL,),
+ 'isInstalled' => false,
+ 'isActivated' => false,
+ 'isInvalid' => true,
+ 'canBeUpdated' => false,
+ 'hasExceededLicense' => false,
+ 'missingRequirements' =>array ( ),
+ 'isMissingLicense' => false
+ );
+ $this->assertEquals($expected, $plugin);
+ }
+
+ public function test_getPluginInfo_notInstalledPlugin_shouldCallCorrectService()
+ {
+ $this->plugins->getPluginInfo('Barometer');
+ $this->assertSame('plugins/Barometer/info', $this->service->action);
+ }
+
+ public function test_searchPlugins_WithSearchAndNoPluginsFound_shouldCallCorrectApi()
+ {
+ $this->service->returnFixture('v2.0_plugins-query-nomatchforthisquery.json');
+ $this->plugins->setPluginsHavingUpdateCache(array());
+ $plugins = $this->plugins->searchPlugins($query = 'nomatchforthisquery', $sort = Sort::DEFAULT_SORT, $themesOnly = false);
+
+ $this->assertSame(array(), $plugins);
+ $this->assertSame('plugins', $this->service->action);
+
+ $params = array(
+ 'keywords' => '',
+ 'purchase_type' => '',
+ 'query' => 'nomatchforthisquery',
+ 'sort' => Sort::DEFAULT_SORT,
+ 'release_channel' => 'latest_stable',
+ 'prefer_stable' => 1,
+ 'piwik' => '2.16.3',
+ 'php' => '7.0.1',
+ 'mysql' => '5.7.1',
+ 'num_users' => 5,
+ 'num_websites' => 21,
+ );
+ $this->assertSame($params, $this->service->params);
+ }
+
+ public function test_searchThemes_ShouldCallCorrectApi()
+ {
+ $this->service->returnFixture('v2.0_themes.json');
+ $this->plugins->setPluginsHavingUpdateCache(array());
+ $plugins = $this->plugins->searchPlugins($query = '', $sort = Sort::DEFAULT_SORT, $themesOnly = true);
+
+ $this->assertCount(8, $plugins);
+ $this->assertSame('AnotherBlackTheme', $plugins[0]['name']);
+ $this->assertSame('themes', $this->service->action);
+
+ $params = array(
+ 'keywords' => '',
+ 'purchase_type' => '',
+ 'query' => '',
+ 'sort' => Sort::DEFAULT_SORT,
+ 'release_channel' => 'latest_stable',
+ 'prefer_stable' => 1,
+ 'piwik' => '2.16.3',
+ 'php' => '7.0.1',
+ 'mysql' => '5.7.1',
+ 'num_users' => 5,
+ 'num_websites' => 21,
+ );
+ $this->assertSame($params, $this->service->params);
+ }
+
+ public function test_searchPlugins_manyPluginsFound_shouldEnrichAll()
+ {
+ $this->service->returnFixture('v2.0_plugins.json');
+ $plugins = $this->plugins->searchPlugins($query = '', $sort = Sort::DEFAULT_SORT, $themesOnly = false);
+
+ $this->assertCount(54, $plugins);
+ $names = array_map(function ($plugin) {
+ return $plugin['name'];
+ }, $plugins);
+ $this->assertSame($this->getExpectedPluginNames(), $names);
+
+ foreach ($plugins as $plugin) {
+ $name = $plugin['name'];
+ $this->assertFalse($plugin['isTheme']);
+ $this->assertNotEmpty($plugin['homepage']);
+
+ $piwikProCampaign = 'pk_campaign=App_ProfessionalServices&pk_medium=Marketplace&pk_source=Piwik_App';
+
+ if ($name === 'SecurityInfo') {
+ $this->assertTrue($plugin['isFree']);
+ $this->assertFalse($plugin['isPaid']);
+ $this->assertTrue(in_array($plugin['isInstalled'], array(true, false), true));
+ $this->assertFalse($plugin['isInvalid']);
+ $this->assertTrue(isset($plugin['canBeUpdated']));
+ $this->assertSame(array(), $plugin['missingRequirements']);
+ $this->assertSame(Plugin\Manager::getInstance()->isPluginActivated('SecurityInfo'), $plugin['isActivated']);
+ } elseif ($name === 'SimplePageBuilder') {
+ // should add campaign parameters if Piwik PRO plugin
+ $this->assertSame('https://github.com/PiwikPRO/SimplePageBuilder?' . $piwikProCampaign . '&pk_content=SimplePageBuilder', $plugin['homepage']);
+ }
+
+ if ($plugin['owner'] === 'PiwikPRO') {
+ $this->assertContains($piwikProCampaign, $plugin['homepage']);
+ } else {
+ $this->assertNotContains($piwikProCampaign, $plugin['homepage']);
+ }
+ }
+ }
+
+ public function test_getAllPaidPlugins_shouldFetchOnlyPaidPlugins()
+ {
+ $this->plugins->getAllPaidPlugins();
+ $this->assertSame('plugins', $this->service->action);
+ $this->assertSame(PurchaseType::TYPE_PAID, $this->service->params['purchase_type']);
+ $this->assertSame('', $this->service->params['query']);
+ }
+
+ public function test_getAllFreePlugins_shouldFetchOnlyFreePlugins()
+ {
+ $this->plugins->getAllFreePlugins();
+ $this->assertSame('plugins', $this->service->action);
+ $this->assertSame(PurchaseType::TYPE_FREE, $this->service->params['purchase_type']);
+ $this->assertSame('', $this->service->params['query']);
+ }
+
+ public function test_getAllPlugins_shouldFetchFreeAndPaidPlugins()
+ {
+ $this->plugins->getAllPlugins();
+ $this->assertSame('plugins', $this->service->action);
+ $this->assertSame(PurchaseType::TYPE_ALL, $this->service->params['purchase_type']);
+ $this->assertSame('', $this->service->params['query']);
+ }
+
+ public function test_getAllThemes_shouldFetchFreeAndPaidThemes()
+ {
+ $this->plugins->getAllThemes();
+ $this->assertSame('themes', $this->service->action);
+ $this->assertSame(PurchaseType::TYPE_ALL, $this->service->params['purchase_type']);
+ $this->assertSame('', $this->service->params['query']);
+ }
+
+ public function test_getPluginsHavingUpdate_shouldReturnEnrichedPluginUpdatesForPluginsFoundOnTheMarketplace()
+ {
+ $this->service->returnFixture(array(
+ 'v2.0_plugins_checkUpdates-pluginspluginsnameAnonymousPi.json',
+ 'emptyObjectResponse.json',
+ 'emptyObjectResponse.json',
+ 'emptyObjectResponse.json',
+ 'emptyObjectResponse.json',
+ 'emptyObjectResponse.json',
+ 'emptyObjectResponse.json',
+ 'emptyObjectResponse.json',
+ 'v2.0_plugins_TreemapVisualization_info.json'
+ ));
+ $apis = array();
+ $this->service->setOnFetchCallback(function ($action, $params) use (&$apis) {
+ $apis[] = $action;
+ });
+
+ $updates = $this->plugins->getPluginsHavingUpdate();
+ $pluginManager = Plugin\Manager::getInstance();
+ $pluginName = 'TreemapVisualization';
+
+ $this->assertCount(1, $updates);
+ $plugin = $updates[0];
+ $this->assertSame($pluginName, $plugin['name']);
+ $this->assertSame($pluginManager->getLoadedPlugin($pluginName)->getVersion(), $plugin['currentVersion']);
+ $this->assertSame($pluginManager->isPluginActivated($pluginName), $plugin['isActivated']);
+ $this->assertSame(array(), $plugin['missingRequirements']);
+ $this->assertSame('https://github.com/piwik/plugin-TreemapVisualization/commits/1.0.1', $plugin['repositoryChangelogUrl']);
+
+ $expectedApiCalls = array(
+ 'plugins/checkUpdates',
+ 'plugins/AnonymousPiwikUsageMeasurement/info',
+ 'plugins/CustomAlerts/info',
+ 'plugins/CustomDimensions/info',
+ 'plugins/LogViewer/info',
+ 'plugins/QueuedTracking/info',
+ 'plugins/SecurityInfo/info',
+ 'plugins/TasksTimetable/info',
+ 'plugins/TreemapVisualization/info'
+ );
+ $this->assertSame($expectedApiCalls, $apis);
+ }
+
+ private function getExpectedPluginNames()
+ {
+ return array (
+ 'AdminNotification',
+ 'AdvancedCampaignReporting',
+ 'AnonymousPiwikUsageMeasurement',
+ 'ApiGetWithSitesInfo',
+ 'Bandwidth',
+ 'Barometer',
+ 'Chat',
+ 'ClickHeat',
+ 'Counter',
+ 'CustomAlerts',
+ 'CustomDimensions',
+ 'CustomOptOut',
+ 'CustomTrackerJs',
+ 'ExcludeByDDNS',
+ 'FeedAnnotation',
+ 'FlagCounter',
+ 'FreeMobileMessaging',
+ 'GoogleAuthenticator',
+ 'GrabGravatar',
+ 'InterSites',
+ 'IntranetGeoIP',
+ 'Ip2Hostname',
+ 'IP2Location',
+ 'IPv6Usage',
+ 'kDebug',
+ 'LdapConnection',
+ 'LdapVisitorInfo',
+ 'LiveTab',
+ 'LoginHttpAuth',
+ 'LoginRevokable',
+ 'LogViewer',
+ 'page2images-visual-link',
+ 'PaidPlugin1',
+ 'PerformanceInfo',
+ 'PerformanceMonitor',
+ 'PlatformsReport',
+ 'QueuedTracking',
+ 'ReferrersManager',
+ 'RerUserDates',
+ 'SecurityInfo',
+ 'ServerMonitor',
+ 'ShibbolethLogin',
+ 'ShortcodeTracker',
+ 'SimplePageBuilder',
+ 'SimpleSysMon',
+ 'SiteMigration',
+ 'SnoopyBehavioralScoring',
+ 'TasksTimetable',
+ 'TopPagesByActions',
+ 'TrackingCodeCustomizer',
+ 'TreemapVisualization',
+ 'UptimeRobotMonitor',
+ 'VisitorAvatar',
+ 'WebsiteGroups'
+ );
+ }
+}
diff --git a/plugins/Marketplace/tests/Integration/ServiceTest.php b/plugins/Marketplace/tests/Integration/ServiceTest.php
new file mode 100644
index 0000000000..703ef77367
--- /dev/null
+++ b/plugins/Marketplace/tests/Integration/ServiceTest.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\Integration\Api;
+
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+use Piwik\Plugins\Marketplace\tests\Framework\Mock\Service as TestService;
+
+/**
+ * @group Plugins
+ * @group Marketplace
+ * @group Service
+ * @group ServiceTest
+ */
+class ServiceTest extends IntegrationTestCase
+{
+
+ /**
+ * @var TestService
+ */
+ private $service;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->service = new TestService();
+ }
+
+ /**
+ * @expectedException \Piwik\Plugins\Marketplace\Api\Service\Exception
+ * @expectedExceptionCode 101
+ * @expectedExceptionMessage Requested plugin does not exist.
+ */
+ public function test_fetch_throwsApiError_WhenMarketplaceReturnsAnError()
+ {
+ $this->service->returnFixture('v2.0_plugins_CustomPlugin1_info-access_token-notexistingtoken.json');
+ $this->service->fetch('plugins/CustomPlugin1/info', array());
+ }
+
+ /**
+ * @expectedException \Piwik\Plugins\Marketplace\Api\Service\Exception
+ * @expectedExceptionCode 100
+ * @expectedExceptionMessage There was an error reading the response from the Marketplace
+ */
+ public function test_fetch_throwsHttpError_WhenMarketplaceReturnsNoResultWhichMeansHttpError()
+ {
+ $this->service->setOnDownloadCallback(function () {
+ return null;
+ });
+ $this->service->fetch('plugins/CustomPlugin1/info', array());
+ }
+
+ public function test_fetch_jsonDecodesTheHttpResponse()
+ {
+ $this->service->returnFixture('v2.0_consumer-access_token-consumer1_paid2_custom1.json');
+ $consumer = $this->service->fetch('consumer', array());
+ $this->assertTrue(is_array($consumer));
+ $this->assertNotEmpty($consumer);
+ }
+
+}
diff --git a/plugins/CorePluginsAdmin/tests/Integration/UpdateCommunicationTest.php b/plugins/Marketplace/tests/Integration/UpdateCommunicationTest.php
index 06681f404d..bb7f46fba7 100644
--- a/plugins/CorePluginsAdmin/tests/Integration/UpdateCommunicationTest.php
+++ b/plugins/Marketplace/tests/Integration/UpdateCommunicationTest.php
@@ -6,20 +6,19 @@
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
-namespace Piwik\Plugins\CorePluginsAdmin\tests\Integration;
+namespace Piwik\Plugins\Marketplace\tests\Integration;
use Piwik\Config;
use Piwik\Container\StaticContainer;
use Piwik\Option;
-use Piwik\Plugins\CorePluginsAdmin\UpdateCommunication;
use Piwik\Plugins\CoreUpdater\SystemSettings;
+use Piwik\Plugins\Marketplace\UpdateCommunication;
use Piwik\Tests\Framework\Fixture;
use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
/**
- * Class Plugins_CorePluginsAdmin_UpdateCommunicationTest
- *
* @group Plugins
+ * @group Marketplace
*/
class UpdateCommunicationTest extends IntegrationTestCase
{
@@ -155,7 +154,7 @@ Installation_HappyAnalysing";
*/
private function getCommunicationMock($pluginsHavingUpdate)
{
- $mock = $this->getMockBuilder('\Piwik\Plugins\CorePluginsAdmin\UpdateCommunication')
+ $mock = $this->getMockBuilder('\Piwik\Plugins\Marketplace\UpdateCommunication')
->setMethods(array('getPluginsHavingUpdate', 'sendEmailNotification'))
->setConstructorArgs(array($this->settings))
->getMock();
diff --git a/plugins/Marketplace/tests/System/Api/ClientTest.php b/plugins/Marketplace/tests/System/Api/ClientTest.php
new file mode 100644
index 0000000000..e38fb809e1
--- /dev/null
+++ b/plugins/Marketplace/tests/System/Api/ClientTest.php
@@ -0,0 +1,299 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\System\Api;
+
+use Piwik\Cache;
+use Piwik\Plugin;
+use Piwik\Plugins\Marketplace\Api\Client;
+use Piwik\Plugins\Marketplace\Api\Service;
+use Piwik\Plugins\Marketplace\Environment;
+use Piwik\Plugins\Marketplace\Input\PurchaseType;
+use Piwik\Tests\Framework\TestCase\SystemTestCase;
+use Piwik\Version;
+use Piwik\Plugins\Marketplace\tests\Framework\Mock\Service as TestService;
+use Psr\Log\NullLogger;
+
+/**
+ * @group Plugins
+ * @group Marketplace
+ * @group ClientTest
+ * @group Client
+ */
+class ClientTest extends SystemTestCase
+{
+ private $domain = 'http://plugins.piwik.org';
+
+ /**
+ * @var Client
+ */
+ private $client;
+
+ /**
+ * @var Environment
+ */
+ private $environment;
+
+ public function setUp()
+ {
+ $releaseChannels = new Plugin\ReleaseChannels(Plugin\Manager::getInstance());
+ $this->environment = new Environment($releaseChannels);
+
+ $this->client = $this->buildClient();
+ $this->getCache()->flushAll();
+ }
+
+ public function test_getPluginInfo_existingPluginOnTheMarketplace()
+ {
+ $plugin = $this->client->getPluginInfo('SecurityInfo');
+
+ $expectedPluginKeys = array(
+ 'name',
+ 'displayName',
+ 'owner',
+ 'description',
+ 'homepage',
+ 'createdDateTime',
+ 'donate',
+ 'support',
+ 'isTheme',
+ 'keywords',
+ 'basePrice',
+ 'authors',
+ 'repositoryUrl',
+ 'lastUpdated',
+ 'latestVersion',
+ 'numDownloads',
+ 'screenshots',
+ 'previews',
+ 'activity',
+ 'featured',
+ 'isFree',
+ 'isPaid',
+ 'isCustomPlugin',
+ 'shop',
+ 'versions',
+ 'isDownloadable',
+ 'consumer');
+
+ $this->assertNotEmpty($plugin);
+ $this->assertEquals($expectedPluginKeys, array_keys($plugin));
+ $this->assertSame('SecurityInfo', $plugin['name']);
+ $this->assertSame('piwik', $plugin['owner']);
+ $this->assertTrue(is_array($plugin['keywords']));
+ $this->assertNotEmpty($plugin['authors']);
+ $this->assertGreaterThan(1000, $plugin['numDownloads']);
+ $this->assertTrue($plugin['isFree']);
+ $this->assertFalse($plugin['isPaid']);
+ $this->assertFalse($plugin['isCustomPlugin']);
+ $this->assertNotEmpty($plugin['versions']);
+
+ $lastVersion = $plugin['versions'][count($plugin['versions']) - 1];
+ $this->assertEquals(array('name', 'release', 'requires', 'numDownloads', 'license', 'repositoryChangelogUrl', 'readmeHtml', 'download'), array_keys($lastVersion));
+ $this->assertNotEmpty($lastVersion['download']);
+ }
+
+ /**
+ * @expectedException \Piwik\Plugins\Marketplace\Api\Exception
+ * @expectedExceptionMessage Requested plugin does not exist.
+ */
+ public function test_getPluginInfo_shouldThrowException_IfPluginDoesNotExistOnMarketplace()
+ {
+ $this->client->getPluginInfo('NotExistingPlugIn');
+ }
+
+ public function test_getConsumer_shouldReturnNullAndNotThrowException_IfNotAuthorized()
+ {
+ $this->assertNull($this->client->getConsumer());
+ }
+
+ public function test_isValidConsumer_shouldReturnFalseAndNotThrowException_IfNotAuthorized()
+ {
+ $this->assertFalse($this->client->isValidConsumer());
+ }
+
+ public function test_searchForPlugins_requestAll()
+ {
+ $plugins = $this->client->searchForPlugins($keywords = '', $query = '', $sort = '', $purchaseType = PurchaseType::TYPE_ALL);
+
+ $this->assertGreaterThan(15, count($plugins));
+
+ foreach ($plugins as $plugin) {
+ $this->assertNotEmpty($plugin['name']);
+ $this->assertFalse($plugin['isTheme']);
+ }
+ }
+
+ public function test_searchForPlugins_onlyFree()
+ {
+ $plugins = $this->client->searchForPlugins($keywords = '', $query = '', $sort = '', $purchaseType = PurchaseType::TYPE_FREE);
+
+ $this->assertGreaterThan(15, count($plugins));
+
+ foreach ($plugins as $plugin) {
+ $this->assertTrue($plugin['isFree']);
+ $this->assertFalse($plugin['isPaid']);
+ $this->assertFalse($plugin['isTheme']);
+ }
+ }
+
+ public function test_searchForPlugins_onlyPaid()
+ {
+ $plugins = $this->client->searchForPlugins($keywords = '', $query = '', $sort = '', $purchaseType = PurchaseType::TYPE_PAID);
+
+ $this->assertLessThan(30, count($plugins));
+
+ foreach ($plugins as $plugin) {
+ $this->assertFalse($plugin['isFree']);
+ $this->assertTrue($plugin['isPaid']);
+ $this->assertFalse($plugin['isTheme']);
+ }
+ }
+
+ public function test_searchForPlugins_withKeyword()
+ {
+ $plugins = $this->client->searchForPlugins($keywords = 'login', $query = '', $sort = '', $purchaseType = PurchaseType::TYPE_ALL);
+
+ $this->assertLessThan(30, count($plugins));
+
+ foreach ($plugins as $plugin) {
+ $this->assertContains($keywords, $plugin['keywords']);
+ }
+ }
+
+ public function test_searchForThemes_requestAll()
+ {
+ $plugins = $this->client->searchForThemes($keywords = '', $query = '', $sort = '', $purchaseType = PurchaseType::TYPE_ALL);
+
+ $this->assertGreaterThanOrEqual(1, count($plugins));
+ $this->assertLessThan(50, count($plugins));
+
+ foreach ($plugins as $plugin) {
+ $this->assertNotEmpty($plugin['name']);
+ $this->assertTrue($plugin['isTheme']);
+ }
+ }
+
+ public function test_getDownloadUrl()
+ {
+ $url = $this->client->getDownloadUrl('SecurityInfo');
+
+ $start = $this->domain . '/api/2.0/plugins/SecurityInfo/download/';
+ $end = '?coreVersion=' . Version::VERSION;
+
+ $this->assertStringStartsWith($start, $url);
+ $this->assertStringEndsWith($end, $url);
+
+ $version = str_replace(array($start, $end), '', $url);
+
+ $this->assertNotEmpty($version);
+ $this->assertRegExp('/\d+\.\d+\.\d+/', $version);
+ }
+
+ public function test_clientResponse_shouldBeCached()
+ {
+ $params = array(
+ 'keywords' => 'login',
+ 'purchase_type' => '',
+ 'query' => '',
+ 'sort' => '',
+ 'release_channel' => 'latest_stable',
+ 'prefer_stable' => 1,
+ 'piwik' => Version::VERSION,
+ 'php' => phpversion(),
+ 'mysql' => $this->environment->getMySQLVersion(),
+ 'num_users' => $this->environment->getNumUsers(),
+ 'num_websites' => $this->environment->getNumWebsites()
+ );
+ $id = 'marketplace.api.2.0.plugins.' . md5(http_build_query($params));
+
+ $cache = $this->getCache();
+ $this->assertFalse($cache->contains($id));
+
+ $this->client->searchForPlugins($keywords = 'login', $query = '', $sort = '', $purchaseType = PurchaseType::TYPE_ALL);
+
+ $this->assertTrue($cache->contains($id));
+ $cachedPlugins = $cache->fetch($id);
+
+ $this->assertInternalType('array', $cachedPlugins);
+ $this->assertNotEmpty($cachedPlugins);
+ $this->assertGreaterThan(30, $cachedPlugins);
+ }
+
+ public function test_cachedClientResponse_shouldBeReturned()
+ {
+ $params = array(
+ 'keywords' => 'login',
+ 'purchase_type' => '',
+ 'query' => '',
+ 'sort' => '',
+ 'release_channel' => 'latest_stable',
+ 'prefer_stable' => 1,
+ 'piwik' => Version::VERSION,
+ 'php' => phpversion(),
+ 'mysql' => $this->environment->getMySQLVersion(),
+ 'num_users' => $this->environment->getNumUsers(),
+ 'num_websites' => $this->environment->getNumWebsites());
+ $id = 'marketplace.api.2.0.plugins.' . md5(http_build_query($params));
+
+ $cache = $this->getCache();
+ $cache->save($id, array('plugins' => array(array('name' => 'foobar'))));
+
+ $result = $this->client->searchForPlugins($keywords = 'login', $query = '', $sort = '', $purchaseType = PurchaseType::TYPE_ALL);
+
+ $this->assertSame(array(array('name' => 'foobar')), $result);
+ }
+
+ public function test_getInfoOfPluginsHavingUpdate()
+ {
+ $service = new TestService($this->domain);
+ $client = $this->buildClient($service);
+
+ $pluginTest = array();
+ if (!Plugin\Manager::getInstance()->isPluginLoaded('CustomAlerts')) {
+ $pluginTest[] = Plugin\Manager::getInstance()->loadPlugin('CustomAlerts');
+ } else {
+ $pluginTest[] = Plugin\Manager::getInstance()->getLoadedPlugin('CustomAlerts');
+ }
+
+ $client->getInfoOfPluginsHavingUpdate($pluginTest);
+
+ $this->assertSame('plugins/checkUpdates', $service->action);
+ $this->assertSame(array('plugins', 'release_channel', 'prefer_stable', 'piwik', 'php', 'mysql', 'num_users', 'num_websites'), array_keys($service->params));
+
+ $plugins = $service->params['plugins'];
+ $this->assertInternalType('string', $plugins);
+ $this->assertJson($plugins);
+ $plugins = json_decode($plugins, true);
+
+ $names = array(
+ 'CustomAlerts' => true,
+ );
+ foreach ($plugins['plugins'] as $plugin) {
+ $this->assertNotEmpty($plugin['version']);
+ unset($names[$plugin['name']]);
+ }
+
+ $this->assertEmpty($names);
+ }
+
+ private function buildClient($service = null)
+ {
+ if (!isset($service)) {
+ $service = new Service($this->domain);
+ }
+
+ return new Client($service, $this->getCache(), new NullLogger(), $this->environment);
+ }
+
+ private function getCache()
+ {
+ return Cache::getLazyCache();
+ }
+
+}
diff --git a/plugins/Marketplace/tests/System/Api/ServiceTest.php b/plugins/Marketplace/tests/System/Api/ServiceTest.php
new file mode 100644
index 0000000000..6a4b4dab29
--- /dev/null
+++ b/plugins/Marketplace/tests/System/Api/ServiceTest.php
@@ -0,0 +1,165 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\System\Api;
+
+use Piwik\Container\StaticContainer;
+use Piwik\Filesystem;
+use Piwik\Plugins\Marketplace\Api\Service;
+use Piwik\Tests\Framework\TestCase\SystemTestCase;
+
+/**
+ * @group Plugins
+ * @group Marketplace
+ * @group ServiceTest
+ * @group Service
+ */
+class ServiceTest extends SystemTestCase
+{
+ private $domain = 'http://plugins.piwik.org';
+
+ public function test_shouldUseVersion2()
+ {
+ $service = $this->buildService();
+ $this->assertSame('2.0', $service->getVersion());
+ }
+
+ public function test_getDomain_shouldReturnPassedDomain()
+ {
+ $service = $this->buildService();
+ $this->assertSame($this->domain, $service->getDomain());
+ }
+
+ public function test_authenticate_getAccessToken_shouldSaveToken_IfOnlyHasAlNumValues()
+ {
+ $service = $this->buildService();
+ $service->authenticate('123456789abcdefghij');
+ $this->assertSame('123456789abcdefghij', $service->getAccessToken());
+ }
+
+ public function test_hasAccessToken()
+ {
+ $service = $this->buildService();
+ $this->assertFalse($service->hasAccessToken());
+ $service->authenticate('123456789abcdefghij');
+ $this->assertTrue($service->hasAccessToken());
+ }
+
+ public function test_authenticate_getAccessToken_emptyTokenShouldUnsetToken()
+ {
+ $service = $this->buildService();
+ $service->authenticate('');
+ $this->assertNull($service->getAccessToken());
+ }
+
+ public function test_authenticate_getAccessToken_invalidTokenContainingInvalidCharactersShouldBeIgnored()
+ {
+ $service = $this->buildService();
+ $service->authenticate('123_-4?');
+ $this->assertNull($service->getAccessToken());
+ }
+
+ public function test_fetch_shouldCallMarketplaceApiWithActionAndReturnArrays()
+ {
+ $service = $this->buildService();
+ $response = $service->fetch('plugins', array());
+
+ $this->assertTrue(is_array($response));
+ $this->assertArrayHasKey('plugins', $response);
+ $this->assertGreaterThanOrEqual(30, count($response['plugins']));
+ foreach ($response['plugins'] as $plugin) {
+ $this->assertArrayHasKey('name', $plugin);
+ }
+ }
+
+ public function test_fetch_shouldCallMarketplaceApiWithGivenParamsAndReturnArrays()
+ {
+ $keyword = 'login';
+ $service = $this->buildService();
+ $response = $service->fetch('plugins', array('keywords' => $keyword));
+
+ $this->assertLessThan(20, count($response['plugins']));
+ foreach ($response['plugins'] as $plugin) {
+ $this->assertContains($keyword, $plugin['keywords']);
+ }
+ }
+
+ /**
+ * @expectedException \Piwik\Plugins\Marketplace\Api\Service\Exception
+ * @expectedExceptionMessage Not authenticated
+ * @expectedExceptionCode 101
+ */
+ public function test_fetch_shouldThrowException_WhenNotBeingAuthenticated()
+ {
+ $service = $this->buildService();
+ $service->fetch('consumer', array());
+ }
+
+ /**
+ * @expectedException \Piwik\Plugins\Marketplace\Api\Service\Exception
+ * @expectedExceptionMessage Not authenticated
+ * @expectedExceptionCode 101
+ */
+ public function test_fetch_shouldThrowException_WhenBeingAuthenticatedWithInvalidTokens()
+ {
+ $service = $this->buildService();
+ $service->authenticate('1234567890');
+ $service->fetch('consumer', array());
+ }
+
+ public function test_download_shouldReturnRawResultForAbsoluteUrl()
+ {
+ $service = $this->buildService();
+ $response = $service->download($this->domain . '/api/2.0/plugins');
+
+ $this->assertInternalType('string', $response);
+ $this->assertNotEmpty($response);
+ $this->assertStringStartsWith('{"plugins"', $response);
+ }
+
+ public function test_download_shouldSaveResultInFileIfPathGiven()
+ {
+ $path = StaticContainer::get('path.tmp') . '/marketplace_test_file.json';
+
+ Filesystem::deleteFileIfExists($path);
+
+ $service = $this->buildService();
+ $response = $service->download($this->domain . '/api/2.0/plugins', $path);
+
+ $this->assertTrue($response);
+ $this->assertFileExists($path);
+ $content = file_get_contents($path);
+ $this->assertNotEmpty($content);
+ $this->assertStringStartsWith('{"plugins"', $content);
+
+ Filesystem::deleteFileIfExists($path);
+ }
+
+ public function test_timeout_invalidService_ShouldFailIfNotReachable()
+ {
+ $start = time();
+
+ $service = $this->buildService();
+ try {
+ $service->download('http://notexisting49.plugins.piwk.org/api/2.0/plugins', null, $timeout = 1);
+ $this->fail('An expected exception has not been thrown');
+ } catch (\Exception $e) {
+
+ }
+
+ $diff = time() - $start;
+ $this->assertLessThanOrEqual(2, $diff);
+ }
+
+ private function buildService()
+ {
+ return new Service($this->domain);
+ }
+
+
+}
diff --git a/plugins/Marketplace/tests/Unit/ConsumerTest.php b/plugins/Marketplace/tests/Unit/ConsumerTest.php
new file mode 100644
index 0000000000..b3e4f144f6
--- /dev/null
+++ b/plugins/Marketplace/tests/Unit/ConsumerTest.php
@@ -0,0 +1,153 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\Marketplace\tests\Unit;
+use Piwik\Plugins\Marketplace\tests\Framework\Mock\Consumer;
+use Piwik\Plugins\Marketplace\tests\Framework\Mock\Service;
+use Piwik\Plugins\Marketplace\tests\Framework\Mock\Consumer as ConsumerBuilder;
+
+/**
+ * @group Marketplace
+ * @group ConsumerTest
+ * @group Consumer
+ * @group Plugins
+ */
+class ConsumerTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var Service
+ */
+ private $service;
+
+ public function setUp()
+ {
+ $this->service = new Service();
+ }
+
+ /**
+ * @dataProvider getConsumerNotAuthenticated
+ */
+ public function test_isValidConsumer_shouldReturnFalse_WhenNotAuthenticedBecauseNoTokenSetOrInvalidToken($fixture)
+ {
+ $this->service->returnFixture($fixture);
+ $this->assertFalse($this->buildConsumer()->isValidConsumer());
+ }
+
+ /**
+ * @dataProvider getConsumerAuthenticated
+ */
+ public function test_isValidConsumer_shouldReturnTrue_WhenValidTokenGiven($fixture)
+ {
+ $this->service->returnFixture($fixture);
+ $this->assertTrue($this->buildConsumer()->isValidConsumer());
+ }
+
+ public function test_getConsumer_shouldReturnConsumerInformation_WhenValid()
+ {
+ $this->service->returnFixture('v2.0_consumer-access_token-consumer1_paid2_custom1.json');
+
+ $expected = array (
+ 'licenses' =>
+ array (
+ 0 =>
+ array (
+ 'startDate' => '2014-05-27 04:46:05',
+ 'endDate' => '2014-06-01 06:22:35',
+ 'nextPaymentDate' => NULL,
+ 'status' => 'Cancelled',
+ 'productType' => 'Up to 4 users',
+ 'isValid' => false,
+ 'isExceeded' => false,
+ 'isExpiredSoon' => false,
+ 'plugin' => array('name' => 'PaidPlugin1', 'displayName' => 'Paid Plugin 1', 'htmlUrl' => 'https://plugins.piwik.org/PaidPlugin1'),
+ ),
+ 1 =>
+ array (
+ 'startDate' => '2016-05-20 04:46:05',
+ 'endDate' => '2030-05-27 11:03:06',
+ 'nextPaymentDate' => '2030-05-27 11:03:06',
+ 'status' => 'Active',
+ 'productType' => '5 to 15 users',
+ 'isValid' => true,
+ 'isExceeded' => NULL,
+ 'isExpiredSoon' => false,
+ 'plugin' => array('name' => 'PaidPlugin2', 'displayName' => 'Paid Plugin 2', 'htmlUrl' => 'https://plugins.piwik.org/PaidPlugin2'),
+ ),
+ 2 =>
+ array (
+ 'startDate' => '2016-05-25 04:46:05',
+ 'endDate' => '2030-06-03 11:03:06',
+ 'nextPaymentDate' => '2030-06-03 11:03:06',
+ 'status' => 'Active',
+ 'productType' => 'Up to 4 users',
+ 'isValid' => true,
+ 'isExceeded' => NULL,
+ 'isExpiredSoon' => false,
+ 'plugin' => array('name' => 'CustomPlugin1', 'displayName' => 'Custom Plugin 1', 'htmlUrl' => ''),
+ ),
+ ),
+ 'loginUrl' => 'https://shop.piwik.org/my-account',
+ );
+ $this->assertEquals($expected, $this->buildConsumer()->getConsumer());
+ }
+
+ public function test_getConsumer_shouldNotReturnAnyInformationWhenNotAuthenticated()
+ {
+ $this->service->returnFixture('v2.0_consumer-access_token-notexistingtoken.json');
+
+ $this->assertSame(array(), $this->buildConsumer()->getConsumer());
+ }
+
+ public function test_getConsumer_shouldNotReturnInformationWhenAuthenticatedButNoLicense()
+ {
+ $this->service->returnFixture('v2.0_consumer-access_token-validbutnolicense.json');
+
+ $expected = array(
+ 'licenses' => array(),
+ 'loginUrl' => 'https://shop.piwik.org/my-account'
+ );
+
+ $this->assertSame($expected, $this->buildConsumer()->getConsumer());
+ }
+
+ public function getConsumerNotAuthenticated()
+ {
+ return array(
+ array('v2.0_consumer_validate.json'),
+ array('v2.0_consumer_validate-access_token-notexistingtoken.json'),
+ );
+ }
+
+ public function getConsumerAuthenticated()
+ {
+ return array(
+ array('v2.0_consumer_validate-access_token-consumer1_paid2_custom1.json'),
+ array('v2.0_consumer_validate-access_token-consumer2_paid1.json'),
+ array('v2.0_consumer_validate-access_token-validbutnolicense.json') // valid token but no license
+ );
+ }
+
+ public function test_buildInvalidLicenseKey()
+ {
+ $isValid = Consumer::buildNoLicense()->isValidConsumer();
+
+ $this->assertFalse($isValid);
+ }
+
+ public function test_buildValidLicenseKey()
+ {
+ $isValid = Consumer::buildValidLicense()->isValidConsumer();
+
+ $this->assertTrue($isValid);
+ }
+
+ private function buildConsumer()
+ {
+ return ConsumerBuilder::build($this->service);
+ }
+}
diff --git a/plugins/Marketplace/tests/resources/emptyObjectResponse.json b/plugins/Marketplace/tests/resources/emptyObjectResponse.json
new file mode 100644
index 0000000000..9e26dfeeb6
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/emptyObjectResponse.json
@@ -0,0 +1 @@
+{} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer1_paid2_custom1.json b/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer1_paid2_custom1.json
new file mode 100644
index 0000000000..be9dbf2e84
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer1_paid2_custom1.json
@@ -0,0 +1,21 @@
+{"licenses":[{"startDate":"2014-05-27 04:46:05",
+ "endDate":"2014-06-01 06:22:35",
+ "nextPaymentDate":null,"status":"Cancelled",
+ "productType":"Up to 4 users",
+ "isValid":false,"isExceeded":false,"isExpiredSoon":false,"plugin":{"name":"PaidPlugin1",
+ "displayName":"Paid Plugin 1",
+ "htmlUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1"}},{"startDate":"2016-05-20 04:46:05",
+ "endDate":"2030-05-27 11:03:06",
+ "nextPaymentDate":"2030-05-27 11:03:06",
+ "status":"Active",
+ "productType":"5 to 15 users",
+ "isValid":true,"isExceeded":null,"isExpiredSoon":false,"plugin":{"name":"PaidPlugin2",
+ "displayName":"Paid Plugin 2",
+ "htmlUrl":"https:\/\/plugins.piwik.org\/PaidPlugin2"}},{"startDate":"2016-05-25 04:46:05",
+ "endDate":"2030-06-03 11:03:06",
+ "nextPaymentDate":"2030-06-03 11:03:06",
+ "status":"Active",
+ "productType":"Up to 4 users",
+ "isValid":true,"isExceeded":null,"isExpiredSoon":false,"plugin":{"name":"CustomPlugin1",
+ "displayName":"Custom Plugin 1",
+ "htmlUrl":""}}],"loginUrl":"https:\/\/shop.piwik.org\/my-account"} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer2_paid1.json b/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer2_paid1.json
new file mode 100644
index 0000000000..f5fce539d4
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer2_paid1.json
@@ -0,0 +1,7 @@
+{"licenses":[{"startDate":"2016-05-21 04:46:05",
+ "endDate":"2029-05-27 11:03:06",
+ "nextPaymentDate":null,"status":"Pending cancellation",
+ "productType":"Up to 4 users",
+ "isValid":true,"isExceeded":null,"isExpiredSoon":false,"plugin":{"name":"PaidPlugin1",
+ "displayName":"Paid Plugin 1",
+ "htmlUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1"}}],"loginUrl":"https:\/\/shop.piwik.org\/my-account"} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer3_paid1_custom2.json b/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer3_paid1_custom2.json
new file mode 100644
index 0000000000..225c302159
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer3_paid1_custom2.json
@@ -0,0 +1,27 @@
+{"licenses":[{"startDate":"2016-05-22 04:46:05",
+ "endDate":"2030-05-29 11:03:06",
+ "nextPaymentDate":"2030-05-29 11:03:06",
+ "status":"Active",
+ "productType":"Unlimited users",
+ "isValid":true,"isExceeded":null,"isExpiredSoon":false,"plugin":{"name":"PaidPlugin1",
+ "displayName":"Paid Plugin 1",
+ "htmlUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1"}},{"startDate":"2016-05-23 04:46:05",
+ "endDate":"2030-06-01 11:03:06",
+ "nextPaymentDate":null,"status":"Cancelled",
+ "productType":"Up to 4 users",
+ "isValid":false,"isExceeded":false,"isExpiredSoon":false,"plugin":{"name":"CustomPlugin1",
+ "displayName":"Custom Plugin 1",
+ "htmlUrl":""}},{"startDate":"2016-05-23 04:46:05",
+ "endDate":"2030-06-01 11:03:06",
+ "nextPaymentDate":null,"status":"Cancelled",
+ "productType":"Up to 4 users",
+ "isValid":false,"isExceeded":false,"isExpiredSoon":false,"plugin":{"name":"PaidPlugin2",
+ "displayName":"Paid Plugin 2",
+ "htmlUrl":"https:\/\/plugins.piwik.org\/PaidPlugin2"}},{"startDate":"2016-05-24 04:46:05",
+ "endDate":"2030-06-02 11:03:06",
+ "nextPaymentDate":"2030-06-02 11:03:06",
+ "status":"Active",
+ "productType":"5 to 15 users",
+ "isValid":true,"isExceeded":null,"isExpiredSoon":false,"plugin":{"name":"CustomPlugin2",
+ "displayName":"Custom Plugin 2",
+ "htmlUrl":""}}],"loginUrl":"https:\/\/shop.piwik.org\/my-account"} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-notexistingtoken.json b/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-notexistingtoken.json
new file mode 100644
index 0000000000..d9651355db
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-notexistingtoken.json
@@ -0,0 +1 @@
+{"error":"Not authenticated"} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-validbutnolicense.json b/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-validbutnolicense.json
new file mode 100644
index 0000000000..4f7b644c44
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_consumer-access_token-validbutnolicense.json
@@ -0,0 +1 @@
+{"licenses":[],"loginUrl":"https:\/\/shop.piwik.org\/my-account"} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_consumer-num_users-201-access_token-consumer1_paid2_custom1.json b/plugins/Marketplace/tests/resources/v2.0_consumer-num_users-201-access_token-consumer1_paid2_custom1.json
new file mode 100644
index 0000000000..ce325fabae
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_consumer-num_users-201-access_token-consumer1_paid2_custom1.json
@@ -0,0 +1,21 @@
+{"licenses":[{"startDate":"2014-05-27 04:46:05",
+ "endDate":"2014-06-01 06:22:35",
+ "nextPaymentDate":null,"status":"Cancelled",
+ "productType":"Up to 4 users",
+ "isValid":false,"isExceeded":false,"isExpiredSoon":false,"plugin":{"name":"PaidPlugin1",
+ "displayName":"Paid Plugin 1",
+ "htmlUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1"}},{"startDate":"2016-05-20 04:46:05",
+ "endDate":"2030-05-27 11:03:06",
+ "nextPaymentDate":"2030-05-27 11:03:06",
+ "status":"Active",
+ "productType":"5 to 15 users",
+ "isValid":true,"isExceeded":true,"isExpiredSoon":false,"plugin":{"name":"PaidPlugin2",
+ "displayName":"Paid Plugin 2",
+ "htmlUrl":"https:\/\/plugins.piwik.org\/PaidPlugin2"}},{"startDate":"2016-05-25 04:46:05",
+ "endDate":"2030-06-03 11:03:06",
+ "nextPaymentDate":"2030-06-03 11:03:06",
+ "status":"Active",
+ "productType":"Up to 4 users",
+ "isValid":true,"isExceeded":true,"isExpiredSoon":false,"plugin":{"name":"CustomPlugin1",
+ "displayName":"Custom Plugin 1",
+ "htmlUrl":""}}],"loginUrl":"https:\/\/shop.piwik.org\/my-account"} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer1_paid2_custom1.json b/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer1_paid2_custom1.json
new file mode 100644
index 0000000000..f8176db609
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer1_paid2_custom1.json
@@ -0,0 +1 @@
+{"isValid":true} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer2_paid1.json b/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer2_paid1.json
new file mode 100644
index 0000000000..f8176db609
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer2_paid1.json
@@ -0,0 +1 @@
+{"isValid":true} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer3_paid1_custom2.json b/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer3_paid1_custom2.json
new file mode 100644
index 0000000000..f8176db609
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer3_paid1_custom2.json
@@ -0,0 +1 @@
+{"isValid":true} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-notexistingtoken.json b/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-notexistingtoken.json
new file mode 100644
index 0000000000..2cc9915708
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-notexistingtoken.json
@@ -0,0 +1 @@
+{"isValid":false} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-validbutnolicense.json b/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-validbutnolicense.json
new file mode 100644
index 0000000000..f8176db609
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-validbutnolicense.json
@@ -0,0 +1 @@
+{"isValid":true} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_consumer_validate.json b/plugins/Marketplace/tests/resources/v2.0_consumer_validate.json
new file mode 100644
index 0000000000..2cc9915708
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_consumer_validate.json
@@ -0,0 +1 @@
+{"isValid":false} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_info.json b/plugins/Marketplace/tests/resources/v2.0_info.json
new file mode 100644
index 0000000000..5e93a342cf
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_info.json
@@ -0,0 +1,3 @@
+{"pluginsUrl":"http:\/\/plugins.piwik.org\/",
+ "themesUrl":"http:\/\/themes.piwik.org\/",
+ "loginUrl":"https:\/\/shop.piwik.org\/my-account"} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-consumer1_paid2_custom1.json b/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-consumer1_paid2_custom1.json
new file mode 100644
index 0000000000..50149150e6
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-consumer1_paid2_custom1.json
@@ -0,0 +1,142 @@
+{"plugins":[{"name":"CustomPlugin1",
+ "displayName":"Custom Plugin 1",
+ "owner":"PiwikPRO",
+ "description":"This plugin allow you visualize links of your website by just one click installation. When user move mouse over the text links, they will see a previe",
+ "homepage":"https:\/\/piwik.org\/recommends\/piwik-pro-compared-to-piwik-community\/",
+ "createdDateTime":"2014-12-23 01:19:22",
+ "donate":{},"support":[],"isTheme":false,"keywords":["page2images",
+ "website",
+ "screenshot"],"basePrice":0,"authors":[{"name":"SuzhouKada",
+ "email":"test5@example.com",
+ "homepage":"http:\/\/www.page2images.com"}],"repositoryUrl":"https:\/\/github.com\/PiwikPRO\/piwik",
+ "lastUpdated":"2014-12-23 01:19:22",
+ "latestVersion":"1.0.4",
+ "numDownloads":null,"screenshots":["https:\/\/plugins.piwik.org\/Page2imagesVisualLink\/images\/1.0.4\/visual-link-screenshot-on-domz.png",
+ "https:\/\/plugins.piwik.org\/Page2imagesVisualLink\/images\/1.0.4\/visual-link-screenshot-on-domz_02.png",
+ "https:\/\/plugins.piwik.org\/Page2imagesVisualLink\/images\/1.0.4\/visual-link-screenshot-on-domz_03.png"],"previews":[],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":null},"featured":false,"isFree":false,"isPaid":true,"isCustomPlugin":true,"shop":null,"versions":[{"name":"1.0.4",
+ "release":"2014-12-23 01:19:22",
+ "requires":{},"numDownloads":null,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":null,"readmeHtml":{"description":"\n\n<p>This plugin allow you visualize links of your website by just one click installation. When user move mouse over the text links, they will see a preview picture of this link. By default, [only extra links] will have preview thumbnails. You can change the setting in the JS files.<\/p>\n\n<p><img src=\"https:\/\/github.com\/SuzhouKada\/piwik\/blob\/master\/screenshots\/visual-link-screenshot-on-domz.png\" alt=\"visual-link-screenshot-on-domz.png\" \/><\/p>\n\n",
+ "faq":"<p>Who need this plugin?\nWebsite master who want to their users see the webpage thumbnail of one extra link before they open this page.\nWhat is the benefit?\nThis plugin can save end users' time. They will know whether they need go to this page or not when they see the preview image. \nIs it free?\nYes, it is totally free. But we will add a small water mark in the bottom of preview picture. The paid version does not have this limitation.\nDoes it support https?\nFree version does not support https.<\/p>",
+ "documentation":"",
+ "changelog":"<p><strong>1.0.0<\/strong>\n* Initial release<\/p>"},"download":"\/api\/2.0\/plugins\/CustomPlugin1\/download\/1.0.4"}],"isDownloadable":true,"consumer":{"license":{"startDate":"2016-05-25 04:46:05",
+ "endDate":"2030-06-03 11:03:06",
+ "nextPaymentDate":"2030-06-03 11:03:06",
+ "status":"Active",
+ "productType":"Up to 4 users",
+ "isValid":true,"isExceeded":null,"isExpiredSoon":false},"loginUrl":"https:\/\/shop.piwik.org\/my-account"}},{"name":"PaidPlugin1",
+ "displayName":"Paid Plugin 1",
+ "owner":"TestVendor",
+ "description":"Clears all temporary cache files",
+ "homepage":"https:\/\/github.com\/JohnDeery\/piwik-clearcache-plugin",
+ "createdDateTime":"2014-12-23 01:18:01",
+ "donate":{},"support":[{"name":"Documentation",
+ "key":"docs",
+ "value":"https:\/\/paidplugin1.org\/docs\/",
+ "type":"url"},{"name":"Wiki",
+ "key":"wiki",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/wiki",
+ "type":"url"},{"name":"Forum",
+ "key":"forum",
+ "value":"https:\/\/forum.paidplugin1.org",
+ "type":"url"},{"name":"Email",
+ "key":"email",
+ "value":"paidplugin1@example.com",
+ "type":"email"},{"name":"IRC",
+ "key":"irc",
+ "value":"irc:\/\/freenode\/paidplugin1",
+ "type":"text"},{"name":"Issues \/ Bugs",
+ "key":"issues",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/issues",
+ "type":"url"},{"name":"Source",
+ "key":"source",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/",
+ "type":"url"},{"name":"RSS",
+ "key":"rss",
+ "value":"https:\/\/paidplugin1.org\/feed\/",
+ "type":"url"}],"isTheme":false,"keywords":["cache",
+ "delete",
+ "tmp"],"basePrice":100,"authors":[{"name":"John Deery",
+ "email":"test3@example.com",
+ "homepage":"http:\/\/fractalice.com"}],"repositoryUrl":"https:\/\/github.com\/TestVendor\/PaidPlugin1",
+ "lastUpdated":null,"latestVersion":"1.1",
+ "numDownloads":null,"screenshots":[],"previews":[{"type":"demo",
+ "provider":"link",
+ "url":"https:\/\/demo.paidplugin1.com"}],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":null},"featured":false,"isFree":false,"isPaid":true,"isCustomPlugin":false,"shop":{"url":"https:\/\/plugins.piwik.org\/PaidPlugin1",
+ "variations":[{"price":"150",
+ "prettyPrice":"150\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=EUR"},{"price":"175",
+ "prettyPrice":"$175",
+ "currency":"USD",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=USD"},{"price":"300",
+ "prettyPrice":"300\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=EUR"},{"price":"345",
+ "prettyPrice":"$345",
+ "currency":"USD",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=USD"},{"price":"600",
+ "prettyPrice":"600\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=EUR"},{"price":"690",
+ "prettyPrice":"$690",
+ "currency":"USD",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=USD"}],"reviews":{"embedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?show_reviews=1&piwik_embed=1",
+ "height":200}},"versions":[{"name":"1.1",
+ "release":null,"requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"numDownloads":null,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":null,"readmeHtml":{"description":"\n\n<p>This plugin will clear out the tmp dir of Piwik. Useful for when you are developing other plugins or just need to kill that file and can't get to your installation to delete it normally<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":null}],"isDownloadable":false,"consumer":{"license":{"startDate":"2014-05-27 04:46:05",
+ "endDate":"2014-06-01 06:22:35",
+ "nextPaymentDate":null,"status":"Cancelled",
+ "productType":"Up to 4 users",
+ "isValid":false,"isExceeded":false,"isExpiredSoon":false},"loginUrl":"https:\/\/shop.piwik.org\/my-account"}},{"name":"PaidPlugin2",
+ "displayName":"Paid Plugin 2",
+ "owner":"TestVendor",
+ "description":"Adds a profile photo from Gravatar based on the email address stored in the User Id field.",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2015-07-24 13:15:01",
+ "donate":{"paypal":"test4@example.com",
+ "bitcoin":null},"support":[],"isTheme":false,"keywords":["GrabGravatar",
+ "avatar",
+ "photo",
+ "profile",
+ "visitor"],"basePrice":250,"authors":[{"name":"Alnoor Pirani",
+ "email":"test2@example.com",
+ "homepage":"http:\/\/alnoorpirani.com\/"}],"repositoryUrl":"https:\/\/github.com\/TestVendor\/PaidPlugin2",
+ "lastUpdated":"2015-07-24 13:56:30",
+ "latestVersion":"0.2.0",
+ "numDownloads":null,"screenshots":["https:\/\/plugins.piwik.org\/GrabGravatar\/images\/0.2.0\/Gravatar_replaces_default_avatar_when_email_known.png"],"previews":[{"type":"demo",
+ "provider":"link",
+ "url":"http:\/\/demo23.paidplugin2.org"}],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":null},"featured":false,"isFree":false,"isPaid":true,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.2.0",
+ "release":"2015-07-24 13:56:30",
+ "requires":{"piwik":">=2.11.2"},"numDownloads":null,"license":{"name":"CommercialLicense",
+ "url":""},"repositoryChangelogUrl":null,"readmeHtml":{"description":"\n\n<p>A Piwik plugin that adds a profile photo from Gravatar based on the email address stored in the User Id field.<\/p>\n\n",
+ "faq":"<p><strong>What information do I need to make this plugin work?<\/strong>\nMake sure you are capturing the email address for your visitors in the User Id field.<\/p>\n\n<p><strong>Why do some of my visitors just display the Gravatar logo instead of a photo?<\/strong>\nEither there is no email address associated with the visitor or they do not have a Gravatar set up at gravatar.com<\/p>",
+ "documentation":"",
+ "changelog":"<ul><li>0.1.11 Fixed screenshot filename<\/li>\n<li>0.1.1 Add screenshots<\/li>\n<li>0.1.0 First beta<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/PaidPlugin2\/download\/0.2.0"}],"isDownloadable":true,"consumer":{"license":{"startDate":"2016-05-20 04:46:05",
+ "endDate":"2030-05-27 11:03:06",
+ "nextPaymentDate":"2030-05-27 11:03:06",
+ "status":"Active",
+ "productType":"5 to 15 users",
+ "isValid":true,"isExceeded":null,"isExpiredSoon":false},"loginUrl":"https:\/\/shop.piwik.org\/my-account"}}]} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-consumer2_paid1.json b/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-consumer2_paid1.json
new file mode 100644
index 0000000000..95ee07e2f5
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-consumer2_paid1.json
@@ -0,0 +1,89 @@
+{"plugins":[{"name":"PaidPlugin1",
+ "displayName":"Paid Plugin 1",
+ "owner":"TestVendor",
+ "description":"Clears all temporary cache files",
+ "homepage":"https:\/\/github.com\/JohnDeery\/piwik-clearcache-plugin",
+ "createdDateTime":"2014-12-23 01:18:01",
+ "donate":{},"support":[{"name":"Documentation",
+ "key":"docs",
+ "value":"https:\/\/paidplugin1.org\/docs\/",
+ "type":"url"},{"name":"Wiki",
+ "key":"wiki",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/wiki",
+ "type":"url"},{"name":"Forum",
+ "key":"forum",
+ "value":"https:\/\/forum.paidplugin1.org",
+ "type":"url"},{"name":"Email",
+ "key":"email",
+ "value":"paidplugin1@example.com",
+ "type":"email"},{"name":"IRC",
+ "key":"irc",
+ "value":"irc:\/\/freenode\/paidplugin1",
+ "type":"text"},{"name":"Issues \/ Bugs",
+ "key":"issues",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/issues",
+ "type":"url"},{"name":"Source",
+ "key":"source",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/",
+ "type":"url"},{"name":"RSS",
+ "key":"rss",
+ "value":"https:\/\/paidplugin1.org\/feed\/",
+ "type":"url"}],"isTheme":false,"keywords":["cache",
+ "delete",
+ "tmp"],"basePrice":100,"authors":[{"name":"John Deery",
+ "email":"test3@example.com",
+ "homepage":"http:\/\/fractalice.com"}],"repositoryUrl":"https:\/\/github.com\/TestVendor\/PaidPlugin1",
+ "lastUpdated":"2014-12-23 01:18:20",
+ "latestVersion":"1.1",
+ "numDownloads":null,"screenshots":[],"previews":[{"type":"demo",
+ "provider":"link",
+ "url":"https:\/\/demo.paidplugin1.com"}],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":null},"featured":false,"isFree":false,"isPaid":true,"isCustomPlugin":false,"shop":{"url":"https:\/\/plugins.piwik.org\/PaidPlugin1",
+ "variations":[{"price":"150",
+ "prettyPrice":"150\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=EUR"},{"price":"175",
+ "prettyPrice":"$175",
+ "currency":"USD",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=USD"},{"price":"300",
+ "prettyPrice":"300\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=EUR"},{"price":"345",
+ "prettyPrice":"$345",
+ "currency":"USD",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=USD"},{"price":"600",
+ "prettyPrice":"600\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=EUR"},{"price":"690",
+ "prettyPrice":"$690",
+ "currency":"USD",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=USD"}],"reviews":{"embedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?show_reviews=1&piwik_embed=1",
+ "height":200}},"versions":[{"name":"1.1",
+ "release":"2014-12-23 01:18:20",
+ "requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"numDownloads":null,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":null,"readmeHtml":{"description":"\n\n<p>This plugin will clear out the tmp dir of Piwik. Useful for when you are developing other plugins or just need to kill that file and can't get to your installation to delete it normally<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<p>~2014~<\/p>\n\n<p>2014-02-21 - Update some more code, added in notification area but it's not quite working yet (TODO - make it work)<\/p>\n\n<p>2014-02-21 - Rebranded to cacheBuster and set version to 1.0, updated code to use a better check for directory seperator<\/p>\n\n<p>2014-02-20 - v2.0\n - Updated plugin to work with Piwik 2.0.3<\/p>\n\n<p>~2013~\n Inital creation of plugin at http:\/\/www.spherexx.com under the name ClearCache<\/p>"},"download":"\/api\/2.0\/plugins\/PaidPlugin1\/download\/1.1"}],"isDownloadable":true,"consumer":{"license":{"startDate":"2016-05-21 04:46:05",
+ "endDate":"2029-05-27 11:03:06",
+ "nextPaymentDate":null,"status":"Pending cancellation",
+ "productType":"Up to 4 users",
+ "isValid":true,"isExceeded":null,"isExpiredSoon":false},"loginUrl":"https:\/\/shop.piwik.org\/my-account"}}]} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-notexistingtoken.json b/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-notexistingtoken.json
new file mode 100644
index 0000000000..739dff5855
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-notexistingtoken.json
@@ -0,0 +1,83 @@
+{"plugins":[{"name":"PaidPlugin1",
+ "displayName":"Paid Plugin 1",
+ "owner":"TestVendor",
+ "description":"Clears all temporary cache files",
+ "homepage":"https:\/\/github.com\/JohnDeery\/piwik-clearcache-plugin",
+ "createdDateTime":"2014-12-23 01:18:01",
+ "donate":{},"support":[{"name":"Documentation",
+ "key":"docs",
+ "value":"https:\/\/paidplugin1.org\/docs\/",
+ "type":"url"},{"name":"Wiki",
+ "key":"wiki",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/wiki",
+ "type":"url"},{"name":"Forum",
+ "key":"forum",
+ "value":"https:\/\/forum.paidplugin1.org",
+ "type":"url"},{"name":"Email",
+ "key":"email",
+ "value":"paidplugin1@example.com",
+ "type":"email"},{"name":"IRC",
+ "key":"irc",
+ "value":"irc:\/\/freenode\/paidplugin1",
+ "type":"text"},{"name":"Issues \/ Bugs",
+ "key":"issues",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/issues",
+ "type":"url"},{"name":"Source",
+ "key":"source",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/",
+ "type":"url"},{"name":"RSS",
+ "key":"rss",
+ "value":"https:\/\/paidplugin1.org\/feed\/",
+ "type":"url"}],"isTheme":false,"keywords":["cache",
+ "delete",
+ "tmp"],"basePrice":100,"authors":[{"name":"John Deery",
+ "email":"test3@example.com",
+ "homepage":"http:\/\/fractalice.com"}],"repositoryUrl":"https:\/\/github.com\/TestVendor\/PaidPlugin1",
+ "lastUpdated":null,"latestVersion":"1.1",
+ "numDownloads":null,"screenshots":[],"previews":[{"type":"demo",
+ "provider":"link",
+ "url":"https:\/\/demo.paidplugin1.com"}],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":null},"featured":false,"isFree":false,"isPaid":true,"isCustomPlugin":false,"shop":{"url":"https:\/\/plugins.piwik.org\/PaidPlugin1",
+ "variations":[{"price":"150",
+ "prettyPrice":"150\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=EUR"},{"price":"175",
+ "prettyPrice":"$175",
+ "currency":"USD",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=USD"},{"price":"300",
+ "prettyPrice":"300\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=EUR"},{"price":"345",
+ "prettyPrice":"$345",
+ "currency":"USD",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=USD"},{"price":"600",
+ "prettyPrice":"600\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=EUR"},{"price":"690",
+ "prettyPrice":"$690",
+ "currency":"USD",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=USD"}],"reviews":{"embedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?show_reviews=1&piwik_embed=1",
+ "height":200}},"versions":[{"name":"1.1",
+ "release":null,"requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"numDownloads":null,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":null,"readmeHtml":{"description":"\n\n<p>This plugin will clear out the tmp dir of Piwik. Useful for when you are developing other plugins or just need to kill that file and can't get to your installation to delete it normally<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":null}],"isDownloadable":false,"consumer":{"license":null,"loginUrl":"https:\/\/shop.piwik.org\/my-account"}}]} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-num_users-201-access_token-consumer1_paid2_custom1.json b/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-num_users-201-access_token-consumer1_paid2_custom1.json
new file mode 100644
index 0000000000..a4fa42813e
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-num_users-201-access_token-consumer1_paid2_custom1.json
@@ -0,0 +1,141 @@
+{"plugins":[{"name":"CustomPlugin1",
+ "displayName":"Custom Plugin 1",
+ "owner":"PiwikPRO",
+ "description":"This plugin allow you visualize links of your website by just one click installation. When user move mouse over the text links, they will see a previe",
+ "homepage":"https:\/\/piwik.org\/recommends\/piwik-pro-compared-to-piwik-community\/",
+ "createdDateTime":"2014-12-23 01:19:22",
+ "donate":{},"support":[],"isTheme":false,"keywords":["page2images",
+ "website",
+ "screenshot"],"basePrice":0,"authors":[{"name":"SuzhouKada",
+ "email":"test5@example.com",
+ "homepage":"http:\/\/www.page2images.com"}],"repositoryUrl":"https:\/\/github.com\/PiwikPRO\/piwik",
+ "lastUpdated":null,"latestVersion":"1.0.4",
+ "numDownloads":null,"screenshots":["https:\/\/plugins.piwik.org\/Page2imagesVisualLink\/images\/1.0.4\/visual-link-screenshot-on-domz.png",
+ "https:\/\/plugins.piwik.org\/Page2imagesVisualLink\/images\/1.0.4\/visual-link-screenshot-on-domz_02.png",
+ "https:\/\/plugins.piwik.org\/Page2imagesVisualLink\/images\/1.0.4\/visual-link-screenshot-on-domz_03.png"],"previews":[],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":null},"featured":false,"isFree":false,"isPaid":true,"isCustomPlugin":true,"shop":null,"versions":[{"name":"1.0.4",
+ "release":null,"requires":{},"numDownloads":null,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":null,"readmeHtml":{"description":"\n\n<p>This plugin allow you visualize links of your website by just one click installation. When user move mouse over the text links, they will see a preview picture of this link. By default, [only extra links] will have preview thumbnails. You can change the setting in the JS files.<\/p>\n\n<p><img src=\"https:\/\/github.com\/SuzhouKada\/piwik\/blob\/master\/screenshots\/visual-link-screenshot-on-domz.png\" alt=\"visual-link-screenshot-on-domz.png\" \/><\/p>\n\n",
+ "faq":"<p>Who need this plugin?\nWebsite master who want to their users see the webpage thumbnail of one extra link before they open this page.\nWhat is the benefit?\nThis plugin can save end users' time. They will know whether they need go to this page or not when they see the preview image. \nIs it free?\nYes, it is totally free. But we will add a small water mark in the bottom of preview picture. The paid version does not have this limitation.\nDoes it support https?\nFree version does not support https.<\/p>",
+ "documentation":"",
+ "changelog":""},"download":null}],"isDownloadable":false,"consumer":{"license":{"startDate":"2016-05-25 04:46:05",
+ "endDate":"2030-06-03 11:03:06",
+ "nextPaymentDate":"2030-06-03 11:03:06",
+ "status":"Active",
+ "productType":"Up to 4 users",
+ "isValid":true,"isExceeded":true,"isExpiredSoon":false},"loginUrl":"https:\/\/shop.piwik.org\/my-account"}},{"name":"PaidPlugin1",
+ "displayName":"Paid Plugin 1",
+ "owner":"TestVendor",
+ "description":"Clears all temporary cache files",
+ "homepage":"https:\/\/github.com\/JohnDeery\/piwik-clearcache-plugin",
+ "createdDateTime":"2014-12-23 01:18:01",
+ "donate":{},"support":[{"name":"Documentation",
+ "key":"docs",
+ "value":"https:\/\/paidplugin1.org\/docs\/",
+ "type":"url"},{"name":"Wiki",
+ "key":"wiki",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/wiki",
+ "type":"url"},{"name":"Forum",
+ "key":"forum",
+ "value":"https:\/\/forum.paidplugin1.org",
+ "type":"url"},{"name":"Email",
+ "key":"email",
+ "value":"paidplugin1@example.com",
+ "type":"email"},{"name":"IRC",
+ "key":"irc",
+ "value":"irc:\/\/freenode\/paidplugin1",
+ "type":"text"},{"name":"Issues \/ Bugs",
+ "key":"issues",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/issues",
+ "type":"url"},{"name":"Source",
+ "key":"source",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/",
+ "type":"url"},{"name":"RSS",
+ "key":"rss",
+ "value":"https:\/\/paidplugin1.org\/feed\/",
+ "type":"url"}],"isTheme":false,"keywords":["cache",
+ "delete",
+ "tmp"],"basePrice":100,"authors":[{"name":"John Deery",
+ "email":"test3@example.com",
+ "homepage":"http:\/\/fractalice.com"}],"repositoryUrl":"https:\/\/github.com\/TestVendor\/PaidPlugin1",
+ "lastUpdated":null,"latestVersion":"1.1",
+ "numDownloads":null,"screenshots":[],"previews":[{"type":"demo",
+ "provider":"link",
+ "url":"https:\/\/demo.paidplugin1.com"}],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":null},"featured":false,"isFree":false,"isPaid":true,"isCustomPlugin":false,"shop":{"url":"https:\/\/plugins.piwik.org\/PaidPlugin1",
+ "variations":[{"price":"150",
+ "prettyPrice":"150\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=EUR"},{"price":"175",
+ "prettyPrice":"$175",
+ "currency":"USD",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=USD",
+ "cheapest":true,"recommended":false},{"price":"300",
+ "prettyPrice":"300\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=EUR"},{"price":"345",
+ "prettyPrice":"$345",
+ "currency":"USD",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=USD",
+ "cheapest":false,"recommended":false},{"price":"600",
+ "prettyPrice":"600\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=EUR"},{"price":"690",
+ "prettyPrice":"$690",
+ "currency":"USD",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=USD",
+ "cheapest":false,"recommended":true}],"reviews":{"embedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?show_reviews=1&piwik_embed=1",
+ "height":200}},"versions":[{"name":"1.1",
+ "release":null,"requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"numDownloads":null,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":null,"readmeHtml":{"description":"\n\n<p>This plugin will clear out the tmp dir of Piwik. Useful for when you are developing other plugins or just need to kill that file and can't get to your installation to delete it normally<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":null}],"isDownloadable":false,"consumer":{"license":{"startDate":"2014-05-27 04:46:05",
+ "endDate":"2014-06-01 06:22:35",
+ "nextPaymentDate":null,"status":"Cancelled",
+ "productType":"Up to 4 users",
+ "isValid":false,"isExceeded":false,"isExpiredSoon":false},"loginUrl":"https:\/\/shop.piwik.org\/my-account"}},{"name":"PaidPlugin2",
+ "displayName":"Paid Plugin 2",
+ "owner":"TestVendor",
+ "description":"Adds a profile photo from Gravatar based on the email address stored in the User Id field.",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2015-07-24 13:15:01",
+ "donate":{"paypal":"test4@example.com",
+ "bitcoin":null},"support":[],"isTheme":false,"keywords":["GrabGravatar",
+ "avatar",
+ "photo",
+ "profile",
+ "visitor"],"basePrice":250,"authors":[{"name":"Alnoor Pirani",
+ "email":"test2@example.com",
+ "homepage":"http:\/\/alnoorpirani.com\/"}],"repositoryUrl":"https:\/\/github.com\/TestVendor\/PaidPlugin2",
+ "lastUpdated":null,"latestVersion":"0.2.0",
+ "numDownloads":null,"screenshots":["https:\/\/plugins.piwik.org\/GrabGravatar\/images\/0.2.0\/Gravatar_replaces_default_avatar_when_email_known.png"],"previews":[{"type":"demo",
+ "provider":"link",
+ "url":"http:\/\/demo23.paidplugin2.org"}],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":null},"featured":false,"isFree":false,"isPaid":true,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.2.0",
+ "release":null,"requires":{"piwik":">=2.11.2"},"numDownloads":null,"license":{"name":"CommercialLicense",
+ "url":""},"repositoryChangelogUrl":null,"readmeHtml":{"description":"\n\n<p>A Piwik plugin that adds a profile photo from Gravatar based on the email address stored in the User Id field.<\/p>\n\n",
+ "faq":"<p><strong>What information do I need to make this plugin work?<\/strong>\nMake sure you are capturing the email address for your visitors in the User Id field.<\/p>\n\n<p><strong>Why do some of my visitors just display the Gravatar logo instead of a photo?<\/strong>\nEither there is no email address associated with the visitor or they do not have a Gravatar set up at gravatar.com<\/p>",
+ "documentation":"",
+ "changelog":""},"download":null}],"isDownloadable":false,"consumer":{"license":{"startDate":"2016-05-20 04:46:05",
+ "endDate":"2030-05-27 11:03:06",
+ "nextPaymentDate":"2030-05-27 11:03:06",
+ "status":"Active",
+ "productType":"5 to 15 users",
+ "isValid":true,"isExceeded":true,"isExpiredSoon":false},"loginUrl":"https:\/\/shop.piwik.org\/my-account"}}]} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-num_users-201-access_token-consumer2_paid1.json b/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-num_users-201-access_token-consumer2_paid1.json
new file mode 100644
index 0000000000..0e84886b88
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-num_users-201-access_token-consumer2_paid1.json
@@ -0,0 +1,90 @@
+{"plugins":[{"name":"PaidPlugin1",
+ "displayName":"Paid Plugin 1",
+ "owner":"TestVendor",
+ "description":"Clears all temporary cache files",
+ "homepage":"https:\/\/github.com\/JohnDeery\/piwik-clearcache-plugin",
+ "createdDateTime":"2014-12-23 01:18:01",
+ "donate":{},"support":[{"name":"Documentation",
+ "key":"docs",
+ "value":"https:\/\/paidplugin1.org\/docs\/",
+ "type":"url"},{"name":"Wiki",
+ "key":"wiki",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/wiki",
+ "type":"url"},{"name":"Forum",
+ "key":"forum",
+ "value":"https:\/\/forum.paidplugin1.org",
+ "type":"url"},{"name":"Email",
+ "key":"email",
+ "value":"paidplugin1@example.com",
+ "type":"email"},{"name":"IRC",
+ "key":"irc",
+ "value":"irc:\/\/freenode\/paidplugin1",
+ "type":"text"},{"name":"Issues \/ Bugs",
+ "key":"issues",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/issues",
+ "type":"url"},{"name":"Source",
+ "key":"source",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/",
+ "type":"url"},{"name":"RSS",
+ "key":"rss",
+ "value":"https:\/\/paidplugin1.org\/feed\/",
+ "type":"url"}],"isTheme":false,"keywords":["cache",
+ "delete",
+ "tmp"],"basePrice":100,"authors":[{"name":"John Deery",
+ "email":"test3@example.com",
+ "homepage":"http:\/\/fractalice.com"}],"repositoryUrl":"https:\/\/github.com\/TestVendor\/PaidPlugin1",
+ "lastUpdated":null,"latestVersion":"1.1",
+ "numDownloads":null,"screenshots":[],"previews":[{"type":"demo",
+ "provider":"link",
+ "url":"https:\/\/demo.paidplugin1.com"}],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":null},"featured":false,"isFree":false,"isPaid":true,"isCustomPlugin":false,"shop":{"url":"https:\/\/plugins.piwik.org\/PaidPlugin1",
+ "variations":[{"price":"150",
+ "prettyPrice":"150\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=EUR"},{"price":"175",
+ "prettyPrice":"$175",
+ "currency":"USD",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=USD",
+ "cheapest":true,"recommended":false},{"price":"300",
+ "prettyPrice":"300\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=EUR"},{"price":"345",
+ "prettyPrice":"$345",
+ "currency":"USD",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=USD",
+ "cheapest":false,"recommended":false},{"price":"600",
+ "prettyPrice":"600\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=EUR"},{"price":"690",
+ "prettyPrice":"$690",
+ "currency":"USD",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=USD",
+ "cheapest":false,"recommended":true}],"reviews":{"embedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?show_reviews=1&piwik_embed=1",
+ "height":200}},"versions":[{"name":"1.1",
+ "release":null,"requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"numDownloads":null,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":null,"readmeHtml":{"description":"\n\n<p>This plugin will clear out the tmp dir of Piwik. Useful for when you are developing other plugins or just need to kill that file and can't get to your installation to delete it normally<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":null}],"isDownloadable":false,"consumer":{"license":{"startDate":"2016-05-21 04:46:05",
+ "endDate":"2029-05-27 11:03:06",
+ "nextPaymentDate":null,"status":"Pending cancellation",
+ "productType":"Up to 4 users",
+ "isValid":true,"isExceeded":true,"isExpiredSoon":false},"loginUrl":"https:\/\/shop.piwik.org\/my-account"}}]} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_plugins-query-nomatchforthisquery.json b/plugins/Marketplace/tests/resources/v2.0_plugins-query-nomatchforthisquery.json
new file mode 100644
index 0000000000..d8a81e419d
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_plugins-query-nomatchforthisquery.json
@@ -0,0 +1 @@
+{"plugins":[]} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_plugins.json b/plugins/Marketplace/tests/resources/v2.0_plugins.json
new file mode 100644
index 0000000000..bbd6d9ef14
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_plugins.json
@@ -0,0 +1,3011 @@
+{"plugins":[{"name":"AdminNotification",
+ "displayName":"Admin Notification",
+ "owner":"jbrule",
+ "description":"Adds the ability for Piwik administrators to include an informative message to all user's dashboards. This uses the built in Notification function.",
+ "homepage":"https:\/\/github.com\/jbrule\/piwikplugin-AdminNotification",
+ "createdDateTime":"2015-04-13 17:52:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/jbrule\/piwikplugin-AdminNotification",
+ "lastUpdated":"2015-11-20 19:16:03",
+ "latestVersion":"0.1.2",
+ "numDownloads":2618,"screenshots":[],"previews":[],"activity":{"numCommits":"7",
+ "numContributors":"1",
+ "lastCommitDate":"2015-11-20 19:14:09"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2015-04-13 17:52:03",
+ "requires":{"piwik":">=2.8.0"},"readme":"",
+ "numDownloads":3,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/jbrule\/piwikplugin-AdminNotification\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AdminNotification\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2015-04-13 18:20:03",
+ "requires":{"piwik":">=2.8.0"},"readme":"",
+ "numDownloads":1302,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/jbrule\/piwikplugin-AdminNotification\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AdminNotification\/download\/0.1.1"},{"name":"0.1.2",
+ "release":"2015-11-20 19:16:03",
+ "requires":{"piwik":">=2.12.0"},"readme":"#Piwik AdminNotification Plugin\n##Description\nAdds the ability for Piwik administrators to include an informative message on all users' dashboards. This may be useful for communicating with users in larger shared environments. In our setup we were tracking 1,900 websites with 250 users. This is a solution we wrote to allow us to easily inform our users of maintainance windows.\n\n##Instructions\nThe easiest way to install is to find the plugin in the [Piwik Marketplace](http:\/\/plugins.piwik.org\/).\n\n##Changelog\n0.1.2 Tested with Piwik v2.15 and included new registerEvents() hook for compatibility with Piwik 3.0\n0.1.1 Cleanup. Removed plugin template verbiage from code files.\n0.1.0 Initial Release\n\n##License\nGPL v3 \/ fair use\n\n## Support\nPlease [report any issues](https:\/\/github.com\/jbrule\/piwikplugin-AdminNotification\/issues). Pull requests welcome.",
+ "numDownloads":1313,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/jbrule\/piwikplugin-AdminNotification\/commits\/v1.0",
+ "readmeHtml":{"description":"\n\n<p>Adds the ability for Piwik administrators to include an informative message on all users' dashboards. This may be useful for communicating with users in larger shared environments. In our setup we were tracking 1,900 websites with 250 users. This is a solution we wrote to allow us to easily inform our users of maintainance windows.<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<p>0.1.2 Tested with Piwik v2.15 and included new registerEvents() hook for compatibility with Piwik 3.0\n0.1.1 Cleanup. Removed plugin template verbiage from code files.\n0.1.0 Initial Release<\/p>"},"download":"\/api\/2.0\/plugins\/AdminNotification\/download\/0.1.2"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"AdvancedCampaignReporting",
+ "displayName":"Advanced Campaign Reporting",
+ "owner":"PiwikPRO",
+ "description":"Track up to five Campaigns parameter (campaign, source, medium, keyword, content). Lets you also segment users by any campaign dimension, and provides",
+ "homepage":"http:\/\/piwik.pro",
+ "createdDateTime":"2014-10-01 03:22:04",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/PiwikPRO\/plugin-AdvancedCampaignReporting",
+ "lastUpdated":"2016-03-22 12:36:04",
+ "latestVersion":"1.3.1",
+ "numDownloads":8360,"screenshots":[],"previews":[],"activity":{"numCommits":"149",
+ "numContributors":"9",
+ "lastCommitDate":"2016-04-15 22:57:32"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0.3",
+ "release":"2014-10-01 03:22:04",
+ "requires":{},"readme":"",
+ "numDownloads":496,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-AdvancedCampaignReporting\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AdvancedCampaignReporting\/download\/1.0.3"},{"name":"1.0.4",
+ "release":"2014-11-03 15:08:04",
+ "requires":{},"readme":"",
+ "numDownloads":213,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-AdvancedCampaignReporting\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AdvancedCampaignReporting\/download\/1.0.4"},{"name":"1.0.5",
+ "release":"2014-11-14 11:16:04",
+ "requires":{},"readme":"",
+ "numDownloads":109,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-AdvancedCampaignReporting\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AdvancedCampaignReporting\/download\/1.0.5"},{"name":"1.0.6",
+ "release":"2014-11-17 04:44:03",
+ "requires":{},"readme":"",
+ "numDownloads":1531,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-AdvancedCampaignReporting\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AdvancedCampaignReporting\/download\/1.0.6"},{"name":"1.0.7",
+ "release":"2015-03-31 20:50:04",
+ "requires":{},"readme":"",
+ "numDownloads":9,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-AdvancedCampaignReporting\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AdvancedCampaignReporting\/download\/1.0.7"},{"name":"1.0.8",
+ "release":"2015-03-31 21:20:04",
+ "requires":{},"readme":"",
+ "numDownloads":1853,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-AdvancedCampaignReporting\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AdvancedCampaignReporting\/download\/1.0.8"},{"name":"1.1.1",
+ "release":"2015-09-03 02:04:03",
+ "requires":{"piwik":">=2.14.0"},"readme":"",
+ "numDownloads":954,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-AdvancedCampaignReporting\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AdvancedCampaignReporting\/download\/1.1.1"},{"name":"1.2.0",
+ "release":"2015-11-11 13:08:02",
+ "requires":{"piwik":">=2.15.0"},"readme":"",
+ "numDownloads":1743,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-AdvancedCampaignReporting\/commits\/2.0.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AdvancedCampaignReporting\/download\/1.2.0"},{"name":"1.3.0",
+ "release":"2016-03-08 14:02:03",
+ "requires":{"piwik":">=2.16.0"},"readme":"",
+ "numDownloads":352,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-AdvancedCampaignReporting\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AdvancedCampaignReporting\/download\/1.3.0"},{"name":"1.3.1",
+ "release":"2016-03-22 12:36:04",
+ "requires":{"piwik":">=2.16.0"},"readme":"# Advanced Campaigns Reporting\n\nMaster [![Build Status](https:\/\/travis-ci.org\/PiwikPRO\/plugin-AdvancedCampaignReporting.svg?branch=master)](https:\/\/travis-ci.org\/PiwikPRO\/plugin-AdvancedCampaignReporting)\nDevelop [![Build Status](https:\/\/travis-ci.org\/PiwikPRO\/plugin-AdvancedCampaignReporting.svg?branch=develop)](https:\/\/travis-ci.org\/PiwikPRO\/plugin-AdvancedCampaignReporting)\n\n## Description\n\nTrack up to five Campaigns parameters (name, source, medium, keyword, content), and access Campaign Analytics reports.\n\n### Measuring campaigns\n\nThe default Campaign parameters are called: pk_campaign, pk_source, pk_medium, pk_keyword, pk_content and pk_cid.\n\nIf you already have URLs tagged with Google Analytics parameters these are supported: utm_campaign, utm_source, utm_medium, utm_term, utm_content and utm_id\n\nAn example landing page URL is:\n```\n\/offer?pk_campaign=Best-Seller&pk_source=Newsletter_7&pk_medium=email\n```\n\n### Features\n * Real time Analytics Reports of all your Campaign Marketing\n * Detects Campaign parameters from the landing page URL, within the query string or in the #hash string\n * The Referrers>Overview report displays a left column \"Referrers Overview\" with a list of reports that can be loaded on click.\n This report viewer now also lists the new Campaign reports under \"View Referrers by Campaign\".\n * The content of Referrers> Campaign will be replaced with the new enhanced Campaigns reports.\n * The default Referrers Campaign widget and API are working as before.\n * The campaign reports are available in Piwik Mobile and can be sent as Scheduled reports (by email, as HTML or PDF)\n * Segment editor: a new \"Campaigns\" category lists the five new segment for each campaign dimension\n * The new Campaign reports can be added as widgets in your personalized Dashboard\n * Access the Campaign Report data by the API\n * Comes with automated tests to ensure the Plugin works as expected\n * Will track up to 250 characters for each of the five Campaign dimension\n\n### Notes\n\nIn the Campaign reports by default Piwik will only archive the first 1000 rows. If you track many campaigns you can configure Piwik so it does not truncate your data. To have data truncated after 10,000 rows, edit your `config\/config.ini.php` and add the following:\n\n```\n[General]\ndatatable_archiving_maximum_rows_referrers = 10000\ndatatable_archiving_maximum_rows_subtable_referrers = 10000\n```\n\n\n### Ideas for improvement\n * To improve data acquisition accuracy, we could extend the piwik.js class to store in first party cookies\n the five campaign dimensions. This would increase the accuracy of Goal conversions and Ecommerce conversions attributions\n for these conversions made at least one day after the first visit with a campaign set. [#10](https:\/\/github.com\/PiwikPRO\/plugin-AdvancedCampaignReporting\/issues\/10)\n * Add friendly Tracking API parameters to collect campaign dimensions.\n campaignName `cn`, campaignSource `cs`, campaignMedium `cm`, campaignContent `cc`, campaignId `ci`.\n\n## Changelog\n * 1.3.1 Better support for campaign parameters behind hash tag (#)\n * 1.3.0 PPCDEV-2609 Compatibility with Piwik 2.16.0\n * 1.2.0 (Nov 10th 2015) - Plugin comaptibility with Piwik 2.15.0\n * 1.1.1 (Sept 3rd 2015) - Campaign reports now display your campaign report data even for campaign data before you activated AdvancedCampaignReporting\n * 1.1.0 (July 28th 2015)\n * 1.0.8 (Apr 1st 2015) - Exclude Google Analytics campaign parameters from the Page URLs\n * 1.0.6 (Nov 17th 2014) - Documentation\n * 1.0.5 (Nov 14th 2014) - Detect new URL parameters: piwik_campaign, pk_cpn and for Keywords: pk_kwd, piwik_keyword\n * 1.0.4 (Nov 4th 2014) - View Goals by Campaign Dimension in the Goals & Ecommerce reports\n * 1.0.3 (Oct 1st 2014) - Released for free on the [Piwik Marketplace](http:\/\/plugins.piwik.org\/)\n\n\n## Support\n\nPlugin provided by [Piwik PRO](https:\/\/example.por) - Cloud and Enterprise analytics from the creators of Piwik.org\n\nIf you find a bug or have a suggestion please create an issue in: https:\/\/github.com\/PiwikPRO\/plugin-AdvancedCampaignReporting\/issues\n",
+ "numDownloads":1100,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-AdvancedCampaignReporting\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>Track up to five Campaigns parameters (name, source, medium, keyword, content), and access Campaign Analytics reports.<\/p>\n\n<h3>Measuring campaigns<\/h3>\n\n<p>The default Campaign parameters are called: pk_campaign, pk_source, pk_medium, pk_keyword, pk_content and pk_cid.<\/p>\n\n<p>If you already have URLs tagged with Google Analytics parameters these are supported: utm_campaign, utm_source, utm_medium, utm_term, utm_content and utm_id<\/p>\n\n<p>An example landing page URL is:<\/p>\n\n<pre><code>\/offer?pk_campaign=Best-Seller&amp;pk_source=Newsletter_7&amp;pk_medium=email\n<\/code><\/pre>\n\n<h3>Features<\/h3>\n\n<ul><li>Real time Analytics Reports of all your Campaign Marketing<\/li>\n<li>Detects Campaign parameters from the landing page URL, within the query string or in the #hash string<\/li>\n<li>The Referrers&gt;Overview report displays a left column \"Referrers Overview\" with a list of reports that can be loaded on click.\nThis report viewer now also lists the new Campaign reports under \"View Referrers by Campaign\".<\/li>\n<li>The content of Referrers&gt; Campaign will be replaced with the new enhanced Campaigns reports.<\/li>\n<li>The default Referrers Campaign widget and API are working as before.<\/li>\n<li>The campaign reports are available in Piwik Mobile and can be sent as Scheduled reports (by email, as HTML or PDF)<\/li>\n<li>Segment editor: a new \"Campaigns\" category lists the five new segment for each campaign dimension<\/li>\n<li>The new Campaign reports can be added as widgets in your personalized Dashboard<\/li>\n<li>Access the Campaign Report data by the API<\/li>\n<li>Comes with automated tests to ensure the Plugin works as expected<\/li>\n<li>Will track up to 250 characters for each of the five Campaign dimension<\/li>\n<\/ul><h3>Notes<\/h3>\n\n<p>In the Campaign reports by default Piwik will only archive the first 1000 rows. If you track many campaigns you can configure Piwik so it does not truncate your data. To have data truncated after 10,000 rows, edit your <code>config\/config.ini.php<\/code> and add the following:<\/p>\n\n<pre><code>[General]\ndatatable_archiving_maximum_rows_referrers = 10000\ndatatable_archiving_maximum_rows_subtable_referrers = 10000\n<\/code><\/pre>\n\n<h3>Ideas for improvement<\/h3>\n\n<ul><li>To improve data acquisition accuracy, we could extend the piwik.js class to store in first party cookies\nthe five campaign dimensions. This would increase the accuracy of Goal conversions and Ecommerce conversions attributions\nfor these conversions made at least one day after the first visit with a campaign set. <a href=\"https:\/\/github.com\/PiwikPRO\/plugin-AdvancedCampaignReporting\/issues\/10\">#10<\/a><\/li>\n<li>Add friendly Tracking API parameters to collect campaign dimensions.\ncampaignName <code>cn<\/code>, campaignSource <code>cs<\/code>, campaignMedium <code>cm<\/code>, campaignContent <code>cc<\/code>, campaignId <code>ci<\/code>.<\/li>\n<\/ul>",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<ul><li>1.3.1 Better support for campaign parameters behind hash tag (#)<\/li>\n<li>1.3.0 PPCDEV-2609 Compatibility with Piwik 2.16.0<\/li>\n<li>1.2.0 (Nov 10th 2015) - Plugin comaptibility with Piwik 2.15.0<\/li>\n<li>1.1.1 (Sept 3rd 2015) - Campaign reports now display your campaign report data even for campaign data before you activated AdvancedCampaignReporting<\/li>\n<li>1.1.0 (July 28th 2015)<\/li>\n<li>1.0.8 (Apr 1st 2015) - Exclude Google Analytics campaign parameters from the Page URLs<\/li>\n<li>1.0.6 (Nov 17th 2014) - Documentation<\/li>\n<li>1.0.5 (Nov 14th 2014) - Detect new URL parameters: piwik_campaign, pk_cpn and for Keywords: pk_kwd, piwik_keyword<\/li>\n<li>1.0.4 (Nov 4th 2014) - View Goals by Campaign Dimension in the Goals &amp; Ecommerce reports<\/li>\n<li>1.0.3 (Oct 1st 2014) - Released for free on the <a href=\"http:\/\/plugins.piwik.org\/\">Piwik Marketplace<\/a><\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/AdvancedCampaignReporting\/download\/1.3.1"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"AnonymousPiwikUsageMeasurement",
+ "displayName":"Anonymous Piwik Usage Measurement",
+ "owner":"piwik",
+ "description":"Help improve your Piwik experience by sending anonymized usage data to the creators of Piwik, to your own Piwik instance or to any other Piwik",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2015-10-19 14:04:02",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/piwik\/plugin-AnonymousPiwikUsageMeasurement",
+ "lastUpdated":"2016-02-10 17:22:03",
+ "latestVersion":"0.2.1",
+ "numDownloads":1662,"screenshots":[],"previews":[],"activity":{"numCommits":"61",
+ "numContributors":"5",
+ "lastCommitDate":"2016-05-17 03:54:49"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.1",
+ "release":"2015-10-19 14:04:03",
+ "requires":{"piwik":">=2.15.0-b17"},"readme":"",
+ "numDownloads":18,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-AnonymousPiwikUsageMeasurement\/commits\/0.1.0",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AnonymousPiwikUsageMeasurement\/download\/0.1.1"},{"name":"0.1.2",
+ "release":"2015-10-20 06:44:03",
+ "requires":{"piwik":">=2.15.0-b17"},"readme":"",
+ "numDownloads":3,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-AnonymousPiwikUsageMeasurement\/commits\/0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AnonymousPiwikUsageMeasurement\/download\/0.1.2"},{"name":"0.1.3",
+ "release":"2015-10-20 10:14:03",
+ "requires":{"piwik":">=2.15.0-rc1"},"readme":"",
+ "numDownloads":15,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-AnonymousPiwikUsageMeasurement\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AnonymousPiwikUsageMeasurement\/download\/0.1.3"},{"name":"0.1.4",
+ "release":"2015-10-21 06:06:03",
+ "requires":{"piwik":">=2.15.0-rc1"},"readme":"",
+ "numDownloads":595,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-AnonymousPiwikUsageMeasurement\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AnonymousPiwikUsageMeasurement\/download\/0.1.4"},{"name":"0.2.0",
+ "release":"2016-01-19 00:20:04",
+ "requires":{"piwik":">=2.15.0"},"readme":"",
+ "numDownloads":291,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-AnonymousPiwikUsageMeasurement\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/AnonymousPiwikUsageMeasurement\/download\/0.2.0"},{"name":"0.2.1",
+ "release":"2016-02-10 17:22:03",
+ "requires":{"piwik":">=2.15.0"},"readme":"# Piwik AnonymousPiwikUsageMeasurement Plugin\n\n[![Build Status](https:\/\/travis-ci.org\/piwik\/plugin-AnonymousPiwikUsageMeasurement.svg?branch=master)](https:\/\/travis-ci.org\/piwik\/plugin-AnonymousPiwikUsageMeasurement)\n\n## Description\n\nHelp us to improve Piwik by sending anonymous usage data of your Piwik service to the creators of Piwik and\/or get usage data yourself.\n\nTrack usage of your Piwik service into up to three Piwiks:\n\n* [demo-anonymous.piwik.org](https:\/\/demo-anonymous.piwik.org) (enabled by default but can be disabled). The tracked data will be used to make Piwik better. Thank you for your help!\n* your own Piwik (can be configured optionally)\n* a custom Piwik (can be configured optionally)\n\n### What is Piwik doing to make sure the data is anonymized?\n\nWe are very careful in what we track and we make sure to anonymize data that could contain user data.\n\n* We overwrite the page title as the title could contain the name of the viewed website\n* We remove any referrer information\n* We replace URL paramaters with a predefined value apart from a few whitelisted ones to make sure no actual token_auth, CSRF token or user defined value will be tracked\n* On demo-anonymous.piwik.org 3 bytes of the IP are anonymised (eg when IP is 192.168.1.1 we track only 192.0.0.0). The original IP is not used to identify your location and provider information is not collected. \n* We do not just track any outlinks or downloads\n\n### When should I not install this plugin?\n\nIf you have developed a custom Piwik plugin that contains for example the name of your business in any of the following names we recommend to not install this plugin as it might be tracked:\n\n* name of a plugin\n* name of a controller action\n* name of a report\n* name of a widget\n* name of an API method\n\nPlugins that are installed via the Marketplace should not pose a problem as their names don't contain any user specific information such as the name of your business.\n\nWe track any information as efficient as possible to not slow down your Piwik. If you have already performance problems with your Piwik we recommend to not install this plugin though.\n\n### Which data is tracked?\n\nWhen the plugin is activated, the following data will be tracked:\n\n* The pages and reports that are viewed\n* The visitors' software and devices data like the used browser and the resolution\n* Some clicks or interactions with certain selectors or buttons. For example we track an event when a segment is selected but we do not track the actual segment.\n* In a daily task we track the following data:\n * Piwik version\n * PHP version\n * Number of websites\n * Number of users\n * Number of segments\n * How often which API method was called (only plugin name and method name but no parameters) and how long the API calls took on average.\n\n## FAQ\n\n__Are there any prerequisites?__\n\n* If sending usage data to Piwik is enabled, the Piwik installation must be connected to the internet\n* If tracking to a custom Piwik installation is enabled, your Piwik installation and your Piwik users must be able to connect to this instance\n* If tracking to a custom Piwik installation is enabled and your Piwik is served via HTTPS, the custom Piwik installation must be available via HTTPS as well\n\n__Why was this plugin created?__\n\nThis plugin was created to provide a simple way to measure how Piwik product itself is being used. The opt-in and anonymised usage tracking information will be used by the Piwik creators to build a better product and a great user experience.\n\n__Who has access to the tracked data at demo-anonymous.piwik.org?__\n\nThe data is public and therefore can be seen by anyone on [http:\/\/demo-anonymous.piwik.org](http:\/\/demo-anonymous.piwik.org).\n\nThis is to assure the tracked data is anonymous (transparency) and to showcase how Piwik can be used to track an application.\n\n## Changelog\n\n* 0.2.1 Track MySQL server version in a custom variable.\n* 0.2.0 Add possibility to enable\/disable anonymization and tracking user login as userID\n* 0.1.4 Fixed a bug that failed to track under HTTPS under circumstances\n* 0.1.3 Updated plugin description only\n* 0.1.2 Bugfixes\n* 0.1.1 Track average API executime time\n* 0.1.0 Initial release\n\n## Support\n\nPlease direct any feedback to [github.com\/piwik\/plugin-AnonymousPiwikUsageMeasurement\/issues](https:\/\/github.com\/piwik\/plugin-AnonymousPiwikUsageMeasurement\/issues)\n",
+ "numDownloads":740,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-AnonymousPiwikUsageMeasurement\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>Help us to improve Piwik by sending anonymous usage data of your Piwik service to the creators of Piwik and\/or get usage data yourself.<\/p>\n\n<p>Track usage of your Piwik service into up to three Piwiks:<\/p>\n\n<ul><li><a href=\"https:\/\/demo-anonymous.piwik.org\">demo-anonymous.piwik.org<\/a> (enabled by default but can be disabled). The tracked data will be used to make Piwik better. Thank you for your help!<\/li>\n<li>your own Piwik (can be configured optionally)<\/li>\n<li>a custom Piwik (can be configured optionally)<\/li>\n<\/ul><h3>What is Piwik doing to make sure the data is anonymized?<\/h3>\n\n<p>We are very careful in what we track and we make sure to anonymize data that could contain user data.<\/p>\n\n<ul><li>We overwrite the page title as the title could contain the name of the viewed website<\/li>\n<li>We remove any referrer information<\/li>\n<li>We replace URL paramaters with a predefined value apart from a few whitelisted ones to make sure no actual token_auth, CSRF token or user defined value will be tracked<\/li>\n<li>On demo-anonymous.piwik.org 3 bytes of the IP are anonymised (eg when IP is 192.168.1.1 we track only 192.0.0.0). The original IP is not used to identify your location and provider information is not collected. <\/li>\n<li>We do not just track any outlinks or downloads<\/li>\n<\/ul><h3>When should I not install this plugin?<\/h3>\n\n<p>If you have developed a custom Piwik plugin that contains for example the name of your business in any of the following names we recommend to not install this plugin as it might be tracked:<\/p>\n\n<ul><li>name of a plugin<\/li>\n<li>name of a controller action<\/li>\n<li>name of a report<\/li>\n<li>name of a widget<\/li>\n<li>name of an API method<\/li>\n<\/ul><p>Plugins that are installed via the Marketplace should not pose a problem as their names don't contain any user specific information such as the name of your business.<\/p>\n\n<p>We track any information as efficient as possible to not slow down your Piwik. If you have already performance problems with your Piwik we recommend to not install this plugin though.<\/p>\n\n<h3>Which data is tracked?<\/h3>\n\n<p>When the plugin is activated, the following data will be tracked:<\/p>\n\n<ul><li>The pages and reports that are viewed<\/li>\n<li>The visitors' software and devices data like the used browser and the resolution<\/li>\n<li>Some clicks or interactions with certain selectors or buttons. For example we track an event when a segment is selected but we do not track the actual segment.<\/li>\n<li>In a daily task we track the following data:\n\n<ul><li>Piwik version<\/li>\n<li>PHP version<\/li>\n<li>Number of websites<\/li>\n<li>Number of users<\/li>\n<li>Number of segments<\/li>\n<li>How often which API method was called (only plugin name and method name but no parameters) and how long the API calls took on average.<\/li>\n<\/ul><\/li>\n<\/ul>",
+ "faq":"<p><strong>Are there any prerequisites?<\/strong><\/p>\n\n<ul><li>If sending usage data to Piwik is enabled, the Piwik installation must be connected to the internet<\/li>\n<li>If tracking to a custom Piwik installation is enabled, your Piwik installation and your Piwik users must be able to connect to this instance<\/li>\n<li>If tracking to a custom Piwik installation is enabled and your Piwik is served via HTTPS, the custom Piwik installation must be available via HTTPS as well<\/li>\n<\/ul><p><strong>Why was this plugin created?<\/strong><\/p>\n\n<p>This plugin was created to provide a simple way to measure how Piwik product itself is being used. The opt-in and anonymised usage tracking information will be used by the Piwik creators to build a better product and a great user experience.<\/p>\n\n<p><strong>Who has access to the tracked data at demo-anonymous.piwik.org?<\/strong><\/p>\n\n<p>The data is public and therefore can be seen by anyone on <a href=\"http:\/\/demo-anonymous.piwik.org\">http:\/\/demo-anonymous.piwik.org<\/a>.<\/p>\n\n<p>This is to assure the tracked data is anonymous (transparency) and to showcase how Piwik can be used to track an application.<\/p>",
+ "documentation":"",
+ "changelog":"<ul><li>0.2.1 Track MySQL server version in a custom variable.<\/li>\n<li>0.2.0 Add possibility to enable\/disable anonymization and tracking user login as userID<\/li>\n<li>0.1.4 Fixed a bug that failed to track under HTTPS under circumstances<\/li>\n<li>0.1.3 Updated plugin description only<\/li>\n<li>0.1.2 Bugfixes<\/li>\n<li>0.1.1 Track average API executime time<\/li>\n<li>0.1.0 Initial release<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/AnonymousPiwikUsageMeasurement\/download\/0.2.1"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"ApiGetWithSitesInfo",
+ "displayName":"Api Get With Sites Info",
+ "owner":"piwik",
+ "description":"Modifies the 'API.get' output to also list the website name and main website URL",
+ "homepage":"http:\/\/github.com\/piwik\/piwik-ApiGetWithSitesInfo",
+ "createdDateTime":"2014-12-09 03:50:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/piwik\/plugin-ApiGetWithSitesInfo",
+ "lastUpdated":"2014-12-11 21:06:04",
+ "latestVersion":"0.1.2",
+ "numDownloads":3009,"screenshots":[],"previews":[],"activity":{"numCommits":"24",
+ "numContributors":"3",
+ "lastCommitDate":"2016-01-25 22:14:51"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2014-12-09 03:50:03",
+ "requires":{"piwik":">=2.9.0"},"readme":"",
+ "numDownloads":1,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-ApiGetWithSitesInfo\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ApiGetWithSitesInfo\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2014-12-09 03:58:03",
+ "requires":{"piwik":">=2.9.0"},"readme":"",
+ "numDownloads":45,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-ApiGetWithSitesInfo\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ApiGetWithSitesInfo\/download\/0.1.1"},{"name":"0.1.2",
+ "release":"2014-12-11 21:06:04",
+ "requires":{"piwik":">=2.9.0"},"readme":"# Piwik ApiGetWithSitesInfo Plugin\n\n\n[![Build Status](https:\/\/travis-ci.org\/piwik\/plugin-ApiGetWithSitesInfo.svg?branch=master)](https:\/\/travis-ci.org\/piwik\/plugin-ApiGetWithSitesInfo)\n\n## Description\n\nModifies the 'API.get' output to also list the website name and main website URL.\n\nWhen calling the API method `API.get` it will be enriched with the following new fields for each website:\n\n * `idsite` - Website ID\n * `site_url` - Website URL\n * `site_name` - Website name\n\n If you specify `&idSite=all` it will decorate each website in the response with the new fields.\n \nThe output will look as follows:\n\n```\n<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<result>\n\t<idsite>2<\/idsite>\n\t<site_url>http:\/\/blog.shacklefordvetclinic.com<\/site_url>\n\t<site_name>Shackleford Road Veterinary Clinic<\/site_name>\n <nb_uniq_visitors>2397<\/nb_uniq_visitors>\n <nb_visits>2758<\/nb_visits>\n <nb_actions>7943<\/nb_actions>\n <bounce_count>1421<\/bounce_count>\n <nb_conversions>987<\/nb_conversions>\n <nb_visits_converted>838<\/nb_visits_converted>\n <revenue>0<\/revenue>\n <nb_pageviews>6370<\/nb_pageviews>\n <nb_uniq_pageviews>5330<\/nb_uniq_pageviews>\n <nb_downloads>368<\/nb_downloads>\n <nb_uniq_downloads>305<\/nb_uniq_downloads>\n <nb_outlinks>951<\/nb_outlinks>\n <nb_uniq_outlinks>871<\/nb_uniq_outlinks>\n <nb_searches>27<\/nb_searches>\n <nb_keywords>25<\/nb_keywords>\n <nb_hits_with_time_generation>5635<\/nb_hits_with_time_generation>\n <conversion_rate>30.38%<\/conversion_rate>\n <bounce_rate>52%<\/bounce_rate>\n <nb_actions_per_visit>2.9<\/nb_actions_per_visit>\n [...]\n```\n\n \n## Changelog\n\n* 0.1.2 - Fixed bug `Call to a member function getRowsCount() on a non-object`\n* 0.1.0 - Initial release\n\n## Support\n\nPlease direct any feedback to the [Issue tracker on Github.](https:\/\/github.com\/piwik\/plugin-ApiGetWithSitesInfo\/issues)\n",
+ "numDownloads":2963,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-ApiGetWithSitesInfo\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>Modifies the 'API.get' output to also list the website name and main website URL.<\/p>\n\n<p>When calling the API method <code>API.get<\/code> it will be enriched with the following new fields for each website:<\/p>\n\n<ul><li><code>idsite<\/code> - Website ID<\/li>\n<li><code>site_url<\/code> - Website URL<\/li>\n<li><p><code>site_name<\/code> - Website name<\/p>\n\n<p>If you specify <code>&amp;idSite=all<\/code> it will decorate each website in the response with the new fields.<\/p><\/li>\n<\/ul><p>The output will look as follows:<\/p>\n\n<pre><code>&lt;?xml version=\"1.0\" encoding=\"utf-8\" ?&gt;\n&lt;result&gt;\n &lt;idsite&gt;2&lt;\/idsite&gt;\n &lt;site_url&gt;http:\/\/blog.shacklefordvetclinic.com&lt;\/site_url&gt;\n &lt;site_name&gt;Shackleford Road Veterinary Clinic&lt;\/site_name&gt;\n &lt;nb_uniq_visitors&gt;2397&lt;\/nb_uniq_visitors&gt;\n &lt;nb_visits&gt;2758&lt;\/nb_visits&gt;\n &lt;nb_actions&gt;7943&lt;\/nb_actions&gt;\n &lt;bounce_count&gt;1421&lt;\/bounce_count&gt;\n &lt;nb_conversions&gt;987&lt;\/nb_conversions&gt;\n &lt;nb_visits_converted&gt;838&lt;\/nb_visits_converted&gt;\n &lt;revenue&gt;0&lt;\/revenue&gt;\n &lt;nb_pageviews&gt;6370&lt;\/nb_pageviews&gt;\n &lt;nb_uniq_pageviews&gt;5330&lt;\/nb_uniq_pageviews&gt;\n &lt;nb_downloads&gt;368&lt;\/nb_downloads&gt;\n &lt;nb_uniq_downloads&gt;305&lt;\/nb_uniq_downloads&gt;\n &lt;nb_outlinks&gt;951&lt;\/nb_outlinks&gt;\n &lt;nb_uniq_outlinks&gt;871&lt;\/nb_uniq_outlinks&gt;\n &lt;nb_searches&gt;27&lt;\/nb_searches&gt;\n &lt;nb_keywords&gt;25&lt;\/nb_keywords&gt;\n &lt;nb_hits_with_time_generation&gt;5635&lt;\/nb_hits_with_time_generation&gt;\n &lt;conversion_rate&gt;30.38%&lt;\/conversion_rate&gt;\n &lt;bounce_rate&gt;52%&lt;\/bounce_rate&gt;\n &lt;nb_actions_per_visit&gt;2.9&lt;\/nb_actions_per_visit&gt;\n [...]\n<\/code><\/pre>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<ul><li>0.1.2 - Fixed bug <code>Call to a member function getRowsCount() on a non-object<\/code><\/li>\n<li>0.1.0 - Initial release<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/ApiGetWithSitesInfo\/download\/0.1.2"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"Bandwidth",
+ "displayName":"Bandwidth",
+ "owner":"piwik",
+ "description":"Monitor Bandwidth for each page, download, and measure overall traffic in bytes",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2015-02-18 03:50:04",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/piwik\/plugin-Bandwidth",
+ "lastUpdated":"2016-01-28 16:10:04",
+ "latestVersion":"0.1.2",
+ "numDownloads":7742,"screenshots":[],"previews":[],"activity":{"numCommits":"62",
+ "numContributors":"6",
+ "lastCommitDate":"2016-03-15 04:19:37"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2015-02-18 03:50:05",
+ "requires":{"piwik":">=2.11.0-b3"},"readme":"",
+ "numDownloads":1809,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-Bandwidth\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Bandwidth\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2015-06-09 04:08:04",
+ "requires":{"piwik":">=2.11.0-b3"},"readme":"",
+ "numDownloads":3477,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-Bandwidth\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Bandwidth\/download\/0.1.1"},{"name":"0.1.2",
+ "release":"2016-01-28 16:10:04",
+ "requires":{"piwik":">=2.14.0"},"readme":"# Piwik Bandwidth Plugin\n\n[![Build Status](https:\/\/travis-ci.org\/piwik\/plugin-Bandwidth.svg?branch=master)](https:\/\/travis-ci.org\/piwik\/plugin-Bandwidth)\n\n## Description\n\nThis plugin allows you to measure the bandwidth that was used by each page view or download. \nIt enriches existing reports and APIs to show the used bandwidth. Find more information in the FAQ.\n\n## FAQ\n\n__How can I track the bandwidth?__\n\nLog analytics:\n\nThe bandwidth will be automatically tracked when using the [log importer](http:\/\/piwik.org\/log-analytics\/) as long as \nyour log format is supported.\n\nTracking API:\n\nIf you are using the [HTTP Tracking API](http:\/\/developer.piwik.org\/api-reference\/tracking-api) \nyou can specify the bandwidth in bytes by appending the URL parameter `bw_bytes=1234` to the tracking URL. In this case \na bandwidth of 1234 bytes will be tracked.\n\n__Which actions support tracking of bandwidth?__\n\nPageviews (Page URLs and Page Titles) as well as Downloads.\n\n__In which reports is the used bandwidth displayed?__\n\n* Page URLs \n* Page Titles\n* Downloads\n\nAll reports will show a column `Average Bandwidth` and `Sum Bandwidth`\n\nThe \"Visitors => Overview\" report shows a total bandwidth overview and it is possible to view the evolution over period.\n\n__Which APIs does this plugin define or enrich?__\n\nThere is a report `Bandwidth.get` returning the total bandwidth (across all actions).\n\nIt also enriches varies reports such as `Actions.get`, `Actions.getPageUrls`, `Actions.getPageTitles` and `Actions.getDownloads`.\nFor example it adds columns such as `avg_bandwidth`, `sum_bandwidth`, `min_bandwidth`, `max_bandwidth` to each page view.\n\n__Why are the bandwidth columns are not displayed in the UI?__\n\nMake sure the Bandwidth plugin is activated by going to `Administration => Plugins`. Also the bandwidth columns are not \ndisplayed if no bandwidth was tracked in the current selected month.\n\n## Changelog\n\n0.1.0 Initial Release\n\n## Support\n\nPlease direct any feedback to [hello@piwik.org](mailto:hello@piwik.org)",
+ "numDownloads":2456,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-Bandwidth\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>This plugin allows you to measure the bandwidth that was used by each page view or download. \nIt enriches existing reports and APIs to show the used bandwidth. Find more information in the FAQ.<\/p>\n\n",
+ "faq":"<p><strong>How can I track the bandwidth?<\/strong><\/p>\n\n<p>Log analytics:<\/p>\n\n<p>The bandwidth will be automatically tracked when using the <a href=\"http:\/\/piwik.org\/log-analytics\/\">log importer<\/a> as long as \nyour log format is supported.<\/p>\n\n<p>Tracking API:<\/p>\n\n<p>If you are using the <a href=\"http:\/\/developer.piwik.org\/api-reference\/tracking-api\">HTTP Tracking API<\/a> \nyou can specify the bandwidth in bytes by appending the URL parameter <code>bw_bytes=1234<\/code> to the tracking URL. In this case \na bandwidth of 1234 bytes will be tracked.<\/p>\n\n<p><strong>Which actions support tracking of bandwidth?<\/strong><\/p>\n\n<p>Pageviews (Page URLs and Page Titles) as well as Downloads.<\/p>\n\n<p><strong>In which reports is the used bandwidth displayed?<\/strong><\/p>\n\n<ul><li>Page URLs <\/li>\n<li>Page Titles<\/li>\n<li>Downloads<\/li>\n<\/ul><p>All reports will show a column <code>Average Bandwidth<\/code> and <code>Sum Bandwidth<\/code><\/p>\n\n<p>The \"Visitors =&gt; Overview\" report shows a total bandwidth overview and it is possible to view the evolution over period.<\/p>\n\n<p><strong>Which APIs does this plugin define or enrich?<\/strong><\/p>\n\n<p>There is a report <code>Bandwidth.get<\/code> returning the total bandwidth (across all actions).<\/p>\n\n<p>It also enriches varies reports such as <code>Actions.get<\/code>, <code>Actions.getPageUrls<\/code>, <code>Actions.getPageTitles<\/code> and <code>Actions.getDownloads<\/code>.\nFor example it adds columns such as <code>avg_bandwidth<\/code>, <code>sum_bandwidth<\/code>, <code>min_bandwidth<\/code>, <code>max_bandwidth<\/code> to each page view.<\/p>\n\n<p><strong>Why are the bandwidth columns are not displayed in the UI?<\/strong><\/p>\n\n<p>Make sure the Bandwidth plugin is activated by going to <code>Administration =&gt; Plugins<\/code>. Also the bandwidth columns are not \ndisplayed if no bandwidth was tracked in the current selected month.<\/p>",
+ "documentation":"",
+ "changelog":"<p>0.1.0 Initial Release<\/p>"},"download":"\/api\/2.0\/plugins\/Bandwidth\/download\/0.1.2"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"Barometer",
+ "displayName":"Barometer",
+ "owner":"halfdan",
+ "description":"Live Plugin that shows the current number of visitors on the page.",
+ "homepage":"http:\/\/github.com\/halfdan\/piwik-barometer-plugin",
+ "createdDateTime":"2014-12-23 00:38:20",
+ "donate":{"flattr":"https:\/\/flattr.com\/profile\/test1",
+ "bitcoin":null},"support":[{"name":"Documentation",
+ "key":"docs",
+ "value":"https:\/\/barometer.org\/docs\/",
+ "type":"url"},{"name":"Wiki",
+ "key":"wiki",
+ "value":"https:\/\/github.com\/barometer\/piwik\/wiki",
+ "type":"url"},{"name":"Forum",
+ "key":"forum",
+ "value":"https:\/\/baromter.forum.org",
+ "type":"url"},{"name":"Email",
+ "key":"email",
+ "value":"barometer@example.com",
+ "type":"email"},{"name":"IRC",
+ "key":"irc",
+ "value":"irc:\/\/freenode\/baromter",
+ "type":"text"},{"name":"Issues \/ Bugs",
+ "key":"issues",
+ "value":"https:\/\/github.com\/barometer\/issues",
+ "type":"url"},{"name":"Source",
+ "key":"source",
+ "value":"https:\/\/github.com\/barometer\/piwik\/",
+ "type":"url"},{"name":"RSS",
+ "key":"rss",
+ "value":"https:\/\/barometer.org\/feed\/",
+ "type":"url"}],"isTheme":false,"keywords":["barometer",
+ "live"],"basePrice":0,"authors":[{"name":"Fabian Becker",
+ "email":"test8@example.com",
+ "homepage":"http:\/\/geekproject.eu"}],"repositoryUrl":"https:\/\/github.com\/halfdan\/piwik-barometer-plugin",
+ "lastUpdated":"2014-12-23 00:41:21",
+ "latestVersion":"0.5.0",
+ "numDownloads":0,"screenshots":["https:\/\/plugins.piwik.org\/Barometer\/images\/piwik-barometer-01.png",
+ "https:\/\/plugins.piwik.org\/Barometer\/images\/piwik-barometer-02.png"],"previews":[{"type":"demo",
+ "provider":"link",
+ "url":"https:\/\/demo.piwik.org"}],"activity":{"numCommits":"31",
+ "numContributors":"3",
+ "lastCommitDate":"1970-01-01 00:33:35"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.1",
+ "release":"2014-12-23 00:38:20",
+ "requires":{"piwik":">=1.10.0"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/halfdan\/piwik-barometer-plugin\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Barometer\/download\/0.1.1"},{"name":"0.5.0",
+ "release":"2014-12-23 00:41:21",
+ "requires":{},"readme":"# Piwik Barometer Plugin\n\n## Description\n\nThis is a plugin for the Open Source Web Analytics platform Piwik. If enabled, it will add a two new widgets that you can add to your dashboard.\n\nThe widgets will show a Visitor Barometer and a Visit Time Barometer that auto-refresh every 5 seconds. It shows the number of visitors or visit time in a N minute period compared to the maximum number of visitors\/average visit time in any N minute period of the last 30 days.\n\nThe idea for this plugin came from [@muesli](http:\/\/github.com\/muesli) who suggested it on #piwik in IRC.\n\n## Documentation\n\n1. Clone the plugin into the plugins directory of your Piwik installation.\n\n ```\n cd plugins\/\n git clone https:\/\/github.com\/halfdan\/piwik-barometer-plugin.git Barometer\n ```\n\n2. Login as superuser into your Piwik installation and activate the plugin under Settings -> Plugins\n\n3. You will now find the widget under the Live! section.\n\n## Contribute \n\nIf you are interested in contributing to this plugin, feel free to send pull requests!\n",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/halfdan\/piwik-barometer-plugin\/commits\/v0.5.0",
+ "readmeHtml":{"description":"\n\n<p>This is a plugin for the Open Source Web Analytics platform Piwik. If enabled, it will add a two new widgets that you can add to your dashboard.<\/p>\n\n<p>The widgets will show a Visitor Barometer and a Visit Time Barometer that auto-refresh every 5 seconds. It shows the number of visitors or visit time in a N minute period compared to the maximum number of visitors\/average visit time in any N minute period of the last 30 days.<\/p>\n\n<p>The idea for this plugin came from <a href=\"http:\/\/github.com\/muesli\">@muesli<\/a> who suggested it on #piwik in IRC.<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/Barometer\/download\/0.5.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"Chat",
+ "displayName":"Chat",
+ "owner":"VincentLahaye",
+ "description":"Not production ready. Join the Beta test ! Engage people you don't know at all, with this targeted and efficient chat system, directly integrated into",
+ "homepage":"https:\/\/github.com\/VincentLahaye\/piwik-chat",
+ "createdDateTime":"2014-06-13 09:10:06",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/VincentLahaye\/piwik-chat",
+ "lastUpdated":"2014-08-20 09:08:05",
+ "latestVersion":"0.2.4",
+ "numDownloads":5705,"screenshots":[],"previews":[],"activity":{"numCommits":"2",
+ "numContributors":"2",
+ "lastCommitDate":"2014-08-20 09:08:00"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.1",
+ "release":"2014-06-13 09:10:06",
+ "requires":{"piwik":">=2.4.0-b4"},"readme":"",
+ "numDownloads":2,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/VincentLahaye\/piwik-chat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Chat\/download\/0.1.1"},{"name":"0.1.2",
+ "release":"2014-06-13 09:52:05",
+ "requires":{"piwik":">=2.4.0-b4"},"readme":"",
+ "numDownloads":92,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/VincentLahaye\/piwik-chat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Chat\/download\/0.1.2"},{"name":"0.1.3",
+ "release":"2014-06-23 11:54:10",
+ "requires":{"piwik":">=2.4.0-b4"},"readme":"",
+ "numDownloads":109,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/VincentLahaye\/piwik-chat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Chat\/download\/0.1.3"},{"name":"0.1.4",
+ "release":"2014-06-29 15:12:05",
+ "requires":{"piwik":">=2.4.0-b4"},"readme":"",
+ "numDownloads":513,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/VincentLahaye\/piwik-chat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Chat\/download\/0.1.4"},{"name":"0.1.5",
+ "release":"2014-08-13 10:42:05",
+ "requires":{"piwik":">=2.4.0"},"readme":"",
+ "numDownloads":70,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/VincentLahaye\/piwik-chat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Chat\/download\/0.1.5"},{"name":"0.1.6",
+ "release":"2014-08-14 10:08:05",
+ "requires":{"piwik":">=2.4.0"},"readme":"",
+ "numDownloads":127,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/VincentLahaye\/piwik-chat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Chat\/download\/0.1.6"},{"name":"0.2.0",
+ "release":"2014-08-19 16:58:06",
+ "requires":{"piwik":">=2.4.0"},"readme":"",
+ "numDownloads":2,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/VincentLahaye\/piwik-chat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Chat\/download\/0.2.0"},{"name":"0.2.1",
+ "release":"2014-08-19 17:16:05",
+ "requires":{"piwik":">=2.4.0"},"readme":"",
+ "numDownloads":31,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/VincentLahaye\/piwik-chat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Chat\/download\/0.2.1"},{"name":"0.2.2",
+ "release":"2014-08-20 07:34:06",
+ "requires":{"piwik":">=2.4.0"},"readme":"",
+ "numDownloads":1,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/VincentLahaye\/piwik-chat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Chat\/download\/0.2.2"},{"name":"0.2.3",
+ "release":"2014-08-20 07:58:06",
+ "requires":{"piwik":">=2.4.0"},"readme":"",
+ "numDownloads":6,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/VincentLahaye\/piwik-chat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Chat\/download\/0.2.3"},{"name":"0.2.4",
+ "release":"2014-08-20 09:08:05",
+ "requires":{"piwik":">=2.4.0"},"readme":"# Chat Plugin for Piwik (BETA)\nThis plugin is not production ready, but any help is appreciate!\n\nEngage people you don't know at all, with this targeted and efficient chat system, directly integrated into Piwik.\n\nThe client has these three states, plus a minimized state :\n\n![Chat Client](\/screenshots\/ClientState2.png?raw=true \"Chat Client State 2\")\n![Chat Client](\/screenshots\/ClientState3.png?raw=true \"Chat Client State 3\")\n![Chat Client](\/screenshots\/ClientState4.png?raw=true \"Chat Client State 4\")\n\nAnd here is the updated Visitor Profile :\n![Backend](\/screenshots\/BackendVisitorProfile.png?raw=true \"Visitor Profile in Backend\")\n\n## Installation\nAdd this code, after Piwik's tracking code :\n\n <!-- Piwik Chat -->\n <script type=\"text\/javascript\">\n (function() {\n var u=((\"https:\" == document.location.protocol) ? \"https:\/\/{$PIWIK_URL}\/\" : \"http:\/\/{$PIWIK_URL}\/\");\n var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.type='text\/javascript';\n g.defer=true; g.async=true; g.src=u+'plugins\/Chat\/javascripts\/client\/client.js'; s.parentNode.insertBefore(g,s);\n })();\n <\/script>\n <!-- End Piwik Chat Code -->\n\n## How it works\nOn the backend, this plugin modifies the visitorProfile popup, by adding to it a \"Chat\" tab next to \"Visited pages\". A new menu \"Chat\" is available, which allows to browse all conversations.\n\nSoon, a reporting system will be integrated, which will show figures like conversion rate or engagement, in function of the Chat's use.\n\nAfter installing the plugin, you have to modify your tracking code. It includes the file \"javascripts\/client\/client.js\", which waits for the Tracker() to initialize, then appends an iframe to the page. This iframe requests the \"popout\" action of the controller, where the plugin tries to identify the visitor based on his visitorID or configID.\n\n## Changelog\n\nv0.2.2 :\n\n+ Add DB Versioning support\n\nv0.2 :\n\n+ Piwik 2.5.0 ready\n+ Add automatic messages based on segment recognition\n+ General improvement on client popout\n + Load message over ajax\n + Add moment.js\n + Update the communication (based on easyXDM) between the popout and its parent iframe. See : https:\/\/github.com\/VincentLahaye\/piwik-chat\/commit\/ced4662d83cc8efc98b58b27e92ab6d06bb29546\n+ Update VisitorProfile popup\n + Improve textarea input interractions\n+ Rewrite a lot of code regarding the \"model\" file Conversation.php, now split in different files\n\nv0.1.6 :\n\n+ Add a reporting bug system\n\n\nv0.1.5 :\n\n+ Notification system improved\n + Add email notification to offline staf members\n + Add title notification\n + Improve sound and menu notification, now working on all CoreHome module\n+ Add german translation\n+ Fix wrong inclusion paths on client popout\n+ Fix online staff indicator on client popout\n\n\nv0.1 :\n\n+ Basic chat functions with client\/server communication and Piwik integration\n+ Add additional inputs in the visitor profile in order to save personal informations about a visitor (name, email, phone and comments)\n+ The client shows if someone of the staff is online or not\n+ Sound notifications on both sides\n+ French translation\n\n## Roadmap\nv0.3 :\n\n* Add a reporting system that shows figures about the module impact\n* Add external cache support for polling (APC, Memcache, opCache...)\n\n## Academical study\nAs an MSc student in eBusiness, this project is part of my final dissertation : \"How to engage people you don't know at all ?\".\n\nThe first part of this project is to develop the module and its reporting system. Then, the second part will be about analyse the impact of this Chat system (conversion rate, engagement, etc..) in function of its use, and eventually try to determine which way of use is the most efficient. From this research it may be possible to provide advices and answer questions like : What is the best behavior to adopt as an operator? What kinds of automatic messages are the more efficient in order to increase conversion?\n\nTo lead this study, I will need data! And if you support my work, you'll be able to send me your reports via the plugin, in an anonymized way.",
+ "numDownloads":4752,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/VincentLahaye\/piwik-chat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<p>v0.2.2 :<\/p>\n\n<ul><li>Add DB Versioning support<\/li>\n<\/ul><p>v0.2 :<\/p>\n\n<ul><li>Piwik 2.5.0 ready<\/li>\n<li>Add automatic messages based on segment recognition<\/li>\n<li>General improvement on client popout\n\n<ul><li>Load message over ajax<\/li>\n<li>Add moment.js<\/li>\n<li>Update the communication (based on easyXDM) between the popout and its parent iframe. See : https:\/\/github.com\/VincentLahaye\/piwik-chat\/commit\/ced4662d83cc8efc98b58b27e92ab6d06bb29546<\/li>\n<\/ul><\/li>\n<li>Update VisitorProfile popup\n\n<ul><li>Improve textarea input interractions<\/li>\n<\/ul><\/li>\n<li>Rewrite a lot of code regarding the \"model\" file Conversation.php, now split in different files<\/li>\n<\/ul><p>v0.1.6 :<\/p>\n\n<ul><li>Add a reporting bug system<\/li>\n<\/ul><p>v0.1.5 :<\/p>\n\n<ul><li>Notification system improved\n\n<ul><li>Add email notification to offline staf members<\/li>\n<li>Add title notification<\/li>\n<li>Improve sound and menu notification, now working on all CoreHome module<\/li>\n<\/ul><\/li>\n<li>Add german translation<\/li>\n<li>Fix wrong inclusion paths on client popout<\/li>\n<li>Fix online staff indicator on client popout<\/li>\n<\/ul><p>v0.1 :<\/p>\n\n<ul><li>Basic chat functions with client\/server communication and Piwik integration<\/li>\n<li>Add additional inputs in the visitor profile in order to save personal informations about a visitor (name, email, phone and comments)<\/li>\n<li>The client shows if someone of the staff is online or not<\/li>\n<li>Sound notifications on both sides<\/li>\n<li>French translation<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/Chat\/download\/0.2.4"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"ClickHeat",
+ "displayName":"Click Heat",
+ "owner":"piwikjapan",
+ "description":"ClickHeat is a visual heatmap of clicks on a HTML page. This plugin based on Dugwood's ClickHeat version 1.14. Plugin not consider IIS. Sorry.",
+ "homepage":"http:\/\/piwikjapan.org",
+ "createdDateTime":"2015-04-23 01:56:04",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/piwikjapan\/plugin-clickheat",
+ "lastUpdated":"2015-04-27 15:46:04",
+ "latestVersion":"0.1.5",
+ "numDownloads":8559,"screenshots":[],"previews":[],"activity":{"numCommits":"13",
+ "numContributors":"3",
+ "lastCommitDate":"2016-05-27 17:59:06"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.1",
+ "release":"2015-04-23 01:56:05",
+ "requires":{"piwik":">=2.11.1"},"readme":"",
+ "numDownloads":49,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwikjapan\/plugin-clickheat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ClickHeat\/download\/0.1.1"},{"name":"0.1.2",
+ "release":"2015-04-23 14:12:04",
+ "requires":{"piwik":">=2.11.0"},"readme":"",
+ "numDownloads":72,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwikjapan\/plugin-clickheat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ClickHeat\/download\/0.1.2"},{"name":"0.1.3",
+ "release":"2015-04-25 14:38:05",
+ "requires":{"piwik":">=2.11.0"},"readme":"",
+ "numDownloads":64,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwikjapan\/plugin-clickheat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ClickHeat\/download\/0.1.3"},{"name":"0.1.4",
+ "release":"2015-04-27 13:54:05",
+ "requires":{"piwik":">=2.11.0"},"readme":"",
+ "numDownloads":21,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwikjapan\/plugin-clickheat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ClickHeat\/download\/0.1.4"},{"name":"0.1.5",
+ "release":"2015-04-27 15:46:04",
+ "requires":{"piwik":">=2.11.0"},"readme":"# Piwik ClickHeat Plugin\n\n## Description\nClickHeat is a visual heatmap of clicks on a HTML page, showing hot and cold click zones. This plugin based on [Dugwood's ClickHeat version 1.14](https:\/\/github.com\/dugwood\/clickheat). It is an OpenSource software, released under GPL licence, and free of charge. \n\n__Plugin not consider the IIS.__ Sorry. We are waiting patches for IIS.\n\n## Installation\nInstall it via Piwik Marketplace.\n\nThis plugin installer will make directories:\n* yourpiwik\/tmp\/cache\/clickheat\/cache\n* yourpiwik\/tmp\/cache\/clickheat\/logs.\n\nThis plugin uses a different tracker. Please click on the link \"JavaScript\" and put the special Javascript codes into your website.\n\n## FAQ\n__What exactly is included in this feature ?__\n\n* pick up a siteid\n* pick up a period\n* pick up a browser type\n* pick up a specific web page\n\n__And what functions are not included in this feature ?__\n\n* remove special addresses defined on the control panel.\n* remove special browsers defined on the control panel.\n* filters based on added segmentation\n\n__Where is the coordinate information from the browser ?__\n\nClickHeat plugin uses text files to record the coordinate data of each browser in directory: yourpiwik\/tmp\/cache\/clickheat\/logs.\n\n__What is \"click.php returned a status code 403\" ?__\n\nYou have to perform the upgrade immediately to version 0.1.5. I forgot to put .htaccess.\n\n__After installing the plugin, Piwik Administration area shows \"page not found 404 error\".__\n\nThis plugin doesn't consider the IIS. Sorry. And please delete the ClickHeat plugin (yourpiwik\/plugins\/ClickHeat) manually via FTP or Explorer. We are waiting patches for IIS.\n\n__Does it withstands high traffics ?__\n\nThis plugin uses minimal text to record data and file based logging. And when click.php is called from a special Javascript for cgi, just append text on end of the each file. And when you analyze the click data and make a heatmap, plugin will create cached heatmap as png image file. \n\nTherefore, we expect the plugin light works, but we don't know what load it has under Piwik 2.x. So we are very glad, when you inform us about your situation. \n\nPlease see the link [Performance and optimization](http:\/\/www.labsmedia.com\/clickheat\/156894.html) about system resources. If you want performance, you need to avoid to use a cgi, that is possible. It method is explained on the link. \n\n__New click data were added, but not updated heatmap. Why ?__\n\nPlugin places heatmap images in the cache directory: yourpiwik\/tmp\/cache\/clickheat\/cache. Therefore when you suddenly met with such probrem, you can delete cache files, but __do not delete cache directory__.\n\n__Showed a heatmap, but not overlay a heatmap to the target web page. Why ?__\n\nCheck that your website does not set the HTTP header __X-FRAME-OPTIONS__ to __SAMEORIGIN__ as this will prevent this plugin from iframing your website for the heatmap report. Please see [Page Overlay Troubleshooting](http:\/\/piwik.org\/docs\/page-overlay\/#page-overlay-troubleshooting), that is same problem.\n\n## Changelog\n\n* 0.1.0 First beta\n* 0.1.2 to append faq\n* 0.1.3 to append faq\n* 0.1.5 to add .htaccess\n\n## License\nGPL v3 or later\n\n## Support\nPlease direct any feedback to [yamachan@piwikjapan.org](mailto:yamachan@piwikjapan.org).\n",
+ "numDownloads":8353,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwikjapan\/plugin-clickheat\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>ClickHeat is a visual heatmap of clicks on a HTML page, showing hot and cold click zones. This plugin based on <a href=\"https:\/\/github.com\/dugwood\/clickheat\">Dugwood's ClickHeat version 1.14<\/a>. It is an OpenSource software, released under GPL licence, and free of charge.<\/p>\n\n<p><strong>Plugin not consider the IIS.<\/strong> Sorry. We are waiting patches for IIS.<\/p>\n\n",
+ "faq":"<p><strong>What exactly is included in this feature ?<\/strong><\/p>\n\n<ul><li>pick up a siteid<\/li>\n<li>pick up a period<\/li>\n<li>pick up a browser type<\/li>\n<li>pick up a specific web page<\/li>\n<\/ul><p><strong>And what functions are not included in this feature ?<\/strong><\/p>\n\n<ul><li>remove special addresses defined on the control panel.<\/li>\n<li>remove special browsers defined on the control panel.<\/li>\n<li>filters based on added segmentation<\/li>\n<\/ul><p><strong>Where is the coordinate information from the browser ?<\/strong><\/p>\n\n<p>ClickHeat plugin uses text files to record the coordinate data of each browser in directory: yourpiwik\/tmp\/cache\/clickheat\/logs.<\/p>\n\n<p><strong>What is \"click.php returned a status code 403\" ?<\/strong><\/p>\n\n<p>You have to perform the upgrade immediately to version 0.1.5. I forgot to put .htaccess.<\/p>\n\n<p><strong>After installing the plugin, Piwik Administration area shows \"page not found 404 error\".<\/strong><\/p>\n\n<p>This plugin doesn't consider the IIS. Sorry. And please delete the ClickHeat plugin (yourpiwik\/plugins\/ClickHeat) manually via FTP or Explorer. We are waiting patches for IIS.<\/p>\n\n<p><strong>Does it withstands high traffics ?<\/strong><\/p>\n\n<p>This plugin uses minimal text to record data and file based logging. And when click.php is called from a special Javascript for cgi, just append text on end of the each file. And when you analyze the click data and make a heatmap, plugin will create cached heatmap as png image file.<\/p>\n\n<p>Therefore, we expect the plugin light works, but we don't know what load it has under Piwik 2.x. So we are very glad, when you inform us about your situation.<\/p>\n\n<p>Please see the link <a href=\"http:\/\/www.labsmedia.com\/clickheat\/156894.html\">Performance and optimization<\/a> about system resources. If you want performance, you need to avoid to use a cgi, that is possible. It method is explained on the link.<\/p>\n\n<p><strong>New click data were added, but not updated heatmap. Why ?<\/strong><\/p>\n\n<p>Plugin places heatmap images in the cache directory: yourpiwik\/tmp\/cache\/clickheat\/cache. Therefore when you suddenly met with such probrem, you can delete cache files, but <strong>do not delete cache directory<\/strong>.<\/p>\n\n<p><strong>Showed a heatmap, but not overlay a heatmap to the target web page. Why ?<\/strong><\/p>\n\n<p>Check that your website does not set the HTTP header <strong>X-FRAME-OPTIONS<\/strong> to <strong>SAMEORIGIN<\/strong> as this will prevent this plugin from iframing your website for the heatmap report. Please see <a href=\"http:\/\/piwik.org\/docs\/page-overlay\/#page-overlay-troubleshooting\">Page Overlay Troubleshooting<\/a>, that is same problem.<\/p>",
+ "documentation":"",
+ "changelog":"<ul><li>0.1.0 First beta<\/li>\n<li>0.1.2 to append faq<\/li>\n<li>0.1.3 to append faq<\/li>\n<li>0.1.5 to add .htaccess<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/ClickHeat\/download\/0.1.5"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"Counter",
+ "displayName":"Counter",
+ "owner":"Globulopolis",
+ "description":"Display Hits\/Visits on image",
+ "homepage":"http:\/\/xn--80aeqbhthr9b.com\/en\/others\/piwik\/10-piwik-graphical-counter.html",
+ "createdDateTime":"2014-12-23 01:14:18",
+ "donate":{},"support":[],"isTheme":false,"keywords":["piwik",
+ "counter image",
+ "image counter",
+ "piwik visible counter",
+ "show hits piwik",
+ "show visits piwik"],"basePrice":0,"authors":[{"name":"Viper",
+ "email":null,"homepage":null}],"repositoryUrl":"https:\/\/github.com\/Globulopolis\/Counter",
+ "lastUpdated":"2014-12-23 01:14:18",
+ "latestVersion":"2.0.2",
+ "numDownloads":0,"screenshots":[],"previews":[],"activity":{"numCommits":"10",
+ "numContributors":"1",
+ "lastCommitDate":"1970-01-01 00:33:35"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"2.0.2",
+ "release":"2014-12-23 01:14:18",
+ "requires":{},"readme":"# Piwik counter plugin\n\n## Description\n\nDisplay Hits\/Visits on image\n\n## FAQ\n\nSee http:\/\/xn--80aeqbhthr9b.com\/en\/others\/piwik\/10-piwik-graphical-counter.html\n\n## Changelog\n2.0.2\n* Fixed a bug where the URL with the image displayed via http, if you are using https(bug only in counters list).\n\n2.0.1\n* Fix for CORS (thanks for aureq for patch)\n* Changing versioning according to requirements\n\n2.0 Initial release\n\n## Support\n\nhttp:\/\/xn--80aeqbhthr9b.com\/en\/contact-form.html",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/Globulopolis\/Counter\/commits\/2.0.2",
+ "readmeHtml":{"description":"\n\n<p>Display Hits\/Visits on image<\/p>\n\n",
+ "faq":"<p>See http:\/\/xn--80aeqbhthr9b.com\/en\/others\/piwik\/10-piwik-graphical-counter.html<\/p>",
+ "documentation":"",
+ "changelog":"<p>2.0.2\n* Fixed a bug where the URL with the image displayed via http, if you are using https(bug only in counters list).<\/p>\n\n<p>2.0.1\n* Fix for CORS (thanks for aureq for patch)\n* Changing versioning according to requirements<\/p>\n\n<p>2.0 Initial release<\/p>"},"download":"\/api\/2.0\/plugins\/Counter\/download\/2.0.2"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"CustomAlerts",
+ "displayName":"Custom Alerts",
+ "owner":"piwik",
+ "description":"Alerts are a great way to get notified of changes on your website. This is a beta version.",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2014-12-23 01:07:23",
+ "donate":{},"support":[],"isTheme":false,"keywords":["alerts",
+ "notification",
+ "report",
+ "monitoring"],"basePrice":0,"authors":[{"name":"Piwik",
+ "email":"test1@example.com",
+ "homepage":"http:\/\/piwik.org"}],"repositoryUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts",
+ "lastUpdated":"2014-12-23 01:16:54",
+ "latestVersion":"0.1.12",
+ "numDownloads":0,"screenshots":["https:\/\/plugins.piwik.org\/CustomAlerts\/images\/Create_Alert.png",
+ "https:\/\/plugins.piwik.org\/CustomAlerts\/images\/History_Of_Alerts.png",
+ "https:\/\/plugins.piwik.org\/CustomAlerts\/images\/List_Of_Alerts.png"],"previews":[],"activity":{"numCommits":"300",
+ "numContributors":"7",
+ "lastCommitDate":"1970-01-01 00:33:35"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2014-12-23 01:07:25",
+ "requires":{"piwik":">=2.0.4"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts\/commits\/0.0.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomAlerts\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2014-12-23 01:08:03",
+ "requires":{"piwik":">=2.0.4"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts\/commits\/0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomAlerts\/download\/0.1.1"},{"name":"0.1.2",
+ "release":"2014-12-23 01:08:23",
+ "requires":{"piwik":">=2.0.4-b1"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts\/commits\/0.1.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomAlerts\/download\/0.1.2"},{"name":"0.1.3",
+ "release":"2014-12-23 01:08:43",
+ "requires":{"piwik":">=2.0.4-b1"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts\/commits\/0.1.3",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomAlerts\/download\/0.1.3"},{"name":"0.1.4",
+ "release":"2014-12-23 01:11:39",
+ "requires":{"piwik":">=2.0.4-b3"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts\/commits\/0.1.4",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomAlerts\/download\/0.1.4"},{"name":"0.1.5",
+ "release":"2014-12-23 01:11:59",
+ "requires":{"piwik":">=2.0.4-b3"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts\/commits\/0.1.5",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomAlerts\/download\/0.1.5"},{"name":"0.1.6",
+ "release":"2014-12-23 01:12:20",
+ "requires":{"piwik":">=2.0.4-b3"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts\/commits\/0.1.6",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomAlerts\/download\/0.1.6"},{"name":"0.1.7",
+ "release":"2014-12-23 01:12:42",
+ "requires":{"piwik":">=2.0.4-b3"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts\/commits\/0.1.7",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomAlerts\/download\/0.1.7"},{"name":"0.1.8-b1",
+ "release":"2014-12-23 01:13:41",
+ "requires":{"piwik":">=2.0.4-b5"},"readme":"",
+ "numDownloads":0,"license":{"name":"Commercial license",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts\/commits\/0.1.8",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomAlerts\/download\/0.1.8-b1"},{"name":"0.1.9",
+ "release":"2014-12-23 01:14:00",
+ "requires":{"piwik":">=2.0.4-b5"},"readme":"",
+ "numDownloads":0,"license":{"name":"Commercial license",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts\/commits\/0.1.9",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomAlerts\/download\/0.1.9"},{"name":"0.1.10",
+ "release":"2014-12-23 01:15:35",
+ "requires":{"piwik":">=2.0.4-b7"},"readme":"",
+ "numDownloads":0,"license":{"name":"Commercial license",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts\/commits\/0.1.10",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomAlerts\/download\/0.1.10"},{"name":"0.1.11",
+ "release":"2014-12-23 01:15:57",
+ "requires":{"piwik":">=2.0.4-b7"},"readme":"",
+ "numDownloads":0,"license":{"name":"Commercial license",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts\/commits\/0.1.11",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomAlerts\/download\/0.1.11"},{"name":"0.1.12",
+ "release":"2014-12-23 01:16:54",
+ "requires":{"piwik":">=2.0.4-b9"},"readme":"# Piwik CustomAlerts Plugin \n\n[![Build Status](https:\/\/travis-ci.org\/piwik\/plugin-CustomAlerts.png?branch=master)](https:\/\/travis-ci.org\/piwik\/plugin-CustomAlerts)\n\n## Description\n\nCreate Custom Alerts + Be notified by email\/SMS!\n\nAlerts are a great way to get notified of changes on your website. Want to know if your new product hits less than 100 sales in a week or your new article attracts more than 200 visitors a day? Create alerts that make sense to you. Be notified by email or SMS when the conditions for your alerts are met. Stay on top of your website!\n\nThe Alert log will help you to better understand the success of your website. You can use it to analyze how often your website hit more than 10000 visits per day or on how many days a product was sold more than 50 times.\n\nThis plugin was [crowdfunded with the support](http:\/\/crowdfunding.piwik.org\/custom-alerts-plugin\/) of 37 Piwik community members!\n\n## Installation\n\nInstall it via Piwik Marketplace\n\n## FAQ\n\n__What exactly is included in this feature?__\n\nHere is the complete list of features that are included in this project:\n\n* Define new Alert (\"Big drop in purchases\")\n* Select a website on which the Alert is defined\n* Receive an alert by email (email will contain Alert description + link to Piwik dashboard URL for the given website ID and period).\n* Receive an alert by SMS (SMS will contain Alert description and numbers that triggered the Alert)\n* Select the Alert period: should it be daily, weekly or monthly?\n* Select the report (Websites, Keywords, Countries, general stats)\n* Define Metrics (visits, page view, avg. visit duration, Goal 1 conversions, total goal conversions, etc.)\n* Define the Alert: when Visits decrease 50%, when purchases are more than 50 per day, etc.\n\n__What reports are available to the Alert system?__\n\nYou can create an alert for any available report in Piwik. Plugins can define new reports which will be automatically picked up by Alerts.\n\n__What alert conditions are available?__\n\nYou can create alerts for the following metrics:\n\n* Visits, Visits Evolution, Unique Visits\n* Actions, Action Evolution\n* Pageviews, Pageviews Evolution\n* Time on page\n* Bounce rate\n* Goal revenue\n* Downloads\n* and many more..\n\nTo define the condition you can select the conditions:\n\n* Greater than, less than\n* Equal, Not Equal\n* Percentage increase\/decrease\n\n## Changelog\n\n* 0.1.0 First beta\n\n## License\n\nGPL v3 or later\n\n## Support\n\n* Please direct any feedback to [hello@piwik.org](mailto:hello@piwik.org)\n\n",
+ "numDownloads":0,"license":{"name":"Commercial license",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts\/commits\/0.1.12",
+ "readmeHtml":{"description":"\n\n<p>Create Custom Alerts + Be notified by email\/SMS!<\/p>\n\n<p>Alerts are a great way to get notified of changes on your website. Want to know if your new product hits less than 100 sales in a week or your new article attracts more than 200 visitors a day? Create alerts that make sense to you. Be notified by email or SMS when the conditions for your alerts are met. Stay on top of your website!<\/p>\n\n<p>The Alert log will help you to better understand the success of your website. You can use it to analyze how often your website hit more than 10000 visits per day or on how many days a product was sold more than 50 times.<\/p>\n\n<p>This plugin was <a href=\"http:\/\/crowdfunding.piwik.org\/custom-alerts-plugin\/\">crowdfunded with the support<\/a> of 37 Piwik community members!<\/p>\n\n",
+ "faq":"<p><strong>What exactly is included in this feature?<\/strong><\/p>\n\n<p>Here is the complete list of features that are included in this project:<\/p>\n\n<ul><li>Define new Alert (\"Big drop in purchases\")<\/li>\n<li>Select a website on which the Alert is defined<\/li>\n<li>Receive an alert by email (email will contain Alert description + link to Piwik dashboard URL for the given website ID and period).<\/li>\n<li>Receive an alert by SMS (SMS will contain Alert description and numbers that triggered the Alert)<\/li>\n<li>Select the Alert period: should it be daily, weekly or monthly?<\/li>\n<li>Select the report (Websites, Keywords, Countries, general stats)<\/li>\n<li>Define Metrics (visits, page view, avg. visit duration, Goal 1 conversions, total goal conversions, etc.)<\/li>\n<li>Define the Alert: when Visits decrease 50%, when purchases are more than 50 per day, etc.<\/li>\n<\/ul><p><strong>What reports are available to the Alert system?<\/strong><\/p>\n\n<p>You can create an alert for any available report in Piwik. Plugins can define new reports which will be automatically picked up by Alerts.<\/p>\n\n<p><strong>What alert conditions are available?<\/strong><\/p>\n\n<p>You can create alerts for the following metrics:<\/p>\n\n<ul><li>Visits, Visits Evolution, Unique Visits<\/li>\n<li>Actions, Action Evolution<\/li>\n<li>Pageviews, Pageviews Evolution<\/li>\n<li>Time on page<\/li>\n<li>Bounce rate<\/li>\n<li>Goal revenue<\/li>\n<li>Downloads<\/li>\n<li>and many more..<\/li>\n<\/ul><p>To define the condition you can select the conditions:<\/p>\n\n<ul><li>Greater than, less than<\/li>\n<li>Equal, Not Equal<\/li>\n<li>Percentage increase\/decrease<\/li>\n<\/ul>",
+ "documentation":"",
+ "changelog":"<ul><li>0.1.0 First beta<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/CustomAlerts\/download\/0.1.12"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"CustomDimensions",
+ "displayName":"Custom Dimensions",
+ "owner":"piwik",
+ "description":"Extend Piwik to your needs by defining and tracking Custom Dimensions in scope Action or Visit",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2015-11-25 03:26:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/piwik\/plugin-CustomDimensions",
+ "lastUpdated":"2016-02-04 19:56:04",
+ "latestVersion":"0.1.4",
+ "numDownloads":2584,"screenshots":[],"previews":[],"activity":{"numCommits":"67",
+ "numContributors":"4",
+ "lastCommitDate":"2016-06-02 15:56:37"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2015-11-25 03:26:04",
+ "requires":{"piwik":">=2.15.1-b3"},"readme":"",
+ "numDownloads":100,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomDimensions\/commits\/0.1.11",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomDimensions\/download\/0.1.0"},{"name":"0.1.2",
+ "release":"2015-12-07 04:16:04",
+ "requires":{"piwik":">=2.15.1-b8"},"readme":"",
+ "numDownloads":477,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomDimensions\/commits\/0.1.12",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomDimensions\/download\/0.1.2"},{"name":"0.1.3",
+ "release":"2016-01-21 19:30:07",
+ "requires":{"piwik":">=2.15.1-b8"},"readme":"",
+ "numDownloads":254,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomDimensions\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomDimensions\/download\/0.1.3"},{"name":"0.1.4",
+ "release":"2016-02-04 19:56:04",
+ "requires":{"piwik":">=2.15.1-b8"},"readme":"# Piwik CustomDimensions Plugin\n\n[![Build Status](https:\/\/travis-ci.org\/piwik\/plugin-CustomDimensions.svg?branch=master)](https:\/\/travis-ci.org\/piwik\/plugin-CustomDimensions)\n\n## Description\n\nThis plugins allows you to configure and track any [Custom Dimensions](https:\/\/piwik.org\/docs\/custom-dimensions\/). You can configure a Custom Dimension\nby giving it a name and a scope (Action or Visit). Afterwards you will see a new menu item in the reporting area\nfor each configured dimension and be able to get its data. You can also export the report as a widget, segment by this\n dimenson, and more. For more information read the [Custom Dimensions user guide](https:\/\/piwik.org\/docs\/custom-dimensions\/) or have a look in the FAQ.\n\n*Warning*: Depending on the database size of your Piwik this plugin may take a long time to install.\n\n## FAQ\n\n__I have a large database, can I install the plugin on the command line?__\n\nYes, this is not only possible but even recommended as the installation may take hours. To do this follow these steps:\n\n* Download the Plugin from [https:\/\/plugins.piwik.org\/CustomDimensions](https:\/\/plugins.piwik.org\/CustomDimensions)\n* Extract the files within the downloaded ZIP file\n* Copy the `CustomDimensions` directory into the `plugins` directory of your Piwik\n* Execute the command `.\/console plugin:activate CustomDimensions` within your Piwik directory\n\n__Where can I manage Custom Dimensions?__\n\nCustom Dimensions can be managed by clicking on your username or user icon in the top right. There will be a menu\nitem \"Custom Dimensions\" within the \"Manage\" section of the left menu. By clicking on it you can manage Custom Dimensions.\nPlease note that the permission Admin is required in order to be able to manage them.\n\n__Where can I find the Id for a Custom Dimension?__\n\nYou can find them by going to the \"Manage Custom Dimensions\" page in your personal area. For each dimension you will\nfind the Id in the table that lists all available Custom Dimensions.\n\n__How do I set a value for a dimension in the JavaScript Tracker?__\n\nPlease have a look at the [JavaScript Tracker guide for Custom Dimensions](https:\/\/developer.piwik.org\/guides\/tracking-javascript-guide#custom-dimensions).\n\n__How do I set a value for a dimension in the PHP Tracker?__\n\n`$tracker->setCustomTrackingParameter('dimension' . $customDimensionId, $value);`\n\nPlease note custom tracking parameters are cleared after each tracking request. If you want to keep the same\nCustom Dimensions over all request make sure to call this method before each tracking call.\n\n__I have configured all available Custom Dimension slots, can I add more?__\n\nYes, this is possible. To make a new Custom Dimension slot available execute the following command including the scope option:\n\n```\n.\/console customdimensions:add-custom-dimension --scope=action\n.\/console customdimensions:add-custom-dimension --scope=visit\n```\n\nBe aware that this can take a long time depending on the size of your database as it requires MySQL schema changes.\nYou can directly create multiple Custom Dimension slots. To do this add the option `--count=X`. Usually it doesn't take much\nlonger to create directly multiple new slots.\n\n__Is it possible to delete a Custom Dimension and all of its data?__\n\nIn the UI it is only possible to deactivate a dimension. However, on the command line you can remove a Custom Dimension\nand report it's log data by executing the following console command:\n\n```\n.\/console customdimensions:remove-custom-dimension --scope=$scope --index=$index\n```\n\nMake sure to replace `$scope` and `$index` with the correct values. To get a list of all available indexes execute `.\/console customdimensions:info`.\n\nRemoving a Custom Dimension may take a long time as it requires MySQL schema changes. Currently, only log data is removed. Archived reports will be\nnot deleted currently.\n\n## Changelog\n\n* 0.1.4 Fix a possible JavaScript error if Transitions plugin is disabled\n* 0.1.3 Fix UI of Custom Dimensions was not working properly when not using English as language\n* 0.1.2\n * New feature: Mark an extraction as case sensitive\n * New feature : Show actions that had no value defined\n * New feature : Link to Page URLs in subtables\n* 0.1.1 Bugfixes\n* 0.1.0 Initial release\n\n## Support\n\nPlease direct any feedback to [github.com\/piwik\/plugin-CustomDimensions\/issues](https:\/\/github.com\/piwik\/plugin-CustomDimensions\/issues)\n",
+ "numDownloads":1753,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomDimensions\/commits\/0.1.4",
+ "readmeHtml":{"description":"\n\n<p>This plugins allows you to configure and track any <a href=\"https:\/\/piwik.org\/docs\/custom-dimensions\/\">Custom Dimensions<\/a>. You can configure a Custom Dimension\nby giving it a name and a scope (Action or Visit). Afterwards you will see a new menu item in the reporting area\nfor each configured dimension and be able to get its data. You can also export the report as a widget, segment by this\n dimenson, and more. For more information read the <a href=\"https:\/\/piwik.org\/docs\/custom-dimensions\/\">Custom Dimensions user guide<\/a> or have a look in the FAQ.<\/p>\n\n<p><em>Warning<\/em>: Depending on the database size of your Piwik this plugin may take a long time to install.<\/p>\n\n",
+ "faq":"<p><strong>I have a large database, can I install the plugin on the command line?<\/strong><\/p>\n\n<p>Yes, this is not only possible but even recommended as the installation may take hours. To do this follow these steps:<\/p>\n\n<ul><li>Download the Plugin from <a href=\"https:\/\/plugins.piwik.org\/CustomDimensions\">https:\/\/plugins.piwik.org\/CustomDimensions<\/a><\/li>\n<li>Extract the files within the downloaded ZIP file<\/li>\n<li>Copy the <code>CustomDimensions<\/code> directory into the <code>plugins<\/code> directory of your Piwik<\/li>\n<li>Execute the command <code>.\/console plugin:activate CustomDimensions<\/code> within your Piwik directory<\/li>\n<\/ul><p><strong>Where can I manage Custom Dimensions?<\/strong><\/p>\n\n<p>Custom Dimensions can be managed by clicking on your username or user icon in the top right. There will be a menu\nitem \"Custom Dimensions\" within the \"Manage\" section of the left menu. By clicking on it you can manage Custom Dimensions.\nPlease note that the permission Admin is required in order to be able to manage them.<\/p>\n\n<p><strong>Where can I find the Id for a Custom Dimension?<\/strong><\/p>\n\n<p>You can find them by going to the \"Manage Custom Dimensions\" page in your personal area. For each dimension you will\nfind the Id in the table that lists all available Custom Dimensions.<\/p>\n\n<p><strong>How do I set a value for a dimension in the JavaScript Tracker?<\/strong><\/p>\n\n<p>Please have a look at the <a href=\"https:\/\/developer.piwik.org\/guides\/tracking-javascript-guide#custom-dimensions\">JavaScript Tracker guide for Custom Dimensions<\/a>.<\/p>\n\n<p><strong>How do I set a value for a dimension in the PHP Tracker?<\/strong><\/p>\n\n<p><code>$tracker-&gt;setCustomTrackingParameter('dimension' . $customDimensionId, $value);<\/code><\/p>\n\n<p>Please note custom tracking parameters are cleared after each tracking request. If you want to keep the same\nCustom Dimensions over all request make sure to call this method before each tracking call.<\/p>\n\n<p><strong>I have configured all available Custom Dimension slots, can I add more?<\/strong><\/p>\n\n<p>Yes, this is possible. To make a new Custom Dimension slot available execute the following command including the scope option:<\/p>\n\n<pre><code>.\/console customdimensions:add-custom-dimension --scope=action\n.\/console customdimensions:add-custom-dimension --scope=visit\n<\/code><\/pre>\n\n<p>Be aware that this can take a long time depending on the size of your database as it requires MySQL schema changes.\nYou can directly create multiple Custom Dimension slots. To do this add the option <code>--count=X<\/code>. Usually it doesn't take much\nlonger to create directly multiple new slots.<\/p>\n\n<p><strong>Is it possible to delete a Custom Dimension and all of its data?<\/strong><\/p>\n\n<p>In the UI it is only possible to deactivate a dimension. However, on the command line you can remove a Custom Dimension\nand report it's log data by executing the following console command:<\/p>\n\n<pre><code>.\/console customdimensions:remove-custom-dimension --scope=$scope --index=$index\n<\/code><\/pre>\n\n<p>Make sure to replace <code>$scope<\/code> and <code>$index<\/code> with the correct values. To get a list of all available indexes execute <code>.\/console customdimensions:info<\/code>.<\/p>\n\n<p>Removing a Custom Dimension may take a long time as it requires MySQL schema changes. Currently, only log data is removed. Archived reports will be\nnot deleted currently.<\/p>",
+ "documentation":"",
+ "changelog":"<ul><li>0.1.4 Fix a possible JavaScript error if Transitions plugin is disabled<\/li>\n<li>0.1.3 Fix UI of Custom Dimensions was not working properly when not using English as language<\/li>\n<li>0.1.2\n\n<ul><li>New feature: Mark an extraction as case sensitive<\/li>\n<li>New feature : Show actions that had no value defined<\/li>\n<li>New feature : Link to Page URLs in subtables<\/li>\n<\/ul><\/li>\n<li>0.1.1 Bugfixes<\/li>\n<li>0.1.0 Initial release<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/CustomDimensions\/download\/0.1.4"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"CustomOptOut",
+ "displayName":"Custom Opt Out",
+ "owner":"Zeichen32",
+ "description":"Create your own opt-out iframe css styles",
+ "homepage":"https:\/\/github.com\/Zeichen32\/PiwikCustomOptOut",
+ "createdDateTime":"2014-12-23 01:06:54",
+ "donate":{"paypal":"test2@example.com",
+ "bitcoin":null},"support":[],"isTheme":false,"keywords":["Opt-Out",
+ "CSS"],"basePrice":0,"authors":[{"name":"Jens Averkamp",
+ "email":"test6@example.com",
+ "homepage":"https:\/\/github.com\/Zeichen32"}],"repositoryUrl":"https:\/\/github.com\/Zeichen32\/PiwikCustomOptOut",
+ "lastUpdated":"2014-12-23 01:22:19",
+ "latestVersion":"0.1.4",
+ "numDownloads":0,"screenshots":["https:\/\/plugins.piwik.org\/CustomOptOut\/images\/CustomOptOut.png"],"previews":[],"activity":{"numCommits":"13",
+ "numContributors":"5",
+ "lastCommitDate":"1970-01-01 00:33:35"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.3",
+ "release":"2014-12-23 01:06:54",
+ "requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/Zeichen32\/PiwikCustomOptOut\/commits\/0.1.3",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomOptOut\/download\/0.1.3"},{"name":"0.1.4",
+ "release":"2014-12-23 01:22:19",
+ "requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"readme":"# Custom Opt-Out Styles Piwik Plugin\n\n## Description\n\nAdd a new admin tab, to change the opt-out CSS Styles for each website.\n\n## Requirements\n\n+ Piwik >= 2.0.0\n\n## Changelog\n\n#### CustomOptOut 0.1.4:\n* Issue #3 Code updated to support Piwik 2.1 and newer\n* Issue #2 Allow relative urls in css file field\n\n#### CustomOptOut 0.1.3:\n* Issue #1 Added a p-tag around the opt-out text for better markup and easier styling. (christianseel)\n\n#### CustomOptOut 0.1.2:\n* Fix wrong css escaping\n\n#### CustomOptOut 0.1.1:\n* Initial Version\n\n\n## Authors\n\n**Jens Averkamp**\n\n+ [https:\/\/github.com\/Zeichen32](https:\/\/github.com\/Zeichen32)\n\n## Support\n**Please direct any feedback to [https:\/\/github.com\/Zeichen32\/PiwikCustomOptOut](https:\/\/github.com\/Zeichen32\/PiwikCustomOptOut)**\n\n## Copyright and license\n\nReleased under the GPL v3 (or later) license, see [misc\/gpl-3.0.txt](misc\/gpl-3.0.txt)",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/Zeichen32\/PiwikCustomOptOut\/commits\/0.1.4",
+ "readmeHtml":{"description":"\n\n<p>Add a new admin tab, to change the opt-out CSS Styles for each website.<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<h4>CustomOptOut 0.1.4:<\/h4>\n\n<ul><li>Issue #3 Code updated to support Piwik 2.1 and newer<\/li>\n<li>Issue #2 Allow relative urls in css file field<\/li>\n<\/ul><h4>CustomOptOut 0.1.3:<\/h4>\n\n<ul><li>Issue #1 Added a p-tag around the opt-out text for better markup and easier styling. (christianseel)<\/li>\n<\/ul><h4>CustomOptOut 0.1.2:<\/h4>\n\n<ul><li>Fix wrong css escaping<\/li>\n<\/ul><h4>CustomOptOut 0.1.1:<\/h4>\n\n<ul><li>Initial Version<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/CustomOptOut\/download\/0.1.4"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"CustomTrackerJs",
+ "displayName":"Custom Tracker Js",
+ "owner":"PiwikPRO",
+ "description":"Lets Super Users and other plugins customise the piwik.js tracker file.",
+ "homepage":"https:\/\/github.com\/PiwikPRO\/plugin-CustomTrackerJs",
+ "createdDateTime":"2014-10-14 04:56:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/PiwikPRO\/plugin-CustomTrackerJs",
+ "lastUpdated":"2016-04-12 16:00:04",
+ "latestVersion":"1.1.2",
+ "numDownloads":4376,"screenshots":[],"previews":[],"activity":{"numCommits":"33",
+ "numContributors":"5",
+ "lastCommitDate":"2016-04-15 22:59:15"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0.2",
+ "release":"2014-10-14 04:56:03",
+ "requires":{"piwik":">=2.8.0-b4"},"readme":"",
+ "numDownloads":64,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-CustomTrackerJs\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomTrackerJs\/download\/1.0.2"},{"name":"1.0.3",
+ "release":"2014-10-15 21:58:03",
+ "requires":{"piwik":">=2.8.0-b4"},"readme":"",
+ "numDownloads":3554,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-CustomTrackerJs\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomTrackerJs\/download\/1.0.3"},{"name":"1.1.0",
+ "release":"2016-03-08 14:28:03",
+ "requires":{"piwik":">=2.8.0-b4"},"readme":"",
+ "numDownloads":315,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-CustomTrackerJs\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomTrackerJs\/download\/1.1.0"},{"name":"1.1.1",
+ "release":"2016-04-12 14:30:04",
+ "requires":{"piwik":">=2.8.0-b4"},"readme":"",
+ "numDownloads":1,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-CustomTrackerJs\/commits\/v0.0.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CustomTrackerJs\/download\/1.1.1"},{"name":"1.1.2",
+ "release":"2016-04-12 16:00:04",
+ "requires":{"piwik":">=2.8.0-b4"},"readme":"# Piwik CustomTrackerJs Plugin\n\n[![Build Status](https:\/\/travis-ci.org\/PiwikPRO\/plugin-CustomTrackerJs.svg?branch=master)](https:\/\/travis-ci.org\/PiwikPRO\/plugin-CustomTrackerJs)\n\n## Description\n\nThe `CustomTrackerJs` plugin lets the super users or other plugins add custom code to the Piwik Javascript Tracker.\n\n## Usage\n\n### Super users\n\nSuper users can set the Javascript code they want to add to the `piwik.js` file in `Settings > Plugin settings`.\n\nAny code added in the *Javascript code* textbox will be appended to `piwik.js` by a scheduled task. Be very careful\nas to write valid Javascript code since invalid code can break the tracker.\n\n### Other plugins\n\nPlugins can make use of the `CustomTrackerJs.getTrackerJsAdditions` event to register Javascript code to add to\nthe tracker.\n\nFor example this will add a `console.log(\"Hello world!\");` line to the tracker:\n\n```php\nclass MyPlugin extends \\Piwik\\Plugin\n{\n public function getListHooksRegistered()\n {\n return array(\n 'CustomTrackerJs.getTrackerJsAdditions' => 'getTrackerJsAdditions',\n );\n }\n\n public function getTrackerJsAdditions(&$code)\n {\n $code .= PHP_EOL . 'console.log(\"Hello world!\");';\n }\n}\n```\n\n## Changelog\n\n* 1.1.2\n - Marketplace release\n* 1.1.0\n - PPCDEV-2609 Compatibility with Piwik 2.16.0\n\n## Support\n\nPlease contact us at contact@piwik.pro in case you are facing a bug.\n\n\nPlugin created and maintained by [Piwik PRO](http:\/\/piwik.pro\/).\n",
+ "numDownloads":442,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-CustomTrackerJs\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>The <code>CustomTrackerJs<\/code> plugin lets the super users or other plugins add custom code to the Piwik Javascript Tracker.<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<ul><li>1.1.2\n\n<ul><li>Marketplace release<\/li>\n<\/ul><\/li>\n<li>1.1.0\n\n<ul><li>PPCDEV-2609 Compatibility with Piwik 2.16.0<\/li>\n<\/ul><\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/CustomTrackerJs\/download\/1.1.2"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"ExcludeByDDNS",
+ "displayName":"Exclude By DDNS",
+ "owner":"sgiehl",
+ "description":"This plugin allows you to dynamically exclude a IP using DDNS update",
+ "homepage":"http:\/\/github.com\/sgiehl\/piwik-plugin-ExcludeByDDNS",
+ "createdDateTime":"2015-02-08 22:10:03",
+ "donate":{"paypal":"stefangiehl@web.de",
+ "flattr":"https:\/\/flattr.com\/thing\/2578144\/sgiehl",
+ "bitcoin":null},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-ExcludeByDDNS",
+ "lastUpdated":"2016-05-08 22:10:03",
+ "latestVersion":"0.5.0",
+ "numDownloads":3069,"screenshots":[],"previews":[],"activity":{"numCommits":"44",
+ "numContributors":"1",
+ "lastCommitDate":"2016-05-08 22:29:45"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1",
+ "release":"2015-02-08 22:10:03",
+ "requires":{"piwik":">=2.0.4",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":149,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-ExcludeByDDNS\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ExcludeByDDNS\/download\/0.1"},{"name":"0.1.1",
+ "release":"2015-02-24 19:52:03",
+ "requires":{"piwik":">=2.0.4",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":68,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-ExcludeByDDNS\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ExcludeByDDNS\/download\/0.1.1"},{"name":"0.2.0",
+ "release":"2015-03-01 11:42:03",
+ "requires":{"piwik":">=2.4.0",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":227,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-ExcludeByDDNS\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ExcludeByDDNS\/download\/0.2.0"},{"name":"0.2.1",
+ "release":"2015-03-28 15:32:03",
+ "requires":{"piwik":">=2.4.0",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":397,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-ExcludeByDDNS\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ExcludeByDDNS\/download\/0.2.1"},{"name":"0.2.2",
+ "release":"2015-06-01 07:08:09",
+ "requires":{"piwik":">=2.4.0",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":341,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-ExcludeByDDNS\/commits\/1.0.8",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ExcludeByDDNS\/download\/0.2.2"},{"name":"0.3.0",
+ "release":"2015-07-28 20:36:02",
+ "requires":{"piwik":">=2.4.0",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":4,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-ExcludeByDDNS\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ExcludeByDDNS\/download\/0.3.0"},{"name":"0.3.1",
+ "release":"2015-07-28 22:30:03",
+ "requires":{"piwik":">=2.9.0",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":157,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-ExcludeByDDNS\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ExcludeByDDNS\/download\/0.3.1"},{"name":"0.4.0",
+ "release":"2015-08-15 18:44:03",
+ "requires":{"piwik":">=2.4.0",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":1529,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-ExcludeByDDNS\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ExcludeByDDNS\/download\/0.4.0"},{"name":"0.5.0",
+ "release":"2016-05-08 22:10:03",
+ "requires":{"piwik":">=2.4.0",
+ "php":">=5.3.3"},"readme":"# Piwik Exclude IP By DDNS\n\n[![Build Status](https:\/\/travis-ci.org\/sgiehl\/piwik-plugin-ExcludeByDDNS.png?branch=master)](https:\/\/travis-ci.org\/sgiehl\/piwik-plugin-ExcludeByDDNS)\n[![Flattr this git repo](http:\/\/api.flattr.com\/button\/flattr-badge-large.png)](https:\/\/flattr.com\/submit\/auto?user_id=sgiehl&url=https:\/\/github.com\/sgiehl\/piwik-plugin-ExcludeByDDNS&title=Piwik Plugin ExcludeByDDNS=&tags=github&category=software)\n\n## Description\n\nThis plugin allows the Piwik users to dynamically exclude their IP address using DDNS update.\n\n### Requirements\n\n[Piwik](https:\/\/github.com\/piwik\/piwik) 2.4.0 or higher is required.\n\n### Features\n\n- Exclude one IP for each Piwik user \n- Exclude and IP using an already updated hostname\n\n## FAQ\n\n__Which update method should i use, _DDNS Update_ or _DDNS Hostname_?__\n\nIf available, ___DDNS Update___ is recommended. This method is a bit more complicated to set up, but it leads to immediately updated IP's, as the client will trigger the update whenever a new IP is assigned.\nBut it may not be viable for all users, eg. \n* Not all DDNS clients allow custom update-URL's.\n* The client may be already serving another Server and have no ability to talk to multiple Servers at the same time.\n\nSo, the ___DDNS Hostname___ can be an alternative. Use a DDNS Service that is compatible with your client and enter the hostname from there to have the plugin resolve your dynamic IP. The downside: Updating happens via a sheduled task every hour, so there might be small windows with the new IP still being tracked, but not the old one.\n\n__What data do I need to set for DDNS Update__\n\nYou need to set a custom URL to be triggered for an update.\nYour personal update-URL is shown in your piwik installation (user-menu > Personal > DDNS Settings).\n\nThe URL has the following scheme:\n```\nhttp{s}:\/\/{piwik.url}\/index.php?module=ExcludeByDDNS&action=update&token_auth={token_auth}\n```\n\n- {s} Use HTTPS if available.\n- {piwik.url}: The URL to your piwik installation.\n- {token_auth}: Your API auth token (user-menu > Platform > API).\n\nThere is no need to set user, password or domain name.\n\n## Changelog\n\n- Version 0.4.0 - Compatibility for Piwik > 2.4.0\n- Version 0.3.0 - Various improvements and translations\n- Version 0.2.0 - Beta Release\n- Version 0.1.0 - Alpha Release\n\n## Support\n\nPlease direct any feedback to [stefan@piwik.org](mailto:stefan@piwik.org)\n\n## Contribute\n\nFeel free to create issues and pull requests.\n\n## License\n\nGPLv3 or later\n\n",
+ "numDownloads":197,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-ExcludeByDDNS\/commits\/1.1.0",
+ "readmeHtml":{"description":"\n\n<p>This plugin allows the Piwik users to dynamically exclude their IP address using DDNS update.<\/p>\n\n<h3>Requirements<\/h3>\n\n<p><a href=\"https:\/\/github.com\/piwik\/piwik\">Piwik<\/a> 2.4.0 or higher is required.<\/p>\n\n<h3>Features<\/h3>\n\n<ul><li>Exclude one IP for each Piwik user <\/li>\n<li>Exclude and IP using an already updated hostname<\/li>\n<\/ul>",
+ "faq":"<p><strong>Which update method should i use, <em>DDNS Update<\/em> or <em>DDNS Hostname<\/em>?<\/strong><\/p>\n\n<p>If available, <strong><em>DDNS Update<\/em><\/strong> is recommended. This method is a bit more complicated to set up, but it leads to immediately updated IP's, as the client will trigger the update whenever a new IP is assigned.\nBut it may not be viable for all users, eg. \n* Not all DDNS clients allow custom update-URL's.\n* The client may be already serving another Server and have no ability to talk to multiple Servers at the same time.<\/p>\n\n<p>So, the <strong><em>DDNS Hostname<\/em><\/strong> can be an alternative. Use a DDNS Service that is compatible with your client and enter the hostname from there to have the plugin resolve your dynamic IP. The downside: Updating happens via a sheduled task every hour, so there might be small windows with the new IP still being tracked, but not the old one.<\/p>\n\n<p><strong>What data do I need to set for DDNS Update<\/strong><\/p>\n\n<p>You need to set a custom URL to be triggered for an update.\nYour personal update-URL is shown in your piwik installation (user-menu &gt; Personal &gt; DDNS Settings).<\/p>\n\n<p>The URL has the following scheme:<\/p>\n\n<pre><code>http{s}:\/\/{piwik.url}\/index.php?module=ExcludeByDDNS&amp;action=update&amp;token_auth={token_auth}\n<\/code><\/pre>\n\n<ul><li>{s} Use HTTPS if available.<\/li>\n<li>{piwik.url}: The URL to your piwik installation.<\/li>\n<li>{token_auth}: Your API auth token (user-menu &gt; Platform &gt; API).<\/li>\n<\/ul><p>There is no need to set user, password or domain name.<\/p>",
+ "documentation":"",
+ "changelog":"<ul><li>Version 0.4.0 - Compatibility for Piwik &gt; 2.4.0<\/li>\n<li>Version 0.3.0 - Various improvements and translations<\/li>\n<li>Version 0.2.0 - Beta Release<\/li>\n<li>Version 0.1.0 - Alpha Release<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/ExcludeByDDNS\/download\/0.5.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"FeedAnnotation",
+ "displayName":"Feed Annotation",
+ "owner":"halfdan",
+ "description":"Plugin to automatically create Annotations by polling a feed (Atom\/RSS)",
+ "homepage":"http:\/\/github.com\/halfdan\/piwik-feedannotation-plugin",
+ "createdDateTime":"2014-12-23 00:39:30",
+ "donate":{},"support":[],"isTheme":false,"keywords":["rss",
+ "atom",
+ "annotation",
+ "feed"],"basePrice":0,"authors":[{"name":"Fabian Becker",
+ "email":"test8@example.com",
+ "homepage":"http:\/\/geekproject.eu"}],"repositoryUrl":"https:\/\/github.com\/halfdan\/piwik-feedannotation-plugin",
+ "lastUpdated":"2014-12-23 00:39:30",
+ "latestVersion":"0.9.5",
+ "numDownloads":0,"screenshots":[],"previews":[],"activity":{"numCommits":"26",
+ "numContributors":"1",
+ "lastCommitDate":"1970-01-01 00:33:33"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.9.5",
+ "release":"2014-12-23 00:39:30",
+ "requires":{"piwik":">=2.0.0"},"readme":"# Piwik FeedAnnotation Plugin\n\n## Description \n\nThis is a plugin for the Open Source Web Analytics platform Piwik. It allows you to automatically fetch RSS\/Atom-Feeds from your website and create new Annotations from feed entries. \n\nSince the \"Annotations\" plugin was introduced in Piwik 1.10, this plugin requires at least Piwik 1.10.\n\n![Annotations in Piwik](http:\/\/cdn.geekmonkey.org\/assets\/files\/000\/000\/038\/screen\/annotations-view.png)\n\n## Documentation\n\n1. Clone the plugin into the plugins directory of your Piwik installation.\n\n ```\n cd plugins\/\n git clone https:\/\/github.com\/halfdan\/piwik-feedannotation-plugin.git FeedAnnotation\n ```\n\n2. Login as superuser into your Piwik installation and activate the plugin under Settings -> Plugins\n\n3. You can add new feeds for sites you have admin access to under Settings -> Feed Annotations\n\nFeeds are fetched once a day using \"scheduled tasks\". After adding a feed you can manually force the plugin to process your feed by clicking the \"Process now\" link.\n\n## Contribute \n\nIf you are interested in contributing to this plugin, feel free to send pull requests!\n",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/halfdan\/piwik-feedannotation-plugin\/commits\/v0.9.5",
+ "readmeHtml":{"description":"\n\n<p>This is a plugin for the Open Source Web Analytics platform Piwik. It allows you to automatically fetch RSS\/Atom-Feeds from your website and create new Annotations from feed entries.<\/p>\n\n<p>Since the \"Annotations\" plugin was introduced in Piwik 1.10, this plugin requires at least Piwik 1.10.<\/p>\n\n<p><img src=\"http:\/\/cdn.geekmonkey.org\/assets\/files\/000\/000\/038\/screen\/annotations-view.png\" alt=\"annotations-view.png\" \/><\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/FeedAnnotation\/download\/0.9.5"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"FlagCounter",
+ "displayName":"Flag Counter",
+ "owner":"sgiehl",
+ "description":"This plugin allows you to include a simple statistic in your website showing the flags and hits of the countries your visitors came from",
+ "homepage":"http:\/\/github.com\/sgiehl\/piwik-plugin-FlagCounter",
+ "createdDateTime":"2015-01-05 23:22:04",
+ "donate":{"paypal":"stefangiehl@web.de",
+ "flattr":"https:\/\/flattr.com\/thing\/2578144\/sgiehl",
+ "bitcoin":null},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-FlagCounter",
+ "lastUpdated":"2015-08-15 18:54:04",
+ "latestVersion":"0.4.0",
+ "numDownloads":4519,"screenshots":[],"previews":[],"activity":{"numCommits":"29",
+ "numContributors":"1",
+ "lastCommitDate":"2016-01-19 20:18:45"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1",
+ "release":"2015-01-05 23:22:04",
+ "requires":{"piwik":">=2.0.4",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":351,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-FlagCounter\/commits\/1.0.6",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/FlagCounter\/download\/0.1"},{"name":"0.2",
+ "release":"2015-02-08 12:22:04",
+ "requires":{"piwik":">=2.0.4",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":523,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-FlagCounter\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/FlagCounter\/download\/0.2"},{"name":"0.3",
+ "release":"2015-04-06 11:44:03",
+ "requires":{"piwik":">=2.0.4",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":155,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-FlagCounter\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/FlagCounter\/download\/0.3"},{"name":"0.3.1",
+ "release":"2015-04-15 20:06:03",
+ "requires":{"piwik":">=2.0.4",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":487,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-FlagCounter\/commits\/v0.1.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/FlagCounter\/download\/0.3.1"},{"name":"0.3.2",
+ "release":"2015-06-01 07:10:04",
+ "requires":{"piwik":">=2.0.4",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":681,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-FlagCounter\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/FlagCounter\/download\/0.3.2"},{"name":"0.4.0",
+ "release":"2015-08-15 18:54:04",
+ "requires":{"piwik":">=2.4.0",
+ "php":">=5.3.0"},"readme":"# Piwik Flag Counter Plugin\n\n[![Flattr this git repo](http:\/\/api.flattr.com\/button\/flattr-badge-large.png)](https:\/\/flattr.com\/submit\/auto?user_id=sgiehl&url=https:\/\/github.com\/sgiehl\/piwik-plugin-FlagCounter&title=Piwik Plugin FlagCounter=&tags=github&category=software) \n\n\n## Description\n\nThis plugin allows you to include a small image or iframe in you website displaying the flags and total hits of the countries your website visitors came from\n\nPlease keep in mind that everyone will be able to see that kind of statistic as this plugin does not consider the access rights.\n\n### Requirements\n\n[Piwik](https:\/\/github.com\/piwik\/piwik) 2.0.4 or higher is required.\nGD library including ttf support\n\n### Features\n\n- Configurable with following parameters:\n - idSite: Website to display\n - period: period to display\n - date: date to display\n - cols: count of columns to display (default 2)\n - rows: count of rows to display (default 5)\n - showflag: show flags (default 1)\n - showcountryode: show country codes (default 0)\n - font: font family to use\n - fontsize: font size to use (between 2 and 30; default 12)\n - fontcolor: font color to use (rgb value; default 0,0,0)\n- Generates a transparent PNG image showing the flag icons and their total hits\n- Generates simple HTML to be included as iframe\n\n## FAQ\n\n__The image is not displayed. What can I do?__\n\nMaybe you have some kind of access restriction for your Piwik like http auth. The Url needs to be public accessible, or at least accessible to those, who should be able to see the counter.\n\n__How can I display the counter for all data in the past__\n\nTo do that, set period to ```range``` and date to something like ```1992-01-01,today```.\n\nThe full URL for the image would then look like:\n```\nhttp:\/\/piwik.url\/index.php?module=FlagCounter&action=image&idSite=1&period=range&date=1992-01-01,today&cols=2&rows=6\n``` \n\n__Can I use a custom font?__\n\nCurrently all ttf fonts located in the ```fonts``` directory within this plugin are available for usage. If you place a new font there, you should be able to use it.\n\n\n## Changelog\n\n- Version 0.1 - Alpha Release\n- Version 0.2 \n - improved image generation (automatic spacing between items)\n - possibility to show country codes besides or instead of the flags\n- Version 0.3\n - added ability to choose a font family, size & color\n\n## Support\n\nPlease direct any feedback to [stefan@piwik.org](mailto:stefan@piwik.org)\n\n## Contribute\n\nFeel free to create issues and pull requests.\n\n## License\n\nGPLv3 or later\n\nNote: The fonts bundled with this plugin are under the following licenses\n- OpenSans: Apache license\n- Roboto: Apache license\n- Raleway: SIL Open Font License\n- Cantarell: SIL Open Font License\n\n",
+ "numDownloads":2322,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-FlagCounter\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>This plugin allows you to include a small image or iframe in you website displaying the flags and total hits of the countries your website visitors came from<\/p>\n\n<p>Please keep in mind that everyone will be able to see that kind of statistic as this plugin does not consider the access rights.<\/p>\n\n<h3>Requirements<\/h3>\n\n<p><a href=\"https:\/\/github.com\/piwik\/piwik\">Piwik<\/a> 2.0.4 or higher is required.\nGD library including ttf support<\/p>\n\n<h3>Features<\/h3>\n\n<ul><li>Configurable with following parameters:\n\n<ul><li>idSite: Website to display<\/li>\n<li>period: period to display<\/li>\n<li>date: date to display<\/li>\n<li>cols: count of columns to display (default 2)<\/li>\n<li>rows: count of rows to display (default 5)<\/li>\n<li>showflag: show flags (default 1)<\/li>\n<li>showcountryode: show country codes (default 0)<\/li>\n<li>font: font family to use<\/li>\n<li>fontsize: font size to use (between 2 and 30; default 12)<\/li>\n<li>fontcolor: font color to use (rgb value; default 0,0,0)<\/li>\n<\/ul><\/li>\n<li>Generates a transparent PNG image showing the flag icons and their total hits<\/li>\n<li>Generates simple HTML to be included as iframe<\/li>\n<\/ul>",
+ "faq":"<p><strong>The image is not displayed. What can I do?<\/strong><\/p>\n\n<p>Maybe you have some kind of access restriction for your Piwik like http auth. The Url needs to be public accessible, or at least accessible to those, who should be able to see the counter.<\/p>\n\n<p><strong>How can I display the counter for all data in the past<\/strong><\/p>\n\n<p>To do that, set period to <code>range<\/code> and date to something like <code>1992-01-01,today<\/code>.<\/p>\n\n<p>The full URL for the image would then look like:<\/p>\n\n<pre><code>http:\/\/piwik.url\/index.php?module=FlagCounter&amp;action=image&amp;idSite=1&amp;period=range&amp;date=1992-01-01,today&amp;cols=2&amp;rows=6\n<\/code><\/pre>\n\n<p><strong>Can I use a custom font?<\/strong><\/p>\n\n<p>Currently all ttf fonts located in the <code>fonts<\/code> directory within this plugin are available for usage. If you place a new font there, you should be able to use it.<\/p>",
+ "documentation":"",
+ "changelog":"<ul><li>Version 0.1 - Alpha Release<\/li>\n<li>Version 0.2 \n\n<ul><li>improved image generation (automatic spacing between items)<\/li>\n<li>possibility to show country codes besides or instead of the flags<\/li>\n<\/ul><\/li>\n<li>Version 0.3\n\n<ul><li>added ability to choose a font family, size &amp; color<\/li>\n<\/ul><\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/FlagCounter\/download\/0.4.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"FreeMobileMessaging",
+ "displayName":"Free Mobile Messaging",
+ "owner":"apapillon",
+ "description":"Mobile Messaging Plugin: Free Mobile support",
+ "homepage":"https:\/\/github.com\/apapillon\/piwik-freemobilesmsprovider-plugin",
+ "createdDateTime":"2015-11-29 23:10:04",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/apapillon\/piwik-freemobilesmsprovider-plugin",
+ "lastUpdated":"2015-11-29 23:10:04",
+ "latestVersion":"1.0.0",
+ "numDownloads":1212,"screenshots":[],"previews":[],"activity":{"numCommits":"1",
+ "numContributors":"1",
+ "lastCommitDate":"2015-11-29 23:09:07"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0.0",
+ "release":"2015-11-29 23:10:04",
+ "requires":{"piwik":">=2.15.1-b2"},"readme":"# Piwik FreeMobileSMSProvider Plugin \n\n## Description\n\nThis is a plugin for the Open Source Web Analytics platform [Piwik][1]. It \nadd Free Mobile SMS provider support to MobileMessaging plugin.\n\n## Installation\n\nInstall the prerequiste MobileMessagin plugin via MarketPlace.\n\nSee http:\/\/piwik.org\/faq\/plugins\/#faq_21\n\n## FAQ\n\n\n## Changelog\n\n__1.0.0__\n* Initial release\n\n## License\n\nGPL v3\n\n## Support\n\nPlease direct any feedback to: \n\n* [https:\/\/github.com\/apapillon\/piwik-freemobilesmsprovider-plugin\/issues][2]\n\n[1]: http:\/\/piwik.org\n[2]: https:\/\/github.com\/apapillon\/piwik-freemobilesmsprovider-plugin\/issues\n",
+ "numDownloads":1212,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/apapillon\/piwik-freemobilesmsprovider-plugin\/commits\/1.0",
+ "readmeHtml":{"description":"\n\n<p>This is a plugin for the Open Source Web Analytics platform <a href=\"http:\/\/piwik.org\">Piwik<\/a>. It \nadd Free Mobile SMS provider support to MobileMessaging plugin.<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<p><strong>1.0.0<\/strong>\n* Initial release<\/p>"},"download":"\/api\/2.0\/plugins\/FreeMobileMessaging\/download\/1.0.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"GoogleAuthenticator",
+ "displayName":"Google Authenticator",
+ "owner":"sgiehl",
+ "description":"Adds Google Authenticator Two Factor Auth to Piwik",
+ "homepage":"https:\/\/github.com\/sgiehl\/piwik-plugin-GoogleAuthenticator",
+ "createdDateTime":"2015-07-21 20:46:05",
+ "donate":{"paypal":"stefangiehl@web.de",
+ "flattr":"https:\/\/flattr.com\/thing\/4453253\/sgiehlpiwik-plugin-GoogleAuthenticator-on-GitHub",
+ "bitcoin":null},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-GoogleAuthenticator",
+ "lastUpdated":"2015-08-15 15:26:03",
+ "latestVersion":"0.1.0",
+ "numDownloads":2267,"screenshots":[],"previews":[],"activity":{"numCommits":"42",
+ "numContributors":"1",
+ "lastCommitDate":"2016-04-30 14:40:55"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.0.1",
+ "release":"2015-07-21 20:46:05",
+ "requires":{"piwik":">=2.14.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":47,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-GoogleAuthenticator\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/GoogleAuthenticator\/download\/0.0.1"},{"name":"0.0.2",
+ "release":"2015-07-26 10:56:04",
+ "requires":{"piwik":">=2.14.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":26,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-GoogleAuthenticator\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/GoogleAuthenticator\/download\/0.0.2"},{"name":"0.0.3",
+ "release":"2015-07-27 20:44:03",
+ "requires":{"piwik":">=2.14.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":19,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-GoogleAuthenticator\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/GoogleAuthenticator\/download\/0.0.3"},{"name":"0.0.4",
+ "release":"2015-07-28 13:18:03",
+ "requires":{"piwik":">=2.14.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":167,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-GoogleAuthenticator\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/GoogleAuthenticator\/download\/0.0.4"},{"name":"0.1.0",
+ "release":"2015-08-15 15:26:03",
+ "requires":{"piwik":">=2.14.0",
+ "php":">=5.4.0"},"readme":"# Piwik Google Authenticator Plugin\n\n[![Build Status](https:\/\/travis-ci.org\/sgiehl\/piwik-plugin-GoogleAuthenticator.png?branch=master)](https:\/\/travis-ci.org\/sgiehl\/piwik-plugin-GoogleAuthenticator)\n[![Flattr this git repo](http:\/\/api.flattr.com\/button\/flattr-badge-large.png)](https:\/\/flattr.com\/submit\/auto?user_id=sgiehl&url=https:\/\/github.com\/sgiehl\/piwik-plugin-GoogleAuthenticator&title=Piwik Plugin GoogleAuthenticator=&tags=github&category=software) \n\n\n## Description\n\nAdds a userbased possibility to use Google Authenticator 2FA as additional login security.\nEach use can enable\/disable this feature in his account settings.\n\nThis Plugin is based on the original Piwik Login plugin and needs this one to be installed but not active.\n\nATTENTION: Activating Google Authenticator for an account, also requires an auth code for direct API requests with the users token auth. Use ```&auth_code={authcode}``` to do that.\n\n### Requirements\n\n[Piwik](https:\/\/github.com\/piwik\/piwik) 2.14.0 or higher is required.\n\n### Features\n\n- Userbased activation of Google Authenticator 2FA\n\n## Changelog\n\n- 0.1.0 Added possibility to define title and description for Google Authenticator app\n- 0.0.4 fixes password reset link\n- 0.0.3 small improvements\n- 0.0.2 Added first translations\n- 0.0.1 Initial release\n\n## Support\n\nPlease direct any feedback to [stefan@piwik.org](mailto:stefan@piwik.org)\n\n## Contribute\n\nFeel free to create issues and pull requests.\n\n## License\n\nGPLv3 or later\n\nThe used library [PHPGangsta\/GoogleAuthenticator](https:\/\/github.com\/PHPGangsta\/GoogleAuthenticator) is licensed under BSD\n\n",
+ "numDownloads":2008,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-GoogleAuthenticator\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>Adds a userbased possibility to use Google Authenticator 2FA as additional login security.\nEach use can enable\/disable this feature in his account settings.<\/p>\n\n<p>This Plugin is based on the original Piwik Login plugin and needs this one to be installed but not active.<\/p>\n\n<p>ATTENTION: Activating Google Authenticator for an account, also requires an auth code for direct API requests with the users token auth. Use <code>&amp;auth_code={authcode}<\/code> to do that.<\/p>\n\n<h3>Requirements<\/h3>\n\n<p><a href=\"https:\/\/github.com\/piwik\/piwik\">Piwik<\/a> 2.14.0 or higher is required.<\/p>\n\n<h3>Features<\/h3>\n\n<ul><li>Userbased activation of Google Authenticator 2FA<\/li>\n<\/ul>",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<ul><li>0.1.0 Added possibility to define title and description for Google Authenticator app<\/li>\n<li>0.0.4 fixes password reset link<\/li>\n<li>0.0.3 small improvements<\/li>\n<li>0.0.2 Added first translations<\/li>\n<li>0.0.1 Initial release<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/GoogleAuthenticator\/download\/0.1.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"GrabGravatar",
+ "displayName":"Grab Gravatar",
+ "owner":"alnoorp",
+ "description":"Adds a profile photo from Gravatar based on the email address stored in the User Id field.",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2015-03-18 04:20:04",
+ "donate":{"paypal":"alnoorp@gmail.com",
+ "bitcoin":null},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/alnoorp\/GrabGravatar",
+ "lastUpdated":"2015-07-24 14:26:11",
+ "latestVersion":"0.2.0",
+ "numDownloads":2018,"screenshots":[],"previews":[],"activity":{"numCommits":"15",
+ "numContributors":"1",
+ "lastCommitDate":"2015-03-22 04:22:05"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2015-03-18 04:20:04",
+ "requires":{"piwik":">=2.11.2"},"readme":"",
+ "numDownloads":6,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/alnoorp\/GrabGravatar\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/GrabGravatar\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2015-07-24 14:26:04",
+ "requires":{"piwik":">=2.11.2"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/alnoorp\/GrabGravatar\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/GrabGravatar\/download\/0.1.1"},{"name":"0.1.11",
+ "release":"2015-07-24 14:26:07",
+ "requires":{"piwik":">=2.11.2"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/alnoorp\/GrabGravatar\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/GrabGravatar\/download\/0.1.11"},{"name":"0.2.0",
+ "release":"2015-07-24 14:26:11",
+ "requires":{"piwik":">=2.11.2"},"readme":"# Piwik GrabGravatar Plugin\n\n## Description\n\nA Piwik plugin that adds a profile photo from Gravatar based on the email address stored in the User Id field.\n\n## Installation\n\nInstall it via Piwik Marketplace\n\n## FAQ\n\n__What information do I need to make this plugin work?__\nMake sure you are capturing the email address for your visitors in the User Id field.\n\n__Why do some of my visitors just display the Gravatar logo instead of a photo?__\nEither there is no email address associated with the visitor or they do not have a Gravatar set up at gravatar.com\n\n## Changelog\n\n* 0.1.11 Fixed screenshot filename\n* 0.1.1 Add screenshots\n* 0.1.0 First beta\n\n## License\n\nGPL v3 or later\n\n## Support\n\nPlease direct any feedback to <alnoorp@gmail.com>\n",
+ "numDownloads":2012,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/alnoorp\/GrabGravatar\/commits\/v0.1.2",
+ "readmeHtml":{"description":"\n\n<p>A Piwik plugin that adds a profile photo from Gravatar based on the email address stored in the User Id field.<\/p>\n\n",
+ "faq":"<p><strong>What information do I need to make this plugin work?<\/strong>\nMake sure you are capturing the email address for your visitors in the User Id field.<\/p>\n\n<p><strong>Why do some of my visitors just display the Gravatar logo instead of a photo?<\/strong>\nEither there is no email address associated with the visitor or they do not have a Gravatar set up at gravatar.com<\/p>",
+ "documentation":"",
+ "changelog":"<ul><li>0.1.11 Fixed screenshot filename<\/li>\n<li>0.1.1 Add screenshots<\/li>\n<li>0.1.0 First beta<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/GrabGravatar\/download\/0.2.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"InterSites",
+ "displayName":"Inter Sites",
+ "owner":"PiwikPRO",
+ "description":"Analyze how many visitors navigate between your websites.",
+ "homepage":"http:\/\/piwik.pro",
+ "createdDateTime":"2014-09-14 04:10:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/PiwikPRO\/plugin-InterSites",
+ "lastUpdated":"2016-03-08 14:16:03",
+ "latestVersion":"0.4.0",
+ "numDownloads":7766,"screenshots":[],"previews":[],"activity":{"numCommits":"69",
+ "numContributors":"6",
+ "lastCommitDate":"2016-04-15 22:58:13"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2014-09-14 04:10:03",
+ "requires":{"piwik":">=2.7.0-b3"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-InterSites\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/InterSites\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2014-09-14 04:40:03",
+ "requires":{"piwik":">=2.7.0-b3"},"readme":"",
+ "numDownloads":1138,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-InterSites\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/InterSites\/download\/0.1.1"},{"name":"0.1.2",
+ "release":"2014-12-08 02:56:04",
+ "requires":{"piwik":">=2.10.0-b6"},"readme":"",
+ "numDownloads":951,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-InterSites\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/InterSites\/download\/0.1.2"},{"name":"0.2.0",
+ "release":"2015-03-04 13:02:04",
+ "requires":{"piwik":">=2.10.0"},"readme":"",
+ "numDownloads":126,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-InterSites\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/InterSites\/download\/0.2.0"},{"name":"0.2.1",
+ "release":"2015-03-05 08:36:03",
+ "requires":{"piwik":">=2.10.0"},"readme":"",
+ "numDownloads":1472,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-InterSites\/commits\/v0.1.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/InterSites\/download\/0.2.1"},{"name":"0.2.3",
+ "release":"2015-07-29 11:10:03",
+ "requires":{"piwik":">=2.11.0"},"readme":"",
+ "numDownloads":1043,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-InterSites\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/InterSites\/download\/0.2.3"},{"name":"0.3.0",
+ "release":"2015-10-21 23:18:03",
+ "requires":{"piwik":">=2.11.0"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-InterSites\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/InterSites\/download\/0.3.0"},{"name":"0.3.1",
+ "release":"2015-10-21 23:24:03",
+ "requires":{"piwik":">=2.15.0-rc4"},"readme":"",
+ "numDownloads":372,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-InterSites\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/InterSites\/download\/0.3.1"},{"name":"0.3.2",
+ "release":"2015-11-11 20:10:03",
+ "requires":{"piwik":">=2.15.0"},"readme":"",
+ "numDownloads":1597,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-InterSites\/commits\/v1.0",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/InterSites\/download\/0.3.2"},{"name":"0.4.0",
+ "release":"2016-03-08 14:16:03",
+ "requires":{"piwik":">=2.16.0"},"readme":"InterSites\n==========\n\nMaster: [![Build Status](https:\/\/travis-ci.org\/PiwikPRO\/plugin-InterSites.svg?branch=master)](https:\/\/travis-ci.org\/PiwikPRO\/plugin-InterSites)\nDevelop: [![Build Status](https:\/\/travis-ci.org\/PiwikPRO\/plugin-InterSites.svg?branch=develop)](https:\/\/travis-ci.org\/PiwikPRO\/plugin-InterSites)\n\n**Analyze how visitors navigate between your websites.**\n\n## Description\n\nMeasures number of visits and unique visitors that have navigated across two or three specific websites.\n\n_(Unique user is determined across websites using the fingerprint config\\_id stored in the DB.)_\n\n## Features\n\n### See how many visitors visited more than one of your websites.\n\nTo access this functionality, navigate to the All Websites dashboard. Here there will be a new link that says _Number of visitors who viewed several websites_. Click this link and a dialog will display that will allow you to get visitor metrics for many sites.\n\nComparison of visits is done in dates for the UTC time, not for the site specific time.\n\n\n## Changelog\n * 0.4.0\n - PPCDEV-2609 Compatibility with Piwik 2.16.0\n * 0.3.2\n - Changed minimum required Piwik to 2.15.0\n * 0.3.1\n - Bumping minimum required Piwik.\n * 0.3.0\n - Compatibility w\/ Piwik 2.15.\n * 0.2.2:\n - Fixed bug with day period\n - Added tests for day and month period\n - Fixed Site names display, as they were displayed as HTML entities\n * 0.2.1: Market release\n * 0.2.0: Introduced composer.json file\n * 0.1.2: Compatibility with Piwik 2.10.0 (Notification will explain that config setting `enable_fingerprinting_across_websites=1` is required)\n * 0.1.0: Initial release\n",
+ "numDownloads":1067,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-InterSites\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>Measures number of visits and unique visitors that have navigated across two or three specific websites.<\/p>\n\n<p><em>(Unique user is determined across websites using the fingerprint config_id stored in the DB.)<\/em><\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<ul><li>0.4.0\n\n<ul><li>PPCDEV-2609 Compatibility with Piwik 2.16.0<\/li>\n<\/ul><\/li>\n<li>0.3.2\n\n<ul><li>Changed minimum required Piwik to 2.15.0<\/li>\n<\/ul><\/li>\n<li>0.3.1\n\n<ul><li>Bumping minimum required Piwik.<\/li>\n<\/ul><\/li>\n<li>0.3.0\n\n<ul><li>Compatibility w\/ Piwik 2.15.<\/li>\n<\/ul><\/li>\n<li>0.2.2:\n\n<ul><li>Fixed bug with day period<\/li>\n<li>Added tests for day and month period<\/li>\n<li>Fixed Site names display, as they were displayed as HTML entities<\/li>\n<\/ul><\/li>\n<li>0.2.1: Market release<\/li>\n<li>0.2.0: Introduced composer.json file<\/li>\n<li>0.1.2: Compatibility with Piwik 2.10.0 (Notification will explain that config setting <code>enable_fingerprinting_across_websites=1<\/code> is required)<\/li>\n<li>0.1.0: Initial release<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/InterSites\/download\/0.4.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"IntranetGeoIP",
+ "displayName":"Intranet Geo IP",
+ "owner":"ThaDafinser",
+ "description":"Piwik plugin to locate all locale data of a user based on the IP address\/subnetwork (country, region, city, latitude, longitude, provider, ...)",
+ "homepage":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP",
+ "createdDateTime":"2014-06-12 08:04:04",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP",
+ "lastUpdated":"2015-07-13 10:54:03",
+ "latestVersion":"2.2.0",
+ "numDownloads":9934,"screenshots":[],"previews":[],"activity":{"numCommits":"67",
+ "numContributors":"1",
+ "lastCommitDate":"2015-10-15 12:35:16"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0",
+ "release":"2014-06-12 08:04:04",
+ "requires":{"piwik":">=2.3.0"},"readme":"",
+ "numDownloads":6,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/1.0"},{"name":"1.0.1",
+ "release":"2014-06-12 08:16:10",
+ "requires":{"piwik":">=2.3.0"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/1.0.1"},{"name":"1.0.2",
+ "release":"2014-06-12 08:20:10",
+ "requires":{"piwik":">=2.3.0"},"readme":"",
+ "numDownloads":1,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/1.0.2"},{"name":"1.0.4",
+ "release":"2014-06-12 08:44:03",
+ "requires":{"piwik":">=2.3.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":3,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/1.0.4"},{"name":"1.0.5",
+ "release":"2014-06-12 09:06:03",
+ "requires":{"piwik":">=2.3.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":2,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/1.0.5"},{"name":"1.0.6",
+ "release":"2014-06-12 11:08:04",
+ "requires":{"piwik":">=2.3.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":1,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/1.0.6"},{"name":"1.0.7",
+ "release":"2014-06-12 11:12:04",
+ "requires":{"piwik":">=2.3.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":24,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/1.0.7"},{"name":"1.0.8",
+ "release":"2014-06-13 09:22:03",
+ "requires":{"piwik":">=2.3.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":208,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/1.0.8"},{"name":"1.0.9",
+ "release":"2014-07-02 12:44:04",
+ "requires":{"piwik":">=2.3.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":201,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/1.0.9"},{"name":"1.1.0",
+ "release":"2014-07-15 15:34:05",
+ "requires":{"piwik":">=2.3.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":63,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/1.1.0"},{"name":"1.2.0",
+ "release":"2014-07-17 11:34:04",
+ "requires":{"piwik":">=2.3.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":166,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/1.2.0"},{"name":"2.0.0",
+ "release":"2014-07-28 07:04:08",
+ "requires":{"piwik":">=2.3.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":420,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/2.0.0"},{"name":"2.0.1",
+ "release":"2014-08-29 08:12:04",
+ "requires":{"piwik":">=2.3.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":1875,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/2.0.1"},{"name":"2.1.0",
+ "release":"2015-01-28 09:40:04",
+ "requires":{"piwik":">=2.9.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":504,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/2.1.0"},{"name":"2.1.3",
+ "release":"2015-02-25 07:50:04",
+ "requires":{"piwik":">=2.9.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":1997,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/2.1.3"},{"name":"2.2.0",
+ "release":"2015-07-13 10:54:03",
+ "requires":{"piwik":">=2.9.0",
+ "php":">=5.4.0"},"readme":"# Piwik IntranetGeoIp Plugin\n\n## Description\n\nPiwik plugin to locate all locale data of a user based on the IP address\/subnetwork (country, region, city, latitude, longitude, provider, ...)\n\n***Please use it only for INTRANET tracking*** everything else just dont make sense :-)\n\n## FAQ\n\n__What does this plugin do?__\n\nIt adds visitor information based on the matched IP address from your configuration. Not more and not less.\nThe database schema and UI stays untouched, so all Piwik statistics can be used like you would use a internet GeoIP database.\n\n\n__How to configure\/install this plugin \/ the networks?__\n\nAfter installation and activation of the plugin, open the file `piwik\/config\/IntranetGeoIP.data.php`\n\nYou can their add your location information and their subnetworks.\n\nSee the file `piwik\/config\/IntranetGeoIP.data.php` or see the readme on github https:\/\/github.com\/ThaDafinser\/IntranetGeoIp\n\n\n__What statistics are available?__\n\nIf you create a full configuration data file, you'll see\n* Visitor -> Realtime visitor map\n* Visitor -> Location and provider\n* and many more...(in generall all statistics are available like using a internet GeoIP database)\n\n\n__Why there stands provider \"unknown\" in my visitor log?__\n\nIf your installation is stock, all visitors will get this \"flag\" to show you, what IPs are not matched.\nYou can adjust or remove this, by changing the \"noMatch\" block in your `IntranetGeoIP.data.php` file.\nIf you remove the complete block, none matched visitors will be skipped by this plugin.\nBut you can also fill all possible visitorInfos like you are used for matched IP addresses.\n\n\n__Can i use this plugin with a internet GeoIP database side by side?__\n\nYes you can.\nJust remove or comment out the `noMatch` block in your configuration file.\n\n__Note about the configuration?__\n\nInside the array key `visitorInfo` you can freely add\/remove all available columns from the `log_visit` table you want.\nThe keys below are just a suggestion, since they are the only one which make sense currently IMO.\nAll available fields, see here: http:\/\/developer.piwik.org\/guides\/persistence-and-the-mysql-backend#visits\n\nInside they key `networks` add all subnetworks which apply to this location.\n\n```php\nreturn [\n \/*\n * If the IP was not matched, apply these data to visitorInfo\n * You can also apply here all possible visitorInformation data if you want\n *\/\n 'noMatch' => [\n 'visitorInfo' => [\n 'location_provider' => 'unknown'\n ]\n ],\n \n [\n 'visitorInfo' => [\n \/\/ISO-3166 alpha-2 code http:\/\/en.wikipedia.org\/wiki\/ISO_3166-1\n 'location_country' => 'at',\n \n \/\/the region code (i take them from piwik\/libs\/MaxMindGeoIp\/geoipregionvars.php\n 'location_region' => '08',\n \n \/\/should be freetext\n 'location_city' => 'Muntlix',\n \n \/\/get this from a picker, e.g. http:\/\/www.tytai.com\/gmap\/\n 'location_latitude' => '47.282024',\n 'location_longitude' => '9.662304',\n \n \/\/enter your company name or do it based on your domain hierarchy\n 'location_provider' => 'myCompany'\n ],\n 'networks' => [\n \/\/enter here all subnetworks for this location\n \/\/use a subnetwork calculator, e.g. http:\/\/jodies.de\/ipcalc\n '10.59.0.0\/19',\n '170.56.251.200\/29'\n ]\n ],\n \n \/\/add more blocks live above\n];\n```\n",
+ "numDownloads":4463,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-IntranetGeoIP\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>Piwik plugin to locate all locale data of a user based on the IP address\/subnetwork (country, region, city, latitude, longitude, provider, ...)<\/p>\n\n<p><strong><em>Please use it only for INTRANET tracking<\/em><\/strong> everything else just dont make sense :-)<\/p>\n\n",
+ "faq":"<p><strong>What does this plugin do?<\/strong><\/p>\n\n<p>It adds visitor information based on the matched IP address from your configuration. Not more and not less.\nThe database schema and UI stays untouched, so all Piwik statistics can be used like you would use a internet GeoIP database.<\/p>\n\n<p><strong>How to configure\/install this plugin \/ the networks?<\/strong><\/p>\n\n<p>After installation and activation of the plugin, open the file <code>piwik\/config\/IntranetGeoIP.data.php<\/code><\/p>\n\n<p>You can their add your location information and their subnetworks.<\/p>\n\n<p>See the file <code>piwik\/config\/IntranetGeoIP.data.php<\/code> or see the readme on github https:\/\/github.com\/ThaDafinser\/IntranetGeoIp<\/p>\n\n<p><strong>What statistics are available?<\/strong><\/p>\n\n<p>If you create a full configuration data file, you'll see\n* Visitor -&gt; Realtime visitor map\n* Visitor -&gt; Location and provider\n* and many more...(in generall all statistics are available like using a internet GeoIP database)<\/p>\n\n<p><strong>Why there stands provider \"unknown\" in my visitor log?<\/strong><\/p>\n\n<p>If your installation is stock, all visitors will get this \"flag\" to show you, what IPs are not matched.\nYou can adjust or remove this, by changing the \"noMatch\" block in your <code>IntranetGeoIP.data.php<\/code> file.\nIf you remove the complete block, none matched visitors will be skipped by this plugin.\nBut you can also fill all possible visitorInfos like you are used for matched IP addresses.<\/p>\n\n<p><strong>Can i use this plugin with a internet GeoIP database side by side?<\/strong><\/p>\n\n<p>Yes you can.\nJust remove or comment out the <code>noMatch<\/code> block in your configuration file.<\/p>\n\n<p><strong>Note about the configuration?<\/strong><\/p>\n\n<p>Inside the array key <code>visitorInfo<\/code> you can freely add\/remove all available columns from the <code>log_visit<\/code> table you want.\nThe keys below are just a suggestion, since they are the only one which make sense currently IMO.\nAll available fields, see here: http:\/\/developer.piwik.org\/guides\/persistence-and-the-mysql-backend#visits<\/p>\n\n<p>Inside they key <code>networks<\/code> add all subnetworks which apply to this location.<\/p>\n\n<pre><code>return [\n \/*\n * If the IP was not matched, apply these data to visitorInfo\n * You can also apply here all possible visitorInformation data if you want\n *\/\n 'noMatch' =&gt; [\n 'visitorInfo' =&gt; [\n 'location_provider' =&gt; 'unknown'\n ]\n ],\n\n [\n 'visitorInfo' =&gt; [\n \/\/ISO-3166 alpha-2 code http:\/\/en.wikipedia.org\/wiki\/ISO_3166-1\n 'location_country' =&gt; 'at',\n\n \/\/the region code (i take them from piwik\/libs\/MaxMindGeoIp\/geoipregionvars.php\n 'location_region' =&gt; '08',\n\n \/\/should be freetext\n 'location_city' =&gt; 'Muntlix',\n\n \/\/get this from a picker, e.g. http:\/\/www.tytai.com\/gmap\/\n 'location_latitude' =&gt; '47.282024',\n 'location_longitude' =&gt; '9.662304',\n\n \/\/enter your company name or do it based on your domain hierarchy\n 'location_provider' =&gt; 'myCompany'\n ],\n 'networks' =&gt; [\n \/\/enter here all subnetworks for this location\n \/\/use a subnetwork calculator, e.g. http:\/\/jodies.de\/ipcalc\n '10.59.0.0\/19',\n '170.56.251.200\/29'\n ]\n ],\n\n \/\/add more blocks live above\n];\n<\/code><\/pre>",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/IntranetGeoIP\/download\/2.2.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"Ip2Hostname",
+ "displayName":"IP 2 Hostname",
+ "owner":"ThaDafinser",
+ "description":"Piwik plugin to get the hostname from the visitor IP",
+ "homepage":"https:\/\/github.com\/ThaDafinser\/Piwik-Ip2Hostname",
+ "createdDateTime":"2015-01-28 11:44:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-Ip2Hostname",
+ "lastUpdated":"2015-10-15 08:00:03",
+ "latestVersion":"v2.0.0",
+ "numDownloads":4169,"screenshots":[],"previews":[],"activity":{"numCommits":"16",
+ "numContributors":"2",
+ "lastCommitDate":"2015-10-15 07:59:15"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0.0",
+ "release":"2015-01-28 11:44:03",
+ "requires":{"piwik":">=2.10.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":2,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-Ip2Hostname\/commits\/0.1.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Ip2Hostname\/download\/1.0.0"},{"name":"1.0.1",
+ "release":"2015-01-28 12:18:02",
+ "requires":{"piwik":">=2.10.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":1827,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-Ip2Hostname\/commits\/2.0.0",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Ip2Hostname\/download\/1.0.1"},{"name":"1.0.3",
+ "release":"2015-08-24 13:28:03",
+ "requires":{"piwik":">=2.10.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":515,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-Ip2Hostname\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Ip2Hostname\/download\/1.0.3"},{"name":"v2.0.0",
+ "release":"2015-10-15 08:00:03",
+ "requires":{"piwik":">=2.15.0",
+ "php":">=5.5.0"},"readme":"# Piwik Ip2Hostname Plugin\n\n## Description\n\nThis plugin (try) to get the hostname of the visitor and write it into the `log_visitor` table.\n\nThere are currently ***no reports or any views available***, to see a result. ***You need to directly query the table!***\nI currently dont have the time and need of such a view, and therefor maybe you'll never see one...if nobody else create oen :-)\n\nNOTE:\n***I use it currently only in an intranet enviroment*** to detect computers which have old enviroment, because the IP might change i log the hostname.\n\nI never tried it and probably wont use it for internet...\n",
+ "numDownloads":1825,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-Ip2Hostname\/commits\/0.1.3",
+ "readmeHtml":{"description":"\n\n<p>This plugin (try) to get the hostname of the visitor and write it into the <code>log_visitor<\/code> table.<\/p>\n\n<p>There are currently <strong><em>no reports or any views available<\/em><\/strong>, to see a result. <strong><em>You need to directly query the table!<\/em><\/strong>\nI currently dont have the time and need of such a view, and therefor maybe you'll never see one...if nobody else create oen :-)<\/p>\n\n<p>NOTE:\n<strong><em>I use it currently only in an intranet enviroment<\/em><\/strong> to detect computers which have old enviroment, because the IP might change i log the hostname.<\/p>\n\n<p>I never tried it and probably wont use it for internet...<\/p>",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/Ip2Hostname\/download\/v2.0.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"IP2Location",
+ "displayName":"IP 2 Location",
+ "owner":"ip2location",
+ "description":"Use IP2Location geolocation database to lookup for accurate visitor location in Piwik.",
+ "homepage":"http:\/\/www.ip2location.com\/developers\/piwik",
+ "createdDateTime":"2014-04-23 09:24:04",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/ip2location\/ip2location-piwik",
+ "lastUpdated":"2016-04-19 03:08:03",
+ "latestVersion":"2.2.0",
+ "numDownloads":16518,"screenshots":[],"previews":[],"activity":{"numCommits":"33",
+ "numContributors":"3",
+ "lastCommitDate":"2016-04-19 03:07:28"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"2.0.0",
+ "release":"2014-04-23 09:24:05",
+ "requires":{"piwik":">=2.0.1",
+ "php":">=5.3.20"},"readme":"",
+ "numDownloads":1589,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ip2location\/ip2location-piwik\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IP2Location\/download\/2.0.0"},{"name":"2.0.2",
+ "release":"2014-07-15 07:50:03",
+ "requires":{"piwik":">=2.0.1",
+ "php":">=5.3.20"},"readme":"",
+ "numDownloads":3772,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ip2location\/ip2location-piwik\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IP2Location\/download\/2.0.2"},{"name":"2.1.1",
+ "release":"2015-03-10 01:36:03",
+ "requires":{"piwik":">=2.0.1",
+ "php":">=5.3.20"},"readme":"",
+ "numDownloads":402,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ip2location\/ip2location-piwik\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IP2Location\/download\/2.1.1"},{"name":"2.1.3",
+ "release":"2015-03-24 01:54:03",
+ "requires":{"piwik":">=2.12.0",
+ "php":">=5.3.20"},"readme":"",
+ "numDownloads":199,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ip2location\/ip2location-piwik\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IP2Location\/download\/2.1.3"},{"name":"2.1.4",
+ "release":"2015-03-30 00:48:04",
+ "requires":{"piwik":">=2.12.0",
+ "php":">=5.3.20"},"readme":"",
+ "numDownloads":221,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ip2location\/ip2location-piwik\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IP2Location\/download\/2.1.4"},{"name":"2.1.5",
+ "release":"2015-04-07 08:08:03",
+ "requires":{"piwik":">=2.12.0",
+ "php":">=5.3.20"},"readme":"",
+ "numDownloads":5127,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ip2location\/ip2location-piwik\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IP2Location\/download\/2.1.5"},{"name":"2.1.8",
+ "release":"2015-12-16 01:22:03",
+ "requires":{"piwik":">=2.12.0",
+ "php":">=5.3.20"},"readme":"",
+ "numDownloads":3639,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ip2location\/ip2location-piwik\/commits\/1.0.0",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IP2Location\/download\/2.1.8"},{"name":"2.2.0",
+ "release":"2016-04-19 03:08:03",
+ "requires":{"piwik":">=2.12.0",
+ "php":">=5.3.20"},"readme":"# Piwik IP2Location Plugin \n\n## Description\n\nThis IP2Location plugin enables more accurate location lookup in your Piwik visitor log.\n\nYou need a IP2Location BIN database to make this plugin works. Database is available for free at\n\nhttp:\/\/lite.ip2location.com or http:\/\/www.ip2location.com for a commercial database.\n\n## Installation \/ Update\n\nSee http:\/\/piwik.org\/faq\/plugins\/#faq_21\n\n## Changelog\n\n__2.2.0__\n* added custom report to view additional information such as Time Zone, ZIP code, usage type.\n\n__2.1.0__\n* updated to IP2Location 7.0.0 library\n\n__2.0.0__\n* first release for Piwik 2.0\n\n## License\n\nGPL v3 \/ fair use\n\n## Support\n\nPlease contact us at support@ip2location.com\n",
+ "numDownloads":1569,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ip2location\/ip2location-piwik\/commits\/0.46",
+ "readmeHtml":{"description":"\n\n<p>This IP2Location plugin enables more accurate location lookup in your Piwik visitor log.<\/p>\n\n<p>You need a IP2Location BIN database to make this plugin works. Database is available for free at<\/p>\n\n<p>http:\/\/lite.ip2location.com or http:\/\/www.ip2location.com for a commercial database.<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<p><strong>2.2.0<\/strong>\n* added custom report to view additional information such as Time Zone, ZIP code, usage type.<\/p>\n\n<p><strong>2.1.0<\/strong>\n* updated to IP2Location 7.0.0 library<\/p>\n\n<p><strong>2.0.0<\/strong>\n* first release for Piwik 2.0<\/p>"},"download":"\/api\/2.0\/plugins\/IP2Location\/download\/2.2.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"IPv6Usage",
+ "displayName":"IPv6 Usage",
+ "owner":"halfdan",
+ "description":"Piwik Plugin to track whether visitors are using IPv4 or IPv6",
+ "homepage":"http:\/\/github.com\/halfdan\/IPv6Usage",
+ "createdDateTime":"2014-12-23 00:40:33",
+ "donate":{"flattr":"https:\/\/flattr.com\/profile\/test1",
+ "bitcoin":null},"support":[],"isTheme":false,"keywords":["ipv4",
+ "ipv6"],"basePrice":0,"authors":[{"name":"Fabian Becker",
+ "email":"test8@example.com",
+ "homepage":"http:\/\/geekproject.eu"}],"repositoryUrl":"https:\/\/github.com\/halfdan\/IPv6Usage",
+ "lastUpdated":"2014-12-23 01:13:18",
+ "latestVersion":"0.6.0",
+ "numDownloads":0,"screenshots":[],"previews":[],"activity":{"numCommits":"35",
+ "numContributors":"2",
+ "lastCommitDate":"1970-01-01 00:33:35"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.5.0",
+ "release":"2014-12-23 00:40:33",
+ "requires":{},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/halfdan\/IPv6Usage\/commits\/0.5.0",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/IPv6Usage\/download\/0.5.0"},{"name":"0.6.0",
+ "release":"2014-12-23 01:13:18",
+ "requires":{},"readme":"# IPv6Usage\n\n## Description\n\nThis is a plugin for Piwik that generates reports about the IPv6 reach of your websites. It is written as a tracker plugin and directly hooks into the Piwik tracking call.\n\n## Documentation\n\nI wrote an article about the development that lead to this plugin and explained how the plugin works internally. You can find the article at [Geekmonkey](http:\/\/geekmonkey.org\/articles\/34-how-to-write-a-piwik-plugin)\n\nThere is also a [short article](http:\/\/geekmonkey.org\/articles\/33-tracking-the-ipv6-reach-of-your-website-with-piwik\/) that explains how to setup the plugin.\n\n## Contribute \n\nIf you are interested in contributing to this plugin, feel free to send pull requests or create issues.\n\n## License\n\nGPLv3 or later\n",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/halfdan\/IPv6Usage\/commits\/0.6.0",
+ "readmeHtml":{"description":"\n\n<p>This is a plugin for Piwik that generates reports about the IPv6 reach of your websites. It is written as a tracker plugin and directly hooks into the Piwik tracking call.<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/IPv6Usage\/download\/0.6.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"kDebug",
+ "displayName":"kDebug",
+ "owner":"XaviTorello",
+ "description":"Activate or deactivate DEBUG tracking mode with a simple click.",
+ "homepage":"http:\/\/xaviertorello.cat",
+ "createdDateTime":"2014-09-09 13:42:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/XaviTorello\/kDebug-piwik",
+ "lastUpdated":"2014-09-19 13:08:03",
+ "latestVersion":"0.1.2",
+ "numDownloads":3626,"screenshots":[],"previews":[],"activity":{"numCommits":"8",
+ "numContributors":"2",
+ "lastCommitDate":"2016-01-11 08:58:56"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2014-09-09 13:42:03",
+ "requires":{"piwik":">=2.4.0-b1",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":1,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/XaviTorello\/kDebug-piwik\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/kDebug\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2014-09-09 14:06:03",
+ "requires":{"piwik":">=2.4.0-b1",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":133,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/XaviTorello\/kDebug-piwik\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/kDebug\/download\/0.1.1"},{"name":"0.1.2",
+ "release":"2014-09-19 13:08:03",
+ "requires":{"piwik":">=2.4.0-b1",
+ "php":">=5.3.0"},"readme":"# Piwik kDebug Plugin\n\n## Description\n\nActivate or deactivate DEBUG tracking mode with a simple click.\n\n![kDebug Screenshot](http:\/\/xaviertorello.cat\/img\/projects\/kDebugEN.png \"kDebug toogle\")\n\nkDebug is a simple plugin for Piwik to enable \/ disable DEBUG mode for tracking requests with a simple click.\n\nBy default, it's needed to modify manually the Piwiks config.ini file.\n\nWith this plugin just navigate to \"Settings > Debug\", and click on the toggle button to change the status of DEBUG mode.\n\n[More information](http:\/\/xaviertorello.cat\/#portfolio \"Xavier Torell\u00f3 Porfolio\")\n\n\n\n## Installation\n\nInstall it via Piwik Marketplace\n\n\n## FAQ\n\n__What exactly is included in this plugin?__\n\n* A simple and quick method to enable\/disable DEBUG mode for tracking requests\n\n\n## Changelog\n\n0.1.0 First version. Toggle button to activate\/deactivate DEBUG mode\n\n\n## License\n\nGPL v3 or later\n\n\n## Support\n\nPlease direct any feedback to [Xavier Torell\u00f3](http:\/\/xaviertorello.cat \"Xavier Torell\u00f3\") [http:\/\/xaviertorello.cat\/#contact]\n",
+ "numDownloads":3492,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/XaviTorello\/kDebug-piwik\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>Activate or deactivate DEBUG tracking mode with a simple click.<\/p>\n\n<p><img src=\"http:\/\/xaviertorello.cat\/img\/projects\/kDebugEN.png\" alt=\"kDebugEN.png\" \/><\/p>\n\n<p>kDebug is a simple plugin for Piwik to enable \/ disable DEBUG mode for tracking requests with a simple click.<\/p>\n\n<p>By default, it's needed to modify manually the Piwiks config.ini file.<\/p>\n\n<p>With this plugin just navigate to \"Settings &gt; Debug\", and click on the toggle button to change the status of DEBUG mode.<\/p>\n\n<p><a href=\"http:\/\/xaviertorello.cat\/#portfolio\">More information<\/a><\/p>\n\n",
+ "faq":"<p><strong>What exactly is included in this plugin?<\/strong><\/p>\n\n<ul><li>A simple and quick method to enable\/disable DEBUG mode for tracking requests<\/li>\n<\/ul>",
+ "documentation":"",
+ "changelog":"<p>0.1.0 First version. Toggle button to activate\/deactivate DEBUG mode<\/p>"},"download":"\/api\/2.0\/plugins\/kDebug\/download\/0.1.2"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"LdapConnection",
+ "displayName":"Ldap Connection",
+ "owner":"ThaDafinser",
+ "description":"Plugin to make a LDAP connection, which can be used by various LDAP plugins",
+ "homepage":"https:\/\/github.com\/ThaDafinser\/Piwik-LdapConnection",
+ "createdDateTime":"2014-08-06 07:50:04",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-LdapConnection",
+ "lastUpdated":"2015-01-29 07:20:09",
+ "latestVersion":"0.3.1",
+ "numDownloads":4104,"screenshots":[],"previews":[],"activity":{"numCommits":"11",
+ "numContributors":"1",
+ "lastCommitDate":"2015-03-12 11:53:13"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2014-08-06 07:50:08",
+ "requires":{"piwik":">=2.5.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":205,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-LdapConnection\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/LdapConnection\/download\/0.1.0"},{"name":"0.2.0",
+ "release":"2014-08-25 09:08:09",
+ "requires":{"piwik":">=2.5.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":22,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-LdapConnection\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/LdapConnection\/download\/0.2.0"},{"name":"0.3.0",
+ "release":"2014-08-26 06:00:09",
+ "requires":{"piwik":">=2.5.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":1119,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-LdapConnection\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/LdapConnection\/download\/0.3.0"},{"name":"0.3.1",
+ "release":"2015-01-29 07:20:09",
+ "requires":{"piwik":">=2.5.0",
+ "php":">=5.4.0"},"readme":"# Piwik LdapConnection Plugin\n\n## Description\n\nConfigurable piwik plugin to create an LDAP connection, which can be reused by other plugins.\nIs uses the ZF2 Ldap component: http:\/\/framework.zend.com\/manual\/2.3\/en\/index.html#zend-ldap\n\nCurrently used by https:\/\/github.com\/ThaDafinser\/LdapVisitorInfo\n\n## FAQ\n\n__Why is PIWIK 2.5 required?__\n\nBecause the configuration (to be explicit accountFilterFormat) is destroyed in the previous version\nSee the ticket here: https:\/\/github.com\/piwik\/piwik\/issues\/5890\n\n\n__What does this plugin do?__\n\nIt creates based on your configuration a connection to LDAP. Not more and not less :-)\n\n\n__How can i use this LDAP connection in another plugin?__\n\nThis is an example to retrieve the connection.\nFor documentation please see http:\/\/framework.zend.com\/manual\/2.3\/en\/index.html#zend-ldap\n\n```php\nnamespace Piwik\\Plugins\\YourPlugin;\n\nuse Piwik\\Plugin;\nuse Piwik\\Plugins\\LdapConnection\\API as APILdapConnection;\nuse Zend\\Ldap\\Ldap;\n\nclass YourPlugin extends Plugin\n{\n private function doSomething()\n {\n \t\/* @var $ldap \\Zend\\Ldap\\Ldap *\/\n $ldap = APILdapConnection::getInstance()->getConnection();\n $ldap->connect();\n \n $filter = sprintf('(&(objectclass=user)(samAccountName=%s))', $visitorUsername);\n $collection = $ldap->search($filter, null, Ldap::SEARCH_SCOPE_SUB, ['displayname']);\n \n if ($collection->count() >= 1) {\n \t$result = $collection->getFirst();\n \t\/\/do something with the result...\n }\n }\n}\n```\n",
+ "numDownloads":2758,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-LdapConnection\/commits\/v0.5.1",
+ "readmeHtml":{"description":"\n\n<p>Configurable piwik plugin to create an LDAP connection, which can be reused by other plugins.\nIs uses the ZF2 Ldap component: http:\/\/framework.zend.com\/manual\/2.3\/en\/index.html#zend-ldap<\/p>\n\n<p>Currently used by https:\/\/github.com\/ThaDafinser\/LdapVisitorInfo<\/p>\n\n",
+ "faq":"<p><strong>Why is PIWIK 2.5 required?<\/strong><\/p>\n\n<p>Because the configuration (to be explicit accountFilterFormat) is destroyed in the previous version\nSee the ticket here: https:\/\/github.com\/piwik\/piwik\/issues\/5890<\/p>\n\n<p><strong>What does this plugin do?<\/strong><\/p>\n\n<p>It creates based on your configuration a connection to LDAP. Not more and not less :-)<\/p>\n\n<p><strong>How can i use this LDAP connection in another plugin?<\/strong><\/p>\n\n<p>This is an example to retrieve the connection.\nFor documentation please see http:\/\/framework.zend.com\/manual\/2.3\/en\/index.html#zend-ldap<\/p>\n\n<pre><code>namespace Piwik\\Plugins\\YourPlugin;\n\nuse Piwik\\Plugin;\nuse Piwik\\Plugins\\LdapConnection\\API as APILdapConnection;\nuse Zend\\Ldap\\Ldap;\n\nclass YourPlugin extends Plugin\n{\n private function doSomething()\n {\n \/* @var $ldap \\Zend\\Ldap\\Ldap *\/\n $ldap = APILdapConnection::getInstance()-&gt;getConnection();\n $ldap-&gt;connect();\n\n $filter = sprintf('(&amp;(objectclass=user)(samAccountName=%s))', $visitorUsername);\n $collection = $ldap-&gt;search($filter, null, Ldap::SEARCH_SCOPE_SUB, ['displayname']);\n\n if ($collection-&gt;count() &gt;= 1) {\n $result = $collection-&gt;getFirst();\n \/\/do something with the result...\n }\n }\n}\n<\/code><\/pre>",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/LdapConnection\/download\/0.3.1"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"LdapVisitorInfo",
+ "displayName":"Ldap Visitor Info",
+ "owner":"ThaDafinser",
+ "description":"Piwik plugin to get visitor details (thumbnail, description) from LDAP",
+ "homepage":"https:\/\/github.com\/ThaDafinser\/Piwik-LdapVisitorInfo",
+ "createdDateTime":"2014-08-06 07:50:42",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-LdapVisitorInfo",
+ "lastUpdated":"2015-02-23 09:30:03",
+ "latestVersion":"1.0.0",
+ "numDownloads":4381,"screenshots":[],"previews":[],"activity":{"numCommits":"16",
+ "numContributors":"1",
+ "lastCommitDate":"2015-03-12 11:51:34"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2014-08-06 07:50:42",
+ "requires":{"piwik":">=2.5.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":175,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-LdapVisitorInfo\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/LdapVisitorInfo\/download\/0.1.0"},{"name":"0.2.0",
+ "release":"2014-08-25 09:06:04",
+ "requires":{"piwik":">=2.5.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":80,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-LdapVisitorInfo\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/LdapVisitorInfo\/download\/0.2.0"},{"name":"0.3.0",
+ "release":"2014-09-01 06:28:05",
+ "requires":{"piwik":">=2.5.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":1089,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-LdapVisitorInfo\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/LdapVisitorInfo\/download\/0.3.0"},{"name":"0.3.1",
+ "release":"2015-01-29 07:20:14",
+ "requires":{"piwik":">=2.5.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":239,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-LdapVisitorInfo\/commits\/v0.5.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/LdapVisitorInfo\/download\/0.3.1"},{"name":"1.0.0",
+ "release":"2015-02-23 09:30:03",
+ "requires":{"piwik":">=2.5.0",
+ "php":">=5.4.0"},"readme":"# Piwik LdapVisitorInfo Plugin\n\n## Description\n\nConfigurable piwik plugin to view a visitor thumbnail and description live from LDAP.\n\n***This plugin requires https:\/\/github.com\/ThaDafinser\/LdapConnection to work!***\n\n## FAQ\n\n__Why is PIWIK 2.5 required?__\n\nBecause the configuration (to be explicit accountFilterFormat) is destroyed in the previous version\nSee the ticket here: https:\/\/github.com\/piwik\/piwik\/issues\/5890\n\n\n__What does this plugin do?__\n\nIt displays live a thumbnail and a description in the visitor detail page from LDAP\n\n\n__How to tell Piwik which user is currently using your website?__\n\nYou need so track a custom user (username, mail, ...) visitor variable, so this plugin know which user shall be fetched from LDAP.\nPlease see the official documentation: http:\/\/piwik.org\/docs\/custom-variables\/ or http:\/\/developer.piwik.org\/api-reference\/tracking-javascript#custom-variables\n\nExample:\n`_paq.push([\"setCustomVariable\", 1, \"username\", \"<?php echo $usenamer; ?>\", \"visit\"]);`\n\n__NEW: Use the userId from Piwik itself__\n\nNow you can use the piwik UserId: http:\/\/piwik.org\/docs\/user-id\/\nJust change the plugin settings to use the piwik UserId instead of a custom variable!\n",
+ "numDownloads":2798,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-LdapVisitorInfo\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>Configurable piwik plugin to view a visitor thumbnail and description live from LDAP.<\/p>\n\n<p><strong><em>This plugin requires https:\/\/github.com\/ThaDafinser\/LdapConnection to work!<\/em><\/strong><\/p>\n\n",
+ "faq":"<p><strong>Why is PIWIK 2.5 required?<\/strong><\/p>\n\n<p>Because the configuration (to be explicit accountFilterFormat) is destroyed in the previous version\nSee the ticket here: https:\/\/github.com\/piwik\/piwik\/issues\/5890<\/p>\n\n<p><strong>What does this plugin do?<\/strong><\/p>\n\n<p>It displays live a thumbnail and a description in the visitor detail page from LDAP<\/p>\n\n<p><strong>How to tell Piwik which user is currently using your website?<\/strong><\/p>\n\n<p>You need so track a custom user (username, mail, ...) visitor variable, so this plugin know which user shall be fetched from LDAP.\nPlease see the official documentation: http:\/\/piwik.org\/docs\/custom-variables\/ or http:\/\/developer.piwik.org\/api-reference\/tracking-javascript#custom-variables<\/p>\n\n<p>Example:\n<code>_paq.push([\"setCustomVariable\", 1, \"username\", \"&lt;?php echo $usenamer; ?&gt;\", \"visit\"]);<\/code><\/p>\n\n<p><strong>NEW: Use the userId from Piwik itself<\/strong><\/p>\n\n<p>Now you can use the piwik UserId: http:\/\/piwik.org\/docs\/user-id\/\nJust change the plugin settings to use the piwik UserId instead of a custom variable!<\/p>",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/LdapVisitorInfo\/download\/1.0.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"LiveTab",
+ "displayName":"Live Tab",
+ "owner":"tsteur",
+ "description":"Keep an eye on the number of live visitors in the browser tab. It displays the number of visitors in the last 30 minutes in the browser tab.",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2014-12-23 00:38:34",
+ "donate":{"paypal":"test1@example.com",
+ "flattr":"https:\/\/flattr.com\/profile\/steurth",
+ "bitcoin":null},"support":[],"isTheme":false,"keywords":["live",
+ "tab"],"basePrice":0,"authors":[{"name":"Thomas Steur",
+ "email":"test10@example.com",
+ "homepage":"http:\/\/piwik.org"}],"repositoryUrl":"https:\/\/github.com\/tsteur\/piwik-livetab-plugin",
+ "lastUpdated":"2014-12-23 01:01:50",
+ "latestVersion":"1.0.8",
+ "numDownloads":0,"screenshots":["https:\/\/plugins.piwik.org\/LiveTab\/images\/Browser_Tab.png"],"previews":[],"activity":{"numCommits":"54",
+ "numContributors":"1",
+ "lastCommitDate":"1970-01-01 00:33:35"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0",
+ "release":"2014-12-23 00:38:34",
+ "requires":{},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/tsteur\/piwik-livetab-plugin\/commits\/0.1.0",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/LiveTab\/download\/1.0"},{"name":"1.0.1",
+ "release":"2014-12-23 00:38:48",
+ "requires":{},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/tsteur\/piwik-livetab-plugin\/commits\/v1.0.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/LiveTab\/download\/1.0.1"},{"name":"1.0.2",
+ "release":"2014-12-23 00:39:02",
+ "requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/tsteur\/piwik-livetab-plugin\/commits\/v1.0.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/LiveTab\/download\/1.0.2"},{"name":"1.0.3",
+ "release":"2014-12-23 00:40:08",
+ "requires":{},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/tsteur\/piwik-livetab-plugin\/commits\/v1.0.3b",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/LiveTab\/download\/1.0.3"},{"name":"1.0.8",
+ "release":"2014-12-23 01:01:50",
+ "requires":{"piwik":">=2.0.0"},"readme":"# Piwik LiveTab Plugin \n\n[![Build Status](https:\/\/travis-ci.org\/tsteur\/piwik-livetab-plugin.png?branch=master)](https:\/\/travis-ci.org\/tsteur\/piwik-livetab-plugin)\n\n## Description\n\nThis is a plugin for the Open Source Web Analytics platform [Piwik](http:\/\/piwik.org). It allows you to keep an eye on the number of live visitors in the browser tab. It displays the number of visits, actions, unique visitors or converted goals in the last X minutes in the browser tab. The number will be updated every minute.\n\nFor better and faster readability the value will be shortened when greater than 1000. For instance to 3.12K or 3.43M.\n\n![Screenshot](https:\/\/raw.github.com\/tsteur\/piwik-livetab-plugin\/master\/screenshots\/Browser_Tab.png)\n\n## Installation\n\nSee http:\/\/piwik.org\/faq\/plugins\/#faq_21\n\n## FAQ\n\n__Is it possible to configure the displayed metric?__\n\nYes, you can choose between Visits, Actions, Converted Visits and Visitors.\n\n__Is it possible to configure the displayed metric per user?__\n\nYes, this is also possible. Each user can configure the plugin differently.\n\n\n## Changelog\n\n__1.0.2__\n* Compatible with Piwik 2.0\n\n__1.0.0__\n* Initial release\n\n## License\n\nGPL v3 or later\n\n## Support\n\nPlease direct any feedback to: \n\n* [https:\/\/github.com\/tsteur\/piwik-livetab-plugin\/issues](https:\/\/github.com\/tsteur\/piwik-livetab-plugin\/issues)\n* [https:\/\/www.twitter.com\/tsteur](https:\/\/www.twitter.com\/tsteur)\n",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/tsteur\/piwik-livetab-plugin\/commits\/1.0.8",
+ "readmeHtml":{"description":"\n\n<p>This is a plugin for the Open Source Web Analytics platform <a href=\"http:\/\/piwik.org\">Piwik<\/a>. It allows you to keep an eye on the number of live visitors in the browser tab. It displays the number of visits, actions, unique visitors or converted goals in the last X minutes in the browser tab. The number will be updated every minute.<\/p>\n\n<p>For better and faster readability the value will be shortened when greater than 1000. For instance to 3.12K or 3.43M.<\/p>\n\n<p><img src=\"https:\/\/raw.github.com\/tsteur\/piwik-livetab-plugin\/master\/screenshots\/Browser_Tab.png\" alt=\"Browser_Tab.png\" \/><\/p>\n\n",
+ "faq":"<p><strong>Is it possible to configure the displayed metric?<\/strong><\/p>\n\n<p>Yes, you can choose between Visits, Actions, Converted Visits and Visitors.<\/p>\n\n<p><strong>Is it possible to configure the displayed metric per user?<\/strong><\/p>\n\n<p>Yes, this is also possible. Each user can configure the plugin differently.<\/p>",
+ "documentation":"",
+ "changelog":"<p><strong>1.0.2<\/strong>\n* Compatible with Piwik 2.0<\/p>\n\n<p><strong>1.0.0<\/strong>\n* Initial release<\/p>"},"download":"\/api\/2.0\/plugins\/LiveTab\/download\/1.0.8"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"LoginHttpAuth",
+ "displayName":"Login Http Auth",
+ "owner":"piwik",
+ "description":"This plugin lets you connect to Piwik using HTTP Auth protocol instead of the standard Login mechanism",
+ "homepage":"http:\/\/plugins.piwik.org\/LoginHttpAuth",
+ "createdDateTime":"2014-12-23 01:16:17",
+ "donate":{},"support":[],"isTheme":false,"keywords":["authentication",
+ "httpAuth",
+ "login"],"basePrice":0,"authors":[{"name":"Piwik",
+ "email":"test1@example.com",
+ "homepage":"http:\/\/piwik.org"}],"repositoryUrl":"https:\/\/github.com\/piwik\/plugin-LoginHttpAuth",
+ "lastUpdated":"2014-12-23 01:16:33",
+ "latestVersion":"1.0.1",
+ "numDownloads":0,"screenshots":[],"previews":[{"type":"demo",
+ "provider":"link",
+ "url":"https:\/\/demo.loginhttpauth.org"}],"activity":{"numCommits":"34",
+ "numContributors":"4",
+ "lastCommitDate":"1970-01-01 00:33:35"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0",
+ "release":"2014-12-23 01:16:17",
+ "requires":{"piwik":">=2.0.4-b12"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-LoginHttpAuth\/commits\/1.0",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/LoginHttpAuth\/download\/1.0"},{"name":"1.0.1",
+ "release":"2014-12-23 01:16:33",
+ "requires":{"piwik":">=2.0.4-b12"},"readme":"# Piwik LoginHttpAuth Plugin\n\n## Description\n\nThis plugin extends the standard Piwik authentication to use Basic HTTP Authentication.\nIt lets you login to Piwik using the HTTP Auth mechanism.\n\nHow do I setup HTTP Auth using Piwik?\n\n* Login your Piwik as Super User. Click Settings, then click Marketplace.\n* Install the LoginHttpAuth plugin, then click Activate.\n* Click Settings, then click Users.\n * Check that there is a user in Piwik for each person that should have access to Piwik.\n* Enable HTTP Auth on the Piwik on your web server.\n\n For example, if you are using Apache webserver:\n\n * generate a .htpasswd file with your encrypted logins and passwords\n * [copy this example .htaccess file](https:\/\/github.com\/piwik\/plugin-LoginHttpAuth\/blob\/master\/TemplateHtaccess\/.htaccess) in the root directory of Piwik, and set the path to your .htpasswd file\n* When you go to Piwik, you will see the Authentication window.\n Congratulations! You are now using HTTP Auth to protect Piwik.\n\n## Changelog\n\n * 1.0 - First public release [compatible with Piwik 2](http:\/\/piwik.org\/blog\/2013\/12\/piwik-2-0-release-announced-biggest-best-release-yet\/)\n\n## License\n\nGPL v3 or later\n\n## Support\n\n* Report bugs for LoginHttpAuth in [our Github issues tracker](https:\/\/github.com\/piwik\/plugin-LoginHttpAuth\/issues)\n* Please direct any feedback to [hello@piwik.org](mailto:hello@piwik.org)\n\n\n",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-LoginHttpAuth\/commits\/1.0.1",
+ "readmeHtml":{"description":"\n\n<p>This plugin extends the standard Piwik authentication to use Basic HTTP Authentication.\nIt lets you login to Piwik using the HTTP Auth mechanism.<\/p>\n\n<p>How do I setup HTTP Auth using Piwik?<\/p>\n\n<ul><li>Login your Piwik as Super User. Click Settings, then click Marketplace.<\/li>\n<li>Install the LoginHttpAuth plugin, then click Activate.<\/li>\n<li>Click Settings, then click Users.\n\n<ul><li>Check that there is a user in Piwik for each person that should have access to Piwik.<\/li>\n<\/ul><\/li>\n<li><p>Enable HTTP Auth on the Piwik on your web server.<\/p>\n\n<p>For example, if you are using Apache webserver:<\/p>\n\n<ul><li>generate a .htpasswd file with your encrypted logins and passwords<\/li>\n<li><a href=\"https:\/\/github.com\/piwik\/plugin-LoginHttpAuth\/blob\/master\/TemplateHtaccess\/.htaccess\">copy this example .htaccess file<\/a> in the root directory of Piwik, and set the path to your .htpasswd file<\/li>\n<\/ul><\/li>\n<li>When you go to Piwik, you will see the Authentication window.\nCongratulations! You are now using HTTP Auth to protect Piwik.<\/li>\n<\/ul>",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<ul><li>1.0 - First public release <a href=\"http:\/\/piwik.org\/blog\/2013\/12\/piwik-2-0-release-announced-biggest-best-release-yet\/\">compatible with Piwik 2<\/a><\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/LoginHttpAuth\/download\/1.0.1"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"LoginRevokable",
+ "displayName":"Login Revokable",
+ "owner":"torosian",
+ "description":"An Authentication plugin that allows a user to log into multiple locations, however remotely logs out of all locations when any of the locations log o",
+ "homepage":"https:\/\/github.com\/torosian\/LoginRevokable",
+ "createdDateTime":"2015-03-06 16:52:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/torosian\/LoginRevokable",
+ "lastUpdated":"2015-03-06 16:52:03",
+ "latestVersion":"0.1.4",
+ "numDownloads":2310,"screenshots":[],"previews":[],"activity":{"numCommits":"10",
+ "numContributors":"1",
+ "lastCommitDate":"2015-03-09 18:52:53"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.4",
+ "release":"2015-03-06 16:52:03",
+ "requires":{"piwik":">=2.11.2-b1"},"readme":"# Piwik LoginRevokable Plugin\n\n## Description\n\nA plugin for Piwik that allows multiple logins by the same user, but remotely terminates sessions when any of them log out. This replaces the existing core Login plugin.\n\n## FAQ\n\n## Changelog\n\nInitial Commit, adding first version\n\n## Support\n\nPlease direct any feedback to https:\/\/github.com\/torosian\/LoginRevokable\/issues",
+ "numDownloads":2310,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/torosian\/LoginRevokable\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>A plugin for Piwik that allows multiple logins by the same user, but remotely terminates sessions when any of them log out. This replaces the existing core Login plugin.<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<p>Initial Commit, adding first version<\/p>"},"download":"\/api\/2.0\/plugins\/LoginRevokable\/download\/0.1.4"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"LogViewer",
+ "displayName":"Log Viewer",
+ "owner":"piwik",
+ "description":"View log messages logged by Piwik",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2015-09-22 09:26:18",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/piwik\/plugin-LogViewer",
+ "lastUpdated":"2015-10-21 23:28:03",
+ "latestVersion":"0.2.0",
+ "numDownloads":2414,"screenshots":[],"previews":[],"activity":{"numCommits":"34",
+ "numContributors":"4",
+ "lastCommitDate":"2016-06-02 07:53:33"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2015-09-22 09:26:18",
+ "requires":{"piwik":">=2.15.0-b4"},"readme":"",
+ "numDownloads":115,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-LogViewer\/commits\/0.1.0",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/LogViewer\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2015-10-02 11:18:04",
+ "requires":{"piwik":">=2.15.0-b4"},"readme":"",
+ "numDownloads":159,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-LogViewer\/commits\/0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/LogViewer\/download\/0.1.1"},{"name":"0.2.0",
+ "release":"2015-10-21 23:28:03",
+ "requires":{"piwik":">=2.15.0-rc4"},"readme":"# Piwik LogViewer Plugin\n\n[![Build Status](https:\/\/travis-ci.org\/piwik\/plugin-LogViewer.svg)](https:\/\/travis-ci.org\/piwik\/plugin-LogViewer)\n\n## Description\n\nView log messages that were logged by Piwik via the Piwik UI or HTTP Reporting API.\n\n## FAQ\n\n__I want to see more than 100 log messages, is it possible?__\n\nYes, there is a `limit` URL parameter that you can change to any number.\n\n__Can I use regular expressions in the search field?__\n\nYes, you can enable regular expressions next to the search field.\n\n__Is the search field case insensitive?__\n\nYes.\n\n__How is a Piwik log line formatted by default?__\n\n`'$severity $tag[$datetime] [$requestId] $message` eg `WARNING Piwik\\Common[2015-01-01 01:02:03] [cf27] The log message`\n\n__Is the search pattern applied to the whole log line?__\n\nYes, this means a search for `WARNING Piwik\\Common` would deliver you all warnings triggered by `Piwik\\Common`.\n\n__How do I find all messages that belong to a certain request?__\n\nEach log message shows a \"Request Id\". By clicking on this Id it selects all log messages of the same request.\nAlternatively you can search for the expression `\\[1234\\]` where `1234` need to be replaced by a Request Id.\n\n__How do I find messages that belong to the same day?__\n\nEither click on a date field or search for it, eg `2012-12-12`.\n\n__What are the known issues?__\n\n* If there are messages being logged while viewing the log messages, the paging might not work 100% correctly.\n* There seems to be a problem when searching for a single quotation mark \"'\".\n\n## Changelog\n\n* 0.2.0 Compatibility w\/ Piwik 2.15.\n* 0.1.1 Fix for IE8\n* 0.1.0 Initial Release\n\n## Support\n\nPlease direct any feedback to [hello@piwik.org](mailto:hello@piwik.org)\n",
+ "numDownloads":2140,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-LogViewer\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>View log messages that were logged by Piwik via the Piwik UI or HTTP Reporting API.<\/p>\n\n",
+ "faq":"<p><strong>I want to see more than 100 log messages, is it possible?<\/strong><\/p>\n\n<p>Yes, there is a <code>limit<\/code> URL parameter that you can change to any number.<\/p>\n\n<p><strong>Can I use regular expressions in the search field?<\/strong><\/p>\n\n<p>Yes, you can enable regular expressions next to the search field.<\/p>\n\n<p><strong>Is the search field case insensitive?<\/strong><\/p>\n\n<p>Yes.<\/p>\n\n<p><strong>How is a Piwik log line formatted by default?<\/strong><\/p>\n\n<p><code>'$severity $tag[$datetime] [$requestId] $message<\/code> eg <code>WARNING Piwik\\Common[2015-01-01 01:02:03] [cf27] The log message<\/code><\/p>\n\n<p><strong>Is the search pattern applied to the whole log line?<\/strong><\/p>\n\n<p>Yes, this means a search for <code>WARNING Piwik\\Common<\/code> would deliver you all warnings triggered by <code>Piwik\\Common<\/code>.<\/p>\n\n<p><strong>How do I find all messages that belong to a certain request?<\/strong><\/p>\n\n<p>Each log message shows a \"Request Id\". By clicking on this Id it selects all log messages of the same request.\nAlternatively you can search for the expression <code>\\[1234\\]<\/code> where <code>1234<\/code> need to be replaced by a Request Id.<\/p>\n\n<p><strong>How do I find messages that belong to the same day?<\/strong><\/p>\n\n<p>Either click on a date field or search for it, eg <code>2012-12-12<\/code>.<\/p>\n\n<p><strong>What are the known issues?<\/strong><\/p>\n\n<ul><li>If there are messages being logged while viewing the log messages, the paging might not work 100% correctly.<\/li>\n<li>There seems to be a problem when searching for a single quotation mark \"'\".<\/li>\n<\/ul>",
+ "documentation":"",
+ "changelog":"<ul><li>0.2.0 Compatibility w\/ Piwik 2.15.<\/li>\n<li>0.1.1 Fix for IE8<\/li>\n<li>0.1.0 Initial Release<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/LogViewer\/download\/0.2.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"page2images-visual-link",
+ "displayName":"page 2images-visual-link",
+ "owner":"SuzhouKada",
+ "description":"This plugin allow you visualize links of your website by just one click installation. When user move mouse over the text links, they will see a previe",
+ "homepage":"http:\/\/www.page2images.com",
+ "createdDateTime":"2014-12-23 01:19:04",
+ "donate":{},"support":[],"isTheme":false,"keywords":["page2images",
+ "website",
+ "screenshot"],"basePrice":0,"authors":[{"name":"SuzhouKada",
+ "email":"test5@example.com",
+ "homepage":"http:\/\/www.page2images.com"}],"repositoryUrl":"https:\/\/github.com\/SuzhouKada\/piwik",
+ "lastUpdated":"2014-12-23 01:19:04",
+ "latestVersion":"1.0.3",
+ "numDownloads":0,"screenshots":["https:\/\/plugins.piwik.org\/page2images-visual-link\/images\/visual-link-screenshot-on-domz.png",
+ "https:\/\/plugins.piwik.org\/page2images-visual-link\/images\/visual-link-screenshot-on-domz_02.png",
+ "https:\/\/plugins.piwik.org\/page2images-visual-link\/images\/visual-link-screenshot-on-domz_03.png"],"previews":[],"activity":{"numCommits":"8",
+ "numContributors":"1",
+ "lastCommitDate":"1970-01-01 00:33:34"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0.3",
+ "release":"2014-12-23 01:19:04",
+ "requires":{},"readme":"# Page2Images Visual Link Plugin\n\n## Description\n\nThis plugin allow you visualize links of your website by just one click installation. When user move mouse over the text links, they will see a preview picture of this link. By default, [only extra links] will have preview thumbnails. You can change the setting in the JS files.\n\n![Screenshot](https:\/\/github.com\/SuzhouKada\/piwik\/blob\/master\/screenshots\/visual-link-screenshot-on-domz.png)\n\n## Installation\n\nJust download the plugin and copy it to the plugin directory.\nIf you need change the default behavior, you can open the js file and change the value as needed.\nEnable preload mode - p2iQuery().run(\"Free\", true, false);\nEnable preload mode & inner link mode - p2iQuery().run(\"Free\", true, true);\n\n## FAQ\n\nWho need this plugin?\nWebsite master who want to their users see the webpage thumbnail of one extra link before they open this page.\nWhat is the benefit?\nThis plugin can save end users' time. They will know whether they need go to this page or not when they see the preview image. \nIs it free?\nYes, it is totally free. But we will add a small water mark in the bottom of preview picture. The paid version does not have this limitation.\nDoes it support https?\nFree version does not support https. \n\n\n## Changelog\n\n__1.0.0__\n* Initial release\n\n## License\n\nGPL v2 or later\n\n## Support\n\nPlease direct any feedback to: \n\n* [https:\/\/github.com\/SuzhouKada\/piwik\/issues](https:\/\/github.com\/SuzhouKada\/piwik\/issues)\n",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/SuzhouKada\/piwik\/commits\/1.0.3",
+ "readmeHtml":{"description":"\n\n<p>This plugin allow you visualize links of your website by just one click installation. When user move mouse over the text links, they will see a preview picture of this link. By default, [only extra links] will have preview thumbnails. You can change the setting in the JS files.<\/p>\n\n<p><img src=\"https:\/\/github.com\/SuzhouKada\/piwik\/blob\/master\/screenshots\/visual-link-screenshot-on-domz.png\" alt=\"visual-link-screenshot-on-domz.png\" \/><\/p>\n\n",
+ "faq":"<p>Who need this plugin?\nWebsite master who want to their users see the webpage thumbnail of one extra link before they open this page.\nWhat is the benefit?\nThis plugin can save end users' time. They will know whether they need go to this page or not when they see the preview image. \nIs it free?\nYes, it is totally free. But we will add a small water mark in the bottom of preview picture. The paid version does not have this limitation.\nDoes it support https?\nFree version does not support https.<\/p>",
+ "documentation":"",
+ "changelog":"<p><strong>1.0.0<\/strong>\n* Initial release<\/p>"},"download":"\/api\/2.0\/plugins\/page2images-visual-link\/download\/1.0.3"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"PaidPlugin1",
+ "displayName":"Paid Plugin 1",
+ "owner":"TestVendor",
+ "description":"Clears all temporary cache files",
+ "homepage":"https:\/\/github.com\/JohnDeery\/piwik-clearcache-plugin",
+ "createdDateTime":"2014-12-23 01:18:01",
+ "donate":{},"support":[{"name":"Documentation",
+ "key":"docs",
+ "value":"https:\/\/paidplugin1.org\/docs\/",
+ "type":"url"},{"name":"Wiki",
+ "key":"wiki",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/wiki",
+ "type":"url"},{"name":"Forum",
+ "key":"forum",
+ "value":"https:\/\/forum.paidplugin1.org",
+ "type":"url"},{"name":"Email",
+ "key":"email",
+ "value":"paidplugin1@example.com",
+ "type":"email"},{"name":"IRC",
+ "key":"irc",
+ "value":"irc:\/\/freenode\/paidplugin1",
+ "type":"text"},{"name":"Issues \/ Bugs",
+ "key":"issues",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/issues",
+ "type":"url"},{"name":"Source",
+ "key":"source",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/",
+ "type":"url"},{"name":"RSS",
+ "key":"rss",
+ "value":"https:\/\/paidplugin1.org\/feed\/",
+ "type":"url"}],"isTheme":false,"keywords":["cache",
+ "delete",
+ "tmp"],"basePrice":100,"authors":[{"name":"John Deery",
+ "email":"test3@example.com",
+ "homepage":"http:\/\/fractalice.com"}],"repositoryUrl":"https:\/\/github.com\/TestVendor\/PaidPlugin1",
+ "lastUpdated":null,"latestVersion":"1.1",
+ "numDownloads":null,"screenshots":[],"previews":[{"type":"demo",
+ "provider":"link",
+ "url":"https:\/\/demo.paidplugin1.com"}],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":null},"featured":false,"isFree":false,"isPaid":true,"isCustomPlugin":false,"shop":{"url":"https:\/\/plugins.piwik.org\/PaidPlugin1",
+ "variations":[{"price":"150",
+ "prettyPrice":"150\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=EUR"},{"price":"175",
+ "prettyPrice":"$175",
+ "currency":"USD",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=USD"},{"price":"300",
+ "prettyPrice":"300\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=EUR"},{"price":"345",
+ "prettyPrice":"$345",
+ "currency":"USD",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=USD"},{"price":"600",
+ "prettyPrice":"600\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=EUR"},{"price":"690",
+ "prettyPrice":"$690",
+ "currency":"USD",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=USD"}],"reviews":{"embedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?show_reviews=1&piwik_embed=1",
+ "height":200}},"versions":[{"name":"1.1",
+ "release":null,"requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"readme":"# Piwik cacheBuster Plugin\n\n## Description\n\nThis plugin will clear out the tmp dir of Piwik. Useful for when you are developing other plugins or just need to kill that file and can't get to your installation to delete it normally\n\n## Changelog\n~2014~\n\n2014-02-21 - Update some more code, added in notification area but it's not quite working yet (TODO - make it work)\n\n2014-02-21 - Rebranded to cacheBuster and set version to 1.0, updated code to use a better check for directory seperator\n\n2014-02-20 - v2.0\n - Updated plugin to work with Piwik 2.0.3\n\n~2013~\n Inital creation of plugin at http:\/\/www.spherexx.com under the name ClearCache\n\n## Support\nPlease direct any feedback to john.deery@gmail.com",
+ "numDownloads":null,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":null,"readmeHtml":{"description":"\n\n<p>This plugin will clear out the tmp dir of Piwik. Useful for when you are developing other plugins or just need to kill that file and can't get to your installation to delete it normally<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":null}],"isDownloadable":false,"consumer":{"license":null,"loginUrl":"https:\/\/shop.piwik.org\/my-account"}},{"name":"PerformanceInfo",
+ "displayName":"Performance Info",
+ "owner":"ThaDafinser",
+ "description":"Piwik plugin to check if the settings are good for security\/performance",
+ "homepage":"https:\/\/github.com\/ThaDafinser\/Piwik-PerformanceInfo",
+ "createdDateTime":"2015-03-31 10:38:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-PerformanceInfo",
+ "lastUpdated":"2015-10-14 07:16:03",
+ "latestVersion":"v0.2.2",
+ "numDownloads":3314,"screenshots":[],"previews":[],"activity":{"numCommits":"18",
+ "numContributors":"1",
+ "lastCommitDate":"2015-10-14 07:15:10"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2015-03-31 10:38:03",
+ "requires":{"piwik":">=2.10.0",
+ "php":">=5.5.0"},"readme":"",
+ "numDownloads":106,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-PerformanceInfo\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/PerformanceInfo\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2015-04-07 06:42:03",
+ "requires":{"piwik":">=2.14.0",
+ "php":">=5.5.0"},"readme":"",
+ "numDownloads":156,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-PerformanceInfo\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/PerformanceInfo\/download\/0.1.1"},{"name":"0.2.0",
+ "release":"2015-04-27 06:04:03",
+ "requires":{"piwik":">=2.13.0",
+ "php":">=5.5.0"},"readme":"",
+ "numDownloads":2,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-PerformanceInfo\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/PerformanceInfo\/download\/0.2.0"},{"name":"v0.2.1",
+ "release":"2015-04-27 06:06:03",
+ "requires":{"piwik":">=2.13.0",
+ "php":">=5.5.0"},"readme":"",
+ "numDownloads":1139,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-PerformanceInfo\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/PerformanceInfo\/download\/v0.2.1"},{"name":"v0.2.2",
+ "release":"2015-10-14 07:16:03",
+ "requires":{"piwik":">=2.13.0",
+ "php":">=5.5.0"},"readme":"# Piwik ConfigurationInfo Plugin\n\n## Description\n\nThis plugin checks your configuration and compare it with some best practice settings.\n- Security\n- Performance\n- other\n\n## FAQ\n\n__Why does this plugin needs PHP 5.5?__\n\nYou look at this plugin because you care about security or performance? Then why you dont upgrade to the latest PHP version?\n",
+ "numDownloads":1911,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ThaDafinser\/Piwik-PerformanceInfo\/commits\/0.1.2",
+ "readmeHtml":{"description":"\n\n<p>This plugin checks your configuration and compare it with some best practice settings.\n- Security\n- Performance\n- other<\/p>\n\n",
+ "faq":"<p><strong>Why does this plugin needs PHP 5.5?<\/strong><\/p>\n\n<p>You look at this plugin because you care about security or performance? Then why you dont upgrade to the latest PHP version?<\/p>",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/PerformanceInfo\/download\/v0.2.2"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"PerformanceMonitor",
+ "displayName":"Performance Monitor",
+ "owner":"chanzler",
+ "description":"Displays the performance index of a site as a widget and adds an performance overview page to the top navigation. The index is calculated by the numbe",
+ "homepage":"http:\/\/github.com\/chanzler\/piwik-performance-monitor",
+ "createdDateTime":"2014-08-16 19:40:04",
+ "donate":{"paypal":"paypal@familiekanzler.de",
+ "bitcoin":null},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/chanzler\/piwik-performance-monitor",
+ "lastUpdated":"2014-11-08 20:20:03",
+ "latestVersion":"0.2.5",
+ "numDownloads":8174,"screenshots":[],"previews":[],"activity":{"numCommits":"13",
+ "numContributors":"1",
+ "lastCommitDate":"2014-11-10 16:18:43"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2014-08-16 19:40:04",
+ "requires":{},"readme":"",
+ "numDownloads":883,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/chanzler\/piwik-performance-monitor\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/PerformanceMonitor\/download\/0.1.0"},{"name":"0.2.0",
+ "release":"2014-10-12 19:44:03",
+ "requires":{"piwik":">=2.7.0"},"readme":"",
+ "numDownloads":468,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/chanzler\/piwik-performance-monitor\/commits\/v0.1.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/PerformanceMonitor\/download\/0.2.0"},{"name":"0.2.1",
+ "release":"2014-11-03 10:32:03",
+ "requires":{"piwik":">=2.0.0"},"readme":"",
+ "numDownloads":124,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/chanzler\/piwik-performance-monitor\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/PerformanceMonitor\/download\/0.2.1"},{"name":"0.2.2",
+ "release":"2014-11-04 18:18:04",
+ "requires":{"piwik":">=2.0.0"},"readme":"",
+ "numDownloads":3,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/chanzler\/piwik-performance-monitor\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/PerformanceMonitor\/download\/0.2.2"},{"name":"0.2.3",
+ "release":"2014-11-04 18:54:04",
+ "requires":{"piwik":">=2.0.0"},"readme":"",
+ "numDownloads":6,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/chanzler\/piwik-performance-monitor\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/PerformanceMonitor\/download\/0.2.3"},{"name":"0.2.4",
+ "release":"2014-11-04 20:32:03",
+ "requires":{"piwik":">=2.0.0"},"readme":"",
+ "numDownloads":128,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/chanzler\/piwik-performance-monitor\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/PerformanceMonitor\/download\/0.2.4"},{"name":"0.2.5",
+ "release":"2014-11-08 20:20:03",
+ "requires":{"piwik":">=2.0.0"},"readme":"# Piwik PerformanceMonitor Plugin\n\n## Description\n\nThis is a plugin for the Open Source Web Analytics platform Piwik. If enabled, it will add a new widget that you can add to your dashboard and a new link in the top navigation.\n\nThe widget will show the performance index of a site that auto-refreshes every 30 seconds. It shows the number of visitors or visit time in a 30 minute period compared to the maximum number of visitors in any 30 minute period of the last 30 days.\n\nThis plugin is inspired by the [piwik barometer plugin](https:\/\/github.com\/halfdan\/piwik-barometer-plugin) and uses a lightly modified jQuery-Dynameter (original by [Tzechiu Lei](http:\/\/tze1.com\/dynameter\/).\n\n**This plugin should run fine with installations with up to 100.000 page impressions per day. If you run a very large piwik installation and have performance issues with this plugin, please contact me - there is a solution for this. I have it up and running in an installation with more than 5 million visits per day.**\n\n(Tested with piwik 2.7.0, but supposed to run with older versions)\n\n## Installation\n\nInstall it via Piwik Marketplace OR install manually:\n\n1. Clone the plugin into the plugins directory of your Piwik installation.\n\n ```\n cd plugins\/\n git clone https:\/\/github.com\/chanzler\/piwik-performance-monitor.git PerformanceMonitor\n ```\n\n2. Login as superuser into your Piwik installation and activate the plugin under Settings -> Plugins\n\n3. You will now find the widget under the Live! section.\n\n4. When you update: As there have been made several changes in v0.2.0 you might have to clear your cache (console core:clear-caches) \n\n## FAQ\n\n###Features\nHere is a list of features that are included in this project:\n\n* Live widget (\"Performance Monitor\") with key performance indices\n* Add an item to the top navigation (\"Performance overview\") which displays the performance monitor widget for all your sites (configurable).\n\n###Configuration\n*Refresh interval*: Defines how often the widgets will be updated. Every 30 seconds is a good value to choose.\n\n*Measurement period*: Defines the measurement period in minutes. 5 minutes is a good value to choose.\n\n*Comparison period*: Defines the period (x * 24h) the last 30 minutes are compared to. A good value for small sites with more or less static content is 30. For sites with peak days on weekends for example 1 will be a good value. 1 is also a good value for very big sites with a lot of traffic. You will have to play around with this value a little bit and figure out whalt will fit your needs.\n\n*Sites in overview*: Defines which sites are displayed in the overview. \n\n## Changelog\n\n### 0.2.5 Bugfix Release\n* fixed bug with timezones that match \/^UTC[+-]*\/\n\n### 0.2.4 Bugfix Release\n* reengineering of scheduled task\n\n### 0.2.3 Bugfix Release\n* fixed bug in scheduled task\n\n### 0.2.2 Bugfix Release\n* fixed an installation bug\n\n### 0.2.0 Second Beta\n* made the plugin configurable by settings\n* added more key performance indices\n* performance overview now links to the dashboards\n* fixed the timezone bug (configure timezone for each site properly)\n* fixed several minor bugs\n\n### 0.1.0 First Beta\n* initial release\n\n## License\n\nGPL v3 or later\n\n## Support\n\n* Please direct any feedback to [frank@intersolve.de](mailto:frank@intersolve.de)\n\n## Contribute\n\nIf you are interested in contributing to this plugin, feel free to send pull requests!\n\n",
+ "numDownloads":6562,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/chanzler\/piwik-performance-monitor\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>This is a plugin for the Open Source Web Analytics platform Piwik. If enabled, it will add a new widget that you can add to your dashboard and a new link in the top navigation.<\/p>\n\n<p>The widget will show the performance index of a site that auto-refreshes every 30 seconds. It shows the number of visitors or visit time in a 30 minute period compared to the maximum number of visitors in any 30 minute period of the last 30 days.<\/p>\n\n<p>This plugin is inspired by the <a href=\"https:\/\/github.com\/halfdan\/piwik-barometer-plugin\">piwik barometer plugin<\/a> and uses a lightly modified jQuery-Dynameter (original by <a href=\"http:\/\/tze1.com\/dynameter\/\">Tzechiu Lei<\/a>.<\/p>\n\n<p><strong>This plugin should run fine with installations with up to 100.000 page impressions per day. If you run a very large piwik installation and have performance issues with this plugin, please contact me - there is a solution for this. I have it up and running in an installation with more than 5 million visits per day.<\/strong><\/p>\n\n<p>(Tested with piwik 2.7.0, but supposed to run with older versions)<\/p>\n\n",
+ "faq":"<h3>Features<\/h3>\n\n<p>Here is a list of features that are included in this project:<\/p>\n\n<ul><li>Live widget (\"Performance Monitor\") with key performance indices<\/li>\n<li>Add an item to the top navigation (\"Performance overview\") which displays the performance monitor widget for all your sites (configurable).<\/li>\n<\/ul><h3>Configuration<\/h3>\n\n<p><em>Refresh interval<\/em>: Defines how often the widgets will be updated. Every 30 seconds is a good value to choose.<\/p>\n\n<p><em>Measurement period<\/em>: Defines the measurement period in minutes. 5 minutes is a good value to choose.<\/p>\n\n<p><em>Comparison period<\/em>: Defines the period (x * 24h) the last 30 minutes are compared to. A good value for small sites with more or less static content is 30. For sites with peak days on weekends for example 1 will be a good value. 1 is also a good value for very big sites with a lot of traffic. You will have to play around with this value a little bit and figure out whalt will fit your needs.<\/p>\n\n<p><em>Sites in overview<\/em>: Defines which sites are displayed in the overview.<\/p>",
+ "documentation":"",
+ "changelog":"<h3>0.2.5 Bugfix Release<\/h3>\n\n<ul><li>fixed bug with timezones that match \/^UTC[+-]*\/<\/li>\n<\/ul><h3>0.2.4 Bugfix Release<\/h3>\n\n<ul><li>reengineering of scheduled task<\/li>\n<\/ul><h3>0.2.3 Bugfix Release<\/h3>\n\n<ul><li>fixed bug in scheduled task<\/li>\n<\/ul><h3>0.2.2 Bugfix Release<\/h3>\n\n<ul><li>fixed an installation bug<\/li>\n<\/ul><h3>0.2.0 Second Beta<\/h3>\n\n<ul><li>made the plugin configurable by settings<\/li>\n<li>added more key performance indices<\/li>\n<li>performance overview now links to the dashboards<\/li>\n<li>fixed the timezone bug (configure timezone for each site properly)<\/li>\n<li>fixed several minor bugs<\/li>\n<\/ul><h3>0.1.0 First Beta<\/h3>\n\n<ul><li>initial release<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/PerformanceMonitor\/download\/0.2.5"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"PlatformsReport",
+ "displayName":"Platforms Report",
+ "owner":"PiwikPRO",
+ "description":"New report in Visitors > Platforms that aggregates visits based on device type, OS version & browser version.",
+ "homepage":"http:\/\/piwik.pro",
+ "createdDateTime":"2015-10-27 02:06:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/PiwikPRO\/plugin-PlatformsReport",
+ "lastUpdated":"2016-01-04 08:42:03",
+ "latestVersion":"1.0.4",
+ "numDownloads":2347,"screenshots":[],"previews":[],"activity":{"numCommits":"24",
+ "numContributors":"3",
+ "lastCommitDate":"2016-03-15 04:25:00"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0.0",
+ "release":"2015-10-27 02:06:03",
+ "requires":{"piwik":">=2.14.3"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-PlatformsReport\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/PlatformsReport\/download\/1.0.0"},{"name":"1.0.1",
+ "release":"2015-10-27 02:14:03",
+ "requires":{"piwik":">=2.14.3"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-PlatformsReport\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/PlatformsReport\/download\/1.0.1"},{"name":"1.0.2",
+ "release":"2015-10-27 02:30:04",
+ "requires":{"piwik":">=2.14.3"},"readme":"",
+ "numDownloads":100,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-PlatformsReport\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/PlatformsReport\/download\/1.0.2"},{"name":"1.0.3",
+ "release":"2015-10-29 23:52:03",
+ "requires":{"piwik":">=2.14.3"},"readme":"",
+ "numDownloads":777,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-PlatformsReport\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/PlatformsReport\/download\/1.0.3"},{"name":"1.0.4",
+ "release":"2016-01-04 08:42:03",
+ "requires":{"piwik":">=2.14.3"},"readme":"# Piwik PlatformReports Plugin\n\n## Description\n\nIncludes two new reports: Platforms and Platforms With Versions. \n\nThe **Platforms** report displays visitors by their device type, OS and browser.\n\nThe **Platforms With Versions** report displays the same information, but includes OS version & browser version information.\n\nThe new reports are available via the Visitors > Platforms menu.\n\n## Changelog\n\n- 1.0.3\n * Moved sub-menu entry after 'Software'\n- 1.0.2\n * Initial release on Marketplace.\n- 1.0.0\n * Initial release.\n",
+ "numDownloads":1470,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-PlatformsReport\/commits\/1.0.1",
+ "readmeHtml":{"description":"\n\n<p>Includes two new reports: Platforms and Platforms With Versions.<\/p>\n\n<p>The <strong>Platforms<\/strong> report displays visitors by their device type, OS and browser.<\/p>\n\n<p>The <strong>Platforms With Versions<\/strong> report displays the same information, but includes OS version &amp; browser version information.<\/p>\n\n<p>The new reports are available via the Visitors &gt; Platforms menu.<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<ul><li>1.0.3\n\n<ul><li>Moved sub-menu entry after 'Software'<\/li>\n<\/ul><\/li>\n<li>1.0.2\n\n<ul><li>Initial release on Marketplace.<\/li>\n<\/ul><\/li>\n<li>1.0.0\n\n<ul><li>Initial release.<\/li>\n<\/ul><\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/PlatformsReport\/download\/1.0.4"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"QueuedTracking",
+ "displayName":"Queued Tracking",
+ "owner":"piwik",
+ "description":"Scale your large traffic Piwik service by queuing tracking requests in Redis for better performance. ",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2015-01-05 23:28:04",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking",
+ "lastUpdated":"2016-04-19 02:26:04",
+ "latestVersion":"0.3.1",
+ "numDownloads":5296,"screenshots":[],"previews":[],"activity":{"numCommits":"115",
+ "numContributors":"4",
+ "lastCommitDate":"2016-06-02 09:32:10"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2015-01-05 23:28:05",
+ "requires":{"piwik":">=2.10.0-b10"},"readme":"",
+ "numDownloads":1,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/1.0.7",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2015-01-06 00:48:04",
+ "requires":{"piwik":">=2.10.0-b10"},"readme":"",
+ "numDownloads":487,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/1.0.7",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.1.1"},{"name":"0.1.2",
+ "release":"2015-03-09 05:56:04",
+ "requires":{"piwik":">=2.10.0"},"readme":"",
+ "numDownloads":221,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.1.2"},{"name":"0.1.3",
+ "release":"2015-03-31 21:44:04",
+ "requires":{"piwik":">=2.10.0"},"readme":"",
+ "numDownloads":611,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.1.3"},{"name":"0.1.4",
+ "release":"2015-06-09 21:38:04",
+ "requires":{"piwik":">=2.13.0"},"readme":"",
+ "numDownloads":80,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.1.4"},{"name":"0.1.5",
+ "release":"2015-06-11 21:56:04",
+ "requires":{"piwik":">=2.13.0"},"readme":"",
+ "numDownloads":163,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.1.5"},{"name":"0.1.6",
+ "release":"2015-06-22 01:00:05",
+ "requires":{"piwik":">=2.13.0"},"readme":"",
+ "numDownloads":328,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.1.6"},{"name":"0.1.7",
+ "release":"2015-07-24 12:44:05",
+ "requires":{"piwik":">=2.14.0"},"readme":"",
+ "numDownloads":26,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.1.7"},{"name":"0.1.8",
+ "release":"2015-07-25 08:08:04",
+ "requires":{"piwik":">=2.14.2"},"readme":"",
+ "numDownloads":736,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.1.8"},{"name":"0.2.0",
+ "release":"2015-10-21 22:36:04",
+ "requires":{"piwik":">=2.15.0-b15"},"readme":"",
+ "numDownloads":481,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.2.0"},{"name":"0.2.1",
+ "release":"2015-12-14 20:46:04",
+ "requires":{"piwik":">=2.15.0-b15"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/0.44",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.2.1"},{"name":"0.2.2",
+ "release":"2015-12-14 21:02:04",
+ "requires":{"piwik":">=2.15.0-b15"},"readme":"",
+ "numDownloads":31,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/1.0",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.2.2"},{"name":"0.2.3",
+ "release":"2015-12-15 21:06:05",
+ "requires":{"piwik":">=2.15.0-b15"},"readme":"",
+ "numDownloads":5,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/v1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.2.3"},{"name":"0.2.4",
+ "release":"2015-12-15 21:58:04",
+ "requires":{"piwik":">=2.15.0-b15"},"readme":"",
+ "numDownloads":350,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/1.0.4",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.2.4"},{"name":"0.2.5",
+ "release":"2016-01-13 19:58:04",
+ "requires":{"piwik":">=2.15.0-b15"},"readme":"",
+ "numDownloads":480,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/1.0.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.2.5"},{"name":"0.2.6",
+ "release":"2016-02-16 16:06:04",
+ "requires":{"piwik":">=2.15.0-b15"},"readme":"",
+ "numDownloads":426,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.2.6"},{"name":"0.3.0",
+ "release":"2016-03-14 19:00:04",
+ "requires":{"piwik":">=2.15.0-b15"},"readme":"",
+ "numDownloads":404,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/0.6.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.3.0"},{"name":"0.3.1",
+ "release":"2016-04-19 02:26:04",
+ "requires":{"piwik":">=2.16.0"},"readme":"# Piwik QueuedTracking Plugin\n\n[![Build Status](https:\/\/travis-ci.org\/piwik\/plugin-QueuedTracking.svg?branch=master)](https:\/\/travis-ci.org\/piwik\/plugin-QueuedTracking)\n\n## Description\n\nThis plugin writes all tracking requests into a [Redis](http:\/\/redis.io\/) instance instead of directly into the database.\nThis is useful if you have too many requests per second and your server cannot handle all of them directly (eg too many connections in nginx or MySQL).\nIt is also useful if you experience peaks sometimes. Those peaks can be handled much better by using this queue.\nWriting a tracking request into the queue is very fast (a tracking request takes in total a few milliseconds) compared to a regular tracking request (that takes multiple hundreds of milliseconds). The queue makes sure to process the tracking requests whenever possible even if it takes a while to process all requests after there was a peak.\n\n*This plugin is currently BETA and there might be issues causing not tracked requests, wrongly tracked requests or duplicated tracked requests.*\n\nHave a look at the FAQ for more information.\n\n## FAQ\n\n__What are the requirements for this plugin?__\n\n* [Redis server 2.8+](http:\/\/redis.io\/) - [Redis quickstart](http:\/\/redis.io\/topics\/quickstart)\n* [phpredis PHP extension](https:\/\/github.com\/nicolasff\/phpredis) - [Install](https:\/\/github.com\/nicolasff\/phpredis#installingconfiguring)\n* Transactions are used and must be supported by the SQL database.\n\n__Where can I configure and enable the queue?__\n\nIn your Piwik instance go to \"Settings => Plugin Settings\". There is a config section for this plugin.\n\n__When will a queued tracking request be processed?__\n\nFirst you should know that multiple tracking requests will be inserted into the database at once using\n[bulk tracking](http:\/\/developer.piwik.org\/api-reference\/tracking-api#bulk-tracking) as soon as a configurable number\nof requests is queued. By default we will check whether enough requests are queued during a regular tracking request\nand start processing them right after sending a response to the browser to make sure a user won't have to wait until\nthe queue has finished to process all requests. Have a look at this graph to see how it works:\n\n![How it works](https:\/\/raw.githubusercontent.com\/piwik\/plugin-QueuedTracking\/master\/docs\/How_it_works.png)\n\n__I do not want to process queued requests within a tracking request, what shall I do?__\n\nDon't worry, if this solution doesn't work out for you for some reason you can disable it and process all queued\nrequests using the [Piwik console](http:\/\/developer.piwik.org\/guides\/piwik-on-the-command-line). Just follow these steps:\n\n* Disable the setting \"Process during tracking request\" in the Piwik UI under \"Settings => Plugin Settings\"\n* Setup a cronjob that executes the command `.\/console queuedtracking:process` for instance every minute\n* That's it\n\nThe `queuedtracking:process` command will make sure to process all queued tracking requests whenever possible and the\ncommand will exit as soon as there are not enough requests queued anymore. That's why you should setup a cronjob to start\nthe command every minute as it will just start processing again as soon as there are enough requests. Be aware that it won't\nspeed up processing queued requests when starting this command multiple times. Only one process will actually replay\nqueued requests at a time.\n\nExample crontab entry that starts the processor every minute:\n\n`* * * * * cd \/piwik && .\/console queuedtracking:process >\/dev\/null 2>&1`\n\n__Can I keep track of the state of the queue?__\n\nYes, you can. Just execute the command `.\/console queuedtracking:monitor`. This will show the current state of the queue.\n\n__Can I improve the speed of inserting requests from the Redis queue to the database?__\n\nYes, you can by adding more workers. By default only one worker is activated at a time and only one worker processes tracking requests from Redis to the database. When inserting tracking requests into the database, at time of writing this, about 80% of the time is spent in PHP and the database might be rather bored. If you have multiple CPUs available on your server you can add more workers. You can do this by going in the Piwik Admin interface to \"Plugin Settings\". There will be a setting \"Number of queue workers\". Increase this number to the number of CPUs you want to dedeciate for processing requests. Best practice is to add more workers step by step. So first increase this number to 2 and check if the tracking request insertions is fast enough for you. If not and you have more CPUs available, increase the number again.\n\nWhen using multiple workers it might be worth to lower the number of \"Number of requests to process\" to eg 15 in \"Plugin Settings\". By default 25 requests are inserted in one step by using transactions. This means different workers might have to wait for each other. By lowering that number each worker will block the DB for less time.\n\nIf you process requests from the command line via `.\/console queuedtracking:process` make sure to always start enough workers. Each time you execute this command one worker will be started. If already enough workers are in process no new worker will be started and the command just finishes immediately.\n\n__How fast are the requests inserted from Redis to the Database?__\n\nThis very much depends on your setup and hardware. With fast CPUs you can achive up to 250req\/s with 1 worker, 400req\/s with 2 workers and 1500req\/s with 8 workers (tested on a AWS c3.x2large instance).\n\n__How should the redis server be configured?__\n\nMake sure to have enough memory to save all tracking requests in the queue. One tracking request in the queue takes about 2KB,\n20.000 tracking requests take about 50MB. All tracking requests of all websites are stored in the same queue.\nThere should be only one Redis server to make sure the data will be replayed in the same order as they were recorded.\nIf you want to configure Redis HA (High Availability) it is possible to use Redis Sentinel see further down.\nWe currently write into the Redis default database by default but you can configure to use a different one.\n\n__Why do some tests fail on my local Piwik instance?__\n\nMake sure the requirements mentioned above are met and Redis needs to run on 127.0.0.1:6379 with no password for the\nintegration tests to work. It will use the database \"15\" and the tests may flush all data it contains. Make sure\nit does not contain any important data.\n\n__What if I want to disable the queue?__\n\nYou might want to disable the queue at some point but there are still some pending requests in the queue. We recommend to\nchange the \"Number of requests to process\" in plugin settings to \"1\" and process all requests using the command\n`.\/console queuedtracking:process` shortly before disabling the queue and directly afterwards. It is still possible to\nprocess remaining request once the queue is disabled but new tracking requests won't be written into the queue.\n\n__How can I access Redis data?__\n\nYou can either acccess data on the command line via `redis-cli` or use a Redis monitor like [phpRedisAdmin](https:\/\/github.com\/ErikDubbelboer\/phpRedisAdmin).\nIn case you are using something like a Redis monitor make sure it is not accessible by everyone.\n\n__The processor won't start processing again as it things another processor is processing the data already, what can I do?__\n\nFirst make sure there is actually no processor processing any requests. For example by executing the command\n`.\/console queuedtracking:monitor`. In case you are using the command line to process tracking requests make sure there\nis no processer running using the Linux command `ps`. If you are sure there is no process running you can release the lock\nby executing the command `.\/console queuedtracking:lock-status`. This will output more information which locks are in use and how to unlock them. Afterwards everything should work as normal again.\nYou should actually never have to do this as a lock automatically expires after a while. It just may take a while depending\non the amount of requests you are importing.\n\n__How can I test my Redis \/ QueuedTracking setup in case I'm getting errors?__\n\nThere is a command to test some the connection to Redis as well as some needed features: `.\/console queuedtracking:test`.\n\nIt might directly give you an error message if something goes wrong that helps you to resolve the issue. If your queue\nis always locked you might be as well interested in executing `.console queuedtracking:lock-status`.\n\n__How can I debug in case something goes wrong?__\n\n* Use the command `.\/console queuedtracking:monitor` to view the current state of all workers\n* Use the command `.\/console queuedtracking:lock-status` to view the current state of all locks\n* Set the option `-vvv` when processing via `.\/console queuedtracking:process -vvv` to enable the tracker debug mode for this run. This will print detailed information to screen.\n* Enable tracker mode in `config.ini.php` via `[Tracker] debug=1` if processing requests during tracking is enabled.\n* Use the command `.\/console queuedtracking:print-queued-requests` to view the next requests to process in each queue. If you execute this command twice within 1-10 minutes, and it outputs the same, the queue is not being processed most likely indicating a problem.\n\n__I am using the Log Importer in combination with Queued Tracking, is there something to consider?__\n\nYes, we recommend to set the \"Number of requests to process\" to `1` as the log importer usually sends multiple requests at once using bulk tracking already.\n\n__How can I configure the QueuedTracking plugin to use Redis Sentinel?__\n\nYou can enable the Sentinel in the plugin settings. Make sure to specify the correct Sentinel \"master\" name.\n\nWhen using Sentinel, the `phpredis` extension is not needed as it uses a PHP class to connect to your Redis. Please note that calls to Redis might be a little bit slower.\n\n__Can I configure multiple Sentinel servers?__\n\nYes, once Sentinel is enabled you can configure multiple servers by specifying multiple hosts and ports comma separated via the UI.\n\n__Are there any known issues?__\n\n* In case you are using bulk tracking the bulk tracking response varies compared to the regular one. We will always return\n either an image or a 204 HTTP response code in case the parameter `send_image=0` is sent.\n* Anything related with Cookies won't work\n* By design this plugin can delay the insertion of tracking requests causing real time plugins to not show the actual data since\n under load tracking requests may take a while until they are replayed.\n\n## Changelog\n\n0.3.1\n\n- Fixed Redis Sentinel was not working properly. Sentinel can be now configured via the UI and not via config. Also\n multiple servers can be configured now.\n\n0.3.0\n\n- Added support to use Redis Sentinel for automatic failover\n\n0.2.6\n\n- When a request takes more than 2 seconds and debug tracker mode is enabled, log information about the request.\n\n0.2.5\n\n- Use a better random number generator if available on the system to more evenly process queues.\n\n0.2.4\n\n- The command `queuedtracking:monitor` will now work even when the queue is disabled\n\n0.2.3\n\n- Added more tests and information to the `queuedtracking:test` command\n- It is now possible to configure up to 16 workers\n\n0.2.2\n\n- Improved output for the new `test` command\n- New FAQ entries\n\n0.2.1\n\n- Added a new command to test the connection to Redis. To test yor connection use `.\/console queuedtracking:test`\n\n0.2.0\n\n- Compatibility w\/ Piwik 2.15.\n\n0.1.6\n \n- For bulk requests we do no longer skip all tracking requests after a tracking request that has an invalid `idSite` set. The same behaviour was changed in Piwik 2.14 for regular bulk requests.\n\n0.1.5\n\n- Fixed a notice in case an incompatible Redis version is used.\n\n0.1.4\n\n- It is now possible to start multiple workers for faster insertion from Redis to the database. This can be configured in the \"Plugin Settings\"\n- Monitor does now output information whether a processor is currently processing the queue.\n- Added a new command `queuedtracking:lock-status` that outputs the status of each queue lock. This command can also unlock a queue by using the option `--unlock`.\n- Added a new command `queuedtracking:print-queued-requests` that outputs the next requests to process in each queue.\n- If someone passes the option `-vvv` to `.\/console queuedtracking:process` the Tracker debug mode will be enabled and additional information will be printed to the screen.\n\n0.1.2\n\n- Updated description on Marketplace\n\n0.1.0\n\n- Initial Release\n\n## Support\n\nPlease direct any feedback to [hello@piwik.org](mailto:hello@piwik.org). In case of any issues with the plugin or\nfeature wishes create a new issues here: https:\/\/github.com\/piwik\/plugin-QueuedTracking\/issues . In case you experience\nany problems please post the output of `.\/console queuedtracking:test` in the issue.\n\n## TODO\n\nFor usage with multiple redis servers we should lock differently:\nhttp:\/\/redis.io\/topics\/distlock eg using https:\/\/github.com\/ronnylt\/redlock-php\n",
+ "numDownloads":466,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>This plugin writes all tracking requests into a <a href=\"http:\/\/redis.io\/\">Redis<\/a> instance instead of directly into the database.\nThis is useful if you have too many requests per second and your server cannot handle all of them directly (eg too many connections in nginx or MySQL).\nIt is also useful if you experience peaks sometimes. Those peaks can be handled much better by using this queue.\nWriting a tracking request into the queue is very fast (a tracking request takes in total a few milliseconds) compared to a regular tracking request (that takes multiple hundreds of milliseconds). The queue makes sure to process the tracking requests whenever possible even if it takes a while to process all requests after there was a peak.<\/p>\n\n<p><em>This plugin is currently BETA and there might be issues causing not tracked requests, wrongly tracked requests or duplicated tracked requests.<\/em><\/p>\n\n<p>Have a look at the FAQ for more information.<\/p>\n\n",
+ "faq":"<p><strong>What are the requirements for this plugin?<\/strong><\/p>\n\n<ul><li><a href=\"http:\/\/redis.io\/\">Redis server 2.8+<\/a> - <a href=\"http:\/\/redis.io\/topics\/quickstart\">Redis quickstart<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/nicolasff\/phpredis\">phpredis PHP extension<\/a> - <a href=\"https:\/\/github.com\/nicolasff\/phpredis#installingconfiguring\">Install<\/a><\/li>\n<li>Transactions are used and must be supported by the SQL database.<\/li>\n<\/ul><p><strong>Where can I configure and enable the queue?<\/strong><\/p>\n\n<p>In your Piwik instance go to \"Settings =&gt; Plugin Settings\". There is a config section for this plugin.<\/p>\n\n<p><strong>When will a queued tracking request be processed?<\/strong><\/p>\n\n<p>First you should know that multiple tracking requests will be inserted into the database at once using\n<a href=\"http:\/\/developer.piwik.org\/api-reference\/tracking-api#bulk-tracking\">bulk tracking<\/a> as soon as a configurable number\nof requests is queued. By default we will check whether enough requests are queued during a regular tracking request\nand start processing them right after sending a response to the browser to make sure a user won't have to wait until\nthe queue has finished to process all requests. Have a look at this graph to see how it works:<\/p>\n\n<p><img src=\"https:\/\/raw.githubusercontent.com\/piwik\/plugin-QueuedTracking\/master\/docs\/How_it_works.png\" alt=\"How_it_works.png\" \/><\/p>\n\n<p><strong>I do not want to process queued requests within a tracking request, what shall I do?<\/strong><\/p>\n\n<p>Don't worry, if this solution doesn't work out for you for some reason you can disable it and process all queued\nrequests using the <a href=\"http:\/\/developer.piwik.org\/guides\/piwik-on-the-command-line\">Piwik console<\/a>. Just follow these steps:<\/p>\n\n<ul><li>Disable the setting \"Process during tracking request\" in the Piwik UI under \"Settings =&gt; Plugin Settings\"<\/li>\n<li>Setup a cronjob that executes the command <code>.\/console queuedtracking:process<\/code> for instance every minute<\/li>\n<li>That's it<\/li>\n<\/ul><p>The <code>queuedtracking:process<\/code> command will make sure to process all queued tracking requests whenever possible and the\ncommand will exit as soon as there are not enough requests queued anymore. That's why you should setup a cronjob to start\nthe command every minute as it will just start processing again as soon as there are enough requests. Be aware that it won't\nspeed up processing queued requests when starting this command multiple times. Only one process will actually replay\nqueued requests at a time.<\/p>\n\n<p>Example crontab entry that starts the processor every minute:<\/p>\n\n<p><code>* * * * * cd \/piwik &amp;&amp; .\/console queuedtracking:process &gt;\/dev\/null 2&gt;&amp;1<\/code><\/p>\n\n<p><strong>Can I keep track of the state of the queue?<\/strong><\/p>\n\n<p>Yes, you can. Just execute the command <code>.\/console queuedtracking:monitor<\/code>. This will show the current state of the queue.<\/p>\n\n<p><strong>Can I improve the speed of inserting requests from the Redis queue to the database?<\/strong><\/p>\n\n<p>Yes, you can by adding more workers. By default only one worker is activated at a time and only one worker processes tracking requests from Redis to the database. When inserting tracking requests into the database, at time of writing this, about 80% of the time is spent in PHP and the database might be rather bored. If you have multiple CPUs available on your server you can add more workers. You can do this by going in the Piwik Admin interface to \"Plugin Settings\". There will be a setting \"Number of queue workers\". Increase this number to the number of CPUs you want to dedeciate for processing requests. Best practice is to add more workers step by step. So first increase this number to 2 and check if the tracking request insertions is fast enough for you. If not and you have more CPUs available, increase the number again.<\/p>\n\n<p>When using multiple workers it might be worth to lower the number of \"Number of requests to process\" to eg 15 in \"Plugin Settings\". By default 25 requests are inserted in one step by using transactions. This means different workers might have to wait for each other. By lowering that number each worker will block the DB for less time.<\/p>\n\n<p>If you process requests from the command line via <code>.\/console queuedtracking:process<\/code> make sure to always start enough workers. Each time you execute this command one worker will be started. If already enough workers are in process no new worker will be started and the command just finishes immediately.<\/p>\n\n<p><strong>How fast are the requests inserted from Redis to the Database?<\/strong><\/p>\n\n<p>This very much depends on your setup and hardware. With fast CPUs you can achive up to 250req\/s with 1 worker, 400req\/s with 2 workers and 1500req\/s with 8 workers (tested on a AWS c3.x2large instance).<\/p>\n\n<p><strong>How should the redis server be configured?<\/strong><\/p>\n\n<p>Make sure to have enough memory to save all tracking requests in the queue. One tracking request in the queue takes about 2KB,\n20.000 tracking requests take about 50MB. All tracking requests of all websites are stored in the same queue.\nThere should be only one Redis server to make sure the data will be replayed in the same order as they were recorded.\nIf you want to configure Redis HA (High Availability) it is possible to use Redis Sentinel see further down.\nWe currently write into the Redis default database by default but you can configure to use a different one.<\/p>\n\n<p><strong>Why do some tests fail on my local Piwik instance?<\/strong><\/p>\n\n<p>Make sure the requirements mentioned above are met and Redis needs to run on 127.0.0.1:6379 with no password for the\nintegration tests to work. It will use the database \"15\" and the tests may flush all data it contains. Make sure\nit does not contain any important data.<\/p>\n\n<p><strong>What if I want to disable the queue?<\/strong><\/p>\n\n<p>You might want to disable the queue at some point but there are still some pending requests in the queue. We recommend to\nchange the \"Number of requests to process\" in plugin settings to \"1\" and process all requests using the command\n<code>.\/console queuedtracking:process<\/code> shortly before disabling the queue and directly afterwards. It is still possible to\nprocess remaining request once the queue is disabled but new tracking requests won't be written into the queue.<\/p>\n\n<p><strong>How can I access Redis data?<\/strong><\/p>\n\n<p>You can either acccess data on the command line via <code>redis-cli<\/code> or use a Redis monitor like <a href=\"https:\/\/github.com\/ErikDubbelboer\/phpRedisAdmin\">phpRedisAdmin<\/a>.\nIn case you are using something like a Redis monitor make sure it is not accessible by everyone.<\/p>\n\n<p><strong>The processor won't start processing again as it things another processor is processing the data already, what can I do?<\/strong><\/p>\n\n<p>First make sure there is actually no processor processing any requests. For example by executing the command\n<code>.\/console queuedtracking:monitor<\/code>. In case you are using the command line to process tracking requests make sure there\nis no processer running using the Linux command <code>ps<\/code>. If you are sure there is no process running you can release the lock\nby executing the command <code>.\/console queuedtracking:lock-status<\/code>. This will output more information which locks are in use and how to unlock them. Afterwards everything should work as normal again.\nYou should actually never have to do this as a lock automatically expires after a while. It just may take a while depending\non the amount of requests you are importing.<\/p>\n\n<p><strong>How can I test my Redis \/ QueuedTracking setup in case I'm getting errors?<\/strong><\/p>\n\n<p>There is a command to test some the connection to Redis as well as some needed features: <code>.\/console queuedtracking:test<\/code>.<\/p>\n\n<p>It might directly give you an error message if something goes wrong that helps you to resolve the issue. If your queue\nis always locked you might be as well interested in executing <code>.console queuedtracking:lock-status<\/code>.<\/p>\n\n<p><strong>How can I debug in case something goes wrong?<\/strong><\/p>\n\n<ul><li>Use the command <code>.\/console queuedtracking:monitor<\/code> to view the current state of all workers<\/li>\n<li>Use the command <code>.\/console queuedtracking:lock-status<\/code> to view the current state of all locks<\/li>\n<li>Set the option <code>-vvv<\/code> when processing via <code>.\/console queuedtracking:process -vvv<\/code> to enable the tracker debug mode for this run. This will print detailed information to screen.<\/li>\n<li>Enable tracker mode in <code>config.ini.php<\/code> via <code>[Tracker] debug=1<\/code> if processing requests during tracking is enabled.<\/li>\n<li>Use the command <code>.\/console queuedtracking:print-queued-requests<\/code> to view the next requests to process in each queue. If you execute this command twice within 1-10 minutes, and it outputs the same, the queue is not being processed most likely indicating a problem.<\/li>\n<\/ul><p><strong>I am using the Log Importer in combination with Queued Tracking, is there something to consider?<\/strong><\/p>\n\n<p>Yes, we recommend to set the \"Number of requests to process\" to <code>1<\/code> as the log importer usually sends multiple requests at once using bulk tracking already.<\/p>\n\n<p><strong>How can I configure the QueuedTracking plugin to use Redis Sentinel?<\/strong><\/p>\n\n<p>You can enable the Sentinel in the plugin settings. Make sure to specify the correct Sentinel \"master\" name.<\/p>\n\n<p>When using Sentinel, the <code>phpredis<\/code> extension is not needed as it uses a PHP class to connect to your Redis. Please note that calls to Redis might be a little bit slower.<\/p>\n\n<p><strong>Can I configure multiple Sentinel servers?<\/strong><\/p>\n\n<p>Yes, once Sentinel is enabled you can configure multiple servers by specifying multiple hosts and ports comma separated via the UI.<\/p>\n\n<p><strong>Are there any known issues?<\/strong><\/p>\n\n<ul><li>In case you are using bulk tracking the bulk tracking response varies compared to the regular one. We will always return\neither an image or a 204 HTTP response code in case the parameter <code>send_image=0<\/code> is sent.<\/li>\n<li>Anything related with Cookies won't work<\/li>\n<li>By design this plugin can delay the insertion of tracking requests causing real time plugins to not show the actual data since\nunder load tracking requests may take a while until they are replayed.<\/li>\n<\/ul>",
+ "documentation":"",
+ "changelog":"<p>0.3.1<\/p>\n\n<ul><li>Fixed Redis Sentinel was not working properly. Sentinel can be now configured via the UI and not via config. Also\nmultiple servers can be configured now.<\/li>\n<\/ul><p>0.3.0<\/p>\n\n<ul><li>Added support to use Redis Sentinel for automatic failover<\/li>\n<\/ul><p>0.2.6<\/p>\n\n<ul><li>When a request takes more than 2 seconds and debug tracker mode is enabled, log information about the request.<\/li>\n<\/ul><p>0.2.5<\/p>\n\n<ul><li>Use a better random number generator if available on the system to more evenly process queues.<\/li>\n<\/ul><p>0.2.4<\/p>\n\n<ul><li>The command <code>queuedtracking:monitor<\/code> will now work even when the queue is disabled<\/li>\n<\/ul><p>0.2.3<\/p>\n\n<ul><li>Added more tests and information to the <code>queuedtracking:test<\/code> command<\/li>\n<li>It is now possible to configure up to 16 workers<\/li>\n<\/ul><p>0.2.2<\/p>\n\n<ul><li>Improved output for the new <code>test<\/code> command<\/li>\n<li>New FAQ entries<\/li>\n<\/ul><p>0.2.1<\/p>\n\n<ul><li>Added a new command to test the connection to Redis. To test yor connection use <code>.\/console queuedtracking:test<\/code><\/li>\n<\/ul><p>0.2.0<\/p>\n\n<ul><li>Compatibility w\/ Piwik 2.15.<\/li>\n<\/ul><p>0.1.6<\/p>\n\n<ul><li>For bulk requests we do no longer skip all tracking requests after a tracking request that has an invalid <code>idSite<\/code> set. The same behaviour was changed in Piwik 2.14 for regular bulk requests.<\/li>\n<\/ul><p>0.1.5<\/p>\n\n<ul><li>Fixed a notice in case an incompatible Redis version is used.<\/li>\n<\/ul><p>0.1.4<\/p>\n\n<ul><li>It is now possible to start multiple workers for faster insertion from Redis to the database. This can be configured in the \"Plugin Settings\"<\/li>\n<li>Monitor does now output information whether a processor is currently processing the queue.<\/li>\n<li>Added a new command <code>queuedtracking:lock-status<\/code> that outputs the status of each queue lock. This command can also unlock a queue by using the option <code>--unlock<\/code>.<\/li>\n<li>Added a new command <code>queuedtracking:print-queued-requests<\/code> that outputs the next requests to process in each queue.<\/li>\n<li>If someone passes the option <code>-vvv<\/code> to <code>.\/console queuedtracking:process<\/code> the Tracker debug mode will be enabled and additional information will be printed to the screen.<\/li>\n<\/ul><p>0.1.2<\/p>\n\n<ul><li>Updated description on Marketplace<\/li>\n<\/ul><p>0.1.0<\/p>\n\n<ul><li>Initial Release<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/QueuedTracking\/download\/0.3.1"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"ReferrersManager",
+ "displayName":"Referrers Manager",
+ "owner":"sgiehl",
+ "description":"This plugin allows to view and manage search engines and social networks that are recognized with piwik.",
+ "homepage":"http:\/\/github.com\/sgiehl\/piwik-plugin-ReferrersManager",
+ "createdDateTime":"2014-12-23 01:14:53",
+ "donate":{"paypal":"test3@example.com",
+ "flattr":null,"bitcoin":null},"support":[],"isTheme":false,"keywords":["referrer",
+ "search",
+ "engine",
+ "social"],"basePrice":0,"authors":[{"name":"Stefan Giehl",
+ "email":"test9@example.com",
+ "homepage":"http:\/\/github.com\/sgiehl"}],"repositoryUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-ReferrersManager",
+ "lastUpdated":"2014-12-23 01:15:17",
+ "latestVersion":"1.1",
+ "numDownloads":0,"screenshots":[],"previews":[],"activity":{"numCommits":"72",
+ "numContributors":"1",
+ "lastCommitDate":"1970-01-01 00:33:35"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0",
+ "release":"2014-12-23 01:14:53",
+ "requires":{"piwik":">=2.0.4",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-ReferrersManager\/commits\/v1.0",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ReferrersManager\/download\/1.0"},{"name":"1.1",
+ "release":"2014-12-23 01:15:17",
+ "requires":{"piwik":">=2.0.4",
+ "php":">=5.3.0"},"readme":"# Piwik Referrers Manager Plugin\n\n[![Build Status](https:\/\/travis-ci.org\/sgiehl\/piwik-plugin-ReferrersManager.png?branch=master)](https:\/\/travis-ci.org\/sgiehl\/piwik-plugin-ReferrersManager) [![Flattr this git repo](http:\/\/api.flattr.com\/button\/flattr-badge-large.png)](https:\/\/flattr.com\/submit\/auto?user_id=sgiehl&url=https:\/\/github.com\/sgiehl\/piwik-plugin-ReferrersManager&title=Piwik Plugin Referrers Manager&language=&tags=github&category=software) \n\n\n## Description\n\nThis plugin allows to view and manage custom search engines and social networks that are recognized with piwik.\n\n### Requirements\n\n[Piwik](https:\/\/github.com\/piwik\/piwik) 2.0.4 or higher is required.\n\n### Features\n\n- Shows a list of all search engines and social networks defined in Piwik core.\n- Abillity to manage custom search engines and social networks\n- Abillity to disable\/enable Piwik's default social network list\n\n## Changelog\n\n- 1.0 Initial release\n\n## Support\n\nPlease direct any feedback to [stefan@piwik.org](mailto:stefan@piwik.org)\n\n## Contribute\n\nFeel free to create issues and pull requests.\n\n## License\n\nGPLv3 or later\n\n",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/sgiehl\/piwik-plugin-ReferrersManager\/commits\/v1.1",
+ "readmeHtml":{"description":"\n\n<p>This plugin allows to view and manage custom search engines and social networks that are recognized with piwik.<\/p>\n\n<h3>Requirements<\/h3>\n\n<p><a href=\"https:\/\/github.com\/piwik\/piwik\">Piwik<\/a> 2.0.4 or higher is required.<\/p>\n\n<h3>Features<\/h3>\n\n<ul><li>Shows a list of all search engines and social networks defined in Piwik core.<\/li>\n<li>Abillity to manage custom search engines and social networks<\/li>\n<li>Abillity to disable\/enable Piwik's default social network list<\/li>\n<\/ul>",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<ul><li>1.0 Initial release<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/ReferrersManager\/download\/1.1"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"RerUserDates",
+ "displayName":"Rer User Dates",
+ "owner":"RegioneER",
+ "description":"Hide custom date range selection from calendar, avoid users to set ranges in their default profile",
+ "homepage":"https:\/\/RegioneER.github.io\/projects\/",
+ "createdDateTime":"2014-05-13 11:40:04",
+ "donate":{"paypal":"supporters@piwik.org",
+ "flattr":"https:\/\/flattr.com\/thing\/131552\/Piwik-Web-Analytics-Open-Source",
+ "bitcoin":null},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/RegioneER\/RerUserDates",
+ "lastUpdated":"2015-02-05 09:36:03",
+ "latestVersion":"1.3.0",
+ "numDownloads":4930,"screenshots":[],"previews":[],"activity":{"numCommits":"28",
+ "numContributors":"3",
+ "lastCommitDate":"2015-02-05 09:35:05"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0.1",
+ "release":"2014-05-13 11:40:04",
+ "requires":{"piwik":">=2.2.3b4"},"readme":"",
+ "numDownloads":30,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/RegioneER\/RerUserDates\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/RerUserDates\/download\/1.0.1"},{"name":"1.0.2",
+ "release":"2014-05-14 14:14:07",
+ "requires":{"piwik":">=2.2.3b4"},"readme":"",
+ "numDownloads":53,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/RegioneER\/RerUserDates\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/RerUserDates\/download\/1.0.2"},{"name":"1.1.0",
+ "release":"2014-05-16 13:12:05",
+ "requires":{"piwik":">=2.2.3b4"},"readme":"",
+ "numDownloads":72,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/RegioneER\/RerUserDates\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/RerUserDates\/download\/1.1.0"},{"name":"1.1.1a",
+ "release":"2014-05-19 07:32:03",
+ "requires":{"piwik":">=2.2.3b4"},"readme":"",
+ "numDownloads":125,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/RegioneER\/RerUserDates\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/RerUserDates\/download\/1.1.1a"},{"name":"1.2.0",
+ "release":"2014-05-29 15:46:04",
+ "requires":{"piwik":">=2.2.3-b4"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/RegioneER\/RerUserDates\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/RerUserDates\/download\/1.2.0"},{"name":"1.2.1",
+ "release":"2014-05-29 16:00:04",
+ "requires":{"piwik":">=2.2.3-b4"},"readme":"",
+ "numDownloads":150,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/RegioneER\/RerUserDates\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/RerUserDates\/download\/1.2.1"},{"name":"1.2.2",
+ "release":"2014-06-17 14:28:05",
+ "requires":{"piwik":">=2.3.0"},"readme":"",
+ "numDownloads":9,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/RegioneER\/RerUserDates\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/RerUserDates\/download\/1.2.2"},{"name":"1.2.3",
+ "release":"2014-06-17 14:50:05",
+ "requires":{"piwik":">=2.3.0"},"readme":"",
+ "numDownloads":20,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/RegioneER\/RerUserDates\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/RerUserDates\/download\/1.2.3"},{"name":"1.2.4",
+ "release":"2014-06-18 14:36:05",
+ "requires":{"piwik":">=2.3.0"},"readme":"",
+ "numDownloads":171,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/RegioneER\/RerUserDates\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/RerUserDates\/download\/1.2.4"},{"name":"1.2.5",
+ "release":"2014-07-08 07:20:05",
+ "requires":{"piwik":">=2.3.0"},"readme":"",
+ "numDownloads":1664,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/RegioneER\/RerUserDates\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/RerUserDates\/download\/1.2.5"},{"name":"1.2.6",
+ "release":"2015-01-29 13:50:04",
+ "requires":{"piwik":">=2.3.0"},"readme":"",
+ "numDownloads":78,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/RegioneER\/RerUserDates\/commits\/2.0.5",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/RerUserDates\/download\/1.2.6"},{"name":"1.3.0",
+ "release":"2015-02-05 09:36:03",
+ "requires":{"piwik":">=2.8.0"},"readme":"# Piwik RerUserDates Plugin\n\n## Description\n\nThis [Piwik](http:\/\/piwik.org) Plugin lets you hide custom date range selection from calendar for regular users,\nalso lets you avoid users setting ranges as default value in their profile.\n\nEach time users asks for ranged date reports, Piwik builds it on the fly during user's browsing.\nThis slows down your server when there is a load of visits and a large number of tracked websites.\nAs you can see this action is resource intensive so when it happens, live websites tracking may become slow or inaccurate.\n\nInstalling this plugin you remove choices in the field _\"Report date to load by default\"_ in _User Settings page_ for all regular users.\nSuperadmin users setting page remains untouched and administrators will see only a notification about plugin's current behavior.\n\nAnother plugin's feature is regular users can't select any more a custom range in the calendar but Superadmins still can build reports.\n\nYou can enable or disable the two features independently by clicking checkboxes in the plugin's configuration page available in the super admin user interface.\n\nThis plugin is translated in: English, Italian and French (just send a pull request to include your favourite language, see _Can I contribute_ f.a.q.)\n\n## Installation\n\nPlease, read official [Piwik's documentation](http:\/\/piwik.org\/faq\/plugins\/#faq_21) about it.\n\n## FAQ\n\n__I would like to see a demonstration...__\nJust take a look at _screenshots_ .\n\n__Can I donate to you?__\nThanks but we can't accept money donations because we're a Government Organization.\nAll donation are linked to official Piwik project's accounts, simply help them to help us.\n\n__Can I contribute on development?__\nSure, you're welcome! Just send a [pull request on Github](https:\/\/github.com\/RegioneER\/RerNewSite\/issues)\n\n## Changelog\n\n### v1.0\n\n - First release and Marketplace integration\n - User Manager screen shot and better readme documentation\n\n### v1.1\n\n- Custom date range selection is disabled in the calendar only for regular users. A shorts jQuery snippet hides radio input and submit button.\n- Regular users who chose a range date as their default are now forced to _yesterday_ report just visiting the index page with a warning notification.\n- New French translation by @gaumondp\n\n### v.1.2\n\n- New plugin settings user interface for super admins, some better improvement and few bugs solved.\n- Solved a regression due to a lack of Settings Feature in Piwik's versions below 2.4.0\n- Merged French translation \n- Fixed Piwik compatibility with 2.10 from 2.7 by @ThaDafinser in PR #6\n\n### v.1.3\n\n- Settings environment breaks compatibility with Piwik versions < 2.8.0, thanks to @ThaDafinser.\n\n## License\n\n[GPL v3](http:\/\/www.gnu.org\/licenses\/gpl-3.0-standalone.html) or later\n\n## Support\n\nYou can ask for support and your feedback is appreciated at plugin's [issue center on Github](https:\/\/github.com\/RegioneER\/RerUserDates\/issues).\n",
+ "numDownloads":2558,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/RegioneER\/RerUserDates\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>This <a href=\"http:\/\/piwik.org\">Piwik<\/a> Plugin lets you hide custom date range selection from calendar for regular users,\nalso lets you avoid users setting ranges as default value in their profile.<\/p>\n\n<p>Each time users asks for ranged date reports, Piwik builds it on the fly during user's browsing.\nThis slows down your server when there is a load of visits and a large number of tracked websites.\nAs you can see this action is resource intensive so when it happens, live websites tracking may become slow or inaccurate.<\/p>\n\n<p>Installing this plugin you remove choices in the field <em>\"Report date to load by default\"<\/em> in <em>User Settings page<\/em> for all regular users.\nSuperadmin users setting page remains untouched and administrators will see only a notification about plugin's current behavior.<\/p>\n\n<p>Another plugin's feature is regular users can't select any more a custom range in the calendar but Superadmins still can build reports.<\/p>\n\n<p>You can enable or disable the two features independently by clicking checkboxes in the plugin's configuration page available in the super admin user interface.<\/p>\n\n<p>This plugin is translated in: English, Italian and French (just send a pull request to include your favourite language, see <em>Can I contribute<\/em> f.a.q.)<\/p>\n\n",
+ "faq":"<p><strong>I would like to see a demonstration...<\/strong>\nJust take a look at <em>screenshots<\/em> .<\/p>\n\n<p><strong>Can I donate to you?<\/strong>\nThanks but we can't accept money donations because we're a Government Organization.\nAll donation are linked to official Piwik project's accounts, simply help them to help us.<\/p>\n\n<p><strong>Can I contribute on development?<\/strong>\nSure, you're welcome! Just send a <a href=\"https:\/\/github.com\/RegioneER\/RerNewSite\/issues\">pull request on Github<\/a><\/p>",
+ "documentation":"",
+ "changelog":"<h3>v1.0<\/h3>\n\n<ul><li>First release and Marketplace integration<\/li>\n<li>User Manager screen shot and better readme documentation<\/li>\n<\/ul><h3>v1.1<\/h3>\n\n<ul><li>Custom date range selection is disabled in the calendar only for regular users. A shorts jQuery snippet hides radio input and submit button.<\/li>\n<li>Regular users who chose a range date as their default are now forced to <em>yesterday<\/em> report just visiting the index page with a warning notification.<\/li>\n<li>New French translation by @gaumondp<\/li>\n<\/ul><h3>v.1.2<\/h3>\n\n<ul><li>New plugin settings user interface for super admins, some better improvement and few bugs solved.<\/li>\n<li>Solved a regression due to a lack of Settings Feature in Piwik's versions below 2.4.0<\/li>\n<li>Merged French translation <\/li>\n<li>Fixed Piwik compatibility with 2.10 from 2.7 by @ThaDafinser in PR #6<\/li>\n<\/ul><h3>v.1.3<\/h3>\n\n<ul><li>Settings environment breaks compatibility with Piwik versions &lt; 2.8.0, thanks to @ThaDafinser.<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/RerUserDates\/download\/1.3.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"SecurityInfo",
+ "displayName":"Security Info",
+ "owner":"piwik",
+ "description":"Provides security information about your PHP environment and offers suggestions based on PhpSecInfo from the PHP Security Consortium.",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2014-12-23 01:17:19",
+ "donate":{},"support":[],"isTheme":false,"keywords":["security",
+ "phpsec"],"basePrice":0,"authors":[{"name":"Piwik",
+ "email":"test1@example.com",
+ "homepage":"http:\/\/piwik.org"}],"repositoryUrl":"https:\/\/github.com\/piwik\/plugin-SecurityInfo",
+ "lastUpdated":"2014-12-23 01:18:41",
+ "latestVersion":"1.0.4",
+ "numDownloads":0,"screenshots":["https:\/\/plugins.piwik.org\/SecurityInfo\/images\/Security_Info.png"],"previews":[],"activity":{"numCommits":"150",
+ "numContributors":"7",
+ "lastCommitDate":"1970-01-01 00:33:35"},"featured":true,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0.3",
+ "release":"2014-12-23 01:17:21",
+ "requires":{"piwik":">=2.0.4-b5",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-SecurityInfo\/commits\/1.0.3",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SecurityInfo\/download\/1.0.3"},{"name":"1.0.4",
+ "release":"2014-12-23 01:18:41",
+ "requires":{"piwik":">=2.0.4-b5",
+ "php":">=5.3.0"},"readme":"# Piwik SecurityInfo Plugin\n\n## Description\n\nWe highly recommend that all Piwik administrators enable the SecurityInfo plugin, and then view the Settings. The plugin is a tool in a multilayered security approach.\n\nPerformed checks include for instance usage of latest PHP version, usage of latest Piwik version, usage of PHP ini settings like magic_quotes_gpc and more. \n\n## FAQ\n\n__Does the plugin replace secure development practices or audit the code\/application?__\n\nNo, it doesn't. It just gives you some information based on PhpSecInfo from the PHP Security Consortium.\n\n## Changelog\n\n1.0 Initial release\n\n## Support\n\nPlease direct any feedback to [hello@piwik.org](mailto:hello@piwik.org)\n",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-SecurityInfo\/commits\/1.0.4",
+ "readmeHtml":{"description":"\n\n<p>We highly recommend that all Piwik administrators enable the SecurityInfo plugin, and then view the Settings. The plugin is a tool in a multilayered security approach.<\/p>\n\n<p>Performed checks include for instance usage of latest PHP version, usage of latest Piwik version, usage of PHP ini settings like magic_quotes_gpc and more.<\/p>\n\n",
+ "faq":"<p><strong>Does the plugin replace secure development practices or audit the code\/application?<\/strong><\/p>\n\n<p>No, it doesn't. It just gives you some information based on PhpSecInfo from the PHP Security Consortium.<\/p>",
+ "documentation":"",
+ "changelog":"<p>1.0 Initial release<\/p>"},"download":"\/api\/2.0\/plugins\/SecurityInfo\/download\/1.0.4"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"ServerMonitor",
+ "displayName":"Server Monitor",
+ "owner":"Invision-Technology-Soultions",
+ "description":"Use Piwik as a front-end application to the Munin server monitoring tool. This is a Piwik plugin that reads the raw RRD data files from your Munin mas",
+ "homepage":"https:\/\/github.com\/Invision-Technology-Soultions\/ServerMonitor",
+ "createdDateTime":"2015-07-13 06:44:04",
+ "donate":{"paypal":"support@invisionit.com.au",
+ "bitcoin":null},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/Invision-Technology-Soultions\/ServerMonitor",
+ "lastUpdated":"2015-08-08 07:10:03",
+ "latestVersion":"0.1.1",
+ "numDownloads":2514,"screenshots":[],"previews":[],"activity":{"numCommits":"12",
+ "numContributors":"1",
+ "lastCommitDate":"2015-08-08 07:09:13"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2015-07-13 06:44:04",
+ "requires":{"piwik":">=2.14.0"},"readme":"",
+ "numDownloads":312,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/Invision-Technology-Soultions\/ServerMonitor\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ServerMonitor\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2015-08-08 07:10:03",
+ "requires":{"piwik":">=2.14.0"},"readme":"# Piwik ServerMonitor Plugin\n\n## Description\n\nUse [Piwik](http:\/\/piwik.org\/) as your analytics platform for statistics collected by [Munin server monitoring](http:\/\/munin-monitoring.org\/) tool. \n\nThis is a [Piwik](http:\/\/piwik.org\/) plugin that reads the raw RRD files from your Munin master server. \n\nThis enhances the Munin functionality by providing all the great benefits of Piwik, such as\n \n* Real time data\n* Customizable Dashboards\n* Enhanced Graphing\n* Access Munin data using Piwik API\n* TODO: Access Munin data using Piwik Mobile\n* TODO: Scheduled reports \n* TODO: Custom alerts for Server Monitoring - using [Piwik CustomAlerts plugin](https:\/\/github.com\/piwik\/plugin-CustomAlerts) \n\n## Screenshots\n\nThe following screenshots show how Munin is integrated with Piwik Analytics platform.\n\n__Piwik Menu__\n\nThe Piwik Menu is automatically populated based on the Munin configuration.\n\n![Piwik Menu](https:\/\/raw.githubusercontent.com\/Invision-Technology-Soultions\/ServerMonitor\/master\/screenshots\/menu.png)\n\nYou can filter results based on Munin node server.\n\n![Server Filter](https:\/\/raw.githubusercontent.com\/Invision-Technology-Soultions\/ServerMonitor\/master\/screenshots\/serverfilter.png)\n\n__Piwik Graphs__\n\nBelow are some example graphs that are created based on Munin data.\n\n![Bind monitoring](https:\/\/raw.githubusercontent.com\/Invision-Technology-Soultions\/ServerMonitor\/master\/screenshots\/bind.png)\n![Ngnix monitoring](https:\/\/raw.githubusercontent.com\/Invision-Technology-Soultions\/ServerMonitor\/master\/screenshots\/nginx.png)\n![CPU monitoring](https:\/\/raw.githubusercontent.com\/Invision-Technology-Soultions\/ServerMonitor\/master\/screenshots\/cpu.png)\n![Memory monitoring](https:\/\/raw.githubusercontent.com\/Invision-Technology-Soultions\/ServerMonitor\/master\/screenshots\/memory.png)\n\n__Piwik Dashboard Widgets__\n\nAll Munin metrics are available as dashboard widgets, allowing you to create custom dashboards for Server Monitoring.\n\n![Widgets](https:\/\/raw.githubusercontent.com\/Invision-Technology-Soultions\/ServerMonitor\/master\/screenshots\/widgets.png)\n\n![Custom Dashboard](https:\/\/raw.githubusercontent.com\/Invision-Technology-Soultions\/ServerMonitor\/master\/screenshots\/dashboard.png)\n\n## Requirements\n\n* Requires [Piwik](http:\/\/piwik.org\/) (Tested >=2.13) and [Munin server monitoring](http:\/\/guide.munin-monitoring.org\/en\/latest\/master\/) (Tested 1.4.6) installed on same server.\n* [PHP RRD Functions](http:\/\/php.net\/manual\/en\/book.rrd.php) (Tested 1.1.3)\n* Piwik requires read-only access to Munin datafile and RRD files located in: \/var\/lib\/munin\/ \n\n## Installation\n\n* Ensure you meet the above requirements\n* Install via Piwik Marketplace\n\n## Roadmap\n \n* Integration with Piwik Scheduled reports\n* Integration with Piwik Custom Alerts\n* Integration with Piwik Mobile App\n* Support data collection from remote Munin master\n* Store Munin RRD data in Piwik database for historical statistics \n\n## Changelog\n\n* 0.1.0 - First beta\n* 0.1.5 - Tested Munin v2.25, Support Piwik Periods: Week, Month, Year\n\n## License\n\nGPL v3 or later\n\n## Support\n\nIf you require proffesional services, please contact us at [support@invisionit.com.au](mailto:support@invisionit.com.au)",
+ "numDownloads":2202,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/Invision-Technology-Soultions\/ServerMonitor\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>Use <a href=\"http:\/\/piwik.org\/\">Piwik<\/a> as your analytics platform for statistics collected by <a href=\"http:\/\/munin-monitoring.org\/\">Munin server monitoring<\/a> tool.<\/p>\n\n<p>This is a <a href=\"http:\/\/piwik.org\/\">Piwik<\/a> plugin that reads the raw RRD files from your Munin master server.<\/p>\n\n<p>This enhances the Munin functionality by providing all the great benefits of Piwik, such as<\/p>\n\n<ul><li>Real time data<\/li>\n<li>Customizable Dashboards<\/li>\n<li>Enhanced Graphing<\/li>\n<li>Access Munin data using Piwik API<\/li>\n<li>TODO: Access Munin data using Piwik Mobile<\/li>\n<li>TODO: Scheduled reports <\/li>\n<li>TODO: Custom alerts for Server Monitoring - using <a href=\"https:\/\/github.com\/piwik\/plugin-CustomAlerts\">Piwik CustomAlerts plugin<\/a> <\/li>\n<\/ul>",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<ul><li>0.1.0 - First beta<\/li>\n<li>0.1.5 - Tested Munin v2.25, Support Piwik Periods: Week, Month, Year<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/ServerMonitor\/download\/0.1.1"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"ShibbolethLogin",
+ "displayName":"Shibboleth Login",
+ "owner":"pouyana",
+ "description":"Shibboleth Login Plugin for Piwik",
+ "homepage":"https:\/\/github.com\/pouyana\/LoginShibboleth",
+ "createdDateTime":"2015-08-10 12:00:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/pouyana\/LoginShibboleth",
+ "lastUpdated":"2015-09-07 08:10:03",
+ "latestVersion":"1.1.4",
+ "numDownloads":1603,"screenshots":[],"previews":[],"activity":{"numCommits":"10",
+ "numContributors":"1",
+ "lastCommitDate":"2015-09-07 08:18:19"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.1.2",
+ "release":"2015-08-10 12:00:03",
+ "requires":{"piwik":">=2.2.0",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":1,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/pouyana\/LoginShibboleth\/commits\/v0.1.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ShibbolethLogin\/download\/1.1.2"},{"name":"1.1.3",
+ "release":"2015-08-10 12:10:02",
+ "requires":{"piwik":">=2.2.0",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":187,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/pouyana\/LoginShibboleth\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ShibbolethLogin\/download\/1.1.3"},{"name":"1.1.4",
+ "release":"2015-09-07 08:10:03",
+ "requires":{"piwik":">=2.2.0",
+ "php":">=5.3.0"},"readme":"# Login Shibboleth\nLogin Shibboleth replaces the default Piwik Login plug-in and offers the user a way to incorporate existing Shibboleth with Piwik.\n\n# Pre-Requirement\nTo use this plug-in you should have the following pre-requirements set:\n- Your Shibboleth system is up and running and is connected to apache.\n- You can access some data from $\\_SERVER so, you can read user data from $\\_SERVER.\n- If you want to combine Shibboleth with LDAP, you should have php5-ldap or your distribution version of php-ldap installed.\n- You are familiar with PHP coding, so you can change the custom parameters and function to suite your system.\n- You follow the same user Model as Piwik. If you change the user Model to contain some extra informations, you should also change this plug-in's handling on the way users are created.\n- This plug-in supports the following user informations retrieval:\n - username (login)\n - email\n - alias (name or fullname)\n - token (random string length 32chars)\n - websites (access_level and websites id)\n\n# Configuration\nFor the configuration following settings should be set in config\/config.ini.php\n\n## Datasource:\n\n```\n[datasource] \ndatasource[\"alias\"] = \"shib\" \ndatasource[\"login\"] = \"shib\"\ndatasource[\"email\"] = \"shib\"\ndatasource[\"websites\"] = \"shib, ldap\"\ndatasource[\"superuser\"] = \"shib\"\n```\n\nThe datasource helps you to change the way this plug-in accesses the information needed to generate or add users to Piwik. Normally Shibboleth (shib) should suffice. The software will look at the options in config.ini.php as it is ordered. If you want Shibboleth to prevail, add it first. If your first choice for an option is LDAP add (ldap) first. The MySQL database will automatically comes to help if none of the options give a valid result, which is most of the time unlikely. if your User Model is different, you can set other options here. If you use other resources than this tree, you can also define it here and the use the AuthLib class as parent class and extend it to your own Model.\n\n## Shib:\n\n```\n[shib]\nshib[\"login\"] = \"uid\"\nshib[\"alias\"] = \"fullName\"\nshib[\"email\"] = \"mail\"\nshib[\"superuser\"] = \"groupMembership\"\nshib[\"superuser_type\"] = \"string\"\nshib[\"superuser_param\"] = \"cn=RZ-Piwik-Admin\"\nshib[\"to_get_view\"] = \"groupMembership\"\nshib[\"to_get_view_type\"] = \"string\"\nshib[\"to_get_view_param\"] = \"cn=RZ-Piwik-View\"\nshib[\"to_get_admin\"] = \"groupMembership\"\nshib[\"to_get_admin_type\"] = \"string\"\nshib[\"to_get_admin_param\"] = \"cn=RZ-Piwik-View\"\n```\n\nIn shib you can set the Shibboleth parameter in which user data is caught. if you have some other settings it is possible to set them here. The information would be available in Shibboleth class.\n\n## LDAP:\n\n```\n[ldap]\nldap[\"host\"] = \"ldaphost\"\nldap[\"port\"] = 636\nldap[\"user\"] = \"binduser\"\nldap[\"password\"] = \"bindpassword\"\nldap[\"dn\"] = \"binddn\"\nldap[\"to_filter_view\"] = \"(userfilter)\"\nldap[\"to_get_view\"] = \"website attr\"\nldap[\"to_get_type_view\"] = \"string\"\nldap[\"to_filter_admin\"] = \"\"\nldap[\"to_get_admin\"] = \"\"\nldap[\"to_get_type_admin\"] = \"\"\n```\n\nThe same applies here just like the Shibboleth settings above.\n\n## Apache Settings\nTo exclude the public part of piwik, mainly the tracker and piwik.php from the Shibboleth Auth, the following settings have been set in apache VirtualHost.\n\n###Apache 2.2\n```\n<LocationMatch \"^\/(?!piwik.js|piwik.php|opt_out.php)\">\n AuthType shibboleth\n ShibRequireSession On\n require shibboleth\n require valid-user\n satisfy any\n<\/LocationMatch>\n```\n\n###Apache 2.4\n```\n<LocationMatch \"^\/(?!piwik.js|piwik.php|opt_out.php)\">\n AuthType Shibboleth\n ShibrequestSetting requireSession 1\n Require valid-user\n satisfy all\n<\/LocationMatch>\n```\n\n# License\n> The MIT License (MIT)\n\n> Copyright (c) 2015 Pouyan Azari \n> Copyright (c) 2015 University of W\u00fcrzburg\n\n> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and\/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\n> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\n> THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
+ "numDownloads":1415,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/pouyana\/LoginShibboleth\/commits\/0.1.3",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/ShibbolethLogin\/download\/1.1.4"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"ShortcodeTracker",
+ "displayName":"Shortcode Tracker",
+ "owner":"mgazdzik",
+ "description":"Plugin allowing user to create shortcodes and track their usage within Piwik. Also integrates with UI to deliver user-friendly interaction.",
+ "homepage":"http:\/\/mgazdzik.pl\/shortcodetracker",
+ "createdDateTime":"2015-11-11 12:46:02",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/mgazdzik\/plugin-ShortcodeTracker",
+ "lastUpdated":"2015-12-12 16:54:03",
+ "latestVersion":"0.6.2",
+ "numDownloads":1454,"screenshots":[],"previews":[],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":"2015-12-20 11:58:06"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.4.3",
+ "release":"2015-11-11 12:46:03",
+ "requires":{"piwik":">=2.14.0"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/mgazdzik\/plugin-ShortcodeTracker\/commits\/0.1.8",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ShortcodeTracker\/download\/0.4.3"},{"name":"0.4.5",
+ "release":"2015-11-11 12:52:03",
+ "requires":{"piwik":">=2.14.0"},"readme":"",
+ "numDownloads":214,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/mgazdzik\/plugin-ShortcodeTracker\/commits\/0.1.9",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ShortcodeTracker\/download\/0.4.5"},{"name":"0.5.0",
+ "release":"2015-12-10 21:22:03",
+ "requires":{"piwik":">=2.14.0"},"readme":"",
+ "numDownloads":46,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/mgazdzik\/plugin-ShortcodeTracker\/commits\/1.0.3",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ShortcodeTracker\/download\/0.5.0"},{"name":"0.6.0",
+ "release":"2015-12-12 16:48:03",
+ "requires":{"piwik":">=2.14.0"},"readme":"",
+ "numDownloads":1,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/mgazdzik\/plugin-ShortcodeTracker\/commits\/0.0.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/ShortcodeTracker\/download\/0.6.0"},{"name":"0.6.2",
+ "release":"2015-12-12 16:54:03",
+ "requires":{"piwik":">=2.14.0"},"readme":"# ShortcodeTracker Plugin\n\n| Branch | Status |\n| --- | --- |\n| Master | [![Build Status](https:\/\/travis-ci.org\/mgazdzik\/plugin-ShortcodeTracker.svg?branch=master)](https:\/\/travis-ci.org\/mgazdzik\/plugin-ShortcodeTracker) |\n| Develop | [![Build Status](https:\/\/travis-ci.org\/mgazdzik\/plugin-ShortcodeTracker.svg?branch=develop)](https:\/\/travis-ci.org\/mgazdzik\/plugin-ShortcodeTracker) |\n\n## Description\n\nPlugin allows to turn Piwik instance into URL Shortener.\n\nBasic features:\n\n* easily create shortcode from any page you track in Piwik (integration with Actions report UI),\n* create shortcode for any custom URL you want,\n* perform redirects using your Piwik instance,\n* get usage statistics for shortcodes handled by your instance\n * get best performing URL's on websites you track,\n * external URLs redirect statistics,\n* see which URLs are being shortened and visited most often - also for external URLs not tracked in your Piwik.\n\nGoodness coming:\n\n* for redirect performance improvement, store your shortcodes in storage like Memcache or Redis,\n* attributing shortcode redirects with actual visits on your page,\n* more advanced reports,\n\nBefore using, please read content in [`Setup`](https:\/\/github.com\/mgazdzik\/plugin-ShortcodeTracker#setup) section \nas it contains steps required to make plugin work with your Piwik instance!\n\n## Usage\n\nAfter correctly setting up this plugin (please see section below), you are ready for shortening your Urls.\n\nThere is one new section in top reporting menu called \"Shortcodes\".\n\nThis view gives you possibility to shorten any URL you want and operate with shortcode retrieved.\n\nAdditionally this plugin integrates with Page URL's report - hover over URL you want to shorten and click scissors icon.\n\nThis will call popup with appropriate shortcode, so you don't need to manually shorten any URL you already track with your\nPiwik instance.\n\nEnjoy!\n\n## Setup\n\n### Webserver\nBesides of functional Piwik instance with this plugin enabled you will also need special configuration for your webserver.\n\nIt's purpose is to redirect any short url hitting your server to proper API method doing the magic.\n\nBelow you can find example configurations\n\n* [for NGINX webserver vhost](docs\/nginx_config.md)\n* [for Apache2 webserver .htaccess file](docs\/apache_config.md)\n\n**Please be aware that in your case this configuration may be different, so please contact your system\/webserver\nadmin for advisory!**\n\n\n### Plugin\n\nBefore you can start shortening your URLs you need to perform following steps:\n\n* go to Administration -> Plugins,\n* find \"ShortcodeTracker\" plugin and click `enable`,\n\nAfter you confirm that plugin has been enabled:\n* go to Administration -> Plugin Settings,\n* go to ShortcodeTracker section,\n* fill in Shortener URL input,\n* if you want to track external sites, you need to decide to which Piwik page those actions will be attributed (see\nExternal redirects tracking section below),\n* click 'save',\n* **additionally you have to make Shortener URL a trusted host for Piwik by entering it in settings section**,\n\nThis is necessary to perform, as otherwise you will not be able to generate shortened URLs or use them with Piwik.\n\n### External redirects tracking\n\nIt is possible to also track redirect actions for external URLs (i.e. which URL doesn't match any page tracked within\nyour Piwik instance). However, it is required to decide to which site this traffic will be attributed to.\n\nIt is recommended to create a separate Website in Piwik instance only dedicated to this traffic, so that other websites\nreports won't be affected by redirect events.\n\nTo select which site should collect redirects:\n\n* go to `Plugin Settings` section,\n* from dropdown you can select site for external redirects,\n* alternatively you can select not to track external redirects by setting `Do not collect external shortcode redirects`,\n* click save\n\n## Changelog\n\n* 0.6.2\n * Sort out mistakenly pushed tag\n\n\n* 0.6.0\n * Shortcode usage report added link to shortened page to for easier recognition of what is being shortened and used most,\n * Display summarized report displaying which URLs were visited most via shortcode redirects,\n\n\n* 0.5.0\n * Add statistics collection for redirects to pages not tracked with Piwik (external pages)\n * collect redirect statistics into Site you choose in interface,\n * aggregate and display report for external shortcodes in separate view\n\n\n* 0.4.5\n * fix README formating for sake of Plugin market\n \n \n* 0.4.4\n * add license to plugin.json\n \n \n* 0.4.3\n * fix plugin.json structure for Plugin market\n\n\n* 0.4.2\n * Added missing changelog\n\n\n* 0.4.0\n * Piwik Plugin market release\n\n\n* 0.3.0\n * Tuned travis build file\n * Mark Shortcodes as internal during creating\n * Track custom event with \"redirect\" category upon each redirect for internal Shortcode\n * Secure API methods from anonymous user usage\n * Add shortcode report for internally tracked URLs:\n * Create new visit during redirect (store referrer)\n * Add Shortcode usage report based on Custom Events plugin API\n\n\n* 0.2.0\n * added Travis build badges for master and develop branches\n * fixed existing unit tests\n * slight refactor in terms of class naming\n * added integration test for API methods\n\n\n* 0.1.0\n * API allowing to create and retrieve shortcodes,\n * basic storage in MySQL, but possible to add other caching layers - for ex. Memcache, Redis,\n * unit tests covering core logic,\n * redirect API method that will preform appropriate redirects for incoming shortcode requests,\n * basic setup guide involving Apache2 and Nginx configs,\n * settings section allowing user to configure Shortener base URL (which may and should be different than Piwik instance)\n\n## Backlog\n\n\n\n* Migrate plugin to work with Piwik 2.15 LTS version,\n* Add advanced report for each shortcode\n * stitch every redirect event with following action,\n * add new referrer type (shortcode),\n * aggregate statistics,\n * add segment for referrer,\n* Refactor plugin so it's possible to cover Model.php with tests,\n* Add queue system for tracking redirect events to improve performance of redirect feature,\n* Add integration test for redirect tracking,\n* Add support for at least one caching system (redis\/memcache),\n* Improve HTML elements designs\/styles,\n* Throw exception\/signal in UI in case Shortener URL is not changed,\n* Introduce Shortener base URL validation (in Settings section),\n* introduce value object to store Shortcode,\n* handle case when given idsite has multiple domains assigned (currently it's only for main domain URL),\n\n\n## Support\n\nPlease direct any feedback regarding plugin to Github repository issue tracker available at\n[https:\/\/github.com\/mgazdzik\/plugin-ShortcodeTracker\/issues](https:\/\/github.com\/mgazdzik\/plugin-ShortcodeTracker\/issues).\n\n\n## Credits\nScissors icon visible in Actions report is originating from\n[https:\/\/icons8.com\/](https:\/\/icons8.com\/).\n",
+ "numDownloads":1193,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/mgazdzik\/plugin-ShortcodeTracker\/commits\/0.43b",
+ "readmeHtml":{"description":"\n\n<p>Plugin allows to turn Piwik instance into URL Shortener.<\/p>\n\n<p>Basic features:<\/p>\n\n<ul><li>easily create shortcode from any page you track in Piwik (integration with Actions report UI),<\/li>\n<li>create shortcode for any custom URL you want,<\/li>\n<li>perform redirects using your Piwik instance,<\/li>\n<li>get usage statistics for shortcodes handled by your instance\n\n<ul><li>get best performing URL's on websites you track,<\/li>\n<li>external URLs redirect statistics,<\/li>\n<\/ul><\/li>\n<li>see which URLs are being shortened and visited most often - also for external URLs not tracked in your Piwik.<\/li>\n<\/ul><p>Goodness coming:<\/p>\n\n<ul><li>for redirect performance improvement, store your shortcodes in storage like Memcache or Redis,<\/li>\n<li>attributing shortcode redirects with actual visits on your page,<\/li>\n<li>more advanced reports,<\/li>\n<\/ul><p>Before using, please read content in <a href=\"https:\/\/github.com\/mgazdzik\/plugin-ShortcodeTracker#setup\"><code>Setup<\/code><\/a> section \nas it contains steps required to make plugin work with your Piwik instance!<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<ul><li><p>0.6.2<\/p>\n\n<ul><li>Sort out mistakenly pushed tag<\/li>\n<\/ul><\/li>\n<li><p>0.6.0<\/p>\n\n<ul><li>Shortcode usage report added link to shortened page to for easier recognition of what is being shortened and used most,<\/li>\n<li>Display summarized report displaying which URLs were visited most via shortcode redirects,<\/li>\n<\/ul><\/li>\n<li><p>0.5.0<\/p>\n\n<ul><li>Add statistics collection for redirects to pages not tracked with Piwik (external pages)\n\n<ul><li>collect redirect statistics into Site you choose in interface,<\/li>\n<li>aggregate and display report for external shortcodes in separate view<\/li>\n<\/ul><\/li>\n<\/ul><\/li>\n<li><p>0.4.5<\/p>\n\n<ul><li>fix README formating for sake of Plugin market<\/li>\n<\/ul><\/li>\n<li><p>0.4.4<\/p>\n\n<ul><li>add license to plugin.json<\/li>\n<\/ul><\/li>\n<li><p>0.4.3<\/p>\n\n<ul><li>fix plugin.json structure for Plugin market<\/li>\n<\/ul><\/li>\n<li><p>0.4.2<\/p>\n\n<ul><li>Added missing changelog<\/li>\n<\/ul><\/li>\n<li><p>0.4.0<\/p>\n\n<ul><li>Piwik Plugin market release<\/li>\n<\/ul><\/li>\n<li><p>0.3.0<\/p>\n\n<ul><li>Tuned travis build file<\/li>\n<li>Mark Shortcodes as internal during creating<\/li>\n<li>Track custom event with \"redirect\" category upon each redirect for internal Shortcode<\/li>\n<li>Secure API methods from anonymous user usage<\/li>\n<li>Add shortcode report for internally tracked URLs:\n\n<ul><li>Create new visit during redirect (store referrer)<\/li>\n<li>Add Shortcode usage report based on Custom Events plugin API<\/li>\n<\/ul><\/li>\n<\/ul><\/li>\n<li><p>0.2.0<\/p>\n\n<ul><li>added Travis build badges for master and develop branches<\/li>\n<li>fixed existing unit tests<\/li>\n<li>slight refactor in terms of class naming<\/li>\n<li>added integration test for API methods<\/li>\n<\/ul><\/li>\n<li><p>0.1.0<\/p>\n\n<ul><li>API allowing to create and retrieve shortcodes,<\/li>\n<li>basic storage in MySQL, but possible to add other caching layers - for ex. Memcache, Redis,<\/li>\n<li>unit tests covering core logic,<\/li>\n<li>redirect API method that will preform appropriate redirects for incoming shortcode requests,<\/li>\n<li>basic setup guide involving Apache2 and Nginx configs,<\/li>\n<li>settings section allowing user to configure Shortener base URL (which may and should be different than Piwik instance)<\/li>\n<\/ul><\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/ShortcodeTracker\/download\/0.6.2"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"SimplePageBuilder",
+ "displayName":"Simple Page Builder",
+ "owner":"PiwikPRO",
+ "description":"Lets you add a custom page to Piwik. The page will be visible to all users in the top menu.",
+ "homepage":"https:\/\/github.com\/PiwikPRO\/SimplePageBuilder",
+ "createdDateTime":"2014-12-08 04:50:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SimplePageBuilder",
+ "lastUpdated":"2016-03-08 12:08:04",
+ "latestVersion":"1.2.0",
+ "numDownloads":3892,"screenshots":[],"previews":[],"activity":{"numCommits":"38",
+ "numContributors":"5",
+ "lastCommitDate":"2016-04-15 22:58:07"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0.1",
+ "release":"2014-12-08 04:50:03",
+ "requires":{"piwik":">=2.10.0-b2"},"readme":"",
+ "numDownloads":14,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SimplePageBuilder\/commits\/v0.1.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SimplePageBuilder\/download\/1.0.1"},{"name":"1.0.2",
+ "release":"2014-12-09 02:18:04",
+ "requires":{"piwik":">=2.10.0-b2"},"readme":"",
+ "numDownloads":52,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SimplePageBuilder\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SimplePageBuilder\/download\/1.0.2"},{"name":"1.0.3",
+ "release":"2014-12-12 14:08:03",
+ "requires":{"piwik":">=2.10.0-b2"},"readme":"",
+ "numDownloads":289,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SimplePageBuilder\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SimplePageBuilder\/download\/1.0.3"},{"name":"1.0.4",
+ "release":"2015-01-19 14:28:03",
+ "requires":{"piwik":">=2.9.0"},"readme":"",
+ "numDownloads":1234,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SimplePageBuilder\/commits\/0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SimplePageBuilder\/download\/1.0.4"},{"name":"1.0.5",
+ "release":"2015-07-28 10:00:03",
+ "requires":{"piwik":">=2.9.0"},"readme":"",
+ "numDownloads":557,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SimplePageBuilder\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SimplePageBuilder\/download\/1.0.5"},{"name":"1.1.0",
+ "release":"2015-10-21 22:20:03",
+ "requires":{"piwik":">=2.15.0-b15"},"readme":"",
+ "numDownloads":1124,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SimplePageBuilder\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SimplePageBuilder\/download\/1.1.0"},{"name":"1.2.0",
+ "release":"2016-03-08 12:08:04",
+ "requires":{"piwik":">=2.16.0"},"readme":"# Piwik CustomPage Plugin\n\nMaster [![Build Status](https:\/\/travis-ci.org\/PiwikPRO\/plugin-SimplePageBuilder.svg?branch=master)](https:\/\/travis-ci.org\/PiwikPRO\/plugin-SimplePageBuilder)\nDevelop [![Build Status](https:\/\/travis-ci.org\/PiwikPRO\/plugin-SimplePageBuilder.svg?branch=develop)](https:\/\/travis-ci.org\/PiwikPRO\/plugin-SimplePageBuilder)\n\n## Description\n\nThis plugin lets you add a page with custom HTML content to Piwik. The page will be linked in the top menu and can be edited by the super user.\n\n## Changelog\n\n**1.2.0**\n\n- PPCDEV-2609 Compatibility with Piwik 2.16.0\n\n**1.1.0**\n\n- Compatibility with Piwik 2.15.\n\n**1.0.3**\n\n- added composer.json\n\n**1.0.2**\n\n- Correctly replace {date} when period is range.\n- In example use H2 in the sample text for the custom page title\n\n**1.0.0**\n\n- First stable version\n\n## Support\n\nPlease direct any feedback to [SimplePageBuilder issues on GitHub](https:\/\/github.com\/PiwikPRO\/plugin-SimplePageBuilder\/issues).\n",
+ "numDownloads":622,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SimplePageBuilder\/commits\/v0.1.2",
+ "readmeHtml":{"description":"\n\n<p>This plugin lets you add a page with custom HTML content to Piwik. The page will be linked in the top menu and can be edited by the super user.<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<p><strong>1.2.0<\/strong><\/p>\n\n<ul><li>PPCDEV-2609 Compatibility with Piwik 2.16.0<\/li>\n<\/ul><p><strong>1.1.0<\/strong><\/p>\n\n<ul><li>Compatibility with Piwik 2.15.<\/li>\n<\/ul><p><strong>1.0.3<\/strong><\/p>\n\n<ul><li>added composer.json<\/li>\n<\/ul><p><strong>1.0.2<\/strong><\/p>\n\n<ul><li>Correctly replace {date} when period is range.<\/li>\n<li>In example use H2 in the sample text for the custom page title<\/li>\n<\/ul><p><strong>1.0.0<\/strong><\/p>\n\n<ul><li>First stable version<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/SimplePageBuilder\/download\/1.2.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"SimpleSysMon",
+ "displayName":"Simple Sys Mon",
+ "owner":"job963",
+ "description":"Simple System Monitor for visualizing system data like cpu load, memory use or network traffic",
+ "homepage":"https:\/\/github.com\/job963\/SimpleSysMon",
+ "createdDateTime":"2014-09-16 07:38:04",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/job963\/SimpleSysMon",
+ "lastUpdated":"2015-03-15 14:34:04",
+ "latestVersion":"0.3.1",
+ "numDownloads":6929,"screenshots":[],"previews":[],"activity":{"numCommits":"25",
+ "numContributors":"2",
+ "lastCommitDate":"2015-03-15 14:33:05"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2014-09-16 07:38:04",
+ "requires":{},"readme":"",
+ "numDownloads":53,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/job963\/SimpleSysMon\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SimpleSysMon\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2014-09-17 18:30:04",
+ "requires":{},"readme":"",
+ "numDownloads":3,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/job963\/SimpleSysMon\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SimpleSysMon\/download\/0.1.1"},{"name":"0.1.3",
+ "release":"2014-09-17 18:50:03",
+ "requires":{},"readme":"",
+ "numDownloads":1108,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/job963\/SimpleSysMon\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SimpleSysMon\/download\/0.1.3"},{"name":"0.2",
+ "release":"2014-12-09 20:36:03",
+ "requires":{},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/job963\/SimpleSysMon\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SimpleSysMon\/download\/0.2"},{"name":"0.2.1",
+ "release":"2014-12-09 20:40:04",
+ "requires":{},"readme":"",
+ "numDownloads":936,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/job963\/SimpleSysMon\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SimpleSysMon\/download\/0.2.1"},{"name":"0.3",
+ "release":"2015-03-01 11:36:04",
+ "requires":{},"readme":"",
+ "numDownloads":339,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/job963\/SimpleSysMon\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SimpleSysMon\/download\/0.3"},{"name":"0.3.1",
+ "release":"2015-03-15 14:34:04",
+ "requires":{},"readme":"# Piwik Simple System Monitor Plugin\n\n## Description\n\nThis plugin shows how much load your webserver does have, where Piwik (and maybe your main website) is running. Additionally the free or the used memory will be displayed.\n\nThe display will be refreshed automatically as often as you like. This can be setup by yourself in the plugin settings.\n\n\n## Screenshots\n**Table Widget** \n![](https:\/\/github.com\/job963\/SimpleSysMon\/raw\/master\/screenshots\/widgetLiveSysLoad-EN.png)\n\n**Graph Widget** \n![](https:\/\/github.com\/job963\/SimpleSysMon\/raw\/master\/screenshots\/widgetLiveSysLoadBars-EN.png)\n\n**Plugin Settings** \n![](https:\/\/github.com\/job963\/SimpleSysMon\/raw\/master\/screenshots\/settingLiveSysLoad-EN.png)\n\n\n## Installation\n\nInstall it via [Piwik Marketplace](http:\/\/plugins.piwik.org\/).\n\nOR \n\nInstall manually:\n\n1. Clone the plugin into the plugins directory of your Piwik installation.\n\n ```\n cd plugins\/\n git clone https:\/\/github.com\/job963\/SimpleSysMon.git SimpleSysMon\n ```\n\n2. Login as superuser into your Piwik installation and activate the plugin under Settings -> Plugins\n\n3. Goto Settings -> Plugin Settings an setup the values for the widget.\n\n4. You will now find the widget under the Simple System Monitor -> Live System Load.\n\n## Changelog\n\n* **0.1.0 Initial release**\n * Display of CPU load\n * Display of free or used memory \n\n* **0.1.1 Initial release**\n * Corrections and error trapping for shared websites where the some values aren't accessible \n \n* **0.2.0 Graphical display**\n * Display as bar chart for system load and memory use added \n \n* **0.2.1 Hungarian language**\n * Hungarian language added \n \n* **0.3.0 Bar charts for network and disk**\n * Two new bars for displaying network traffic (upload and download) and disk usage added\n * Additional to the percentage values, the real values are display on hover \n\n\n## FAQ\n\n**How is the CPU load calculated?** \nFor the CPU load the PHP function sys_getloadavg() is used and divided by the number of cores.\n \n**Why is there a difference between free and used memory?** \nThere are three \"memory parts\" under Linux: \n\n* Used memory\n* Cache\n* Free memory\n \nThe sum of these three parts will be equal to the total memory. But only `used memory` and `free memory` are available as options (in settings).\n \n**What is the value for 100% network traffic** \nYou can setup the value for the maximum network traffic in the plugin system settings. The value there must be specified in kB\/s (kilobyte per second). This value is used as 100% network traffic.\n \n**Does the plugin work on a shared webspace?** \nIn the most cases a shared webspace doesn't have access to system information. Therefore in these cases, the plugin cannot be used. The pseudo file system \/proc must be accessible.\n \n**Does the plugin work on Windows system?** \nIf the server where Piwik is running is using Windows as OS, the plugin doesn't work yet. \nIf just your browser is running under Windows (and the server runs under Linux) this plugin works well.\n\n## License\n\nGPL v3 or later\n\n## Support\n\nPlease report any issues directly in [Github](https:\/\/github.com\/job963\/SimpleSysMon\/issues). \n\n## Contribute \n\nIf you are interested in contributing to this plugin, feel free to send pull requests!\n\n",
+ "numDownloads":4490,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/job963\/SimpleSysMon\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>This plugin shows how much load your webserver does have, where Piwik (and maybe your main website) is running. Additionally the free or the used memory will be displayed.<\/p>\n\n<p>The display will be refreshed automatically as often as you like. This can be setup by yourself in the plugin settings.<\/p>\n\n",
+ "faq":"<p><strong>How is the CPU load calculated?<\/strong>\nFor the CPU load the PHP function sys_getloadavg() is used and divided by the number of cores.<\/p>\n\n<p><strong>Why is there a difference between free and used memory?<\/strong>\nThere are three \"memory parts\" under Linux:<\/p>\n\n<ul><li>Used memory<\/li>\n<li>Cache<\/li>\n<li>Free memory<\/li>\n<\/ul><p>The sum of these three parts will be equal to the total memory. But only <code>used memory<\/code> and <code>free memory<\/code> are available as options (in settings).<\/p>\n\n<p><strong>What is the value for 100% network traffic<\/strong>\nYou can setup the value for the maximum network traffic in the plugin system settings. The value there must be specified in kB\/s (kilobyte per second). This value is used as 100% network traffic.<\/p>\n\n<p><strong>Does the plugin work on a shared webspace?<\/strong>\nIn the most cases a shared webspace doesn't have access to system information. Therefore in these cases, the plugin cannot be used. The pseudo file system \/proc must be accessible.<\/p>\n\n<p><strong>Does the plugin work on Windows system?<\/strong>\nIf the server where Piwik is running is using Windows as OS, the plugin doesn't work yet.\nIf just your browser is running under Windows (and the server runs under Linux) this plugin works well.<\/p>",
+ "documentation":"",
+ "changelog":"<ul><li><p><strong>0.1.0 Initial release<\/strong><\/p>\n\n<ul><li>Display of CPU load<\/li>\n<li>Display of free or used memory <\/li>\n<\/ul><\/li>\n<li><p><strong>0.1.1 Initial release<\/strong><\/p>\n\n<ul><li>Corrections and error trapping for shared websites where the some values aren't accessible <\/li>\n<\/ul><\/li>\n<li><p><strong>0.2.0 Graphical display<\/strong><\/p>\n\n<ul><li>Display as bar chart for system load and memory use added <\/li>\n<\/ul><\/li>\n<li><p><strong>0.2.1 Hungarian language<\/strong><\/p>\n\n<ul><li>Hungarian language added <\/li>\n<\/ul><\/li>\n<li><p><strong>0.3.0 Bar charts for network and disk<\/strong><\/p>\n\n<ul><li>Two new bars for displaying network traffic (upload and download) and disk usage added<\/li>\n<li>Additional to the percentage values, the real values are display on hover <\/li>\n<\/ul><\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/SimpleSysMon\/download\/0.3.1"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"SiteMigration",
+ "displayName":"Site Migration",
+ "owner":"PiwikPRO",
+ "description":"Migrate your website and all website data between two Piwik installations.",
+ "homepage":"https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration",
+ "createdDateTime":"2014-11-11 21:28:05",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration",
+ "lastUpdated":"2015-11-27 02:46:04",
+ "latestVersion":"1.0.8",
+ "numDownloads":5191,"screenshots":[],"previews":[],"activity":{"numCommits":"70",
+ "numContributors":"7",
+ "lastCommitDate":"2016-05-11 09:27:40"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0.0",
+ "release":"2014-11-11 21:28:05",
+ "requires":{"php":">=5.3.0",
+ "piwik":">=2.9.0-b9"},"readme":"",
+ "numDownloads":77,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SiteMigration\/download\/1.0.0"},{"name":"1.0.1",
+ "release":"2014-11-12 23:34:05",
+ "requires":{"php":">=5.3.0",
+ "piwik":">=2.9.0-b9"},"readme":"",
+ "numDownloads":127,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SiteMigration\/download\/1.0.1"},{"name":"1.0.2",
+ "release":"2014-11-17 00:04:05",
+ "requires":{"php":">=5.3.0",
+ "piwik":">=2.9.0-b9"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SiteMigration\/download\/1.0.2"},{"name":"1.0.3",
+ "release":"2014-11-17 00:06:05",
+ "requires":{"php":">=5.3.0",
+ "piwik":">=2.9.0-b9"},"readme":"",
+ "numDownloads":19,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SiteMigration\/download\/1.0.3"},{"name":"1.0.4",
+ "release":"2014-11-17 11:06:05",
+ "requires":{"php":">=5.3.0",
+ "piwik":">=2.9.1"},"readme":"",
+ "numDownloads":202,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SiteMigration\/download\/1.0.4"},{"name":"1.0.5",
+ "release":"2014-11-26 21:26:06",
+ "requires":{"php":">=5.3.0",
+ "piwik":">=2.9.1"},"readme":"",
+ "numDownloads":130,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SiteMigration\/download\/1.0.5"},{"name":"1.0.6",
+ "release":"2014-12-03 03:34:05",
+ "requires":{"php":">=5.3.0",
+ "piwik":">=2.9.1"},"readme":"",
+ "numDownloads":174,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SiteMigration\/download\/1.0.6"},{"name":"1.0.7",
+ "release":"2014-12-21 22:04:05",
+ "requires":{"php":">=5.3.0",
+ "piwik":">=2.10.0-b10"},"readme":"",
+ "numDownloads":2669,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SiteMigration\/download\/1.0.7"},{"name":"1.0.8",
+ "release":"2015-11-27 02:46:04",
+ "requires":{"php":">=5.3.3",
+ "piwik":">=2.11.0"},"readme":"# Piwik SiteMigration Plugin\n\n[![Build Status](https:\/\/travis-ci.org\/PiwikPRO\/plugin-SiteMigration.svg?branch=master)](https:\/\/travis-ci.org\/PiwikPRO\/plugin-SiteMigration)\n\n## Description\n\nMigrate websites, and all the tracking data between two Piwik installations. \n\nThis tool is useful in case you want to merge two Piwik installations, or if you want to move one or several websites to another Piwik server.\n\n### Requirements\n\nTo migrate data from one Piwik server to another server, you must:\n\n * First make sure that both Piwik servers are using the latest Piwik version.\n * You must be able to connect to the Mysql server of the Target Piwik Server.\n * You must run the console command on the Piwik Server that data will be copied from.\n \n### Migrating the data\n\nStart the migration by calling from the command line CLI the following command:\n\n .\/console migration:site idSite --db-prefix piwik_\n \nThe command will ask for the credentials to the target database.\n \nIt will then migrate the data from the current Piwik to the target Piwik.\n\n### Options\n\nRun `.\/console migration:site --help` to get a full list of options.\n \n## FAQ\n\n**How do I migrate site data between two dates only?**\n\nYou can use command options: `--date-from` and `--date-to`.\n\n**How do I migrate tracking log data only, and skip migrating archived data?**\n\nJust add the `--skip-archive-data` option.\n\n\n**How do I migrate the archived data and skip the tracking data?**\n\nJust add the `--skip-log-data` option.\n\n**Can I run the command on the Target Piwik server (where data will be imported)?**\n\nNo, you must run the command from the source Piwik server (the server which contains the data you want to migrate).\n\n## Changelog\n\n**v1.0.7**\n\n- Updated the plugin for compatibility with Piwik 2.10.\n\n**v1.0.6**\n\n- [#6](https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/issues\/6): fixed a PHP 5.3 incompatibility\n\n**v1.0.5**\n\n- [#5](https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/issues\/5): fixed `Integrity constraint violation: 1048 Column 'idaction_url' cannot be null`\n\n**v1.0.4**\n\n- [#3](https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/issues\/3): fixed `--db-prefix` option\n\n**v1.0.3**\n\n- Documentation update\n\n**v1.0.2**\n\n- Documentation update & fixed bug when archive_blob tables are not found \n\n**v1.0.1**\n\n- Documentation update\n\n**v1.0.0**\n\n- First stable release\n- Bugfixes\n\n**v0.1.1**\n\n- Changed license to free plugin\n- Changed name to SiteMigration\n\n**v0.1.0**\n\n- Initial release\n\n## Credits\n\nCreated by [Piwik PRO](http:\/\/piwik.pro\/)\n",
+ "numDownloads":1793,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/commits\/v1.0.0",
+ "readmeHtml":{"description":"\n\n<p>Migrate websites, and all the tracking data between two Piwik installations.<\/p>\n\n<p>This tool is useful in case you want to merge two Piwik installations, or if you want to move one or several websites to another Piwik server.<\/p>\n\n<h3>Requirements<\/h3>\n\n<p>To migrate data from one Piwik server to another server, you must:<\/p>\n\n<ul><li>First make sure that both Piwik servers are using the latest Piwik version.<\/li>\n<li>You must be able to connect to the Mysql server of the Target Piwik Server.<\/li>\n<li>You must run the console command on the Piwik Server that data will be copied from.<\/li>\n<\/ul><h3>Migrating the data<\/h3>\n\n<p>Start the migration by calling from the command line CLI the following command:<\/p>\n\n<pre><code>.\/console migration:site idSite --db-prefix piwik_\n<\/code><\/pre>\n\n<p>The command will ask for the credentials to the target database.<\/p>\n\n<p>It will then migrate the data from the current Piwik to the target Piwik.<\/p>\n\n<h3>Options<\/h3>\n\n<p>Run <code>.\/console migration:site --help<\/code> to get a full list of options.<\/p>\n\n",
+ "faq":"<p><strong>How do I migrate site data between two dates only?<\/strong><\/p>\n\n<p>You can use command options: <code>--date-from<\/code> and <code>--date-to<\/code>.<\/p>\n\n<p><strong>How do I migrate tracking log data only, and skip migrating archived data?<\/strong><\/p>\n\n<p>Just add the <code>--skip-archive-data<\/code> option.<\/p>\n\n<p><strong>How do I migrate the archived data and skip the tracking data?<\/strong><\/p>\n\n<p>Just add the <code>--skip-log-data<\/code> option.<\/p>\n\n<p><strong>Can I run the command on the Target Piwik server (where data will be imported)?<\/strong><\/p>\n\n<p>No, you must run the command from the source Piwik server (the server which contains the data you want to migrate).<\/p>",
+ "documentation":"",
+ "changelog":"<p><strong>v1.0.7<\/strong><\/p>\n\n<ul><li>Updated the plugin for compatibility with Piwik 2.10.<\/li>\n<\/ul><p><strong>v1.0.6<\/strong><\/p>\n\n<ul><li><a href=\"https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/issues\/6\">#6<\/a>: fixed a PHP 5.3 incompatibility<\/li>\n<\/ul><p><strong>v1.0.5<\/strong><\/p>\n\n<ul><li><a href=\"https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/issues\/5\">#5<\/a>: fixed <code>Integrity constraint violation: 1048 Column 'idaction_url' cannot be null<\/code><\/li>\n<\/ul><p><strong>v1.0.4<\/strong><\/p>\n\n<ul><li><a href=\"https:\/\/github.com\/PiwikPRO\/plugin-SiteMigration\/issues\/3\">#3<\/a>: fixed <code>--db-prefix<\/code> option<\/li>\n<\/ul><p><strong>v1.0.3<\/strong><\/p>\n\n<ul><li>Documentation update<\/li>\n<\/ul><p><strong>v1.0.2<\/strong><\/p>\n\n<ul><li>Documentation update &amp; fixed bug when archive_blob tables are not found <\/li>\n<\/ul><p><strong>v1.0.1<\/strong><\/p>\n\n<ul><li>Documentation update<\/li>\n<\/ul><p><strong>v1.0.0<\/strong><\/p>\n\n<ul><li>First stable release<\/li>\n<li>Bugfixes<\/li>\n<\/ul><p><strong>v0.1.1<\/strong><\/p>\n\n<ul><li>Changed license to free plugin<\/li>\n<li>Changed name to SiteMigration<\/li>\n<\/ul><p><strong>v0.1.0<\/strong><\/p>\n\n<ul><li>Initial release<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/SiteMigration\/download\/1.0.8"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"SnoopyBehavioralScoring",
+ "displayName":"Snoopy Behavioral Scoring",
+ "owner":"spletnik",
+ "description":"User behaviour scoring plugin for piwik. It allows you to score your visitors depending on goals reached, pages visited, email campaigns opened and ot",
+ "homepage":"http:\/\/spletnik.si\/",
+ "createdDateTime":"2015-12-08 20:52:03",
+ "donate":{"paypal":"srle@spletnik.si",
+ "bitcoin":null},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/spletnik\/snoopy-behavioral-scoring",
+ "lastUpdated":"2016-01-27 08:50:04",
+ "latestVersion":"0.1.2",
+ "numDownloads":1211,"screenshots":[],"previews":[],"activity":{"numCommits":"18",
+ "numContributors":"1",
+ "lastCommitDate":"2016-01-27 08:49:31"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2015-12-08 20:52:03",
+ "requires":{"piwik":">=2.15.0",
+ "php":">=5.4.0"},"readme":"",
+ "numDownloads":428,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/spletnik\/snoopy-behavioral-scoring\/commits\/v1.0.0",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/SnoopyBehavioralScoring\/download\/0.1.0"},{"name":"0.1.2",
+ "release":"2016-01-27 08:50:04",
+ "requires":{"piwik":">=2.15.0",
+ "php":">=5.4.0"},"readme":"# Piwik Snoopy Plugin\n\n## Description\n\nSnoopy is a User behaviour scoring plugin for piwik. It allows you to score your visitors depending on goals reached, pages visited, email campaigns opened and other factors. In other words this plugin enables you to score your visitors based on actions they do on your website. It has a robust scoring methodology for heating and cooling visitor score.\n\nSnoopy also provides some basic API functions to integrate it with third party applications souch as your CRM or lead management system. More information about the API can be found on [github wiki](https:\/\/github.com\/spletnik\/snoopy-behavioral-scoring\/wiki\/API).\n\n## Installation\n### Automatic\nInstall it via Piwik Marketplace and [configure](#configuration) plugin.\n### Manual installation\n1. Download whole repository as ZIP.\n2. Go to your piwik Administration => Marketplace and click on \"upload a plugin in .zip format.\"\n3. After plugin is upladed press \"Activate plugin\"\n\nIf you did everything correct and you have sufficient permissions on folder plugins, Snoopy should be successfully installed.\n\nNow all you need to do is to set your configuration and create a cron that calculates visitors scores.\n\n## Configuration\n\n### Required\nBefore Snoopy starts scoring visitors you first have to tweak some configuration to match your needs.\n\n1. Matching site:\nYou have to select for which site your Snoopy scores visitors. For now only one website property tracking is available.\n\n2. Matching goal:\nSnoopy will start scoring only when specific goal is achieved. You can choose one or more goals as entry point to start .ing.\n\n3. Cooling factor:\nCooling factor is number that tells you how fast visitors will loose their score.\n\nHere is example simulation for factor 1.1\n\n|Number of inactive days|Current penalty|Penalty since beginning|Visitor scores\t|\n|-----------------------|---------------|-----------------------|---------------|\n|\t\t\t\t\t\t|\t\t\t\t|\t\t\t\t\t\t|\t100\t\t \t|\n|\t1\t\t\t\t\t|\t1\t\t\t|\t1\t\t\t\t\t|\t99\t\t \t|\n|\t2\t\t\t\t\t|\t1.1\t\t\t|\t2.1\t\t\t\t\t|\t97.9\t\t|\n|\t3\t\t\t\t\t|\t1.21\t\t|\t3.31\t\t\t\t|\t96.69\t\t|\n|\t4\t\t\t\t\t|\t1.331\t\t|\t4.641\t\t\t\t|\t95.359\t\t|\n|\t5\t\t\t\t\t|\t1.4641\t\t|\t6.1051\t\t\t\t|\t93.8949\t\t|\n|\t6\t\t\t\t\t|\t1.61051\t\t|\t7.71561\t\t\t\t|\t92.28439\t|\n|\t7\t\t\t\t\t|\t1.771561\t|\t9.487171\t\t\t|\t90.512829\t|\n|\t8\t\t\t\t\t|\t1.9487171\t|\t11.4358881\t\t\t|\t88.5641119\t|\n|\t9\t\t\t\t\t|\t2.14358881\t|\t13.57947691\t\t\t|\t86.42052309\t|\n|\t10\t\t\t\t\t|\t2.357947691\t|\t15.9374246\t\t\t|\t84.0625754\t|\n\nFrom the table you can see that on first inactive day you loose 1 point, \non the second inactive day you loose 1*factor point\non third inactive day you loose second_day*factor point etc.\n\nThose configuration parameters are mandatory for Snoopy to score propperly. \n\nWhen you are finished with configuration you just need to put this comand as your cronjob and Snoopy will start to calculate your score (This will recalcualte score hourly):\n\n**0 * * * * \/piwik_root\/console snoopybehavioralscoring:recalculate-score**\n\nAdditionally you can pipe log to file:\n\n**0 * * * * \/piwik_root\/console snoopybehavioralscoring:recalculate-score >> \/var\/log\/snoopy.log**\n\n### Optional\n- Enable console full debug\n\nWhen running recalculate-score some additional info is printed, that could be usefull when debuging why score is not calculated properly.\n\n- Campaign entry bonus\n\nBonus score that is added when visitor visits your webpage trough campaign (Google Analytics campaign parameters are set)\n- Special URL's\n\nURL;SCORE pairs that make some of your URL's worth more score points. \n\nFor example:\n\nhttp:\/\/example.com\/contact;3\n\nHere contact page is worth 3 points instead just one.\n## FAQ\n\n__When does my visitors gets scored.__\n\nWhen they reach specified goal in plugin settings.\n\n## Changelog\n0.1.2 - Bugfix for settings + Fixed fatal error on first calculation + Fixed permissions problems\n0.1.1 - Readme changes + minor fixes\n0.1.0 - Initial version\n\n## Support\n\nPlease direct any feedback to info@spletnik.si\n",
+ "numDownloads":783,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/spletnik\/snoopy-behavioral-scoring\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>Snoopy is a User behaviour scoring plugin for piwik. It allows you to score your visitors depending on goals reached, pages visited, email campaigns opened and other factors. In other words this plugin enables you to score your visitors based on actions they do on your website. It has a robust scoring methodology for heating and cooling visitor score.<\/p>\n\n<p>Snoopy also provides some basic API functions to integrate it with third party applications souch as your CRM or lead management system. More information about the API can be found on <a href=\"https:\/\/github.com\/spletnik\/snoopy-behavioral-scoring\/wiki\/API\">github wiki<\/a>.<\/p>\n\n",
+ "faq":"<p><strong>When does my visitors gets scored.<\/strong><\/p>\n\n<p>When they reach specified goal in plugin settings.<\/p>",
+ "documentation":"",
+ "changelog":"<p>0.1.2 - Bugfix for settings + Fixed fatal error on first calculation + Fixed permissions problems\n0.1.1 - Readme changes + minor fixes\n0.1.0 - Initial version<\/p>"},"download":"\/api\/2.0\/plugins\/SnoopyBehavioralScoring\/download\/0.1.2"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"TasksTimetable",
+ "displayName":"Tasks Timetable",
+ "owner":"piwik",
+ "description":"Plugin to list all scheduled tasks: Name of the tasks and next execution time displayed in a table.",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2014-12-23 01:09:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":["monitoring",
+ "tasks",
+ "scheduled",
+ "timetable"],"basePrice":0,"authors":[{"name":"Megan Liang",
+ "email":null,"homepage":null},{"name":"Jay Deshpande",
+ "email":null,"homepage":null},{"name":"Piwik",
+ "email":"test1@example.com",
+ "homepage":"http:\/\/piwik.org"}],"repositoryUrl":"https:\/\/github.com\/piwik\/plugin-TasksTimetable",
+ "lastUpdated":"2014-12-23 01:14:35",
+ "latestVersion":"0.1.3",
+ "numDownloads":0,"screenshots":["https:\/\/plugins.piwik.org\/TasksTimetable\/images\/Timetable.png"],"previews":[],"activity":{"numCommits":"64",
+ "numContributors":"6",
+ "lastCommitDate":"1970-01-01 00:33:35"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2014-12-23 01:09:03",
+ "requires":{"piwik":">=2.0.4-b1"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPL v3",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-TasksTimetable\/commits\/0.1.0",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/TasksTimetable\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2014-12-23 01:09:21",
+ "requires":{},"readme":"",
+ "numDownloads":0,"license":{"name":"GPL v3",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-TasksTimetable\/commits\/0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/TasksTimetable\/download\/0.1.1"},{"name":"0.1.2",
+ "release":"2014-12-23 01:13:01",
+ "requires":{},"readme":"",
+ "numDownloads":0,"license":{"name":"GPL v3",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-TasksTimetable\/commits\/0.1.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/TasksTimetable\/download\/0.1.2"},{"name":"0.1.3",
+ "release":"2014-12-23 01:14:35",
+ "requires":{"piwik":">=2.0.4-b5"},"readme":"# Piwik TasksTimetable Plugin\n\n## Description\n\nPlugin to list all scheduled tasks: Name of the tasks and next execution time displayed in a table. \n\n## FAQ\n\n__Where can I find the timetable?__\n\nIt is located as a menu item \"Scheduled Tasks\" under diagnostics on the settings page.\n\n## Changelog\n\n* 0.1.0 Initial release\n\n## Support\n\nPlease direct any feedback to [hello@piwik.org](mailto:hello@piwik.org)\n\n",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-TasksTimetable\/commits\/0.1.3",
+ "readmeHtml":{"description":"\n\n<p>Plugin to list all scheduled tasks: Name of the tasks and next execution time displayed in a table.<\/p>\n\n",
+ "faq":"<p><strong>Where can I find the timetable?<\/strong><\/p>\n\n<p>It is located as a menu item \"Scheduled Tasks\" under diagnostics on the settings page.<\/p>",
+ "documentation":"",
+ "changelog":"<ul><li>0.1.0 Initial release<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/TasksTimetable\/download\/0.1.3"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"TopPagesByActions",
+ "displayName":"Top Pages By Actions",
+ "owner":"chanzler",
+ "description":"Live widget that displays the top pages by actions in a 20 minute timespan. Auto-refresh interval can be configured.",
+ "homepage":"http:\/\/github.com\/chanzler\/piwik-top-pages-by-actions",
+ "createdDateTime":"2014-11-03 15:20:05",
+ "donate":{"paypal":"paypal@familiekanzler.de",
+ "bitcoin":null},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/chanzler\/piwik-top-pages-by-actions",
+ "lastUpdated":"2014-11-11 10:32:03",
+ "latestVersion":"0.1.3",
+ "numDownloads":4124,"screenshots":[],"previews":[],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":"2014-11-11 10:30:36"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.1",
+ "release":"2014-11-03 15:20:05",
+ "requires":{"piwik":">=2.0.0"},"readme":"",
+ "numDownloads":112,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/chanzler\/piwik-top-pages-by-actions\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/TopPagesByActions\/download\/0.1.1"},{"name":"0.1.2",
+ "release":"2014-11-08 20:26:03",
+ "requires":{"piwik":">=2.0.0"},"readme":"",
+ "numDownloads":60,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/chanzler\/piwik-top-pages-by-actions\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/TopPagesByActions\/download\/0.1.2"},{"name":"0.1.3",
+ "release":"2014-11-11 10:32:03",
+ "requires":{"piwik":">=2.0.0"},"readme":"# Piwik TopPagesByActions Plugin\n\n## Description\n\nThis is a plugin for the Open Source Web Analytics platform Piwik. If enabled, it will add a new widget that you can add to your dashboard.\n\nThe widget will show the top performing pages of a site that auto-refreshes every x seconds. It shows the number of actions and the title of the page. It is auto-sorting the entries.\n\n**This plugin should run fine with installations with up to 100.000 page impressions per day. If you run a very large piwik installation and have performance issues with this plugin, please contact me - there is a solution for this. I have it up and running in an installation with more than 10 million visits per day.**\n\n(Tested with piwik 2.8.3, but supposed to run with older versions)\n\n## Installation\n\nInstall it via Piwik Marketplace OR install manually:\n\n1. Clone the plugin into the plugins directory of your Piwik installation.\n\n ```\n cd plugins\/\n git clone https:\/\/github.com\/chanzler\/piwik-top-pages-by-actions.git TopPagesByActions\n ```\n\n2. Login as superuser into your Piwik installation and activate the plugin under Settings -> Plugins\n\n3. You will now find the widget under the Live! section.\n\n## FAQ\n\n###Features\nHere is a list of features that are included in this project:\n\n* Live widget (\"Bestperforming pages\") with key performance indices\n\n###Configuration\n*Refresh interval*: Defines how often the widgets will be updated. Every 30 seconds is a good value to choose.\n\n*Number of entries*: Defines the number of entries to show in the widget.\n\n## Changelog\n\n### 0.1.3 Bugfix Release\n* fixed english translation of table headers\n\n### 0.1.2 Bugfix Release\n* removed javascript console log\n* fixed bug with timezones that match \/^UTC[+-]*\/\n\n### 0.1.1 Bugfix Release\n* fixed css bug in internet explorer\n\n### 0.1.0 First Beta\n* initial release\n\n## License\n\nGPL v3 or later\n\n## Support\n\n* Please direct any feedback to [frank@intersolve.de](mailto:frank@intersolve.de)\n\n## Contribute\n\nIf you are interested in contributing to this plugin, feel free to send pull requests!\n\n",
+ "numDownloads":3952,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/chanzler\/piwik-top-pages-by-actions\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>This is a plugin for the Open Source Web Analytics platform Piwik. If enabled, it will add a new widget that you can add to your dashboard.<\/p>\n\n<p>The widget will show the top performing pages of a site that auto-refreshes every x seconds. It shows the number of actions and the title of the page. It is auto-sorting the entries.<\/p>\n\n<p><strong>This plugin should run fine with installations with up to 100.000 page impressions per day. If you run a very large piwik installation and have performance issues with this plugin, please contact me - there is a solution for this. I have it up and running in an installation with more than 10 million visits per day.<\/strong><\/p>\n\n<p>(Tested with piwik 2.8.3, but supposed to run with older versions)<\/p>\n\n",
+ "faq":"<h3>Features<\/h3>\n\n<p>Here is a list of features that are included in this project:<\/p>\n\n<ul><li>Live widget (\"Bestperforming pages\") with key performance indices<\/li>\n<\/ul><h3>Configuration<\/h3>\n\n<p><em>Refresh interval<\/em>: Defines how often the widgets will be updated. Every 30 seconds is a good value to choose.<\/p>\n\n<p><em>Number of entries<\/em>: Defines the number of entries to show in the widget.<\/p>",
+ "documentation":"",
+ "changelog":"<h3>0.1.3 Bugfix Release<\/h3>\n\n<ul><li>fixed english translation of table headers<\/li>\n<\/ul><h3>0.1.2 Bugfix Release<\/h3>\n\n<ul><li>removed javascript console log<\/li>\n<li>fixed bug with timezones that match \/^UTC[+-]*\/<\/li>\n<\/ul><h3>0.1.1 Bugfix Release<\/h3>\n\n<ul><li>fixed css bug in internet explorer<\/li>\n<\/ul><h3>0.1.0 First Beta<\/h3>\n\n<ul><li>initial release<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/TopPagesByActions\/download\/0.1.3"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"TrackingCodeCustomizer",
+ "displayName":"Tracking Code Customizer",
+ "owner":"jbrule",
+ "description":"Allows Piwik admininstrators to customize the tracking code that is autogenerated for users. This is useful for directing requests to the correct serv",
+ "homepage":"https:\/\/github.com\/jbrule\/piwikplugin-TrackingCodeCustomizer",
+ "createdDateTime":"2015-04-20 16:28:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/jbrule\/piwikplugin-TrackingCodeCustomizer",
+ "lastUpdated":"2015-11-20 19:16:09",
+ "latestVersion":"0.1.2",
+ "numDownloads":2647,"screenshots":[],"previews":[],"activity":{"numCommits":"5",
+ "numContributors":"1",
+ "lastCommitDate":"2016-04-26 07:22:01"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2015-04-20 16:28:03",
+ "requires":{"piwik":">=v2.9.0"},"readme":"",
+ "numDownloads":1,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/jbrule\/piwikplugin-TrackingCodeCustomizer\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/TrackingCodeCustomizer\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2015-04-20 16:30:04",
+ "requires":{"piwik":">=v2.9.0"},"readme":"",
+ "numDownloads":1298,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/jbrule\/piwikplugin-TrackingCodeCustomizer\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/TrackingCodeCustomizer\/download\/0.1.1"},{"name":"0.1.2",
+ "release":"2015-11-20 19:16:09",
+ "requires":{"piwik":">=v2.12.0"},"readme":"# Piwik TrackingCodeCustomizer Plugin\nTracking Code Customizer plugin for the Piwik Web Analytics software package\n\n##Description\nAllows Piwik admininstrators to customize the tracking code that is autogenerated for users. This is useful for directing requests to the correct servers in a multi-server setup, include additional parameters in default tracking, or to perform conditional checks before initiating a tracking call. \n\n##Instructions\nThe easiest way to install is to find the plugin in the [Piwik Marketplace](http:\/\/plugins.piwik.org\/).\n\n##Usage\n\nSet additional default OPTIONS for tracking. The follwing is entered into the \"options\" field.\n\n```javascript\nif (typeof(hash) !== 'undefined'){_paq.push(['setCustomVariable','1','U',hash,'visit']); _paq.push(['setUserId',hash]);};\n```\nResultant tracking code\n```javascript\n<!-- Piwik -->\n<script type=\"text\/javascript\">\n var _paq = _paq || [];\n if (typeof(hash) !== 'undefined'){_paq.push(['setCustomVariable','1','U',hash,'visit']); _paq.push(['setUserId',hash]);};\n _paq.push(['trackPageView']);\n _paq.push(['enableLinkTracking']);\n var u=\"\/\/webanalytics-tracker.XXXX.XXX\/\";\n_paq.push(['setTrackerUrl', u+'piwik.php']);\n _paq.push(['setSiteId', 1]);\n var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];\n g.type='text\/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);\n })();\n<\/script>\n<noscript><p><img src=\"\/\/webanalytics-tracker.XXXX.XXX\/piwik.php?idsite=1\" style=\"border:0;\" alt=\"\" \/><\/p><\/noscript>\n<!-- End Piwik Code -->\n```\n\n##Changelog\n0.1.2 Updated for compatibility with Piwik v2.15 and included new registerEvents() hook for compatibility with Piwik 3.0\n0.1.1 Version bump to activation Marketplace hook\n0.1.0 Initial Release\n\n##License\nGPL v3 \/ fair use\n\n## Support\nPlease [report any issues](https:\/\/github.com\/jbrule\/piwikplugin-TrackingCodeCustomizer\/issues). Pull requests welcome.\n",
+ "numDownloads":1348,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/jbrule\/piwikplugin-TrackingCodeCustomizer\/commits\/v1.1",
+ "readmeHtml":{"description":"\n\n<p>Allows Piwik admininstrators to customize the tracking code that is autogenerated for users. This is useful for directing requests to the correct servers in a multi-server setup, include additional parameters in default tracking, or to perform conditional checks before initiating a tracking call.<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<p>0.1.2 Updated for compatibility with Piwik v2.15 and included new registerEvents() hook for compatibility with Piwik 3.0\n0.1.1 Version bump to activation Marketplace hook\n0.1.0 Initial Release<\/p>"},"download":"\/api\/2.0\/plugins\/TrackingCodeCustomizer\/download\/0.1.2"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"TreemapVisualization",
+ "displayName":"Treemap Visualization",
+ "owner":"piwik",
+ "description":"This Piwik plugin adds a new report visualization: the treemap. With this plugin you can view your data as a graph that displays rows with rectangles ",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2014-12-23 00:37:31",
+ "donate":{},"support":[],"isTheme":false,"keywords":["treemap",
+ "graph",
+ "visualization",
+ "infovis",
+ "jit"],"basePrice":0,"authors":[{"name":"Piwik",
+ "email":"test1@example.com",
+ "homepage":"http:\/\/piwik.org"}],"repositoryUrl":"https:\/\/github.com\/piwik\/plugin-TreemapVisualization",
+ "lastUpdated":"2014-12-23 00:38:05",
+ "latestVersion":"1.0.1",
+ "numDownloads":0,"screenshots":["https:\/\/plugins.piwik.org\/TreemapVisualization\/images\/Treemap.png",
+ "https:\/\/plugins.piwik.org\/TreemapVisualization\/images\/screen-resolution-treemap.png",
+ "https:\/\/plugins.piwik.org\/TreemapVisualization\/images\/social-network-treemap.png"],"previews":[],"activity":{"numCommits":"106",
+ "numContributors":"6",
+ "lastCommitDate":"1970-01-01 00:33:35"},"featured":true,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0",
+ "release":"2014-12-23 00:37:32",
+ "requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-TreemapVisualization\/commits\/1.0",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/TreemapVisualization\/download\/1.0"},{"name":"1.0.1",
+ "release":"2014-12-23 00:38:05",
+ "requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"readme":"# Description\n\nTreemapVisualization is a Piwik plugin that provides the treemap report visualization. \n\n## Features\n\nThe treemap visualization will show rows of data tables as squares whose size correspond to the metric of the row. For example, if you're looking at visits, the row with the most visits will take up the most space. Just like the other graph visualizations, you can use it to easily see which rows have the largest values. The treemap differs from other graphs though, in that it shows much more rows.\n\nThe treemap visualization will also show you one thing that no other visualization included with Piwik does: the evolution of each row. You can hover over a treemap square to see how much the row changed from the last period. Each treemap square is also colored based on the evolution. A red square means the change is negative; a green square means the change is positive. The more green the bigger the change; the more red the smaller the change.\n\n## Limitations\n\nCurrently there is one known limitation:\n\n* Treemaps will not work with flattened tables. Currently, if a table is flattened, the treemap icon will be removed.\n",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":"http:\/\/plugins.piwik.org\/TreemapVisualization\/1.0.1\/license"},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-TreemapVisualization\/commits\/1.0.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/TreemapVisualization\/download\/1.0.1"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"UptimeRobotMonitor",
+ "displayName":"Uptime Robot Monitor",
+ "owner":"job963",
+ "description":"Plugin for displaying UptimeRobot data in Piwik",
+ "homepage":"https:\/\/github.com\/job963\/UptimeRobot-Monitor",
+ "createdDateTime":"2014-10-26 13:14:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/job963\/UptimeRobot-Monitor",
+ "lastUpdated":"2014-12-04 20:20:04",
+ "latestVersion":"0.2",
+ "numDownloads":4572,"screenshots":[],"previews":[],"activity":{"numCommits":"9",
+ "numContributors":"2",
+ "lastCommitDate":"2016-02-12 19:01:07"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2014-10-26 13:14:03",
+ "requires":{},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/job963\/UptimeRobot-Monitor\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/UptimeRobotMonitor\/download\/0.1.0"},{"name":"0.1.1",
+ "release":"2014-10-26 13:24:04",
+ "requires":{},"readme":"",
+ "numDownloads":440,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/job963\/UptimeRobot-Monitor\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/UptimeRobotMonitor\/download\/0.1.1"},{"name":"0.1.3",
+ "release":"2014-11-26 06:38:03",
+ "requires":{},"readme":"",
+ "numDownloads":152,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/job963\/UptimeRobot-Monitor\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/UptimeRobotMonitor\/download\/0.1.3"},{"name":"0.2",
+ "release":"2014-12-04 20:20:04",
+ "requires":{},"readme":"# UptimeRobot Monitor Plugin\n\n## Description\n\nThis plugin shows data, collected by UptimeRobot, in Piwik. This helps you to see all information about your website in one system without having the need to login into several systems.\n\nThe access to UptimeRobot will be managed by an individual monitor API key, which you have to create in UptimeRobot (see FAQ).\n\n\n## Screenshots\n**LogList Widget** \n![](https:\/\/github.com\/job963\/UptimeRobot-Monitor\/raw\/master\/screenshots\/widgetLogList.png)\n\n**TimeBar Widget** \n![](https:\/\/github.com\/job963\/UptimeRobot-Monitor\/raw\/master\/screenshots\/widgetTimeBar.png)\n\n**Plugin Settings** \n![](https:\/\/github.com\/job963\/UptimeRobot-Monitor\/raw\/master\/screenshots\/SettingsDE.png)\n\n\n## Installation\n\nInstall it via [Piwik Marketplace](http:\/\/plugins.piwik.org\/).\n\nOR \n\nInstall manually:\n\n1. Clone the plugin into the plugins directory of your Piwik installation.\n\n ```\n cd plugins\/\n git clone https:\/\/github.com\/job963\/UptimeRobot-Monitor.git UptimeRobotMonitor\n ```\n\n2. Login as superuser into your Piwik installation and activate the plugin under Settings -> Plugins\n\n3. Goto Settings -> Plugin Settings an setup the values for the widget.\n\n4. You will now find the widget under the UptimeRobotMonitor -> UptimeRobot - Latest Events.\n\n## Changelog\n\n* **0.1.0 Initial release**\n * Widget for latest events\n * Setting for monitor API key\n\n* **0.1.1 Small corrections**\n * Small correction in README\n\n* **0.1.2 New Language added**\n * Now usable in hungarian language (thanks to sagikazarmark)\n\n* **0.1.3 Readme updated**\n * New version description added\n\n* **0.2 Timebar widget added**\n * New widget for displaying the log as a time bar\n * Support of multiple servers\n\n## FAQ\n\n**Where do I get the API key?** \n\nGo in UptimeRoboto to \"My Settings\" and click in the paragraph \"Monitor-specific API Keys\" on \"Show\/hide it\". Search as next for the name of the desired website. Now you will see the API key for monitoring this website. \nThis API key you have to enter in the plugin settings of _UptimeRobotMonitor_.\n\n## License\n\nGPL v3 or later\n\n## Support\n\nPlease report any issues directly in [Github](https:\/\/github.com\/job963\/UptimeRobot-Monitor\/issues). \n\n## Contribute \n\nIf you are interested in contributing to this plugin, feel free to send pull requests!\n\n",
+ "numDownloads":3980,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/job963\/UptimeRobot-Monitor\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>This plugin shows data, collected by UptimeRobot, in Piwik. This helps you to see all information about your website in one system without having the need to login into several systems.<\/p>\n\n<p>The access to UptimeRobot will be managed by an individual monitor API key, which you have to create in UptimeRobot (see FAQ).<\/p>\n\n",
+ "faq":"<p><strong>Where do I get the API key?<\/strong><\/p>\n\n<p>Go in UptimeRoboto to \"My Settings\" and click in the paragraph \"Monitor-specific API Keys\" on \"Show\/hide it\". Search as next for the name of the desired website. Now you will see the API key for monitoring this website.\nThis API key you have to enter in the plugin settings of <em>UptimeRobotMonitor<\/em>.<\/p>",
+ "documentation":"",
+ "changelog":"<ul><li><p><strong>0.1.0 Initial release<\/strong><\/p>\n\n<ul><li>Widget for latest events<\/li>\n<li>Setting for monitor API key<\/li>\n<\/ul><\/li>\n<li><p><strong>0.1.1 Small corrections<\/strong><\/p>\n\n<ul><li>Small correction in README<\/li>\n<\/ul><\/li>\n<li><p><strong>0.1.2 New Language added<\/strong><\/p>\n\n<ul><li>Now usable in hungarian language (thanks to sagikazarmark)<\/li>\n<\/ul><\/li>\n<li><p><strong>0.1.3 Readme updated<\/strong><\/p>\n\n<ul><li>New version description added<\/li>\n<\/ul><\/li>\n<li><p><strong>0.2 Timebar widget added<\/strong><\/p>\n\n<ul><li>New widget for displaying the log as a time bar<\/li>\n<li>Support of multiple servers<\/li>\n<\/ul><\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/UptimeRobotMonitor\/download\/0.2"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"VisitorAvatar",
+ "displayName":"Visitor Avatar",
+ "owner":"surenjie",
+ "description":"Custom display visitors avatar(URL) and description(Title & Alt)",
+ "homepage":"http:\/\/plugins.piwik.org\/VisitorAvatar",
+ "createdDateTime":"2014-11-06 11:42:04",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/surenjie\/VisitorAvatar",
+ "lastUpdated":"2014-11-18 06:08:04",
+ "latestVersion":"0.2.2",
+ "numDownloads":3871,"screenshots":[],"previews":[],"activity":{"numCommits":"14",
+ "numContributors":"1",
+ "lastCommitDate":"2014-11-18 06:08:02"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2014-11-06 11:42:04",
+ "requires":{"piwik":">=2.8.3"},"readme":"",
+ "numDownloads":110,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/surenjie\/VisitorAvatar\/commits\/v0.1.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/VisitorAvatar\/download\/0.1.0"},{"name":"0.2.0",
+ "release":"2014-11-13 08:36:03",
+ "requires":{"piwik":">=2.8.3"},"readme":"",
+ "numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/surenjie\/VisitorAvatar\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/VisitorAvatar\/download\/0.2.0"},{"name":"0.2.1",
+ "release":"2014-11-13 09:34:03",
+ "requires":{"piwik":">=2.8.3"},"readme":"",
+ "numDownloads":120,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/surenjie\/VisitorAvatar\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/VisitorAvatar\/download\/0.2.1"},{"name":"0.2.2",
+ "release":"2014-11-18 06:08:04",
+ "requires":{"piwik":">=2.5.0"},"readme":"# Piwik VisitorAvatar Plugin\n\nCustom display visitors avatar(URL) and description(Title & Alt)\n\nhttp:\/\/plugins.piwik.org\/VisitorAvatar\n\n## Description\n\n * Step1 Tracking Code\n\n Reference:\n \n http:\/\/piwik.org\/docs\/custom-variables\/#track-a-custom-variable-in-javascript\n \n http:\/\/developer.piwik.org\/api-reference\/tracking-javascript#custom-variables\n\n Example:\n\n ```JavaScript\n \/\/ you can set up to 5 custom variables for each visitor\n _paq.push([\n \"setCustomVariable\", \n 1, \n \"RTX\", \n document.cookie.match(new RegExp(\"(^| )_login_name=([^;]*)(;|$)\"))[2], \n \"visit\"\n ]);\n ```\n\n * Step2 Plugin Settings\n\n 1. Name of the custom variable : \n \n \"RTX\" (Previous set of custom variable names, no default)\n\n 2. Visitor avatar url rules : \n \n \"\/\/rtx.oa.com\/avatars\/%s\/profile.jpg\" (The default is \"plugins\/VisitorAvatar\/images\/default_avatar.gif\")\n\n 3. Visitor description text rules : \n \n \"my rtx is %s\" (The default is \"%s\")\n\n\n * Step3 Visitor Profile\n\n _View Visitors custom avatar and description_\n\n## Tutorial\n\n![Step1 Tracking Code](https:\/\/raw.githubusercontent.com\/surenjie\/VisitorAvatar\/master\/screenshots\/Step1_Tracking_Code.png \"Step1 Tracking Code\")\n\n![Step2 Plugin Settings](https:\/\/raw.githubusercontent.com\/surenjie\/VisitorAvatar\/master\/screenshots\/Step2_Plugin_Settings.png \"Step2 Plugin Settings\")\n\n![Step3 Visitor Profile](https:\/\/raw.githubusercontent.com\/surenjie\/VisitorAvatar\/master\/screenshots\/Step3_Visitor_Profile.png \"Step3 Visitor Profile\")\n\n## Support\n\nPlease direct any feedback to i@renjie.me\n\nIf you experience any issues feel free to file an issue at https:\/\/github.com\/surenjie\/VisitorAvatar\/issues",
+ "numDownloads":3641,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/surenjie\/VisitorAvatar\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<ul><li><p>Step1 Tracking Code<\/p>\n\n<p>Reference:<\/p>\n\n<p>http:\/\/piwik.org\/docs\/custom-variables\/#track-a-custom-variable-in-javascript<\/p>\n\n<p>http:\/\/developer.piwik.org\/api-reference\/tracking-javascript#custom-variables<\/p>\n\n<p>Example:<\/p>\n\n<pre><code>\/\/ you can set up to 5 custom variables for each visitor\n_paq.push([\n \"setCustomVariable\", \n 1, \n \"RTX\", \n document.cookie.match(new RegExp(\"(^| )_login_name=([^;]*)(;|$)\"))[2], \n \"visit\"\n]);\n<\/code><\/pre><\/li>\n<li><p>Step2 Plugin Settings<\/p>\n\n<ol><li><p>Name of the custom variable :<\/p>\n\n<p>\"RTX\" (Previous set of custom variable names, no default)<\/p><\/li>\n<li><p>Visitor avatar url rules :<\/p>\n\n<p>\"\/\/rtx.oa.com\/avatars\/%s\/profile.jpg\" (The default is \"plugins\/VisitorAvatar\/images\/default_avatar.gif\")<\/p><\/li>\n<li><p>Visitor description text rules :<\/p>\n\n<p>\"my rtx is %s\" (The default is \"%s\")<\/p><\/li>\n<\/ol><\/li>\n<li><p>Step3 Visitor Profile<\/p>\n\n<p><em>View Visitors custom avatar and description<\/em><\/p><\/li>\n<\/ul>",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/VisitorAvatar\/download\/0.2.2"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"WebsiteGroups",
+ "displayName":"Website Groups",
+ "owner":"PiwikPRO",
+ "description":"Assign websites to groups in your 'All Websites' dashboard. Useful to get a view by client or category.",
+ "homepage":"http:\/\/piwik.pro",
+ "createdDateTime":"2014-11-07 03:14:03",
+ "donate":{},"support":[],"isTheme":false,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/PiwikPRO\/plugin-WebsiteGroups",
+ "lastUpdated":"2016-03-09 18:04:03",
+ "latestVersion":"0.3.0",
+ "numDownloads":5489,"screenshots":[],"previews":[],"activity":{"numCommits":"70",
+ "numContributors":"6",
+ "lastCommitDate":"2016-04-15 22:56:33"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.2",
+ "release":"2014-11-07 03:14:03",
+ "requires":{"piwik":">=2.1.1-b1"},"readme":"",
+ "numDownloads":3,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-WebsiteGroups\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/WebsiteGroups\/download\/0.1.2"},{"name":"0.1.3",
+ "release":"2014-11-07 03:18:03",
+ "requires":{"piwik":">=2.1.1-b1"},"readme":"",
+ "numDownloads":2156,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-WebsiteGroups\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/WebsiteGroups\/download\/0.1.3"},{"name":"0.1.4",
+ "release":"2015-07-28 14:50:03",
+ "requires":{"piwik":">=2.1.1-b1"},"readme":"",
+ "numDownloads":992,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-WebsiteGroups\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/WebsiteGroups\/download\/0.1.4"},{"name":"0.2.0",
+ "release":"2015-11-11 14:48:03",
+ "requires":{"piwik":">=2.15.0"},"readme":"",
+ "numDownloads":1320,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-WebsiteGroups\/commits\/0.1.3",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/WebsiteGroups\/download\/0.2.0"},{"name":"0.3.0",
+ "release":"2016-03-09 18:04:03",
+ "requires":{"piwik":">=2.16.0"},"readme":"# Piwik WebsiteGroups Plugin\n\nMaster [![Build Status](https:\/\/travis-ci.org\/PiwikPRO\/plugin-WebsiteGroups.svg?branch=master)](https:\/\/travis-ci.org\/PiwikPRO\/plugin-WebsiteGroups)\nDevelop [![Build Status](https:\/\/travis-ci.org\/PiwikPRO\/plugin-WebsiteGroups.svg?branch=develop)](https:\/\/travis-ci.org\/PiwikPRO\/plugin-WebsiteGroups)\n\n## Description\n\nFeatures of this plugin:\n\n * Users can assign websites to groups in the UI\n * 'All Websites' dashboard shows websites grouped by Group, and for each group it shows the sum of visits\/actions\/revenue for websites within this group\n * In the 'All Websites' dashboard, the Super User can see an aggregated number of visits\/pages for the group. Websites appear below their group row\n * A user can set the groups within the 'All Websites' dashboard\n * The website selector shows the website groups prefixed with the group name\n * The website selector allows you to search for groups\n\n## Changelog\n\n__v.0.3.0__\n- PPCDEV-2609 Compatibility with Piwik 2.16.0\n\n__v.0.2.0__\n- compatibility with Piwik 2.15.0\n\n## Support\n\nPlease direct any feedback to [contact@piwik.pro](mailto:contact@piwik.pro)\n\nIf you experience any issues feel free to file an issue at [https:\/\/github.com\/PiwikPRO\/plugin-WebsiteGroups\/issues](https:\/\/github.com\/PiwikPRO\/plugin-WebsiteGroups\/issues)\n",
+ "numDownloads":1018,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/PiwikPRO\/plugin-WebsiteGroups\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>Features of this plugin:<\/p>\n\n<ul><li>Users can assign websites to groups in the UI<\/li>\n<li>'All Websites' dashboard shows websites grouped by Group, and for each group it shows the sum of visits\/actions\/revenue for websites within this group<\/li>\n<li>In the 'All Websites' dashboard, the Super User can see an aggregated number of visits\/pages for the group. Websites appear below their group row<\/li>\n<li>A user can set the groups within the 'All Websites' dashboard<\/li>\n<li>The website selector shows the website groups prefixed with the group name<\/li>\n<li>The website selector allows you to search for groups<\/li>\n<\/ul>",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<p><strong>v.0.3.0<\/strong>\n- PPCDEV-2609 Compatibility with Piwik 2.16.0<\/p>\n\n<p><strong>v.0.2.0<\/strong>\n- compatibility with Piwik 2.15.0<\/p>"},"download":"\/api\/2.0\/plugins\/WebsiteGroups\/download\/0.3.0"}],"isDownloadable":true,"consumer":{"license":null}}]} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_plugins_Barometer_info.json b/plugins/Marketplace/tests/resources/v2.0_plugins_Barometer_info.json
new file mode 100644
index 0000000000..d63cce22ec
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_plugins_Barometer_info.json
@@ -0,0 +1,57 @@
+{"name":"Barometer",
+ "displayName":"Barometer",
+ "owner":"halfdan",
+ "description":"Live Plugin that shows the current number of visitors on the page.",
+ "homepage":"http:\/\/github.com\/halfdan\/piwik-barometer-plugin",
+ "createdDateTime":"2014-12-23 00:38:20",
+ "donate":{"flattr":"https:\/\/flattr.com\/profile\/test1",
+ "bitcoin":null},"support":[{"name":"Documentation",
+ "key":"docs",
+ "value":"https:\/\/barometer.org\/docs\/",
+ "type":"url"},{"name":"Wiki",
+ "key":"wiki",
+ "value":"https:\/\/github.com\/barometer\/piwik\/wiki",
+ "type":"url"},{"name":"Forum",
+ "key":"forum",
+ "value":"https:\/\/baromter.forum.org",
+ "type":"url"},{"name":"Email",
+ "key":"email",
+ "value":"barometer@example.com",
+ "type":"email"},{"name":"IRC",
+ "key":"irc",
+ "value":"irc:\/\/freenode\/baromter",
+ "type":"text"},{"name":"Issues \/ Bugs",
+ "key":"issues",
+ "value":"https:\/\/github.com\/barometer\/issues",
+ "type":"url"},{"name":"Source",
+ "key":"source",
+ "value":"https:\/\/github.com\/barometer\/piwik\/",
+ "type":"url"},{"name":"RSS",
+ "key":"rss",
+ "value":"https:\/\/barometer.org\/feed\/",
+ "type":"url"}],"isTheme":false,"keywords":["barometer",
+ "live"],"basePrice":0,"authors":[{"name":"Fabian Becker",
+ "email":"test8@example.com",
+ "homepage":"http:\/\/geekproject.eu"}],"repositoryUrl":"https:\/\/github.com\/halfdan\/piwik-barometer-plugin",
+ "lastUpdated":"2014-12-23 00:41:21",
+ "latestVersion":"0.5.0",
+ "numDownloads":0,"screenshots":["https:\/\/plugins.piwik.org\/Barometer\/images\/0.5.0\/piwik-barometer-01.png",
+ "https:\/\/plugins.piwik.org\/Barometer\/images\/0.5.0\/piwik-barometer-02.png"],"previews":[{"type":"demo",
+ "provider":"link",
+ "url":"https:\/\/demo.piwik.org"}],"activity":{"numCommits":"31",
+ "numContributors":"3",
+ "lastCommitDate":"1970-01-01 00:33:35"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.1",
+ "release":"2014-12-23 00:38:20",
+ "requires":{"piwik":">=1.10.0"},"numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/halfdan\/piwik-barometer-plugin\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Barometer\/download\/0.1.1"},{"name":"0.5.0",
+ "release":"2014-12-23 00:41:21",
+ "requires":{},"numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/halfdan\/piwik-barometer-plugin\/commits\/v0.5.0",
+ "readmeHtml":{"description":"\n\n<p>This is a plugin for the Open Source Web Analytics platform Piwik. If enabled, it will add a two new widgets that you can add to your dashboard.<\/p>\n\n<p>The widgets will show a Visitor Barometer and a Visit Time Barometer that auto-refresh every 5 seconds. It shows the number of visitors or visit time in a N minute period compared to the maximum number of visitors\/average visit time in any N minute period of the last 30 days.<\/p>\n\n<p>The idea for this plugin came from <a href=\"http:\/\/github.com\/muesli\">@muesli<\/a> who suggested it on #piwik in IRC.<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/Barometer\/download\/0.5.0"}],"isDownloadable":true,"consumer":{"license":null}} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_plugins_CustomPlugin1_info-access_token-notexistingtoken.json b/plugins/Marketplace/tests/resources/v2.0_plugins_CustomPlugin1_info-access_token-notexistingtoken.json
new file mode 100644
index 0000000000..df7f985b9c
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_plugins_CustomPlugin1_info-access_token-notexistingtoken.json
@@ -0,0 +1 @@
+{"error":"Requested plugin does not exist."} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info-access_token-consumer3_paid1_custom2.json b/plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info-access_token-consumer3_paid1_custom2.json
new file mode 100644
index 0000000000..0415fa3320
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info-access_token-consumer3_paid1_custom2.json
@@ -0,0 +1,90 @@
+{"name":"PaidPlugin1",
+ "displayName":"Paid Plugin 1",
+ "owner":"TestVendor",
+ "description":"Clears all temporary cache files",
+ "homepage":"https:\/\/github.com\/JohnDeery\/piwik-clearcache-plugin",
+ "createdDateTime":"2014-12-23 01:18:01",
+ "donate":{},"support":[{"name":"Documentation",
+ "key":"docs",
+ "value":"https:\/\/paidplugin1.org\/docs\/",
+ "type":"url"},{"name":"Wiki",
+ "key":"wiki",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/wiki",
+ "type":"url"},{"name":"Forum",
+ "key":"forum",
+ "value":"https:\/\/forum.paidplugin1.org",
+ "type":"url"},{"name":"Email",
+ "key":"email",
+ "value":"paidplugin1@example.com",
+ "type":"email"},{"name":"IRC",
+ "key":"irc",
+ "value":"irc:\/\/freenode\/paidplugin1",
+ "type":"text"},{"name":"Issues \/ Bugs",
+ "key":"issues",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/issues",
+ "type":"url"},{"name":"Source",
+ "key":"source",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/",
+ "type":"url"},{"name":"RSS",
+ "key":"rss",
+ "value":"https:\/\/paidplugin1.org\/feed\/",
+ "type":"url"}],"isTheme":false,"keywords":["cache",
+ "delete",
+ "tmp"],"basePrice":100,"authors":[{"name":"John Deery",
+ "email":"test3@example.com",
+ "homepage":"http:\/\/fractalice.com"}],"repositoryUrl":"https:\/\/github.com\/TestVendor\/PaidPlugin1",
+ "lastUpdated":"2014-12-23 01:18:20",
+ "latestVersion":"1.1",
+ "numDownloads":null,"screenshots":[],"previews":[{"type":"demo",
+ "provider":"link",
+ "url":"https:\/\/demo.paidplugin1.com"}],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":null},"featured":false,"isFree":false,"isPaid":true,"isCustomPlugin":false,"shop":{"url":"https:\/\/plugins.piwik.org\/PaidPlugin1",
+ "variations":[{"price":"150",
+ "prettyPrice":"150\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=EUR"},{"price":"175",
+ "prettyPrice":"$175",
+ "currency":"USD",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=USD"},{"price":"300",
+ "prettyPrice":"300\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=EUR"},{"price":"345",
+ "prettyPrice":"$345",
+ "currency":"USD",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=USD"},{"price":"600",
+ "prettyPrice":"600\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=EUR"},{"price":"690",
+ "prettyPrice":"$690",
+ "currency":"USD",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=USD"}],"reviews":{"embedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?show_reviews=1&piwik_embed=1",
+ "height":200}},"versions":[{"name":"1.1",
+ "release":"2014-12-23 01:18:20",
+ "requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"numDownloads":null,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":null,"readmeHtml":{"description":"\n\n<p>This plugin will clear out the tmp dir of Piwik. Useful for when you are developing other plugins or just need to kill that file and can't get to your installation to delete it normally<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<p>~2014~<\/p>\n\n<p>2014-02-21 - Update some more code, added in notification area but it's not quite working yet (TODO - make it work)<\/p>\n\n<p>2014-02-21 - Rebranded to cacheBuster and set version to 1.0, updated code to use a better check for directory seperator<\/p>\n\n<p>2014-02-20 - v2.0\n - Updated plugin to work with Piwik 2.0.3<\/p>\n\n<p>~2013~\n Inital creation of plugin at http:\/\/www.spherexx.com under the name ClearCache<\/p>"},"download":"\/api\/2.0\/plugins\/PaidPlugin1\/download\/1.1"}],"isDownloadable":true,"consumer":{"license":{"startDate":"2016-05-22 04:46:05",
+ "endDate":"2030-05-29 11:03:06",
+ "nextPaymentDate":"2030-05-29 11:03:06",
+ "status":"Active",
+ "productType":"Unlimited users",
+ "isValid":true,"isExceeded":null,"isExpiredSoon":false},"loginUrl":"https:\/\/shop.piwik.org\/my-account"}} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info-purchase_type-paid-num_users-201-access_token-consumer2_paid1.json b/plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info-purchase_type-paid-num_users-201-access_token-consumer2_paid1.json
new file mode 100644
index 0000000000..79ba7be907
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info-purchase_type-paid-num_users-201-access_token-consumer2_paid1.json
@@ -0,0 +1,90 @@
+{"name":"PaidPlugin1",
+ "displayName":"Paid Plugin 1",
+ "owner":"TestVendor",
+ "description":"Clears all temporary cache files",
+ "homepage":"https:\/\/github.com\/JohnDeery\/piwik-clearcache-plugin",
+ "createdDateTime":"2014-12-23 01:18:01",
+ "donate":{},"support":[{"name":"Documentation",
+ "key":"docs",
+ "value":"https:\/\/paidplugin1.org\/docs\/",
+ "type":"url"},{"name":"Wiki",
+ "key":"wiki",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/wiki",
+ "type":"url"},{"name":"Forum",
+ "key":"forum",
+ "value":"https:\/\/forum.paidplugin1.org",
+ "type":"url"},{"name":"Email",
+ "key":"email",
+ "value":"paidplugin1@example.com",
+ "type":"email"},{"name":"IRC",
+ "key":"irc",
+ "value":"irc:\/\/freenode\/paidplugin1",
+ "type":"text"},{"name":"Issues \/ Bugs",
+ "key":"issues",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/issues",
+ "type":"url"},{"name":"Source",
+ "key":"source",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/",
+ "type":"url"},{"name":"RSS",
+ "key":"rss",
+ "value":"https:\/\/paidplugin1.org\/feed\/",
+ "type":"url"}],"isTheme":false,"keywords":["cache",
+ "delete",
+ "tmp"],"basePrice":100,"authors":[{"name":"John Deery",
+ "email":"test3@example.com",
+ "homepage":"http:\/\/fractalice.com"}],"repositoryUrl":"https:\/\/github.com\/TestVendor\/PaidPlugin1",
+ "lastUpdated":null,"latestVersion":"1.1",
+ "numDownloads":null,"screenshots":[],"previews":[{"type":"demo",
+ "provider":"link",
+ "url":"https:\/\/demo.paidplugin1.com"}],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":null},"featured":false,"isFree":false,"isPaid":true,"isCustomPlugin":false,"shop":{"url":"https:\/\/plugins.piwik.org\/PaidPlugin1",
+ "variations":[{"price":"150",
+ "prettyPrice":"150\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=EUR"},{"price":"175",
+ "prettyPrice":"$175",
+ "currency":"USD",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=USD",
+ "cheapest":true,"recommended":false},{"price":"300",
+ "prettyPrice":"300\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=EUR"},{"price":"345",
+ "prettyPrice":"$345",
+ "currency":"USD",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=USD",
+ "cheapest":false,"recommended":false},{"price":"600",
+ "prettyPrice":"600\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=EUR"},{"price":"690",
+ "prettyPrice":"$690",
+ "currency":"USD",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=USD",
+ "cheapest":false,"recommended":true}],"reviews":{"embedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?show_reviews=1&piwik_embed=1",
+ "height":200}},"versions":[{"name":"1.1",
+ "release":null,"requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"numDownloads":null,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":null,"readmeHtml":{"description":"\n\n<p>This plugin will clear out the tmp dir of Piwik. Useful for when you are developing other plugins or just need to kill that file and can't get to your installation to delete it normally<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":null}],"isDownloadable":false,"consumer":{"license":{"startDate":"2016-05-21 04:46:05",
+ "endDate":"2029-05-27 11:03:06",
+ "nextPaymentDate":null,"status":"Pending cancellation",
+ "productType":"Up to 4 users",
+ "isValid":true,"isExceeded":true,"isExpiredSoon":false},"loginUrl":"https:\/\/shop.piwik.org\/my-account"}} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info.json b/plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info.json
new file mode 100644
index 0000000000..80711e4ae5
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info.json
@@ -0,0 +1,83 @@
+{"name":"PaidPlugin1",
+ "displayName":"Paid Plugin 1",
+ "owner":"TestVendor",
+ "description":"Clears all temporary cache files",
+ "homepage":"https:\/\/github.com\/JohnDeery\/piwik-clearcache-plugin",
+ "createdDateTime":"2014-12-23 01:18:01",
+ "donate":{},"support":[{"name":"Documentation",
+ "key":"docs",
+ "value":"https:\/\/paidplugin1.org\/docs\/",
+ "type":"url"},{"name":"Wiki",
+ "key":"wiki",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/wiki",
+ "type":"url"},{"name":"Forum",
+ "key":"forum",
+ "value":"https:\/\/forum.paidplugin1.org",
+ "type":"url"},{"name":"Email",
+ "key":"email",
+ "value":"paidplugin1@example.com",
+ "type":"email"},{"name":"IRC",
+ "key":"irc",
+ "value":"irc:\/\/freenode\/paidplugin1",
+ "type":"text"},{"name":"Issues \/ Bugs",
+ "key":"issues",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/issues",
+ "type":"url"},{"name":"Source",
+ "key":"source",
+ "value":"https:\/\/github.com\/paidplugin1\/piwik\/",
+ "type":"url"},{"name":"RSS",
+ "key":"rss",
+ "value":"https:\/\/paidplugin1.org\/feed\/",
+ "type":"url"}],"isTheme":false,"keywords":["cache",
+ "delete",
+ "tmp"],"basePrice":100,"authors":[{"name":"John Deery",
+ "email":"test3@example.com",
+ "homepage":"http:\/\/fractalice.com"}],"repositoryUrl":"https:\/\/github.com\/TestVendor\/PaidPlugin1",
+ "lastUpdated":null,"latestVersion":"1.1",
+ "numDownloads":null,"screenshots":[],"previews":[{"type":"demo",
+ "provider":"link",
+ "url":"https:\/\/demo.paidplugin1.com"}],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":null},"featured":false,"isFree":false,"isPaid":true,"isCustomPlugin":false,"shop":{"url":"https:\/\/plugins.piwik.org\/PaidPlugin1",
+ "variations":[{"price":"150",
+ "prettyPrice":"150\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=EUR"},{"price":"175",
+ "prettyPrice":"$175",
+ "currency":"USD",
+ "period":"year",
+ "name":"Up to 4 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=s&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Up+to+4+users&add-to-cart=0&variation_id=372&wcj-currency=USD"},{"price":"300",
+ "prettyPrice":"300\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=EUR"},{"price":"345",
+ "prettyPrice":"$345",
+ "currency":"USD",
+ "period":"year",
+ "name":"5 to 15 users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=m&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=5+to+15+users&add-to-cart=0&variation_id=373&wcj-currency=USD"},{"price":"600",
+ "prettyPrice":"600\u20ac",
+ "currency":"EUR",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=EUR",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=EUR"},{"price":"690",
+ "prettyPrice":"$690",
+ "currency":"USD",
+ "period":"year",
+ "name":"Unlimited users",
+ "addToCartUrl":"https:\/\/plugins.piwik.org\/PaidPlugin1?add-to-cart=l&currency=USD",
+ "addToCartEmbedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?attribute_type=Unlimited+users&add-to-cart=0&variation_id=374&wcj-currency=USD"}],"reviews":{"embedUrl":"http:\/\/myshop.piwik\/product\/PaidPlugin1?show_reviews=1&piwik_embed=1",
+ "height":200}},"versions":[{"name":"1.1",
+ "release":null,"requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"numDownloads":null,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":null,"readmeHtml":{"description":"\n\n<p>This plugin will clear out the tmp dir of Piwik. Useful for when you are developing other plugins or just need to kill that file and can't get to your installation to delete it normally<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":null}],"isDownloadable":false,"consumer":{"license":null,"loginUrl":"https:\/\/shop.piwik.org\/my-account"}} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_plugins_TreemapVisualization_info.json b/plugins/Marketplace/tests/resources/v2.0_plugins_TreemapVisualization_info.json
new file mode 100644
index 0000000000..b36f0e8bcd
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_plugins_TreemapVisualization_info.json
@@ -0,0 +1,36 @@
+{"name":"TreemapVisualization",
+ "displayName":"Treemap Visualization",
+ "owner":"piwik",
+ "description":"This Piwik plugin adds a new report visualization: the treemap. With this plugin you can view your data as a graph that displays rows with rectangles ",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2014-12-23 00:37:31",
+ "donate":{},"support":[],"isTheme":false,"keywords":["treemap",
+ "graph",
+ "visualization",
+ "infovis",
+ "jit"],"basePrice":0,"authors":[{"name":"Piwik",
+ "email":"test1@example.com",
+ "homepage":"http:\/\/piwik.org"}],"repositoryUrl":"https:\/\/github.com\/piwik\/plugin-TreemapVisualization",
+ "lastUpdated":"2014-12-23 00:38:05",
+ "latestVersion":"1.0.1",
+ "numDownloads":0,"screenshots":["https:\/\/plugins.piwik.org\/TreemapVisualization\/images\/1.0.1\/Treemap.png",
+ "https:\/\/plugins.piwik.org\/TreemapVisualization\/images\/1.0.1\/screen-resolution-treemap.png",
+ "https:\/\/plugins.piwik.org\/TreemapVisualization\/images\/1.0.1\/social-network-treemap.png"],"previews":[],"activity":{"numCommits":"106",
+ "numContributors":"6",
+ "lastCommitDate":"1970-01-01 00:33:35"},"featured":true,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"1.0",
+ "release":"2014-12-23 00:37:32",
+ "requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"numDownloads":0,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-TreemapVisualization\/commits\/1.0",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/TreemapVisualization\/download\/1.0"},{"name":"1.0.1",
+ "release":"2014-12-23 00:38:05",
+ "requires":{"piwik":">=2.0.0",
+ "php":">=5.3.0"},"numDownloads":0,"license":{"name":"GPLv3+",
+ "url":"http:\/\/plugins.piwik.org\/TreemapVisualization\/1.0.1\/license"},"repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-TreemapVisualization\/commits\/1.0.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/TreemapVisualization\/download\/1.0.1"}],"isDownloadable":true,"consumer":{"license":null}} \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_plugins_checkUpdates-pluginspluginsnameAnonymousPi.json b/plugins/Marketplace/tests/resources/v2.0_plugins_checkUpdates-pluginspluginsnameAnonymousPi.json
new file mode 100644
index 0000000000..57214819eb
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_plugins_checkUpdates-pluginspluginsnameAnonymousPi.json
@@ -0,0 +1,17 @@
+[{"name":"AnonymousPiwikUsageMeasurement",
+ "version":"0.2.1",
+ "repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-AnonymousPiwikUsageMeasurement\/commits\/v0.1.1"},{"name":"CustomAlerts",
+ "version":"0.1.12",
+ "repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomAlerts\/compare\/0.0.1...0.1.12"},{"name":"CustomDimensions",
+ "version":"0.1.4",
+ "repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-CustomDimensions\/compare\/0.1.11...0.1.4"},{"name":"LogViewer",
+ "version":"0.2.0",
+ "repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-LogViewer\/compare\/0.1.0...v0.1.1"},{"name":"QueuedTracking",
+ "version":"0.3.1",
+ "repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-QueuedTracking\/compare\/1.0.7...v0.1.1"},{"name":"SecurityInfo",
+ "version":"1.0.4",
+ "repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-SecurityInfo\/commits\/1.0.4"},{"name":"TasksTimetable",
+ "version":"0.1.3",
+ "repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-TasksTimetable\/compare\/0.1.0...0.1.3"},{"name":"TreemapVisualization",
+ "version":"1.0.1",
+ "repositoryChangelogUrl":"https:\/\/github.com\/piwik\/plugin-TreemapVisualization\/commits\/1.0.1"}] \ No newline at end of file
diff --git a/plugins/Marketplace/tests/resources/v2.0_themes.json b/plugins/Marketplace/tests/resources/v2.0_themes.json
new file mode 100644
index 0000000000..62fc282a34
--- /dev/null
+++ b/plugins/Marketplace/tests/resources/v2.0_themes.json
@@ -0,0 +1,182 @@
+{"plugins":[{"name":"AnotherBlackTheme",
+ "displayName":"Another Black Theme",
+ "owner":"tsteur",
+ "description":"A black theme based on PleineLune",
+ "homepage":"http:\/\/piwik.org",
+ "createdDateTime":"2014-12-23 00:39:16",
+ "donate":{},"support":[],"isTheme":true,"keywords":["live",
+ "tab"],"basePrice":0,"authors":[{"name":"The Piwik Team",
+ "email":"test1@example.com",
+ "homepage":"http:\/\/piwik.org"}],"repositoryUrl":"https:\/\/github.com\/tsteur\/piwik-anotherblack-theme",
+ "lastUpdated":"2014-12-23 00:39:16",
+ "latestVersion":"0.1.0",
+ "numDownloads":0,"screenshots":["https:\/\/themes.piwik.org\/AnotherBlackTheme\/images\/0.1.0\/Admin.png",
+ "https:\/\/themes.piwik.org\/AnotherBlackTheme\/images\/0.1.0\/All_Websites_Dashboard.png",
+ "https:\/\/themes.piwik.org\/AnotherBlackTheme\/images\/0.1.0\/Report.png"],"previews":[],"activity":{"numCommits":"3",
+ "numContributors":"1",
+ "lastCommitDate":"1970-01-01 00:33:33"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2014-12-23 00:39:16",
+ "requires":{},"numDownloads":0,"license":{"name":"GPLv3+",
+ "url":"http:\/\/themes.piwik.org\/AnotherBlackTheme\/0.1.0\/license"},"repositoryChangelogUrl":"https:\/\/github.com\/tsteur\/piwik-anotherblack-theme\/commits\/0.1.0",
+ "readmeHtml":{"description":"\n\n<p>A beautiful dark theme.<\/p>\n\n",
+ "faq":"<p><strong>Does it contain black?<\/strong>\nYes, it does.<\/p>\n\n<p><strong>Does it contain blue?<\/strong>\nYes, quite a lot.<\/p>",
+ "documentation":"",
+ "changelog":"<p><strong>0.1.0<\/strong>\n* Initial release<\/p>"},"download":"\/api\/2.0\/plugins\/AnotherBlackTheme\/download\/0.1.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"Darkness",
+ "displayName":"Darkness",
+ "owner":"TheCrowsJoker",
+ "description":"A dark theme for piwik",
+ "homepage":"https:\/\/github.com\/TheCrowsJoker\/DarknessPiwikTheme",
+ "createdDateTime":"2015-01-14 01:32:03",
+ "donate":{},"support":[],"isTheme":true,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/TheCrowsJoker\/DarknessPiwikTheme",
+ "lastUpdated":"2015-01-15 21:18:04",
+ "latestVersion":"0.1.4",
+ "numDownloads":3624,"screenshots":[],"previews":[],"activity":{"numCommits":"6",
+ "numContributors":"1",
+ "lastCommitDate":"2015-01-15 21:17:10"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.3",
+ "release":"2015-01-14 01:32:04",
+ "requires":{"piwik":">=2.11.0-b2"},"numDownloads":18,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/TheCrowsJoker\/DarknessPiwikTheme\/commits\/v0.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Darkness\/download\/0.1.3"},{"name":"0.1.4",
+ "release":"2015-01-15 21:18:04",
+ "requires":{"piwik":">=2.11.0-b2"},"numDownloads":3606,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/TheCrowsJoker\/DarknessPiwikTheme\/commits\/0.1.0",
+ "readmeHtml":{"description":"\n\n<p>This is a dark theme for piwik. It is great for viewing your piwik dashboard after the sun has gone down or just for lovers of darkness.<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<p>1.0 Initial release<\/p>"},"download":"\/api\/2.0\/plugins\/Darkness\/download\/0.1.4"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"Proteus_Bold",
+ "displayName":"Proteus_ Bold",
+ "owner":"ldidsbury",
+ "description":"The Proteus_Bold theme uses bold font that is easy-to-read with colors like that of the sea.",
+ "homepage":"https:\/\/github.com\/ldidsbury\/Proteus_Bold-PiwikTheme",
+ "createdDateTime":"2015-02-20 22:02:03",
+ "donate":{"paypal":"lawrence@winchesterresearch.it",
+ "bitcoin":null},"support":[],"isTheme":true,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/ldidsbury\/Proteus_Bold-PiwikTheme",
+ "lastUpdated":"2015-02-21 02:48:03",
+ "latestVersion":"0.1.4",
+ "numDownloads":2838,"screenshots":[],"previews":[],"activity":{"numCommits":"8",
+ "numContributors":"1",
+ "lastCommitDate":"2015-09-15 19:11:07"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.3",
+ "release":"2015-02-20 22:02:03",
+ "requires":{},"numDownloads":10,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ldidsbury\/Proteus_Bold-PiwikTheme\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Proteus_Bold\/download\/0.1.3"},{"name":"0.1.4",
+ "release":"2015-02-21 02:48:03",
+ "requires":{},"numDownloads":2828,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ldidsbury\/Proteus_Bold-PiwikTheme\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>Bold and easy-to-read...the Proteus_Bold theme for Piwik Analytics. Proteus was an early Greek god of the sea, capable of changing his appearance. The Proteus_Bold theme uses bold font that is easy-to-read with calming colors like that of the sea.<\/p>\n\n",
+ "faq":"<p>Q. Want to design a theme for Piwik? \nA. Go to https:\/\/developer.piwik.org\/guides\/theming to learn how.<\/p>",
+ "documentation":"",
+ "changelog":"<p>Version 0.1.2 \n-Basic theme adjustments, testing the process.<\/p>\n\n<p>Version 0.1.3 \n-Updated version number and Changelog<\/p>\n\n<p>Version 0.1.4 \n-Updated version number in json, \n-Updated Changelog, and \n-Edited description text length for the Piwik marketplace viewing<\/p>"},"download":"\/api\/2.0\/plugins\/Proteus_Bold\/download\/0.1.4"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"Terrano",
+ "displayName":"Terrano",
+ "owner":"nstallinger",
+ "description":"A neutral and natural theme made during the Catalyst Project Week.",
+ "homepage":"https:\/\/github.com\/nstallinger\/piwik_theme_terrano",
+ "createdDateTime":"2016-01-20 20:14:03",
+ "donate":{},"support":[],"isTheme":true,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/nstallinger\/piwik_theme_terrano",
+ "lastUpdated":"2016-01-20 20:58:02",
+ "latestVersion":"1.4.0",
+ "numDownloads":1014,"screenshots":[],"previews":[],"activity":{"numCommits":"9",
+ "numContributors":"1",
+ "lastCommitDate":"2016-01-20 20:57:44"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2016-01-20 20:14:03",
+ "requires":{},"numDownloads":2,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/nstallinger\/piwik_theme_terrano\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Terrano\/download\/0.1.0"},{"name":"1.4.0",
+ "release":"2016-01-20 20:58:02",
+ "requires":{},"numDownloads":1012,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/nstallinger\/piwik_theme_terrano\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>A neutral and natural theme made during the Catalyst Project Week.<\/p>",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/Terrano\/download\/1.4.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"CoffeeCup",
+ "displayName":"Coffee Cup",
+ "owner":"RobotHoboDanceParty",
+ "description":"A shoothing background for those who missed their first cup of the day.",
+ "homepage":"https:\/\/github.com\/RobotHoboDanceParty\/piwik-coffee-cup-theme",
+ "createdDateTime":"2016-01-20 20:18:03",
+ "donate":{},"support":[],"isTheme":true,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/RobotHoboDanceParty\/piwik-coffee-cup-theme",
+ "lastUpdated":"2016-01-20 20:34:02",
+ "latestVersion":"0.1.3",
+ "numDownloads":961,"screenshots":[],"previews":[],"activity":{"numCommits":"6",
+ "numContributors":"1",
+ "lastCommitDate":"2016-01-20 20:33:03"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2016-01-20 20:18:03",
+ "requires":{},"numDownloads":1,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/RobotHoboDanceParty\/piwik-coffee-cup-theme\/commits\/v0.1.2",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/CoffeeCup\/download\/0.1.0"},{"name":"0.1.3",
+ "release":"2016-01-20 20:34:02",
+ "requires":{},"numDownloads":960,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/RobotHoboDanceParty\/piwik-coffee-cup-theme\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>A soothing background for those who missed their first cup of the day.<\/p>",
+ "faq":"",
+ "documentation":"",
+ "changelog":""},"download":"\/api\/2.0\/plugins\/CoffeeCup\/download\/0.1.3"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"Vale",
+ "displayName":"Vale",
+ "owner":"ashleighpearson",
+ "description":"A muted red, white, and blue theme",
+ "homepage":"https:\/\/github.com\/ashleighpearson\/apcustomthemes",
+ "createdDateTime":"2016-01-20 20:20:03",
+ "donate":{},"support":[],"isTheme":true,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/ashleighpearson\/apcustomthemes",
+ "lastUpdated":"2016-01-20 21:08:03",
+ "latestVersion":"0.1.3",
+ "numDownloads":930,"screenshots":[],"previews":[],"activity":{"numCommits":"4",
+ "numContributors":"1",
+ "lastCommitDate":"2016-01-20 21:07:41"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2016-01-20 20:20:03",
+ "requires":{},"numDownloads":4,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ashleighpearson\/apcustomthemes\/commits\/v0.1.1",
+ "readmeHtml":{"description":"",
+ "faq":"",
+ "changelog":"",
+ "documentation":""},"download":"\/api\/2.0\/plugins\/Vale\/download\/0.1.0"},{"name":"0.1.3",
+ "release":"2016-01-20 21:08:03",
+ "requires":{},"numDownloads":926,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/ashleighpearson\/apcustomthemes\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>A muted red, white, and blue theme, giving Piwik a cool, calm feel. Good colour contrast makes things easy to read, as well as creating an overall pleasant aesthetic. Created during the 2016 Catalyst Open Source Academy.<\/p>\n\n",
+ "faq":"",
+ "documentation":"",
+ "changelog":"<p>Version 0.1.0: First public release\nVersion 0.1.1: Minor code fixes\nVersion 0.1.2: Typo fixes\nVersion 0.1.3: Typo fixes, more screenshots<\/p>"},"download":"\/api\/2.0\/plugins\/Vale\/download\/0.1.3"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"ModernBlue",
+ "displayName":"Modern Blue",
+ "owner":"cuzzea",
+ "description":"Modern Blue theme for Piwik.",
+ "homepage":null,"createdDateTime":"2016-03-18 09:06:03",
+ "donate":{"paypal":"cuzzea@gmail.com",
+ "bitcoin":null},"support":[],"isTheme":true,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/cuzzea\/piwik-theme-modern-blue",
+ "lastUpdated":"2016-03-18 09:06:03",
+ "latestVersion":"0.1.0",
+ "numDownloads":757,"screenshots":[],"previews":[],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":"2016-03-18 09:04:26"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2016-03-18 09:06:03",
+ "requires":{},"numDownloads":757,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/cuzzea\/piwik-theme-modern-blue\/commits\/v0.1.1",
+ "readmeHtml":{"description":"\n\n<p>Modern Blue theme for Piwik<\/p>\n\n",
+ "faq":"<p><strong>No FAQ at the moment<\/strong><\/p>",
+ "documentation":"",
+ "changelog":"<ul><li>0.1.0: First beta<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/ModernBlue\/download\/0.1.0"}],"isDownloadable":true,"consumer":{"license":null}},{"name":"ModernGreen",
+ "displayName":"Modern Green",
+ "owner":"cuzzea",
+ "description":"Modern Green theme for Piwik.",
+ "homepage":null,"createdDateTime":"2016-03-18 09:22:03",
+ "donate":{"paypal":"cuzzea@gmail.com",
+ "bitcoin":null},"support":[],"isTheme":true,"keywords":[],"basePrice":0,"authors":[],"repositoryUrl":"https:\/\/github.com\/cuzzea\/piwik-theme-modern-green",
+ "lastUpdated":"2016-03-18 09:22:03",
+ "latestVersion":"0.1.0",
+ "numDownloads":710,"screenshots":[],"previews":[],"activity":{"numCommits":null,"numContributors":null,"lastCommitDate":"2016-03-18 09:21:07"},"featured":false,"isFree":true,"isPaid":false,"isCustomPlugin":false,"shop":null,"versions":[{"name":"0.1.0",
+ "release":"2016-03-18 09:22:03",
+ "requires":{},"numDownloads":710,"license":{"name":"GPLv3+",
+ "url":""},"repositoryChangelogUrl":"https:\/\/github.com\/cuzzea\/piwik-theme-modern-green\/commits\/v0.1.2",
+ "readmeHtml":{"description":"\n\n<p>Modern Green theme for Piwik<\/p>\n\n",
+ "faq":"<p><strong>No FAQ at the moment<\/strong><\/p>",
+ "documentation":"",
+ "changelog":"<ul><li>0.1.0: First beta<\/li>\n<\/ul>"},"download":"\/api\/2.0\/plugins\/ModernGreen\/download\/0.1.0"}],"isDownloadable":true,"consumer":{"license":null}}]} \ No newline at end of file
diff --git a/plugins/Morpheus/templates/javascriptCode.twig b/plugins/Morpheus/templates/javascriptCode.twig
index c18d850d34..da29f9b6b8 100644
--- a/plugins/Morpheus/templates/javascriptCode.twig
+++ b/plugins/Morpheus/templates/javascriptCode.twig
@@ -12,7 +12,7 @@
})();
</script>
-{% if not loadAsync %}<script type='text/javascript' src="{$protocol}{$piwikUrl}/piwik.js">
+{% if not loadAsync %}<script type='text/javascript' src="{$protocol}{$piwikUrl}/piwik.js"></script>
{% endif %}
<noscript><p><img src="{$protocol}{$piwikUrl}/piwik.php?idsite={$idSite}" style="border:0;" alt="" /></p></noscript>
<!-- End Piwik Code -->
diff --git a/plugins/Widgetize/Menu.php b/plugins/Widgetize/Menu.php
index 7ce5b188ba..611292883c 100644
--- a/plugins/Widgetize/Menu.php
+++ b/plugins/Widgetize/Menu.php
@@ -18,7 +18,7 @@ class Menu extends \Piwik\Plugin\Menu
$tooltip = Piwik::translate('Widgetize_TopLinkTooltip');
$urlParams = $this->urlForAction('index', array('segment' => false));
- $menu->addPlatformItem('General_Widgets', $urlParams, 5, $tooltip);
+ $menu->addPlatformItem('General_Widgets', $urlParams, 6, $tooltip);
}
}
diff --git a/plugins/Widgetize/tests/System/WidgetTest.php b/plugins/Widgetize/tests/System/WidgetTest.php
index ebc5a5a7bd..4596913d05 100644
--- a/plugins/Widgetize/tests/System/WidgetTest.php
+++ b/plugins/Widgetize/tests/System/WidgetTest.php
@@ -986,10 +986,10 @@ class WidgetTest extends SystemTestCase
),
), array (
'name' => 'Latest Piwik Plugin Updates',
- 'uniqueId' => 'widgetCorePluginsAdmingetNewPlugins',
+ 'uniqueId' => 'widgetMarketplacegetNewPlugins',
'parameters' =>
array (
- 'module' => 'CorePluginsAdmin',
+ 'module' => 'Marketplace',
'action' => 'getNewPlugins',
),
), array (
diff --git a/tests/PHPUnit/Integration/DependencyTest.php b/tests/PHPUnit/Integration/DependencyTest.php
index 0f0de5562c..8a6d98d2ce 100644
--- a/tests/PHPUnit/Integration/DependencyTest.php
+++ b/tests/PHPUnit/Integration/DependencyTest.php
@@ -147,7 +147,7 @@ class DependencyTest extends IntegrationTestCase
public function test_getMissingVersion_EmptyCurrentVersion_ShouldBeDeclaredAsMissing()
{
- $this->assertMissingVersion('', '5.5', array('>=5.5'));
+ $this->assertMissingVersion('', '>=5.5', array('>=5.5'));
}
public function test_getMissingVersion_EmptyRequiredVersion_ShouldBeIgnored()
@@ -248,6 +248,26 @@ class DependencyTest extends IntegrationTestCase
$this->assertMissingVersion('6.4', '>=5.2,<=9.0', array());
}
+ /**
+ * @dataProvider getHasDepenedencyToDisabledPluginProvider
+ */
+ public function test_hasDependencyToDisabledPlugin($expectedHasDependency, $requires)
+ {
+ $this->assertSame($expectedHasDependency, $this->dependency->hasDependencyToDisabledPlugin($requires));
+ }
+
+ public function getHasDepenedencyToDisabledPluginProvider()
+ {
+ return array(
+ array($expected = false, $requires = null),
+ array($expected = false, $requires = array()),
+ array($expected = false, $requires = array('php' => '<5.2', 'piwik' => '<2.0')),
+ array($expected = false, $requires = array('php' => '<5.2', 'piwik' => '<2.0', 'CoreHome' => '2.15.0')),
+ array($expected = false, $requires = array('CoreHome' => '<2.0', 'Actions' => '>=2.15.0')),
+ array($expected = true, $requires = array('php' => '<5.2', 'piwik' => '<2.0', 'FooBar' => '2.15.0')),
+ );
+ }
+
private function missingPiwik($requiredVersion, $causedBy = null)
{
return $this->buildMissingDependecy('piwik', Version::VERSION, $requiredVersion, $causedBy);
diff --git a/tests/PHPUnit/Integration/Plugin/ManagerTest.php b/tests/PHPUnit/Integration/Plugin/ManagerTest.php
index 5822e22c05..b533b1e056 100644
--- a/tests/PHPUnit/Integration/Plugin/ManagerTest.php
+++ b/tests/PHPUnit/Integration/Plugin/ManagerTest.php
@@ -127,14 +127,14 @@ class ManagerTest extends IntegrationTestCase
array(true, 'pluginNameTest'),
array(true, 'PluginNameTest'),
array(true, 'PluginNameTest92323232eerwrwere938'),
+ array(true, 'a_ererer'),
+ array(true, 'a_'),
array(false, ''),
array(false, '0'),
array(false, '0a'),
array(false, 'a.'),
array(false, 'a-'),
- array(false, 'a_'),
array(false, 'a-ererer'),
- array(false, 'a_ererer'),
array(false, '..'),
array(false, '/'),
);
diff --git a/tests/PHPUnit/Integration/Tracker/TrackerCodeGeneratorTest.php b/tests/PHPUnit/Integration/Tracker/TrackerCodeGeneratorTest.php
index c8bce62d51..a8787cc5ee 100644
--- a/tests/PHPUnit/Integration/Tracker/TrackerCodeGeneratorTest.php
+++ b/tests/PHPUnit/Integration/Tracker/TrackerCodeGeneratorTest.php
@@ -179,7 +179,7 @@ class TrackerCodeGeneratorTest extends IntegrationTestCase
})();
&lt;/script&gt;
-&lt;script type='text/javascript' src=&quot;//localhost/piwik/piwik.js&quot;&gt;
+&lt;script type='text/javascript' src=&quot;//localhost/piwik/piwik.js&quot;&gt;&lt;/script&gt;
&lt;noscript&gt;&lt;p&gt;&lt;img src=&quot;//localhost/piwik/piwik.php?idsite=1&quot; style=&quot;border:0;&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;&lt;/noscript&gt;
&lt;!-- End Piwik Code --&gt;
";
diff --git a/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getWidgetMetadata.xml b/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getWidgetMetadata.xml
index 447ef39261..250f47704a 100644
--- a/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getWidgetMetadata.xml
+++ b/tests/PHPUnit/System/expected/test_apiGetReportMetadata__API.getWidgetMetadata.xml
@@ -576,7 +576,7 @@
<isReport>1</isReport>
</row>
<row>
- <name>Region</name>
+ <name>Visitor Map</name>
<category>
<id>General_Visitors</id>
<name>Visitors</name>
@@ -588,20 +588,18 @@
<name>Locations</name>
<order>25</order>
</subcategory>
- <module>UserCountry</module>
- <action>getRegion</action>
- <order>107</order>
+ <module>UserCountryMap</module>
+ <action>visitorMap</action>
+ <order>1</order>
<parameters>
- <module>UserCountry</module>
- <action>getRegion</action>
+ <module>UserCountryMap</module>
+ <action>visitorMap</action>
</parameters>
- <uniqueId>widgetUserCountrygetRegion</uniqueId>
+ <uniqueId>widgetUserCountryMapvisitorMap</uniqueId>
<isWide>0</isWide>
- <viewDataTable>table</viewDataTable>
- <isReport>1</isReport>
</row>
<row>
- <name>Visitor Map</name>
+ <name>Region</name>
<category>
<id>General_Visitors</id>
<name>Visitors</name>
@@ -613,15 +611,17 @@
<name>Locations</name>
<order>25</order>
</subcategory>
- <module>UserCountryMap</module>
- <action>visitorMap</action>
- <order>1</order>
+ <module>UserCountry</module>
+ <action>getRegion</action>
+ <order>107</order>
<parameters>
- <module>UserCountryMap</module>
- <action>visitorMap</action>
+ <module>UserCountry</module>
+ <action>getRegion</action>
</parameters>
- <uniqueId>widgetUserCountryMapvisitorMap</uniqueId>
+ <uniqueId>widgetUserCountrygetRegion</uniqueId>
<isWide>0</isWide>
+ <viewDataTable>table</viewDataTable>
+ <isReport>1</isReport>
</row>
<row>
<name>Continent</name>
@@ -778,7 +778,7 @@
<isReport>1</isReport>
</row>
<row>
- <name>Visits per visit duration</name>
+ <name>Visits by Visit Number</name>
<category>
<id>General_Visitors</id>
<name>Visitors</name>
@@ -791,15 +791,15 @@
<order>30</order>
</subcategory>
<module>VisitorInterest</module>
- <action>getNumberOfVisitsPerVisitDuration</action>
- <order>115</order>
+ <action>getNumberOfVisitsByVisitCount</action>
+ <order>125</order>
<parameters>
<module>VisitorInterest</module>
- <action>getNumberOfVisitsPerVisitDuration</action>
+ <action>getNumberOfVisitsByVisitCount</action>
</parameters>
- <uniqueId>widgetVisitorInterestgetNumberOfVisitsPerVisitDuration</uniqueId>
+ <uniqueId>widgetVisitorInterestgetNumberOfVisitsByVisitCount</uniqueId>
<isWide>0</isWide>
- <viewDataTable>cloud</viewDataTable>
+ <viewDataTable>table</viewDataTable>
<isReport>1</isReport>
</row>
<row>
@@ -822,7 +822,7 @@
<isWide>0</isWide>
</row>
<row>
- <name>Visits by Visit Number</name>
+ <name>Visits per visit duration</name>
<category>
<id>General_Visitors</id>
<name>Visitors</name>
@@ -835,15 +835,15 @@
<order>30</order>
</subcategory>
<module>VisitorInterest</module>
- <action>getNumberOfVisitsByVisitCount</action>
- <order>125</order>
+ <action>getNumberOfVisitsPerVisitDuration</action>
+ <order>115</order>
<parameters>
<module>VisitorInterest</module>
- <action>getNumberOfVisitsByVisitCount</action>
+ <action>getNumberOfVisitsPerVisitDuration</action>
</parameters>
- <uniqueId>widgetVisitorInterestgetNumberOfVisitsByVisitCount</uniqueId>
+ <uniqueId>widgetVisitorInterestgetNumberOfVisitsPerVisitDuration</uniqueId>
<isWide>0</isWide>
- <viewDataTable>table</viewDataTable>
+ <viewDataTable>cloud</viewDataTable>
<isReport>1</isReport>
</row>
<row>
@@ -1864,7 +1864,7 @@
<isWide>0</isWide>
</row>
<row>
- <name>Product SKU</name>
+ <name>Product Category</name>
<category>
<id>Goals_Ecommerce</id>
<name>Ecommerce</name>
@@ -1877,19 +1877,19 @@
<order>10</order>
</subcategory>
<module>Goals</module>
- <action>getItemsSku</action>
- <order>130</order>
+ <action>getItemsCategory</action>
+ <order>132</order>
<parameters>
<module>Goals</module>
- <action>getItemsSku</action>
+ <action>getItemsCategory</action>
</parameters>
- <uniqueId>widgetGoalsgetItemsSku</uniqueId>
+ <uniqueId>widgetGoalsgetItemsCategory</uniqueId>
<isWide>0</isWide>
<viewDataTable>table</viewDataTable>
<isReport>1</isReport>
</row>
<row>
- <name>Product Category</name>
+ <name>Product SKU</name>
<category>
<id>Goals_Ecommerce</id>
<name>Ecommerce</name>
@@ -1902,13 +1902,13 @@
<order>10</order>
</subcategory>
<module>Goals</module>
- <action>getItemsCategory</action>
- <order>132</order>
+ <action>getItemsSku</action>
+ <order>130</order>
<parameters>
<module>Goals</module>
- <action>getItemsCategory</action>
+ <action>getItemsSku</action>
</parameters>
- <uniqueId>widgetGoalsgetItemsCategory</uniqueId>
+ <uniqueId>widgetGoalsgetItemsSku</uniqueId>
<isWide>0</isWide>
<viewDataTable>table</viewDataTable>
<isReport>1</isReport>
@@ -2748,7 +2748,7 @@
<isReport>1</isReport>
</row>
<row>
- <name>Piwik Changelog</name>
+ <name>Piwik.org Blog</name>
<category>
<id>About Piwik</id>
<name>About Piwik</name>
@@ -2757,17 +2757,17 @@
</category>
<subcategory />
<module>RssWidget</module>
- <action>rssChangelog</action>
+ <action>rssPiwik</action>
<order>99</order>
<parameters>
<module>RssWidget</module>
- <action>rssChangelog</action>
+ <action>rssPiwik</action>
</parameters>
- <uniqueId>widgetRssWidgetrssChangelog</uniqueId>
+ <uniqueId>widgetRssWidgetrssPiwik</uniqueId>
<isWide>0</isWide>
</row>
<row>
- <name>System Summary</name>
+ <name>Example Widget Name</name>
<category>
<id>About Piwik</id>
<name>About Piwik</name>
@@ -2775,18 +2775,18 @@
<icon />
</category>
<subcategory />
- <module>CoreHome</module>
- <action>getSystemSummary</action>
- <order>15</order>
+ <module>ExamplePlugin</module>
+ <action>myExampleWidget</action>
+ <order>99</order>
<parameters>
- <module>CoreHome</module>
- <action>getSystemSummary</action>
+ <module>ExamplePlugin</module>
+ <action>myExampleWidget</action>
</parameters>
- <uniqueId>widgetCoreHomegetSystemSummary</uniqueId>
+ <uniqueId>widgetExamplePluginmyExampleWidget</uniqueId>
<isWide>0</isWide>
</row>
<row>
- <name>Top Keywords for Page URL</name>
+ <name>SEO Rankings</name>
<category>
<id>SEO</id>
<name>SEO</name>
@@ -2794,18 +2794,18 @@
<icon />
</category>
<subcategory />
- <module>Referrers</module>
- <action>getKeywordsForPage</action>
+ <module>SEO</module>
+ <action>getRank</action>
<order>99</order>
<parameters>
- <module>Referrers</module>
- <action>getKeywordsForPage</action>
+ <module>SEO</module>
+ <action>getRank</action>
</parameters>
- <uniqueId>widgetReferrersgetKeywordsForPage</uniqueId>
+ <uniqueId>widgetSEOgetRank</uniqueId>
<isWide>0</isWide>
</row>
<row>
- <name>Piwik.org Blog</name>
+ <name>System Check</name>
<category>
<id>About Piwik</id>
<name>About Piwik</name>
@@ -2813,18 +2813,18 @@
<icon />
</category>
<subcategory />
- <module>RssWidget</module>
- <action>rssPiwik</action>
- <order>99</order>
+ <module>Installation</module>
+ <action>getSystemCheck</action>
+ <order>16</order>
<parameters>
- <module>RssWidget</module>
- <action>rssPiwik</action>
+ <module>Installation</module>
+ <action>getSystemCheck</action>
</parameters>
- <uniqueId>widgetRssWidgetrssPiwik</uniqueId>
+ <uniqueId>widgetInstallationgetSystemCheck</uniqueId>
<isWide>0</isWide>
</row>
<row>
- <name>Latest Marketplace Updates</name>
+ <name>Support Piwik!</name>
<category>
<id>About Piwik</id>
<name>About Piwik</name>
@@ -2832,33 +2832,33 @@
<icon />
</category>
<subcategory />
- <module>CorePluginsAdmin</module>
- <action>getNewPlugins</action>
- <order>19</order>
+ <module>CoreHome</module>
+ <action>getDonateForm</action>
+ <order>5</order>
<parameters>
- <module>CorePluginsAdmin</module>
- <action>getNewPlugins</action>
+ <module>CoreHome</module>
+ <action>getDonateForm</action>
</parameters>
- <uniqueId>widgetCorePluginsAdmingetNewPlugins</uniqueId>
+ <uniqueId>widgetCoreHomegetDonateForm</uniqueId>
<isWide>0</isWide>
</row>
<row>
- <name>SEO Rankings</name>
+ <name>Piwik Changelog</name>
<category>
- <id>SEO</id>
- <name>SEO</name>
+ <id>About Piwik</id>
+ <name>About Piwik</name>
<order>99</order>
<icon />
</category>
<subcategory />
- <module>SEO</module>
- <action>getRank</action>
+ <module>RssWidget</module>
+ <action>rssChangelog</action>
<order>99</order>
<parameters>
- <module>SEO</module>
- <action>getRank</action>
+ <module>RssWidget</module>
+ <action>rssChangelog</action>
</parameters>
- <uniqueId>widgetSEOgetRank</uniqueId>
+ <uniqueId>widgetRssWidgetrssChangelog</uniqueId>
<isWide>0</isWide>
</row>
<row>
@@ -2881,7 +2881,7 @@
<isWide>0</isWide>
</row>
<row>
- <name>Welcome!</name>
+ <name>System Summary</name>
<category>
<id>About Piwik</id>
<name>About Piwik</name>
@@ -2890,36 +2890,36 @@
</category>
<subcategory />
<module>CoreHome</module>
- <action>getPromoVideo</action>
- <order>10</order>
+ <action>getSystemSummary</action>
+ <order>15</order>
<parameters>
<module>CoreHome</module>
- <action>getPromoVideo</action>
+ <action>getSystemSummary</action>
</parameters>
- <uniqueId>widgetCoreHomegetPromoVideo</uniqueId>
+ <uniqueId>widgetCoreHomegetSystemSummary</uniqueId>
<isWide>0</isWide>
</row>
<row>
- <name>Movers and Shakers</name>
+ <name>Latest Marketplace Updates</name>
<category>
- <id>Insights_WidgetCategory</id>
- <name>Insights</name>
+ <id>About Piwik</id>
+ <name>About Piwik</name>
<order>99</order>
<icon />
</category>
<subcategory />
- <module>Insights</module>
- <action>getOverallMoversAndShakers</action>
- <order>99</order>
+ <module>Marketplace</module>
+ <action>getNewPlugins</action>
+ <order>19</order>
<parameters>
- <module>Insights</module>
- <action>getOverallMoversAndShakers</action>
+ <module>Marketplace</module>
+ <action>getNewPlugins</action>
</parameters>
- <uniqueId>widgetInsightsgetOverallMoversAndShakers</uniqueId>
+ <uniqueId>widgetMarketplacegetNewPlugins</uniqueId>
<isWide>0</isWide>
</row>
<row>
- <name>System Check</name>
+ <name>Welcome!</name>
<category>
<id>About Piwik</id>
<name>About Piwik</name>
@@ -2927,56 +2927,56 @@
<icon />
</category>
<subcategory />
- <module>Installation</module>
- <action>getSystemCheck</action>
- <order>16</order>
+ <module>CoreHome</module>
+ <action>getPromoVideo</action>
+ <order>10</order>
<parameters>
- <module>Installation</module>
- <action>getSystemCheck</action>
+ <module>CoreHome</module>
+ <action>getPromoVideo</action>
</parameters>
- <uniqueId>widgetInstallationgetSystemCheck</uniqueId>
+ <uniqueId>widgetCoreHomegetPromoVideo</uniqueId>
<isWide>0</isWide>
</row>
<row>
- <name>Example Widget Name</name>
+ <name>Top Keywords for Page URL</name>
<category>
- <id>About Piwik</id>
- <name>About Piwik</name>
+ <id>SEO</id>
+ <name>SEO</name>
<order>99</order>
<icon />
</category>
<subcategory />
- <module>ExamplePlugin</module>
- <action>myExampleWidget</action>
+ <module>Referrers</module>
+ <action>getKeywordsForPage</action>
<order>99</order>
<parameters>
- <module>ExamplePlugin</module>
- <action>myExampleWidget</action>
+ <module>Referrers</module>
+ <action>getKeywordsForPage</action>
</parameters>
- <uniqueId>widgetExamplePluginmyExampleWidget</uniqueId>
+ <uniqueId>widgetReferrersgetKeywordsForPage</uniqueId>
<isWide>0</isWide>
</row>
<row>
- <name>Support Piwik!</name>
+ <name>Insights Overview</name>
<category>
- <id>About Piwik</id>
- <name>About Piwik</name>
+ <id>Insights_WidgetCategory</id>
+ <name>Insights</name>
<order>99</order>
<icon />
</category>
<subcategory />
- <module>CoreHome</module>
- <action>getDonateForm</action>
- <order>5</order>
+ <module>Insights</module>
+ <action>getInsightsOverview</action>
+ <order>99</order>
<parameters>
- <module>CoreHome</module>
- <action>getDonateForm</action>
+ <module>Insights</module>
+ <action>getInsightsOverview</action>
</parameters>
- <uniqueId>widgetCoreHomegetDonateForm</uniqueId>
+ <uniqueId>widgetInsightsgetInsightsOverview</uniqueId>
<isWide>0</isWide>
</row>
<row>
- <name>Insights Overview</name>
+ <name>Movers and Shakers</name>
<category>
<id>Insights_WidgetCategory</id>
<name>Insights</name>
@@ -2985,13 +2985,13 @@
</category>
<subcategory />
<module>Insights</module>
- <action>getInsightsOverview</action>
+ <action>getOverallMoversAndShakers</action>
<order>99</order>
<parameters>
<module>Insights</module>
- <action>getInsightsOverview</action>
+ <action>getOverallMoversAndShakers</action>
</parameters>
- <uniqueId>widgetInsightsgetInsightsOverview</uniqueId>
+ <uniqueId>widgetInsightsgetOverallMoversAndShakers</uniqueId>
<isWide>0</isWide>
</row>
</result> \ No newline at end of file
diff --git a/tests/UI/expected-screenshots/DashboardManager_expanded.png b/tests/UI/expected-screenshots/DashboardManager_expanded.png
index cf63988ca4..03c98e51cf 100644
--- a/tests/UI/expected-screenshots/DashboardManager_expanded.png
+++ b/tests/UI/expected-screenshots/DashboardManager_expanded.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:94ab82e309d168027b35fed07b6d294d19f61a5cc6a7e15da2747c8a47a41ec7
+oid sha256:6c1030ac68bbe0fb9baf84a4be91c92b37c52541e13edd3c67515f9e865dc45c
size 44477
diff --git a/tests/UI/expected-screenshots/DashboardManager_removed.png b/tests/UI/expected-screenshots/DashboardManager_removed.png
index 9a67088181..493649357f 100644
--- a/tests/UI/expected-screenshots/DashboardManager_removed.png
+++ b/tests/UI/expected-screenshots/DashboardManager_removed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:67f102d004d96f1838a5e89e8b6947b25322216aaf97ea89e53a6a69503e0c57
-size 219517
+oid sha256:30f231fc59852a5daba1380f6988723885135744c6efdf8cc891443aa3509277
+size 361692
diff --git a/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png b/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png
index 208c5545f9..2211071116 100644
--- a/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png
+++ b/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c2002a14c40355462fb24a20f770eab28f1f5f4d3ec6917f9a47b54d1dd1b61a
-size 54387
+oid sha256:ce88bb1a6ad4eec0abd85f53526fe464e5a423ff8f28ad63ea9f7aac679d9471
+size 54385
diff --git a/tests/UI/expected-screenshots/DashboardManager_widget_preview.png b/tests/UI/expected-screenshots/DashboardManager_widget_preview.png
index 5494fb208c..03f18720d0 100644
--- a/tests/UI/expected-screenshots/DashboardManager_widget_preview.png
+++ b/tests/UI/expected-screenshots/DashboardManager_widget_preview.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e6f88f00f657d056c2cf021b560b65104e920220ac481e754fb78eae00eb3deb
-size 65821
+oid sha256:d84f304a6f52151dd3dc34e72dd938e0a2078632041af219a935c431191fff69
+size 65779
diff --git a/tests/UI/expected-screenshots/Dashboard_removed.png b/tests/UI/expected-screenshots/Dashboard_removed.png
index dd384cfc69..07085c8a8d 100644
--- a/tests/UI/expected-screenshots/Dashboard_removed.png
+++ b/tests/UI/expected-screenshots/Dashboard_removed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:805b8c45f5d3a164970322440412267c56b84843793c23f5df8e21aea214a81d
-size 335467
+oid sha256:a7b19d0b352d391ef1567f053862b82f1b6fc2d6b0b4b7e4e70062a459e6374e
+size 530636
diff --git a/tests/UI/expected-screenshots/Marketplace_free_plugin_details_multiUserEnvironment.png b/tests/UI/expected-screenshots/Marketplace_free_plugin_details_multiUserEnvironment.png
new file mode 100644
index 0000000000..2b865112d3
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_free_plugin_details_multiUserEnvironment.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5abd8822925543da92bfe8952a7c72a1c3a33d35c5ee21373f24a83231846880
+size 65886
diff --git a/tests/UI/expected-screenshots/Marketplace_free_plugin_details_superuser.png b/tests/UI/expected-screenshots/Marketplace_free_plugin_details_superuser.png
new file mode 100644
index 0000000000..96215d6885
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_free_plugin_details_superuser.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d831b82a0569fae95cb3445282f95be26586c6a78361a060cc7a509253f75088
+size 47793
diff --git a/tests/UI/expected-screenshots/Marketplace_free_plugin_details_user.png b/tests/UI/expected-screenshots/Marketplace_free_plugin_details_user.png
new file mode 100644
index 0000000000..bdb8a34697
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_free_plugin_details_user.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:92e1339288dbbeecbb6e404c3f6a07f40248ae814958220f7448ee25f06c4975
+size 45867
diff --git a/tests/UI/expected-screenshots/Marketplace_notification_plugincheck_exceededLicense.png b/tests/UI/expected-screenshots/Marketplace_notification_plugincheck_exceededLicense.png
new file mode 100644
index 0000000000..00a8ac2874
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_notification_plugincheck_exceededLicense.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b3592aa6614d0d93e28573fa06e3e5b45d49f5405a03ce52f32e60c9cdad9162
+size 26736
diff --git a/tests/UI/expected-screenshots/Marketplace_notification_plugincheck_expiredLicense.png b/tests/UI/expected-screenshots/Marketplace_notification_plugincheck_expiredLicense.png
new file mode 100644
index 0000000000..61b9a684ee
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_notification_plugincheck_expiredLicense.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8884e2d728e9b59dda5b2a186fe355fa1e316ea000c0c1cb172c000cc747ca10
+size 25172
diff --git a/tests/UI/expected-screenshots/Marketplace_notification_plugincheck_noLicense.png b/tests/UI/expected-screenshots/Marketplace_notification_plugincheck_noLicense.png
new file mode 100644
index 0000000000..fc0a2a43f5
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_notification_plugincheck_noLicense.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a9cbd3fb3d800ec594bbcae770b5bd87a47793e6ddd08a513847fa6de3069e3f
+size 22054
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_multiUserEnvironment.png b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_multiUserEnvironment.png
new file mode 100644
index 0000000000..870eb3a98b
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_multiUserEnvironment.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:091cebccaa5c599d78504a10a83537beafbfd6f100add6116b7c9f388ce772f2
+size 78678
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_superuser.png b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_superuser.png
new file mode 100644
index 0000000000..ac5b71c9cf
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_superuser.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a4aa8c4d21824b42f38f86ea9ed45293ba8cd703f857a2f66960c859b80dd37b
+size 59189
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_user.png b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_user.png
new file mode 100644
index 0000000000..ef638514d6
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_user.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:87c51ce152fd1674f1775a3ea079bc2e7604a4515c13d712af954bd7e1689559
+size 42977
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_multiUserEnvironment.png b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_multiUserEnvironment.png
new file mode 100644
index 0000000000..769990ad67
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_multiUserEnvironment.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:65121175a97eaafbaeaadcd11c2e81a0795cf202bee85947da8612453af13501
+size 76262
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_superuser.png b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_superuser.png
new file mode 100644
index 0000000000..40e09d34e0
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_superuser.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:53eddafe234c4d1f844aaed9b1593f75a08d8670f0de87ff3a52a41e40ebe7d6
+size 57088
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_user.png b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_user.png
new file mode 100644
index 0000000000..ffdb015738
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_user.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ef479063eacbffac314a1665d52e90bc22676b9c7eb1f16c942af84f93c30e04
+size 42812
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_multiUserEnvironment.png b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_multiUserEnvironment.png
new file mode 100644
index 0000000000..f7b97bca7e
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_multiUserEnvironment.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6f7ad3b0cc4aa9958ecd749537042ce83976a78ee380a4dcbe1645e2ef7ded7d
+size 60698
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_multiUserEnvironment_installed.png b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_multiUserEnvironment_installed.png
new file mode 100644
index 0000000000..eaedf044fa
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_multiUserEnvironment_installed.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1d9c2a16a149c69f5a39b9fbc4ec92a09631a713c711d93fed867a38b3fcda93
+size 60513
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_superuser.png b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_superuser.png
new file mode 100644
index 0000000000..1ebe59347e
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_superuser.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6073abcfe24ee30ae2bc07ac124e51799bbf7233d0533f7624aeb5fa53a4367d
+size 41591
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_superuser_installed.png b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_superuser_installed.png
new file mode 100644
index 0000000000..0701015fc3
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_superuser_installed.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:12d5e077f5a534e6addb59256749354f5568a875302bbc17ef01c713b851ba7f
+size 40927
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_user.png b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_user.png
new file mode 100644
index 0000000000..30351a385b
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_user.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e802d83a1252c3d8208371563293f9019b9b3c80418f3e71f1b3d6449f900b72
+size 40504
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_user_installed.png b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_user_installed.png
new file mode 100644
index 0000000000..b30e7fbffe
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_user_installed.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1efcc7ef4fb4b9c6b1e4fc072018b68db4405240b2443f2e5578faaf53e1091b
+size 40036
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_multiUserEnvironment.png b/tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_multiUserEnvironment.png
new file mode 100644
index 0000000000..a310068f1e
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_multiUserEnvironment.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1015369551fefd1d1d6c02ffc6cc2c283dc9fab2934b4740c2e5b2ea70a58d07
+size 58749
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_superuser.png b/tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_superuser.png
new file mode 100644
index 0000000000..a310068f1e
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_superuser.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1015369551fefd1d1d6c02ffc6cc2c283dc9fab2934b4740c2e5b2ea70a58d07
+size 58749
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_user.png b/tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_user.png
new file mode 100644
index 0000000000..54668033ea
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_user.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b17183f64cb12053ffd03cb0af06c3a04b4869ac5121fce0022a8c2aba6b5e8b
+size 58902
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_multiUserEnvironment.png b/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_multiUserEnvironment.png
new file mode 100644
index 0000000000..90d6808f55
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_multiUserEnvironment.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2bdbbd347a480de157a356989b4120996c63e3ff52fd880feca50a320b9ea7cc
+size 68042
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_superuser.png b/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_superuser.png
new file mode 100644
index 0000000000..90d6808f55
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_superuser.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2bdbbd347a480de157a356989b4120996c63e3ff52fd880feca50a320b9ea7cc
+size 68042
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_user.png b/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_user.png
new file mode 100644
index 0000000000..5453092aa8
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_user.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c327de86da1d791e6ee45696623806de7008451d0aaa7d4adb73dfa254c9e0c0
+size 47804
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_multiUserEnvironment.png b/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_multiUserEnvironment.png
new file mode 100644
index 0000000000..72fa3aab47
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_multiUserEnvironment.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:de5a65a95c03e83831231ba5bd5d2a891f8502220dd83312c02c7f618b733361
+size 69415
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_superuser.png b/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_superuser.png
new file mode 100644
index 0000000000..e1eaf7ec8d
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_superuser.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:79ad5b16c1b840f853199d5a6580f7096bfceb11c03169a21c0950565a2c93fd
+size 70296
diff --git a/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_user.png b/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_user.png
new file mode 100644
index 0000000000..6b71115837
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_user.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7d0e0e5631cf9647253c48beec28282d47c3a4af271ec49696ce82bae072822c
+size 50165
diff --git a/tests/UI/expected-screenshots/Marketplace_subscription_overview_exceededLicense.png b/tests/UI/expected-screenshots/Marketplace_subscription_overview_exceededLicense.png
new file mode 100644
index 0000000000..1cdeaed41f
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_subscription_overview_exceededLicense.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fdca8506b9fe7ddd1fc5dffaf651f4b2a08fd73d2b82981d9421525cdf60fb3b
+size 74190
diff --git a/tests/UI/expected-screenshots/Marketplace_subscription_overview_expiredLicense.png b/tests/UI/expected-screenshots/Marketplace_subscription_overview_expiredLicense.png
new file mode 100644
index 0000000000..6b93808648
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_subscription_overview_expiredLicense.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f597ca665ae22b315ff592ea86c2da57b483586941a085dd6e96db8383769444
+size 80834
diff --git a/tests/UI/expected-screenshots/Marketplace_subscription_overview_noLicense.png b/tests/UI/expected-screenshots/Marketplace_subscription_overview_noLicense.png
new file mode 100644
index 0000000000..500196cfb4
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_subscription_overview_noLicense.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6df609ba58f749ddd01e10b92df0fc73410907f3b2ce4473dc16493d35f43552
+size 17178
diff --git a/tests/UI/expected-screenshots/Marketplace_subscription_overview_validLicense.png b/tests/UI/expected-screenshots/Marketplace_subscription_overview_validLicense.png
new file mode 100644
index 0000000000..db86de03cc
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_subscription_overview_validLicense.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ea6e9267c01a185178e4e99f3ee4da94e69e56376d7372e618fb144238f63763
+size 52494
diff --git a/tests/UI/expected-screenshots/Marketplace_superuser_enable_plugins_admin.png b/tests/UI/expected-screenshots/Marketplace_superuser_enable_plugins_admin.png
new file mode 100644
index 0000000000..6efcbbf06b
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_superuser_enable_plugins_admin.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:81007a5c45c3778ce3873814d6a89c1e3e5f4582b24221408a7f1fcc03257c66
+size 987671
diff --git a/tests/UI/expected-screenshots/Marketplace_superuser_enable_plugins_admin_with_multiserver_enabled.png b/tests/UI/expected-screenshots/Marketplace_superuser_enable_plugins_admin_with_multiserver_enabled.png
new file mode 100644
index 0000000000..a26bb41cc3
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_superuser_enable_plugins_admin_with_multiserver_enabled.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0f3d5b57078a2fc4f9eff69e6562dba1e3f9b88728b5edb9172f81bf2889b846
+size 1012645
diff --git a/tests/UI/expected-screenshots/Marketplace_superuser_install_all_paid_plugins_at_once.png b/tests/UI/expected-screenshots/Marketplace_superuser_install_all_paid_plugins_at_once.png
new file mode 100644
index 0000000000..62e8cf7bea
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_superuser_install_all_paid_plugins_at_once.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b2d0a30b1a7d67673fece45bc664f46c188ca27b1c1b6bd92cf1a0e5b377d6ec
+size 19257
diff --git a/tests/UI/expected-screenshots/Marketplace_superuser_invalid_license_key_entered.png b/tests/UI/expected-screenshots/Marketplace_superuser_invalid_license_key_entered.png
new file mode 100644
index 0000000000..073f732aae
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_superuser_invalid_license_key_entered.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f86cb3daefbf5b9a30a4632ef9246c5b2b41fdee4f83cc8d093e0c2a7a42678f
+size 1003216
diff --git a/tests/UI/expected-screenshots/Marketplace_superuser_remove_license_key_confirmation.png b/tests/UI/expected-screenshots/Marketplace_superuser_remove_license_key_confirmation.png
new file mode 100644
index 0000000000..31446ff085
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_superuser_remove_license_key_confirmation.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aaebbf81af29fb9d7445df16938460504202430053dc6b243257c9b4dfe64811
+size 17625
diff --git a/tests/UI/expected-screenshots/Marketplace_superuser_remove_license_key_confirmed.png b/tests/UI/expected-screenshots/Marketplace_superuser_remove_license_key_confirmed.png
new file mode 100644
index 0000000000..1f3c2fb252
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_superuser_remove_license_key_confirmed.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fa681ee188f0be2bc5b2b27df6a6f039ec18d3d76e3b3c17165d29669e92e916
+size 997521
diff --git a/tests/UI/expected-screenshots/Marketplace_superuser_valid_license_key_entered.png b/tests/UI/expected-screenshots/Marketplace_superuser_valid_license_key_entered.png
new file mode 100644
index 0000000000..d135e1127d
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_superuser_valid_license_key_entered.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d2599021aef3e9d6f2cca6166a095146a07fc72768cbe33910042cbb05cf764b
+size 1007231
diff --git a/tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_multiUserEnvironment.png b/tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_multiUserEnvironment.png
new file mode 100644
index 0000000000..74b8aa9e5c
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_multiUserEnvironment.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6044dafcf676224f14994abac215db06862354a60d0ad8fc20778fa8247f26bb
+size 186406
diff --git a/tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_superuser.png b/tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_superuser.png
new file mode 100644
index 0000000000..bd6b4293fd
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_superuser.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:884501a947c6780adc56dca08c17f7369844835c8fc28106e52ef37183e9ba3d
+size 186005
diff --git a/tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_user.png b/tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_user.png
new file mode 100644
index 0000000000..c88a30b7c4
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_user.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:20cd616e23f508c1fa678c6a7de1b29d3b552b475db8e97f79f32b29c85791c1
+size 163098
diff --git a/tests/UI/expected-screenshots/Marketplace_updates_multiUserEnvironment.png b/tests/UI/expected-screenshots/Marketplace_updates_multiUserEnvironment.png
new file mode 100644
index 0000000000..10daf00c64
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_updates_multiUserEnvironment.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d34f5ac92c5efff376a6008f6359c323aec2931cb2a61157a1a820f750dd49d0
+size 33517
diff --git a/tests/UI/expected-screenshots/Marketplace_updates_superuser.png b/tests/UI/expected-screenshots/Marketplace_updates_superuser.png
new file mode 100644
index 0000000000..e37583dfb5
--- /dev/null
+++ b/tests/UI/expected-screenshots/Marketplace_updates_superuser.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1d9b0afeb3d48d6ba34ce8e6f56405161e3ad134d19a64da3367b92452d7cc40
+size 32907
diff --git a/tests/UI/expected-screenshots/Theme_home.png b/tests/UI/expected-screenshots/Theme_home.png
index 9c9dce3051..dd0803cef4 100644
--- a/tests/UI/expected-screenshots/Theme_home.png
+++ b/tests/UI/expected-screenshots/Theme_home.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:476f0dd0c90b946c9cd3fd7769a3140d235cea598d20673c72884aba378cde9b
-size 349850
+oid sha256:ad0a3bc714dc7d013f2689e8d4b98ca9273cfcbd6e30756c702a2a76c9a550f3
+size 532284
diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_admin_diagnostics_configfile.png b/tests/UI/expected-screenshots/UIIntegrationTest_admin_diagnostics_configfile.png
index af51f6365c..1bcfab0c77 100644
--- a/tests/UI/expected-screenshots/UIIntegrationTest_admin_diagnostics_configfile.png
+++ b/tests/UI/expected-screenshots/UIIntegrationTest_admin_diagnostics_configfile.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2b6a90fde9010211ea01953fd8ba870c22d663b62fff1bb8331d0ed8ac5b2173
-size 3460616
+oid sha256:b3a90f8c6b3cd5e51c0af063de6a5c2968146d2fd16f323740eb849b0ea715d4
+size 3480547
diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_admin_home.png b/tests/UI/expected-screenshots/UIIntegrationTest_admin_home.png
index 13b87a2026..5d80d181cc 100644
--- a/tests/UI/expected-screenshots/UIIntegrationTest_admin_home.png
+++ b/tests/UI/expected-screenshots/UIIntegrationTest_admin_home.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:92e8926494823dcbcc6b944ef4828b4dc02131cc42d6451e55db9ef2d524a2fe
-size 116898
+oid sha256:cadbcc587c3a1099ab121a4d0a08e0cfaa57f8c6b2822f8833a253cdc0163cba
+size 113505
diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins.png b/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins.png
index d4f4e56103..3270c941e4 100644
--- a/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins.png
+++ b/tests/UI/expected-screenshots/UIIntegrationTest_admin_plugins.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:63e3ed59831496b0679f6c08f08c75c598d6a6991823a183473817da7e976b0b
-size 933080
+oid sha256:82d99d54325c7b0335a78d3715770e61937f6c9157dcb8d2f4c79a48af1424ff
+size 933835
diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_admin_themes.png b/tests/UI/expected-screenshots/UIIntegrationTest_admin_themes.png
index dad4ec975c..59120fe451 100644
--- a/tests/UI/expected-screenshots/UIIntegrationTest_admin_themes.png
+++ b/tests/UI/expected-screenshots/UIIntegrationTest_admin_themes.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:84e59ce08370286d50e24f3446509365786a254846a91affa418566f48d005a9
-size 74942
+oid sha256:f8713d9dca666ac6f2532cc3dee5a4b30bd5e8f3a6fa3181d6b9005c87e2c9b2
+size 80901
diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_api_listing.png b/tests/UI/expected-screenshots/UIIntegrationTest_api_listing.png
index 9eb0c90cc9..420fc0f714 100644
--- a/tests/UI/expected-screenshots/UIIntegrationTest_api_listing.png
+++ b/tests/UI/expected-screenshots/UIIntegrationTest_api_listing.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9ed26d862dbc303fe4b282f4af325cb70d689e115a4487f8dc64882f109b8edf
-size 4115881
+oid sha256:0a531c7f19980d3952e511ca54f17c95f38e04dc15f95a3dccb5e14ae09e0450
+size 4138237
diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard1.png b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard1.png
index 96a7c7324a..1b0a7c87e4 100644
--- a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard1.png
+++ b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a85aa6b5ef60f42ec6f719f44c64d574d7eb7677a4f79166306fb713efa2accf
-size 300657
+oid sha256:0286e06276f123c5e120de3b08779dcff874ef873507d99d89185fb690fd53cf
+size 479766
diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard2.png b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard2.png
index f0495e85bc..6a6af904d6 100644
--- a/tests/UI/expected-screenshots/UIIntegrationTest_dashboard2.png
+++ b/tests/UI/expected-screenshots/UIIntegrationTest_dashboard2.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a140a97e21b4c77617eed57fb924605adabf6ca750563ee478bcdcf50a872eb7
-size 1457388
+oid sha256:33eb54cef59315a69aae32573c55261044e5352b0616f3477ac61d14343c2a1d
+size 1445972
diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_menu_apidisallowed.png b/tests/UI/expected-screenshots/UIIntegrationTest_menu_apidisallowed.png
index cb8cbe4265..80b9e7c5a5 100644
--- a/tests/UI/expected-screenshots/UIIntegrationTest_menu_apidisallowed.png
+++ b/tests/UI/expected-screenshots/UIIntegrationTest_menu_apidisallowed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3ca95a4d415379e0946e569adf4a388602809d379062f62ebc571e56daebe513
-size 488891
+oid sha256:66a8770070d0ed3acf91ff1c5538b6c702dcb351a23b0da2624f3f0157ff5cb9
+size 486648
diff --git a/tests/UI/expected-screenshots/UIIntegrationTest_widgets_listing.png b/tests/UI/expected-screenshots/UIIntegrationTest_widgets_listing.png
index 2d7732163e..eaf22538af 100644
--- a/tests/UI/expected-screenshots/UIIntegrationTest_widgets_listing.png
+++ b/tests/UI/expected-screenshots/UIIntegrationTest_widgets_listing.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:be0fb31f6a219012173cf497a22afc4e0e327348a371a12c67b510769f3f2a3f
-size 179937
+oid sha256:b013eab8d4b17be33b9d3c956531b72bf5d931e27fa3a459d44a6af45b92a77c
+size 179766
diff --git a/tests/UI/specs/Marketplace_spec.js b/tests/UI/specs/Marketplace_spec.js
new file mode 100644
index 0000000000..dab2e3a5e6
--- /dev/null
+++ b/tests/UI/specs/Marketplace_spec.js
@@ -0,0 +1,265 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * Screenshot tests for main, top and admin menus.
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("Marketplace", function () {
+ this.timeout(0);
+
+ this.fixture = "Piwik\\Plugins\\Marketplace\\tests\\Fixtures\\SimpleFixtureTrackFewVisits";
+
+ var urlBase = '?module=Marketplace&action=overview&';
+ var paidPluginsUrl = urlBase + 'show=premium';
+ var themesUrl = urlBase + 'show=themes';
+ var pluginsUrl = urlBase;
+
+ var noLicense = 'noLicense';
+ var expiredLicense = 'expiredLicense';
+ var exceededLicense = 'exceededLicense';
+ var validLicense = 'validLicense';
+
+ function loadPluginDetailPage(page, pluginName, isFreePlugin)
+ {
+ page.load(isFreePlugin ? pluginsUrl : paidPluginsUrl);
+ page.click('.card-title [piwik-plugin-name="' + pluginName + '"]');
+ }
+
+ function captureSelector(done, screenshotName, test, selector)
+ {
+ expect.screenshot(screenshotName).to.be.captureSelector(selector, test, done);
+ }
+
+ function captureMarketplace(done, screenshotName, test, selector)
+ {
+ if (!selector) {
+ selector = '';
+ }
+
+ captureSelector(done, screenshotName, test, '.marketplace' + selector);
+ }
+
+ function captureWithNotification(done, screenshotName, test)
+ {
+ captureMarketplace(done, screenshotName, test, ',#notificationContainer');
+ }
+
+ function captureWithDialog(done, screenshotName, test)
+ {
+ captureSelector(done, screenshotName, test, '.ui-dialog:visible');
+ }
+
+ function assumePaidPluginsActivated()
+ {
+ testEnvironment.mockMarketplaceAssumePluginNamesActivated = ['CustomPlugin1','CustomPlugin2','PaidPlugin1','PaidPlugin2'];
+ testEnvironment.save();
+ }
+
+ function setEnvironment(mode, consumer)
+ {
+ if (mode === 'user') {
+ testEnvironment.idSitesViewAccess = [1];
+ } else {
+ // superuser
+ testEnvironment.idSitesViewAccess = [];
+ }
+
+ if (mode === 'multiUserEnvironment') {
+ testEnvironment.overrideConfig('General', 'multi_server_environment', '1');
+ } else {
+ testEnvironment.overrideConfig('General', 'multi_server_environment', '0');
+ }
+
+ testEnvironment.overrideConfig('General', 'enable_plugins_admin', '1');
+
+ delete testEnvironment.mockMarketplaceAssumePluginNamesActivated;
+
+ testEnvironment.consumer = consumer;
+ testEnvironment.mockMarketplaceApiService = 1;
+ testEnvironment.save();
+ }
+
+ ['superuser', 'user', 'multiUserEnvironment'].forEach(function (mode) {
+
+ if (mode !== 'user') {
+ it('should show available updates in plugins page', function (done) {
+ setEnvironment(mode, noLicense);
+
+ captureSelector(done, 'updates_' + mode, function (page) {
+ page.load('?module=CorePluginsAdmin&action=plugins&idSite=1&period=day&date=yesterday&activated=');
+ }, '#content .card:first');
+ });
+ }
+
+ it(mode + ' for a user without license key should be able to open paid plugins', function (done) {
+ setEnvironment(mode, noLicense);
+
+ captureMarketplace(done, 'paid_plugins_no_license_' + mode, function (page) {
+ page.load(paidPluginsUrl);
+ });
+ });
+
+ it(mode + ' for a user with license key should be able to open paid plugins', function (done) {
+ setEnvironment(mode, validLicense);
+
+ captureMarketplace(done, 'paid_plugins_with_license_' + mode, function (page) {
+ page.load(paidPluginsUrl);
+ });
+ });
+
+ it(mode + ' for a user with exceeded license key should be able to open paid plugins', function (done) {
+ setEnvironment(mode, exceededLicense);
+ assumePaidPluginsActivated();
+
+ captureMarketplace(done, 'paid_plugins_with_exceeded_license_' + mode, function (page) {
+ page.load(paidPluginsUrl);
+ });
+ });
+
+ it('should show themes page', function (done) {
+ setEnvironment(mode, validLicense);
+
+ captureMarketplace(done, 'themes_with_valid_license_' + mode, function (page) {
+ page.load(themesUrl);
+ });
+ });
+
+ it('should show free plugin details', function (done) {
+ setEnvironment(mode, noLicense);
+
+ captureWithDialog(done, 'free_plugin_details_' + mode, function (page) {
+ var isFree = true;
+ loadPluginDetailPage(page, 'TreemapVisualization', isFree);
+ });
+ });
+
+ it('should show paid plugin details when having no license', function (done) {
+ setEnvironment(mode, noLicense);
+
+ captureWithDialog(done, 'paid_plugin_details_no_license_' + mode, function (page) {
+ assumePaidPluginsActivated();
+ var isFree = false;
+ loadPluginDetailPage(page, 'PaidPlugin1', isFree);
+ });
+ });
+
+ it('should show paid plugin details when having valid license', function (done) {
+ setEnvironment(mode, validLicense);
+
+ captureWithDialog(done, 'paid_plugin_details_valid_license_' + mode + '_installed', function (page) {
+ assumePaidPluginsActivated();
+ var isFree = false;
+ loadPluginDetailPage(page, 'PaidPlugin1', isFree);
+ });
+ });
+
+ it('should show paid plugin details when having valid license', function (done) {
+ setEnvironment(mode, exceededLicense);
+
+ captureWithDialog(done, 'paid_plugin_details_exceeded_license_' + mode, function (page) {
+ assumePaidPluginsActivated();
+ var isFree = false;
+ loadPluginDetailPage(page, 'PaidPlugin1', isFree);
+ });
+ });
+ });
+
+ var mode = 'superuser';
+
+ it('should show a dialog showing a list of all possible plugins to install', function (done) {
+ setEnvironment(mode, validLicense);
+
+ captureSelector(done, mode + '_install_all_paid_plugins_at_once', function (page) {
+ page.load(pluginsUrl);
+ page.click('.installAllPaidPlugins');
+ }, '.modal.open');
+ });
+
+ it('should show an error message when invalid license key entered', function (done) {
+ setEnvironment(mode, noLicense);
+
+ captureWithNotification(done, mode + '_invalid_license_key_entered', function (page) {
+ page.load(pluginsUrl);
+ page.sendKeys('#license_key', 'invalid');
+ page.click('.marketplace-paid-intro'); // click outside so change event is triggered
+ page.click('#submit_license_key input');
+ });
+ });
+
+ it('should show a confirmation before removing a license key', function (done) {
+ setEnvironment(mode, validLicense);
+
+ captureSelector(done, mode + '_remove_license_key_confirmation', function (page) {
+ page.load(pluginsUrl);
+ page.click('#remove_license_key input');
+ }, '.modal.open');
+ });
+
+ it('should show a confirmation before removing a license key', function (done) {
+ setEnvironment(mode, noLicense);
+
+ captureMarketplace(done, mode + '_remove_license_key_confirmed', function (page) {
+ page.click('.modal.open .modal-footer a:contains(Yes)')
+ });
+ });
+
+ it('should show a success message when valid license key entered', function (done) {
+ setEnvironment(mode, noLicense);
+
+ captureMarketplace(done, mode + '_valid_license_key_entered', function (page) {
+ page.load(pluginsUrl);
+ page.sendKeys('#license_key', 'valid');
+ page.execCallback(function () {
+ setEnvironment(mode, validLicense);
+ });
+ page.click('#submit_license_key input');
+ });
+ });
+
+ it('should hide activate / deactivate buttons if plugins admin is disabled', function (done) {
+ setEnvironment(mode, noLicense);
+ testEnvironment.overrideConfig('General', 'enable_plugins_admin', '0');
+ testEnvironment.save();
+
+ captureMarketplace(done, mode + '_enable_plugins_admin', function (page) {
+ page.load(pluginsUrl);
+ });
+ });
+
+ it('should hide activate / deactivate buttons if plugins admin is disabled when also multi server environment is enabled', function (done) {
+ setEnvironment('multiUserEnvironment', noLicense);
+ testEnvironment.overrideConfig('General', 'enable_plugins_admin', '0');
+ testEnvironment.save();
+
+ captureMarketplace(done, mode + '_enable_plugins_admin_with_multiserver_enabled', function (page) {
+ page.load(pluginsUrl);
+ });
+ });
+
+ [expiredLicense, exceededLicense, validLicense, noLicense].forEach(function (consumer) {
+ it('should show a subscription overview for ' + consumer, function (done) {
+ setEnvironment('superuser', consumer);
+
+ captureSelector(done, 'subscription_overview_' + consumer, function (page) {
+ page.load('?module=Marketplace&action=subscriptionOverview');
+ }, '#content');
+ });
+ });
+
+ [noLicense, expiredLicense, exceededLicense].forEach(function (consumer) {
+ // when there is no license it should not show a warning! as it could be due to network problems etc
+ it('should show a warning if license is ' + consumer, function (done) {
+ setEnvironment('superuser', consumer);
+
+ assumePaidPluginsActivated();
+
+ captureSelector(done, 'notification_plugincheck_' + consumer, function (page) {
+ page.load('?module=UsersManager&action=index');
+ }, '#notificationContainer');
+ });
+ });
+
+}); \ No newline at end of file
diff --git a/tests/lib/screenshot-testing/support/page-renderer.js b/tests/lib/screenshot-testing/support/page-renderer.js
index 1551a68511..fe83cffcbc 100644
--- a/tests/lib/screenshot-testing/support/page-renderer.js
+++ b/tests/lib/screenshot-testing/support/page-renderer.js
@@ -110,6 +110,8 @@ PageRenderer.prototype.evaluate = function (impl, waitTime) {
this.queuedEvents.push([this._evaluate, waitTime, impl]);
};
+// like .evaluate() but doesn't call `impl` in context of the webpage. Useful if you want to change eg a testEnvironment
+// before a click. Makes sure this callback `impl` will be executed just before the next action instead of immediately
PageRenderer.prototype.execCallback = function (callback, waitTime) {
this.queuedEvents.push([this._execCallback, waitTime, callback]);
};