From 587cc39e0362719332d410b7a4d5ddcc68788eeb Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Tue, 15 Nov 2016 14:03:59 +1300 Subject: 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 --- CHANGELOG.md | 5 + composer.json | 3 +- composer.lock | 64 +- config/global.ini.php | 12 +- core/API/DocumentationGenerator.php | 2 + core/Http.php | 15 +- core/Notification.php | 7 + core/Plugin.php | 2 - core/Plugin/ControllerAdmin.php | 47 + core/Plugin/Dependency.php | 81 +- core/Plugin/Manager.php | 9 +- core/Plugin/MetadataLoader.php | 37 +- core/Plugin/ReleaseChannels.php | 12 + core/ProxyHttp.php | 12 +- core/SettingsPiwik.php | 37 + core/Twig.php | 26 + core/UpdateCheck.php | 7 +- core/UpdateCheck/ReleaseChannel.php | 9 + core/Updates/3.0.0-b2.php | 24 - core/Updates/3.0.0-b3.php | 65 + core/View.php | 2 + libs/bower_components/iframe-resizer/.bower.json | 55 + libs/bower_components/iframe-resizer/.gitignore | 9 + libs/bower_components/iframe-resizer/.travis.yml | 6 + libs/bower_components/iframe-resizer/LICENSE | 21 + libs/bower_components/iframe-resizer/bower.json | 45 + libs/bower_components/iframe-resizer/index.js | 4 + .../iframe-resizer/js/ie8.polyfils.map | 1 + .../iframe-resizer/js/ie8.polyfils.min.js | 4 + .../js/iframeResizer.contentWindow.js | 1108 +++++++ .../js/iframeResizer.contentWindow.map | 1 + .../js/iframeResizer.contentWindow.min.js | 10 + .../iframe-resizer/js/iframeResizer.js | 1002 +++++++ .../iframe-resizer/js/iframeResizer.map | 1 + .../iframe-resizer/js/iframeResizer.min.js | 9 + libs/bower_components/iframe-resizer/js/index.js | 2 + libs/bower_components/iframe-resizer/karma.conf.js | 92 + .../iframe-resizer/src/ie8.polyfils.js | 64 + .../src/iframeResizer.contentWindow.js | 1123 ++++++++ .../iframe-resizer/src/iframeResizer.js | 1002 +++++++ libs/bower_components/iframe-resizer/test-main.js | 33 + plugins/API/Menu.php | 2 +- plugins/CoreAdminHome/Controller.php | 5 +- plugins/CoreAdminHome/templates/home.twig | 2 +- plugins/CoreConsole/Commands/GeneratePlugin.php | 5 +- .../CoreConsole/Commands/GeneratePluginBase.php | 14 +- plugins/CoreHome/Controller.php | 5 +- plugins/CoreHome/CoreHome.php | 3 - plugins/CoreHome/lang/en.json | 1 + plugins/CoreHome/templates/_headerMessage.twig | 8 +- plugins/CorePluginsAdmin/Controller.php | 257 +- plugins/CorePluginsAdmin/CorePluginsAdmin.php | 12 +- plugins/CorePluginsAdmin/Marketplace.php | 199 -- plugins/CorePluginsAdmin/MarketplaceApiClient.php | 200 -- .../CorePluginsAdmin/MarketplaceApiException.php | 17 - plugins/CorePluginsAdmin/Menu.php | 43 +- plugins/CorePluginsAdmin/PluginInstaller.php | 85 +- plugins/CorePluginsAdmin/Tasks.php | 45 - plugins/CorePluginsAdmin/UpdateCommunication.php | 210 -- plugins/CorePluginsAdmin/Widgets/GetNewPlugins.php | 56 - .../marketplace/marketplace.controller.js | 23 - .../angularjs/marketplace/marketplace.directive.js | 59 - .../plugins/plugin-management.directive.js | 20 - .../angularjs/plugins/plugin-name.directive.js | 65 - plugins/CorePluginsAdmin/config/config.php | 7 - .../CorePluginsAdmin/images/rating_important.png | Bin 673 -> 0 bytes plugins/CorePluginsAdmin/lang/en.json | 64 +- plugins/CorePluginsAdmin/lang/hr.json | 5 - .../stylesheets/marketplace-widget.less | 44 - .../CorePluginsAdmin/stylesheets/marketplace.less | 91 - .../stylesheets/plugin-details.less | 83 - .../stylesheets/plugins_admin.less | 32 +- .../CorePluginsAdmin/templates/getNewPlugins.twig | 22 - .../templates/getNewPluginsAdmin.twig | 32 - .../CorePluginsAdmin/templates/installPlugin.twig | 41 - plugins/CorePluginsAdmin/templates/license.twig | 4 + plugins/CorePluginsAdmin/templates/macros.twig | 315 +- .../CorePluginsAdmin/templates/marketplace.twig | 99 - .../templates/marketplace/plugin-list.twig | 82 - .../CorePluginsAdmin/templates/pluginDetails.twig | 209 -- plugins/CorePluginsAdmin/templates/plugins.twig | 55 +- .../CorePluginsAdmin/templates/updatePlugin.twig | 41 - .../CorePluginsAdmin/templates/uploadPlugin.twig | 18 +- .../tests/Integration/UpdateCommunicationTest.php | 186 -- plugins/CoreUpdater/Controller.php | 16 +- .../CoreUpdater/ReleaseChannel/Latest2XBeta.php | 5 + plugins/CoreUpdater/ReleaseChannel/LatestBeta.php | 5 + plugins/CoreUpdater/SystemSettings.php | 2 +- .../Test/Integration/ReleaseChannelTest.php | 5 + plugins/CoreUpdater/Updater.php | 41 +- plugins/CoreUpdater/templates/layout.twig | 2 +- plugins/ExamplePlugin/plugin.json | 2 +- plugins/ExampleTheme/plugin.json | 2 +- plugins/Marketplace/API.php | 106 + plugins/Marketplace/Api/Client.php | 326 +++ plugins/Marketplace/Api/Exception.php | 17 + plugins/Marketplace/Api/Service.php | 158 + plugins/Marketplace/Api/Service/Exception.php | 19 + plugins/Marketplace/Consumer.php | 68 + plugins/Marketplace/Controller.php | 459 +++ plugins/Marketplace/Environment.php | 104 + plugins/Marketplace/Input/Mode.php | 29 + plugins/Marketplace/Input/PluginName.php | 42 + plugins/Marketplace/Input/PurchaseType.php | 20 + plugins/Marketplace/Input/Sort.php | 41 + plugins/Marketplace/LicenseKey.php | 44 + plugins/Marketplace/Marketplace.php | 63 + plugins/Marketplace/Menu.php | 28 + plugins/Marketplace/Plugins.php | 318 +++ plugins/Marketplace/Plugins/InvalidLicenses.php | 238 ++ plugins/Marketplace/Tasks.php | 47 + plugins/Marketplace/UpdateCommunication.php | 208 ++ plugins/Marketplace/Widgets/GetNewPlugins.php | 53 + .../angularjs/licensekey/licensekey.controller.js | 63 + .../marketplace/marketplace.controller.js | 23 + .../angularjs/marketplace/marketplace.directive.js | 132 + .../angularjs/plugins/plugin-name.directive.js | 65 + plugins/Marketplace/config/config.php | 32 + plugins/Marketplace/config/test.php | 144 + plugins/Marketplace/images/rating_important.png | Bin 0 -> 673 bytes plugins/Marketplace/lang/en.json | 112 + .../stylesheets/marketplace-widget.less | 44 + plugins/Marketplace/stylesheets/marketplace.less | 161 ++ .../Marketplace/stylesheets/plugin-details.less | 97 + plugins/Marketplace/templates/getNewPlugins.twig | 22 + .../Marketplace/templates/getNewPluginsAdmin.twig | 32 + plugins/Marketplace/templates/installPlugin.twig | 41 + plugins/Marketplace/templates/licenseform.twig | 75 + plugins/Marketplace/templates/macros.twig | 25 + plugins/Marketplace/templates/overview.twig | 96 + .../templates/paid-plugins-install-list.twig | 22 + plugins/Marketplace/templates/plugin-details.twig | 361 +++ plugins/Marketplace/templates/plugin-list.twig | 159 ++ .../templates/subscription-overview.twig | 101 + plugins/Marketplace/templates/updatePlugin.twig | 41 + .../tests/Fixtures/SimpleFixtureTrackFewVisits.php | 50 + .../Marketplace/tests/Framework/Mock/Client.php | 22 + .../Marketplace/tests/Framework/Mock/Consumer.php | 91 + .../tests/Framework/Mock/Environment.php | 53 + .../Marketplace/tests/Framework/Mock/Service.php | 122 + plugins/Marketplace/tests/Integration/ApiTest.php | 198 ++ .../Marketplace/tests/Integration/ClientTest.php | 72 + .../tests/Integration/EnvironmentTest.php | 87 + .../tests/Integration/Input/PluginNameTest.php | 53 + .../tests/Integration/LicenseKeyTest.php | 126 + .../Integration/Plugins/InvalidLicensesTest.php | 290 ++ .../Marketplace/tests/Integration/PluginsTest.php | 486 ++++ .../Marketplace/tests/Integration/ServiceTest.php | 67 + .../tests/Integration/UpdateCommunicationTest.php | 185 ++ .../Marketplace/tests/System/Api/ClientTest.php | 299 ++ .../Marketplace/tests/System/Api/ServiceTest.php | 165 ++ plugins/Marketplace/tests/Unit/ConsumerTest.php | 153 + .../tests/resources/emptyObjectResponse.json | 1 + ...sumer-access_token-consumer1_paid2_custom1.json | 21 + ...v2.0_consumer-access_token-consumer2_paid1.json | 7 + ...sumer-access_token-consumer3_paid1_custom2.json | 27 + ...2.0_consumer-access_token-notexistingtoken.json | 1 + ....0_consumer-access_token-validbutnolicense.json | 1 + ...s-201-access_token-consumer1_paid2_custom1.json | 21 + ...idate-access_token-consumer1_paid2_custom1.json | 1 + ...umer_validate-access_token-consumer2_paid1.json | 1 + ...idate-access_token-consumer3_paid1_custom2.json | 1 + ...mer_validate-access_token-notexistingtoken.json | 1 + ...er_validate-access_token-validbutnolicense.json | 1 + .../tests/resources/v2.0_consumer_validate.json | 1 + plugins/Marketplace/tests/resources/v2.0_info.json | 3 + ...-paid-access_token-consumer1_paid2_custom1.json | 142 + ...ase_type-paid-access_token-consumer2_paid1.json | 89 + ...se_type-paid-access_token-notexistingtoken.json | 83 + ...s-201-access_token-consumer1_paid2_custom1.json | 141 + ...num_users-201-access_token-consumer2_paid1.json | 90 + .../v2.0_plugins-query-nomatchforthisquery.json | 1 + .../Marketplace/tests/resources/v2.0_plugins.json | 3011 ++++++++++++++++++++ .../resources/v2.0_plugins_Barometer_info.json | 57 + ...Plugin1_info-access_token-notexistingtoken.json | 1 + ..._info-access_token-consumer3_paid1_custom2.json | 90 + ...num_users-201-access_token-consumer2_paid1.json | 90 + .../resources/v2.0_plugins_PaidPlugin1_info.json | 83 + .../v2.0_plugins_TreemapVisualization_info.json | 36 + ...checkUpdates-pluginspluginsnameAnonymousPi.json | 17 + .../Marketplace/tests/resources/v2.0_themes.json | 182 ++ plugins/Morpheus/templates/javascriptCode.twig | 2 +- plugins/Widgetize/Menu.php | 2 +- plugins/Widgetize/tests/System/WidgetTest.php | 4 +- tests/PHPUnit/Integration/DependencyTest.php | 22 +- tests/PHPUnit/Integration/Plugin/ManagerTest.php | 4 +- .../Tracker/TrackerCodeGeneratorTest.php | 2 +- ...apiGetReportMetadata__API.getWidgetMetadata.xml | 238 +- .../DashboardManager_expanded.png | 2 +- .../DashboardManager_removed.png | 4 +- .../DashboardManager_widget_list_shown.png | 4 +- .../DashboardManager_widget_preview.png | 4 +- .../UI/expected-screenshots/Dashboard_removed.png | 4 +- ...ce_free_plugin_details_multiUserEnvironment.png | 3 + .../Marketplace_free_plugin_details_superuser.png | 3 + .../Marketplace_free_plugin_details_user.png | 3 + ...ce_notification_plugincheck_exceededLicense.png | 3 + ...ace_notification_plugincheck_expiredLicense.png | 3 + ...ketplace_notification_plugincheck_noLicense.png | 3 + ...tails_exceeded_license_multiUserEnvironment.png | 3 + ...d_plugin_details_exceeded_license_superuser.png | 3 + ...e_paid_plugin_details_exceeded_license_user.png | 3 + ...gin_details_no_license_multiUserEnvironment.png | 3 + ...ce_paid_plugin_details_no_license_superuser.png | 3 + ...etplace_paid_plugin_details_no_license_user.png | 3 + ..._details_valid_license_multiUserEnvironment.png | 3 + ...alid_license_multiUserEnvironment_installed.png | 3 + ...paid_plugin_details_valid_license_superuser.png | 3 + ...n_details_valid_license_superuser_installed.png | 3 + ...lace_paid_plugin_details_valid_license_user.png | 3 + ...plugin_details_valid_license_user_installed.png | 3 + ...aid_plugins_no_license_multiUserEnvironment.png | 3 + ...rketplace_paid_plugins_no_license_superuser.png | 3 + .../Marketplace_paid_plugins_no_license_user.png | 3 + ..._with_exceeded_license_multiUserEnvironment.png | 3 + ...aid_plugins_with_exceeded_license_superuser.png | 3 + ...ace_paid_plugins_with_exceeded_license_user.png | 3 + ...d_plugins_with_license_multiUserEnvironment.png | 3 + ...etplace_paid_plugins_with_license_superuser.png | 3 + .../Marketplace_paid_plugins_with_license_user.png | 3 + ...place_subscription_overview_exceededLicense.png | 3 + ...tplace_subscription_overview_expiredLicense.png | 3 + ...Marketplace_subscription_overview_noLicense.png | 3 + ...ketplace_subscription_overview_validLicense.png | 3 + .../Marketplace_superuser_enable_plugins_admin.png | 3 + ...able_plugins_admin_with_multiserver_enabled.png | 3 + ..._superuser_install_all_paid_plugins_at_once.png | 3 + ...place_superuser_invalid_license_key_entered.png | 3 + ...e_superuser_remove_license_key_confirmation.png | 3 + ...lace_superuser_remove_license_key_confirmed.png | 3 + ...etplace_superuser_valid_license_key_entered.png | 3 + ...mes_with_valid_license_multiUserEnvironment.png | 3 + ...etplace_themes_with_valid_license_superuser.png | 3 + .../Marketplace_themes_with_valid_license_user.png | 3 + .../Marketplace_updates_multiUserEnvironment.png | 3 + .../Marketplace_updates_superuser.png | 3 + tests/UI/expected-screenshots/Theme_home.png | 4 +- ...ntegrationTest_admin_diagnostics_configfile.png | 4 +- .../UIIntegrationTest_admin_home.png | 4 +- .../UIIntegrationTest_admin_plugins.png | 4 +- .../UIIntegrationTest_admin_themes.png | 4 +- .../UIIntegrationTest_api_listing.png | 4 +- .../UIIntegrationTest_dashboard1.png | 4 +- .../UIIntegrationTest_dashboard2.png | 4 +- .../UIIntegrationTest_menu_apidisallowed.png | 4 +- .../UIIntegrationTest_widgets_listing.png | 4 +- tests/UI/specs/Marketplace_spec.js | 265 ++ .../screenshot-testing/support/page-renderer.js | 2 + 248 files changed, 17024 insertions(+), 2576 deletions(-) delete mode 100644 core/Updates/3.0.0-b2.php create mode 100644 core/Updates/3.0.0-b3.php create mode 100644 libs/bower_components/iframe-resizer/.bower.json create mode 100644 libs/bower_components/iframe-resizer/.gitignore create mode 100644 libs/bower_components/iframe-resizer/.travis.yml create mode 100644 libs/bower_components/iframe-resizer/LICENSE create mode 100644 libs/bower_components/iframe-resizer/bower.json create mode 100644 libs/bower_components/iframe-resizer/index.js create mode 100644 libs/bower_components/iframe-resizer/js/ie8.polyfils.map create mode 100644 libs/bower_components/iframe-resizer/js/ie8.polyfils.min.js create mode 100644 libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.js create mode 100644 libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.map create mode 100644 libs/bower_components/iframe-resizer/js/iframeResizer.contentWindow.min.js create mode 100644 libs/bower_components/iframe-resizer/js/iframeResizer.js create mode 100644 libs/bower_components/iframe-resizer/js/iframeResizer.map create mode 100644 libs/bower_components/iframe-resizer/js/iframeResizer.min.js create mode 100644 libs/bower_components/iframe-resizer/js/index.js create mode 100644 libs/bower_components/iframe-resizer/karma.conf.js create mode 100644 libs/bower_components/iframe-resizer/src/ie8.polyfils.js create mode 100644 libs/bower_components/iframe-resizer/src/iframeResizer.contentWindow.js create mode 100644 libs/bower_components/iframe-resizer/src/iframeResizer.js create mode 100644 libs/bower_components/iframe-resizer/test-main.js delete mode 100644 plugins/CorePluginsAdmin/Marketplace.php delete mode 100644 plugins/CorePluginsAdmin/MarketplaceApiClient.php delete mode 100644 plugins/CorePluginsAdmin/MarketplaceApiException.php delete mode 100644 plugins/CorePluginsAdmin/Tasks.php delete mode 100644 plugins/CorePluginsAdmin/UpdateCommunication.php delete mode 100644 plugins/CorePluginsAdmin/Widgets/GetNewPlugins.php delete mode 100644 plugins/CorePluginsAdmin/angularjs/marketplace/marketplace.controller.js delete mode 100644 plugins/CorePluginsAdmin/angularjs/marketplace/marketplace.directive.js delete mode 100644 plugins/CorePluginsAdmin/angularjs/plugins/plugin-name.directive.js delete mode 100644 plugins/CorePluginsAdmin/config/config.php delete mode 100755 plugins/CorePluginsAdmin/images/rating_important.png delete mode 100644 plugins/CorePluginsAdmin/lang/hr.json delete mode 100644 plugins/CorePluginsAdmin/stylesheets/marketplace-widget.less delete mode 100644 plugins/CorePluginsAdmin/stylesheets/marketplace.less delete mode 100644 plugins/CorePluginsAdmin/stylesheets/plugin-details.less delete mode 100644 plugins/CorePluginsAdmin/templates/getNewPlugins.twig delete mode 100644 plugins/CorePluginsAdmin/templates/getNewPluginsAdmin.twig delete mode 100644 plugins/CorePluginsAdmin/templates/installPlugin.twig create mode 100644 plugins/CorePluginsAdmin/templates/license.twig delete mode 100644 plugins/CorePluginsAdmin/templates/marketplace.twig delete mode 100644 plugins/CorePluginsAdmin/templates/marketplace/plugin-list.twig delete mode 100644 plugins/CorePluginsAdmin/templates/pluginDetails.twig delete mode 100644 plugins/CorePluginsAdmin/templates/updatePlugin.twig delete mode 100644 plugins/CorePluginsAdmin/tests/Integration/UpdateCommunicationTest.php create mode 100644 plugins/Marketplace/API.php create mode 100644 plugins/Marketplace/Api/Client.php create mode 100644 plugins/Marketplace/Api/Exception.php create mode 100644 plugins/Marketplace/Api/Service.php create mode 100644 plugins/Marketplace/Api/Service/Exception.php create mode 100644 plugins/Marketplace/Consumer.php create mode 100644 plugins/Marketplace/Controller.php create mode 100644 plugins/Marketplace/Environment.php create mode 100644 plugins/Marketplace/Input/Mode.php create mode 100644 plugins/Marketplace/Input/PluginName.php create mode 100644 plugins/Marketplace/Input/PurchaseType.php create mode 100644 plugins/Marketplace/Input/Sort.php create mode 100644 plugins/Marketplace/LicenseKey.php create mode 100644 plugins/Marketplace/Marketplace.php create mode 100644 plugins/Marketplace/Menu.php create mode 100644 plugins/Marketplace/Plugins.php create mode 100644 plugins/Marketplace/Plugins/InvalidLicenses.php create mode 100644 plugins/Marketplace/Tasks.php create mode 100644 plugins/Marketplace/UpdateCommunication.php create mode 100644 plugins/Marketplace/Widgets/GetNewPlugins.php create mode 100644 plugins/Marketplace/angularjs/licensekey/licensekey.controller.js create mode 100644 plugins/Marketplace/angularjs/marketplace/marketplace.controller.js create mode 100644 plugins/Marketplace/angularjs/marketplace/marketplace.directive.js create mode 100644 plugins/Marketplace/angularjs/plugins/plugin-name.directive.js create mode 100644 plugins/Marketplace/config/config.php create mode 100644 plugins/Marketplace/config/test.php create mode 100755 plugins/Marketplace/images/rating_important.png create mode 100644 plugins/Marketplace/lang/en.json create mode 100644 plugins/Marketplace/stylesheets/marketplace-widget.less create mode 100644 plugins/Marketplace/stylesheets/marketplace.less create mode 100644 plugins/Marketplace/stylesheets/plugin-details.less create mode 100644 plugins/Marketplace/templates/getNewPlugins.twig create mode 100644 plugins/Marketplace/templates/getNewPluginsAdmin.twig create mode 100644 plugins/Marketplace/templates/installPlugin.twig create mode 100644 plugins/Marketplace/templates/licenseform.twig create mode 100644 plugins/Marketplace/templates/macros.twig create mode 100644 plugins/Marketplace/templates/overview.twig create mode 100644 plugins/Marketplace/templates/paid-plugins-install-list.twig create mode 100644 plugins/Marketplace/templates/plugin-details.twig create mode 100644 plugins/Marketplace/templates/plugin-list.twig create mode 100644 plugins/Marketplace/templates/subscription-overview.twig create mode 100644 plugins/Marketplace/templates/updatePlugin.twig create mode 100644 plugins/Marketplace/tests/Fixtures/SimpleFixtureTrackFewVisits.php create mode 100644 plugins/Marketplace/tests/Framework/Mock/Client.php create mode 100644 plugins/Marketplace/tests/Framework/Mock/Consumer.php create mode 100644 plugins/Marketplace/tests/Framework/Mock/Environment.php create mode 100644 plugins/Marketplace/tests/Framework/Mock/Service.php create mode 100644 plugins/Marketplace/tests/Integration/ApiTest.php create mode 100644 plugins/Marketplace/tests/Integration/ClientTest.php create mode 100644 plugins/Marketplace/tests/Integration/EnvironmentTest.php create mode 100644 plugins/Marketplace/tests/Integration/Input/PluginNameTest.php create mode 100644 plugins/Marketplace/tests/Integration/LicenseKeyTest.php create mode 100644 plugins/Marketplace/tests/Integration/Plugins/InvalidLicensesTest.php create mode 100644 plugins/Marketplace/tests/Integration/PluginsTest.php create mode 100644 plugins/Marketplace/tests/Integration/ServiceTest.php create mode 100644 plugins/Marketplace/tests/Integration/UpdateCommunicationTest.php create mode 100644 plugins/Marketplace/tests/System/Api/ClientTest.php create mode 100644 plugins/Marketplace/tests/System/Api/ServiceTest.php create mode 100644 plugins/Marketplace/tests/Unit/ConsumerTest.php create mode 100644 plugins/Marketplace/tests/resources/emptyObjectResponse.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer1_paid2_custom1.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer2_paid1.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_consumer-access_token-consumer3_paid1_custom2.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_consumer-access_token-notexistingtoken.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_consumer-access_token-validbutnolicense.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_consumer-num_users-201-access_token-consumer1_paid2_custom1.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer1_paid2_custom1.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer2_paid1.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-consumer3_paid1_custom2.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-notexistingtoken.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_consumer_validate-access_token-validbutnolicense.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_consumer_validate.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_info.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-consumer1_paid2_custom1.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-consumer2_paid1.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-access_token-notexistingtoken.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-num_users-201-access_token-consumer1_paid2_custom1.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_plugins-purchase_type-paid-num_users-201-access_token-consumer2_paid1.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_plugins-query-nomatchforthisquery.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_plugins.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_plugins_Barometer_info.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_plugins_CustomPlugin1_info-access_token-notexistingtoken.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info-access_token-consumer3_paid1_custom2.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info-purchase_type-paid-num_users-201-access_token-consumer2_paid1.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_plugins_PaidPlugin1_info.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_plugins_TreemapVisualization_info.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_plugins_checkUpdates-pluginspluginsnameAnonymousPi.json create mode 100644 plugins/Marketplace/tests/resources/v2.0_themes.json create mode 100644 tests/UI/expected-screenshots/Marketplace_free_plugin_details_multiUserEnvironment.png create mode 100644 tests/UI/expected-screenshots/Marketplace_free_plugin_details_superuser.png create mode 100644 tests/UI/expected-screenshots/Marketplace_free_plugin_details_user.png create mode 100644 tests/UI/expected-screenshots/Marketplace_notification_plugincheck_exceededLicense.png create mode 100644 tests/UI/expected-screenshots/Marketplace_notification_plugincheck_expiredLicense.png create mode 100644 tests/UI/expected-screenshots/Marketplace_notification_plugincheck_noLicense.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_multiUserEnvironment.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_superuser.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugin_details_exceeded_license_user.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_multiUserEnvironment.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_superuser.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugin_details_no_license_user.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_multiUserEnvironment.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_multiUserEnvironment_installed.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_superuser.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_superuser_installed.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_user.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugin_details_valid_license_user_installed.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_multiUserEnvironment.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_superuser.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugins_no_license_user.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_multiUserEnvironment.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_superuser.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugins_with_exceeded_license_user.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_multiUserEnvironment.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_superuser.png create mode 100644 tests/UI/expected-screenshots/Marketplace_paid_plugins_with_license_user.png create mode 100644 tests/UI/expected-screenshots/Marketplace_subscription_overview_exceededLicense.png create mode 100644 tests/UI/expected-screenshots/Marketplace_subscription_overview_expiredLicense.png create mode 100644 tests/UI/expected-screenshots/Marketplace_subscription_overview_noLicense.png create mode 100644 tests/UI/expected-screenshots/Marketplace_subscription_overview_validLicense.png create mode 100644 tests/UI/expected-screenshots/Marketplace_superuser_enable_plugins_admin.png create mode 100644 tests/UI/expected-screenshots/Marketplace_superuser_enable_plugins_admin_with_multiserver_enabled.png create mode 100644 tests/UI/expected-screenshots/Marketplace_superuser_install_all_paid_plugins_at_once.png create mode 100644 tests/UI/expected-screenshots/Marketplace_superuser_invalid_license_key_entered.png create mode 100644 tests/UI/expected-screenshots/Marketplace_superuser_remove_license_key_confirmation.png create mode 100644 tests/UI/expected-screenshots/Marketplace_superuser_remove_license_key_confirmed.png create mode 100644 tests/UI/expected-screenshots/Marketplace_superuser_valid_license_key_entered.png create mode 100644 tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_multiUserEnvironment.png create mode 100644 tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_superuser.png create mode 100644 tests/UI/expected-screenshots/Marketplace_themes_with_valid_license_user.png create mode 100644 tests/UI/expected-screenshots/Marketplace_updates_multiUserEnvironment.png create mode 100644 tests/UI/expected-screenshots/Marketplace_updates_superuser.png create mode 100644 tests/UI/specs/Marketplace_spec.js 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 @@ -7,6 +7,68 @@ "hash": "75544710dc04491e5d39085ee8b7ae05", "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", @@ -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 @@ -80,6 +80,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. */ 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 @@ -232,6 +232,43 @@ class SettingsPiwik return true; } + /** + * 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. * 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 @@ -33,6 +33,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 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 @@ -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 " + ], + "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 " + ], + "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 (sizemax) { + 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 + + {% 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 %} + + {% endif %} + + + {% endif %} + + + +{% 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 %} +
+ {% for plugin in pluginsToShow %} +
+ {% embed 'contentBlock.twig' with {'title': ''} %} + {% block content %} +
+

+ {{ plugin.displayName }} +

+ +

+ {{ plugin.description }} + + › {{ 'General_MoreLowerCase'|translate }} +

+ + {% if showThemes %} + {# Screenshot for themes #} + + + {% endif %} + + + + {% macro moreDetailsLink(plugin) %} + {% set canBePurchased = not plugin.isDownloadable and plugin.shop is defined and plugin.shop and plugin.shop.url %} + + + {% 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 %} + + + {% endmacro %} + + + {% if isSuperUser %} + + {% else %} + + {% endif %} + +
+ {% endblock %} + {% endembed %} +
+ {% endfor %} +
+{% endif %} + +{% if pluginsToShow|length == 0 %} +
+ {% if showThemes %} + {{ 'Marketplace_NoThemesFound'|translate }} + {% else %} + {{ 'Marketplace_NoPluginsFound'|translate }} + {% endif %} +
+{% 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 %} + +
+ + {% if hasLicenseKey %} +

+ {{ 'Marketplace_PluginSubscriptionsList'|translate }} + {% if loginUrl %} + {{ 'Marketplace_OverviewPluginSubscriptionsAllDetails'|translate }} + {% endif %} +
+ {{ 'Marketplace_OverviewPluginSubscriptionsMissingInfo'|translate }} +
+ + {{ 'Marketplace_NoValidSubscriptionNoUpdates'|translate }} + {{ 'Marketplace_CurrentNumPiwikUsers'|translate('' ~ numUsers ~ '')|raw }} +

+ +
+ + + + + + + + + + + + + + {% if subscriptions|length %} + {% for subscription in subscriptions %} + + + + + + + + + + {% endfor %} + {% else %} + + {% endif %} + +
{{ 'General_Name'|translate }}{{ 'Marketplace_SubscriptionType'|translate }}{{ 'CorePluginsAdmin_Status'|translate }}{{ 'Marketplace_SubscriptionStartDate'|translate }}{{ 'Marketplace_SubscriptionEndDate'|translate }}{{ 'Marketplace_SubscriptionNextPaymentDate'|translate }}
+ {% if subscription.plugin.htmlUrl %} + + {% endif %} + + {{ subscription.plugin.displayName }} + + {% if subscription.plugin.htmlUrl %} + + {% endif %} + {{ subscription.productType }} + {% if not subscription.isValid %} + + {% elseif subscription.isExpiredSoon %} + + {% else %} + + {% endif %} + + {{ subscription.status }} + + {% if subscription.isExceeded is defined and subscription.isExceeded %} + {{ 'Marketplace_Exceeded'|translate }} + {% endif %} + {{ subscription.start }}{% if subscription.isValid and subscription.nextPayment %} + {{ 'Marketplace_LicenseRenewsNextPaymentDate'|translate }} + {% else %} + {{ subscription.end }} + {% endif %} + {{ subscription.nextPayment }}
{{ 'Marketplace_NoSubscriptionsFound'|translate }}
+ + + + {% else %} +

{{ 'Marketplace_OverviewPluginSubscriptionsMissingLicense'|translate('', '')|raw }}

+ {% endif %} +
+ +{% 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 %} + +
+ +

{{ 'Marketplace_UpdatingPlugin'|translate(plugin.name) }}

+ +
+ + {% if plugin.isTheme %} + +

{{ 'Marketplace_StepDownloadingThemeFromMarketplace'|translate }}

+ +

{{ 'Marketplace_StepUnzippingTheme'|translate }}

+ +

{{ 'Marketplace_StepReplaceExistingTheme'|translate }}

+ +

{{ 'Marketplace_StepThemeSuccessfullyUpdated'|translate(plugin.name, plugin.latestVersion) }}

+ + {% else %} + +

{{ 'Marketplace_StepDownloadingPluginFromMarketplace'|translate }}

+ +

{{ 'Marketplace_StepUnzippingPlugin'|translate }}

+ +

{{ 'Marketplace_StepReplaceExistingPlugin'|translate }}

+ +

{{ 'Marketplace_StepPluginSuccessfullyUpdated'|translate(plugin.name, plugin.latestVersion) }}

+ + {% endif %} + +

{{ 'General_Plugins'|translate }} + | + {{ 'CorePluginsAdmin_Themes'|translate }} + | + {{ 'Marketplace_Marketplace'|translate }}

+
+
+ +{% 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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: PaidPlugin1.
To resolve this issue either update your license key, get a subscription now or deactivate the plugin.
View your plugin subscriptions.', $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: PaidPlugin1.
You will no longer receive any updates for these plugins. To resolve this issue either renew your subscription now, or deactivate the plugin if you no longer use it.
View your plugin subscriptions.', $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: PaidPlugin2.
You will not be able to download updates for these plugins. To resolve this issue either delete some users or upgrade the subscription now.
View your plugin subscriptions.', $expired->getMessageExceededLicenses()); + $this->assertEquals('The licenses for the following plugins are expired: PaidPlugin1.
You will no longer receive any updates for these plugins. To resolve this issue either renew your subscription now, or deactivate the plugin if you no longer use it.
View your plugin subscriptions.', $expired->getMessageExpiredLicenses()); + } + + public function test_getMessageMissingLicenses_getMessageMissingLicenses_PaidPluginActivated() + { + $expired = $this->buildWithNoLicense(); + $this->assertEquals('You are using the following plugins without a license: PaidPlugin1.
To resolve this issue either update your license key, get a subscription now or deactivate the plugin.
View your plugin subscriptions.', $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 @@ +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 @@ +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/Marketplace/tests/Integration/UpdateCommunicationTest.php b/plugins/Marketplace/tests/Integration/UpdateCommunicationTest.php new file mode 100644 index 0000000000..bb7f46fba7 --- /dev/null +++ b/plugins/Marketplace/tests/Integration/UpdateCommunicationTest.php @@ -0,0 +1,185 @@ +settings = StaticContainer::get('Piwik\Plugins\CoreUpdater\SystemSettings'); + $this->settings->sendPluginUpdateEmail->setValue(true); + + $this->updateCommunication = new UpdateCommunication($this->settings); + } + + public function test_canBeEnabled() + { + $this->assertTrue(UpdateCommunication::canBeEnabled()); + + Config::getInstance()->General['enable_update_communication'] = 0; + $this->assertFalse(UpdateCommunication::canBeEnabled()); + + Config::getInstance()->General['enable_update_communication'] = 1; + $this->assertTrue(UpdateCommunication::canBeEnabled()); + } + + public function test_isEnabled_shouldReturnFalse_IfCannotBeEnabled() + { + $this->assertTrue($this->updateCommunication->isEnabled()); + + Config::getInstance()->General['enable_update_communication'] = 0; + $this->assertFalse($this->updateCommunication->isEnabled()); + } + + public function test_sendNotificationIfUpdatesAvailable_shouldNotSendNotification_IfNoUpdateAvailable() + { + $mock = $this->getCommunicationMock(array()); + $mock->expects($this->never())->method('sendEmailNotification'); + $mock->sendNotificationIfUpdatesAvailable(); + } + + /** + * @dataProvider provideSendNotificationData + */ + public function test_sendNotificationIfUpdatesAvailable($latestVersion, $lastSentVersion, $expects, $expectedLastSentVersion) + { + $pluginsHavingUpdate = array( + array('name' => 'MyTest', 'latestVersion' => $latestVersion, 'isTheme' => false) + ); + $this->setLastSentVersion('MyTest', $lastSentVersion); + + $mock = $this->getCommunicationMock($pluginsHavingUpdate); + $mock->expects($expects)->method('sendEmailNotification'); + $mock->sendNotificationIfUpdatesAvailable(); + + $this->assertEquals($expectedLastSentVersion, $this->getLastSentVersion('MyTest')); + } + + public function provideSendNotificationData() + { + return array( + array('33.0.0', '33.0.0', $this->never(), '33.0.0'), // shouldNotSend_IfAlreadyNotified + array('31.0.0', '33.0.0', $this->never(), '33.0.0'), // shouldNotSend_IfAlreadyNotifiedAboutLaterRelease + array('33.0.0', false, $this->once(), '33.0.0'), // shouldSend_IfUpdateAvailableAndNeverSentAnyBefore + array('33.0.0', '31.0.0', $this->once(), '33.0.0'), // shouldSend_IfUpdateAvailable, + ); + } + + public function test_sendNotificationIfUpdatesAvailable_ShouldSendOnlyOneEmail_IfMultipleUpdatesAreAvailable() + { + $mock = $this->getCommunicationMockHavingManyUpdates(); + $mock->expects($this->once())->method('sendEmailNotification'); + $mock->sendNotificationIfUpdatesAvailable(); + } + + public function test_sendNotificationIfUpdatesAvailable_ShouldUpdateAllSentVersions_IfMultipleUpdatesAreAvailable() + { + $mock = $this->getCommunicationMockHavingManyUpdates(); + $mock->expects($this->once())->method('sendEmailNotification'); + $mock->sendNotificationIfUpdatesAvailable(); + + $this->assertEquals('33.0.0', $this->getLastSentVersion('MyTest1')); + $this->assertEquals('32.0.0', $this->getLastSentVersion('MyTest2')); + $this->assertEquals('31.0.0', $this->getLastSentVersion('MyTest3')); + } + + public function test_sendNotificationIfUpdatesAvailable_ShouldSendCorrectText() + { + $subject = 'CoreUpdater_NotificationSubjectAvailablePluginUpdate'; + $rootUrl = Fixture::getTestRootUrl(); + $message = "ScheduledReports_EmailHello + +CoreUpdater_ThereIsNewPluginVersionAvailableForUpdate + + * MyTest1 33.0.0 + * MyTest2 32.0.0 + * MyTest3 31.0.0 + +CoreUpdater_NotificationClickToUpdatePlugins +{$rootUrl}index.php?module=CorePluginsAdmin&action=plugins + +Installation_HappyAnalysing"; + + $mock = $this->getCommunicationMockHavingManyUpdates(); + + $mock->expects($this->once())->method('sendEmailNotification') + ->with($this->equalTo($subject), $this->equalTo($message)); + + $mock->sendNotificationIfUpdatesAvailable(); + } + + private function setLastSentVersion($pluginName, $version) + { + Option::set('last_update_communication_sent_plugin_' . $pluginName, $version); + } + + private function getLastSentVersion($pluginName) + { + return Option::get('last_update_communication_sent_plugin_' . $pluginName); + } + + /** + * @param array $pluginsHavingUpdate + * @return UpdateCommunication + */ + private function getCommunicationMock($pluginsHavingUpdate) + { + $mock = $this->getMockBuilder('\Piwik\Plugins\Marketplace\UpdateCommunication') + ->setMethods(array('getPluginsHavingUpdate', 'sendEmailNotification')) + ->setConstructorArgs(array($this->settings)) + ->getMock(); + + $mock->expects($this->any()) + ->method('getPluginsHavingUpdate') + ->will($this->returnValue($pluginsHavingUpdate)); + + return $mock; + } + + private function getCommunicationMockHavingManyUpdates() + { + $pluginsHavingUpdate = array( + array('name' => 'MyTest1', 'latestVersion' => '33.0.0', 'isTheme' => false), + array('name' => 'MyTest2', 'latestVersion' => '32.0.0', 'isTheme' => false), + array('name' => 'MyTest3', 'latestVersion' => '31.0.0', 'isTheme' => false), + ); + + $this->setLastSentVersion('MyTest1', false); + $this->setLastSentVersion('MyTest2', false); + $this->setLastSentVersion('MyTest3', false); + + $mock = $this->getCommunicationMock($pluginsHavingUpdate); + + return $mock; + } +} 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 @@ +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 @@ +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 @@ +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

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

\"visual-link-screenshot-on-domz.png\"<\/p>\n\n", + "faq":"

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":"

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¤cy=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¤cy=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¤cy=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¤cy=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¤cy=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¤cy=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

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

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":"

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

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":"