From e2a76b53a8c626b772e7d836a4073a66a544bc21 Mon Sep 17 00:00:00 2001 From: Rick Peacock Date: Fri, 5 May 2017 16:57:36 +0100 Subject: [PATCH 1/4] Move original repo code into new repo structure --- .gitignore => magento1/.gitignore | 0 .htaccess => magento1/.htaccess | 0 CHANGELOG.md => magento1/CHANGELOG.md | 0 INSTALL.md => magento1/INSTALL.md | 0 LICENSE => magento1/LICENSE | 0 README.md => magento1/README.md | 0 behat.yml => magento1/behat.yml | 0 composer.json => magento1/composer.json | 0 composer.lock => magento1/composer.lock | 0 {features => magento1/features}/admin/delete_image.feature | 0 .../features}/admin/extension_disable_enable.feature | 0 .../features}/admin/prompted_to_sign_up_to_cloudinary.feature | 0 .../features}/bootstrap/Domain/ConfigurationContext.php | 0 .../features}/bootstrap/Domain/DeleteImageDomainContext.php | 0 .../features}/bootstrap/Domain/DomainContext.php | 0 .../features}/bootstrap/Domain/TransformationContext.php | 0 {features => magento1/features}/bootstrap/Fixtures/Admin.yaml | 0 .../features}/bootstrap/ImageProviders/ConfigImageProvider.php | 0 .../features}/bootstrap/ImageProviders/FakeImageProvider.php | 0 .../bootstrap/ImageProviders/TransformingImageProvider.php | 0 {features => magento1/features}/bootstrap/Page/AdminLogin.php | 0 .../bootstrap/Page/CloudinaryAdminSystemConfiguration.php | 0 .../features}/bootstrap/Page/CloudinaryManagement.php | 0 .../features}/bootstrap/Ui/AdminCredentialsContext.php | 0 .../features}/bootstrap/Ui/ModuleEnableContext.php | 0 {features => magento1/features}/configuration.feature | 0 {features => magento1/features}/image_provider_transform.feature | 0 {features => magento1/features}/image_provider_upload.feature | 0 .../features}/migration/admin_migrates_images.feature | 0 .../features}/migration/cloudinary_enable_disable.feature | 0 {features => magento1/features}/validate_credentials.feature | 0 modman => magento1/modman | 0 phpspec.yml => magento1/phpspec.yml | 0 phpunit.xml.dist => magento1/phpunit.xml.dist | 0 .../spec}/Cloudinary/Cloudinary/Model/MigrationSpec.php | 0 .../Cloudinary/Cloudinary/Model/SynchronisedMediaUnifierSpec.php | 0 {spec => magento1/spec}/CloudinaryExtension/CloudSpec.php | 0 .../spec}/CloudinaryExtension/CloudinaryImageProviderSpec.php | 0 {spec => magento1/spec}/CloudinaryExtension/ConfigurationSpec.php | 0 {spec => magento1/spec}/CloudinaryExtension/CredentialsSpec.php | 0 .../CloudinaryExtension/Image/Transformation/DimensionsSpec.php | 0 .../spec}/CloudinaryExtension/Image/TransformationSpec.php | 0 {spec => magento1/spec}/CloudinaryExtension/ImageSpec.php | 0 .../spec}/CloudinaryExtension/Migration/BatchUploaderSpec.php | 0 .../spec}/CloudinaryExtension/Migration/QueueSpec.php | 0 .../Security/CloudinaryEnvironmentVariableSpec.php | 0 {spec => magento1/spec}/CloudinaryExtension/Security/KeySpec.php | 0 .../spec}/CloudinaryExtension/Security/SecretSpec.php | 0 .../community/Cloudinary/Cloudinary/Block/Adminhtml/Manage.php | 0 .../Cloudinary/Cloudinary/Block/Adminhtml/Manage/Grid.php | 0 .../community/Cloudinary/Cloudinary/Block/Adminhtml/Page/Menu.php | 0 .../Cloudinary/Block/Adminhtml/System/Config/Signup.php | 0 .../code/community/Cloudinary/Cloudinary/Helper/Autoloader.php | 0 .../app/code/community/Cloudinary/Cloudinary/Helper/Console.php | 0 .../src}/app/code/community/Cloudinary/Cloudinary/Helper/Data.php | 0 .../app/code/community/Cloudinary/Cloudinary/Helper/Image.php | 0 .../Cloudinary/Cloudinary/Model/Catalog/Product/Image.php | 0 .../Cloudinary/Cloudinary/Model/Catalog/Product/Media.php | 0 .../Cloudinary/Cloudinary/Model/Catalog/Product/Media/Config.php | 0 .../Cloudinary/Cloudinary/Model/Cms/Adminhtml/Template/Filter.php | 0 .../community/Cloudinary/Cloudinary/Model/Cms/Synchronisation.php | 0 .../community/Cloudinary/Cloudinary/Model/Cms/Template/Filter.php | 0 .../code/community/Cloudinary/Cloudinary/Model/Cms/Uploader.php | 0 .../Cloudinary/Cloudinary/Model/Cms/Wysiwyg/Images/Storage.php | 0 .../community/Cloudinary/Cloudinary/Model/CollectionCounter.php | 0 .../src}/app/code/community/Cloudinary/Cloudinary/Model/Cron.php | 0 .../Cloudinary/Model/Exception/BadFilePathException.php | 0 .../src}/app/code/community/Cloudinary/Cloudinary/Model/Image.php | 0 .../app/code/community/Cloudinary/Cloudinary/Model/Logger.php | 0 .../Cloudinary/Cloudinary/Model/MagentoFolderTranslator.php | 0 .../app/code/community/Cloudinary/Cloudinary/Model/Migration.php | 0 .../code/community/Cloudinary/Cloudinary/Model/MigrationError.php | 0 .../app/code/community/Cloudinary/Cloudinary/Model/Observer.php | 0 .../Cloudinary/Model/Resource/Cms/Synchronisation/Collection.php | 0 .../Cloudinary/Cloudinary/Model/Resource/Media/Collection.php | 0 .../community/Cloudinary/Cloudinary/Model/Resource/Migration.php | 0 .../Cloudinary/Cloudinary/Model/Resource/MigrationError.php | 0 .../Cloudinary/Model/Resource/MigrationError/Collection.php | 0 .../Cloudinary/Cloudinary/Model/Resource/Synchronisation.php | 0 .../Cloudinary/Model/Resource/Synchronisation/Collection.php | 0 .../community/Cloudinary/Cloudinary/Model/Synchronisation.php | 0 .../Cloudinary/Cloudinary/Model/SynchronisedMediaUnifier.php | 0 .../Cloudinary/Model/System/Config/Source/Dropdown/Dpr.php | 0 .../Cloudinary/Model/System/Config/Source/Dropdown/Gravity.php | 0 .../Cloudinary/Model/System/Config/Source/Dropdown/Quality.php | 0 .../Cloudinary/controllers/Adminhtml/CloudinaryController.php | 0 .../Cloudinary/data/cloudinary_setup/data-upgrade-0.1.0-0.1.1.php | 0 .../Cloudinary/data/cloudinary_setup/data-upgrade-1.1.2-1.1.3.php | 0 .../app/code/community/Cloudinary/Cloudinary/etc/adminhtml.xml | 0 .../src}/app/code/community/Cloudinary/Cloudinary/etc/config.xml | 0 .../src}/app/code/community/Cloudinary/Cloudinary/etc/system.xml | 0 .../Cloudinary/Cloudinary/sql/cloudinary_setup/install-0.1.0.php | 0 .../Cloudinary/sql/cloudinary_setup/upgrade-0.1.0-0.1.1.php | 0 .../Cloudinary/sql/cloudinary_setup/upgrade-1.1.3-1.1.4.php | 0 .../Cloudinary/sql/cloudinary_setup/upgrade-1.1.4-1.1.5.php | 0 .../Cloudinary/sql/cloudinary_setup/upgrade-1.1.5-1.1.6.php | 0 .../adminhtml/default/default/layout/cloudinary/cloudinary.xml | 0 .../adminhtml/default/default/template/cloudinary/manage.phtml | 0 .../default/template/cloudinary/system/config/signup.phtml | 0 {src => magento1/src}/app/etc/modules/Cloudinary_Cloudinary.xml | 0 {src => magento1/src}/var/connect/Cloudinary_Cloudinary.xml | 0 101 files changed, 0 insertions(+), 0 deletions(-) rename .gitignore => magento1/.gitignore (100%) rename .htaccess => magento1/.htaccess (100%) rename CHANGELOG.md => magento1/CHANGELOG.md (100%) rename INSTALL.md => magento1/INSTALL.md (100%) rename LICENSE => magento1/LICENSE (100%) rename README.md => magento1/README.md (100%) rename behat.yml => magento1/behat.yml (100%) rename composer.json => magento1/composer.json (100%) rename composer.lock => magento1/composer.lock (100%) rename {features => magento1/features}/admin/delete_image.feature (100%) rename {features => magento1/features}/admin/extension_disable_enable.feature (100%) rename {features => magento1/features}/admin/prompted_to_sign_up_to_cloudinary.feature (100%) rename {features => magento1/features}/bootstrap/Domain/ConfigurationContext.php (100%) rename {features => magento1/features}/bootstrap/Domain/DeleteImageDomainContext.php (100%) rename {features => magento1/features}/bootstrap/Domain/DomainContext.php (100%) rename {features => magento1/features}/bootstrap/Domain/TransformationContext.php (100%) rename {features => magento1/features}/bootstrap/Fixtures/Admin.yaml (100%) rename {features => magento1/features}/bootstrap/ImageProviders/ConfigImageProvider.php (100%) rename {features => magento1/features}/bootstrap/ImageProviders/FakeImageProvider.php (100%) rename {features => magento1/features}/bootstrap/ImageProviders/TransformingImageProvider.php (100%) rename {features => magento1/features}/bootstrap/Page/AdminLogin.php (100%) rename {features => magento1/features}/bootstrap/Page/CloudinaryAdminSystemConfiguration.php (100%) rename {features => magento1/features}/bootstrap/Page/CloudinaryManagement.php (100%) rename {features => magento1/features}/bootstrap/Ui/AdminCredentialsContext.php (100%) rename {features => magento1/features}/bootstrap/Ui/ModuleEnableContext.php (100%) rename {features => magento1/features}/configuration.feature (100%) rename {features => magento1/features}/image_provider_transform.feature (100%) rename {features => magento1/features}/image_provider_upload.feature (100%) rename {features => magento1/features}/migration/admin_migrates_images.feature (100%) rename {features => magento1/features}/migration/cloudinary_enable_disable.feature (100%) rename {features => magento1/features}/validate_credentials.feature (100%) rename modman => magento1/modman (100%) rename phpspec.yml => magento1/phpspec.yml (100%) rename phpunit.xml.dist => magento1/phpunit.xml.dist (100%) rename {spec => magento1/spec}/Cloudinary/Cloudinary/Model/MigrationSpec.php (100%) rename {spec => magento1/spec}/Cloudinary/Cloudinary/Model/SynchronisedMediaUnifierSpec.php (100%) rename {spec => magento1/spec}/CloudinaryExtension/CloudSpec.php (100%) rename {spec => magento1/spec}/CloudinaryExtension/CloudinaryImageProviderSpec.php (100%) rename {spec => magento1/spec}/CloudinaryExtension/ConfigurationSpec.php (100%) rename {spec => magento1/spec}/CloudinaryExtension/CredentialsSpec.php (100%) rename {spec => magento1/spec}/CloudinaryExtension/Image/Transformation/DimensionsSpec.php (100%) rename {spec => magento1/spec}/CloudinaryExtension/Image/TransformationSpec.php (100%) rename {spec => magento1/spec}/CloudinaryExtension/ImageSpec.php (100%) rename {spec => magento1/spec}/CloudinaryExtension/Migration/BatchUploaderSpec.php (100%) rename {spec => magento1/spec}/CloudinaryExtension/Migration/QueueSpec.php (100%) rename {spec => magento1/spec}/CloudinaryExtension/Security/CloudinaryEnvironmentVariableSpec.php (100%) rename {spec => magento1/spec}/CloudinaryExtension/Security/KeySpec.php (100%) rename {spec => magento1/spec}/CloudinaryExtension/Security/SecretSpec.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Manage.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Manage/Grid.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Page/Menu.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/System/Config/Signup.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Helper/Autoloader.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Helper/Console.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Helper/Data.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Helper/Image.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Image.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Media.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Media/Config.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Cms/Adminhtml/Template/Filter.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Cms/Synchronisation.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Cms/Template/Filter.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Cms/Uploader.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Cms/Wysiwyg/Images/Storage.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/CollectionCounter.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Cron.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Exception/BadFilePathException.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Image.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Logger.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/MagentoFolderTranslator.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Migration.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/MigrationError.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Observer.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Resource/Cms/Synchronisation/Collection.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Resource/Media/Collection.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Resource/Migration.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Resource/MigrationError.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Resource/MigrationError/Collection.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Resource/Synchronisation.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Resource/Synchronisation/Collection.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/Synchronisation.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/SynchronisedMediaUnifier.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Dpr.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Gravity.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Quality.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/controllers/Adminhtml/CloudinaryController.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/data/cloudinary_setup/data-upgrade-0.1.0-0.1.1.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/data/cloudinary_setup/data-upgrade-1.1.2-1.1.3.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/etc/adminhtml.xml (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/etc/config.xml (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/etc/system.xml (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/install-0.1.0.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-0.1.0-0.1.1.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.3-1.1.4.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.4-1.1.5.php (100%) rename {src => magento1/src}/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.5-1.1.6.php (100%) rename {src => magento1/src}/app/design/adminhtml/default/default/layout/cloudinary/cloudinary.xml (100%) rename {src => magento1/src}/app/design/adminhtml/default/default/template/cloudinary/manage.phtml (100%) rename {src => magento1/src}/app/design/adminhtml/default/default/template/cloudinary/system/config/signup.phtml (100%) rename {src => magento1/src}/app/etc/modules/Cloudinary_Cloudinary.xml (100%) rename {src => magento1/src}/var/connect/Cloudinary_Cloudinary.xml (100%) diff --git a/.gitignore b/magento1/.gitignore similarity index 100% rename from .gitignore rename to magento1/.gitignore diff --git a/.htaccess b/magento1/.htaccess similarity index 100% rename from .htaccess rename to magento1/.htaccess diff --git a/CHANGELOG.md b/magento1/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to magento1/CHANGELOG.md diff --git a/INSTALL.md b/magento1/INSTALL.md similarity index 100% rename from INSTALL.md rename to magento1/INSTALL.md diff --git a/LICENSE b/magento1/LICENSE similarity index 100% rename from LICENSE rename to magento1/LICENSE diff --git a/README.md b/magento1/README.md similarity index 100% rename from README.md rename to magento1/README.md diff --git a/behat.yml b/magento1/behat.yml similarity index 100% rename from behat.yml rename to magento1/behat.yml diff --git a/composer.json b/magento1/composer.json similarity index 100% rename from composer.json rename to magento1/composer.json diff --git a/composer.lock b/magento1/composer.lock similarity index 100% rename from composer.lock rename to magento1/composer.lock diff --git a/features/admin/delete_image.feature b/magento1/features/admin/delete_image.feature similarity index 100% rename from features/admin/delete_image.feature rename to magento1/features/admin/delete_image.feature diff --git a/features/admin/extension_disable_enable.feature b/magento1/features/admin/extension_disable_enable.feature similarity index 100% rename from features/admin/extension_disable_enable.feature rename to magento1/features/admin/extension_disable_enable.feature diff --git a/features/admin/prompted_to_sign_up_to_cloudinary.feature b/magento1/features/admin/prompted_to_sign_up_to_cloudinary.feature similarity index 100% rename from features/admin/prompted_to_sign_up_to_cloudinary.feature rename to magento1/features/admin/prompted_to_sign_up_to_cloudinary.feature diff --git a/features/bootstrap/Domain/ConfigurationContext.php b/magento1/features/bootstrap/Domain/ConfigurationContext.php similarity index 100% rename from features/bootstrap/Domain/ConfigurationContext.php rename to magento1/features/bootstrap/Domain/ConfigurationContext.php diff --git a/features/bootstrap/Domain/DeleteImageDomainContext.php b/magento1/features/bootstrap/Domain/DeleteImageDomainContext.php similarity index 100% rename from features/bootstrap/Domain/DeleteImageDomainContext.php rename to magento1/features/bootstrap/Domain/DeleteImageDomainContext.php diff --git a/features/bootstrap/Domain/DomainContext.php b/magento1/features/bootstrap/Domain/DomainContext.php similarity index 100% rename from features/bootstrap/Domain/DomainContext.php rename to magento1/features/bootstrap/Domain/DomainContext.php diff --git a/features/bootstrap/Domain/TransformationContext.php b/magento1/features/bootstrap/Domain/TransformationContext.php similarity index 100% rename from features/bootstrap/Domain/TransformationContext.php rename to magento1/features/bootstrap/Domain/TransformationContext.php diff --git a/features/bootstrap/Fixtures/Admin.yaml b/magento1/features/bootstrap/Fixtures/Admin.yaml similarity index 100% rename from features/bootstrap/Fixtures/Admin.yaml rename to magento1/features/bootstrap/Fixtures/Admin.yaml diff --git a/features/bootstrap/ImageProviders/ConfigImageProvider.php b/magento1/features/bootstrap/ImageProviders/ConfigImageProvider.php similarity index 100% rename from features/bootstrap/ImageProviders/ConfigImageProvider.php rename to magento1/features/bootstrap/ImageProviders/ConfigImageProvider.php diff --git a/features/bootstrap/ImageProviders/FakeImageProvider.php b/magento1/features/bootstrap/ImageProviders/FakeImageProvider.php similarity index 100% rename from features/bootstrap/ImageProviders/FakeImageProvider.php rename to magento1/features/bootstrap/ImageProviders/FakeImageProvider.php diff --git a/features/bootstrap/ImageProviders/TransformingImageProvider.php b/magento1/features/bootstrap/ImageProviders/TransformingImageProvider.php similarity index 100% rename from features/bootstrap/ImageProviders/TransformingImageProvider.php rename to magento1/features/bootstrap/ImageProviders/TransformingImageProvider.php diff --git a/features/bootstrap/Page/AdminLogin.php b/magento1/features/bootstrap/Page/AdminLogin.php similarity index 100% rename from features/bootstrap/Page/AdminLogin.php rename to magento1/features/bootstrap/Page/AdminLogin.php diff --git a/features/bootstrap/Page/CloudinaryAdminSystemConfiguration.php b/magento1/features/bootstrap/Page/CloudinaryAdminSystemConfiguration.php similarity index 100% rename from features/bootstrap/Page/CloudinaryAdminSystemConfiguration.php rename to magento1/features/bootstrap/Page/CloudinaryAdminSystemConfiguration.php diff --git a/features/bootstrap/Page/CloudinaryManagement.php b/magento1/features/bootstrap/Page/CloudinaryManagement.php similarity index 100% rename from features/bootstrap/Page/CloudinaryManagement.php rename to magento1/features/bootstrap/Page/CloudinaryManagement.php diff --git a/features/bootstrap/Ui/AdminCredentialsContext.php b/magento1/features/bootstrap/Ui/AdminCredentialsContext.php similarity index 100% rename from features/bootstrap/Ui/AdminCredentialsContext.php rename to magento1/features/bootstrap/Ui/AdminCredentialsContext.php diff --git a/features/bootstrap/Ui/ModuleEnableContext.php b/magento1/features/bootstrap/Ui/ModuleEnableContext.php similarity index 100% rename from features/bootstrap/Ui/ModuleEnableContext.php rename to magento1/features/bootstrap/Ui/ModuleEnableContext.php diff --git a/features/configuration.feature b/magento1/features/configuration.feature similarity index 100% rename from features/configuration.feature rename to magento1/features/configuration.feature diff --git a/features/image_provider_transform.feature b/magento1/features/image_provider_transform.feature similarity index 100% rename from features/image_provider_transform.feature rename to magento1/features/image_provider_transform.feature diff --git a/features/image_provider_upload.feature b/magento1/features/image_provider_upload.feature similarity index 100% rename from features/image_provider_upload.feature rename to magento1/features/image_provider_upload.feature diff --git a/features/migration/admin_migrates_images.feature b/magento1/features/migration/admin_migrates_images.feature similarity index 100% rename from features/migration/admin_migrates_images.feature rename to magento1/features/migration/admin_migrates_images.feature diff --git a/features/migration/cloudinary_enable_disable.feature b/magento1/features/migration/cloudinary_enable_disable.feature similarity index 100% rename from features/migration/cloudinary_enable_disable.feature rename to magento1/features/migration/cloudinary_enable_disable.feature diff --git a/features/validate_credentials.feature b/magento1/features/validate_credentials.feature similarity index 100% rename from features/validate_credentials.feature rename to magento1/features/validate_credentials.feature diff --git a/modman b/magento1/modman similarity index 100% rename from modman rename to magento1/modman diff --git a/phpspec.yml b/magento1/phpspec.yml similarity index 100% rename from phpspec.yml rename to magento1/phpspec.yml diff --git a/phpunit.xml.dist b/magento1/phpunit.xml.dist similarity index 100% rename from phpunit.xml.dist rename to magento1/phpunit.xml.dist diff --git a/spec/Cloudinary/Cloudinary/Model/MigrationSpec.php b/magento1/spec/Cloudinary/Cloudinary/Model/MigrationSpec.php similarity index 100% rename from spec/Cloudinary/Cloudinary/Model/MigrationSpec.php rename to magento1/spec/Cloudinary/Cloudinary/Model/MigrationSpec.php diff --git a/spec/Cloudinary/Cloudinary/Model/SynchronisedMediaUnifierSpec.php b/magento1/spec/Cloudinary/Cloudinary/Model/SynchronisedMediaUnifierSpec.php similarity index 100% rename from spec/Cloudinary/Cloudinary/Model/SynchronisedMediaUnifierSpec.php rename to magento1/spec/Cloudinary/Cloudinary/Model/SynchronisedMediaUnifierSpec.php diff --git a/spec/CloudinaryExtension/CloudSpec.php b/magento1/spec/CloudinaryExtension/CloudSpec.php similarity index 100% rename from spec/CloudinaryExtension/CloudSpec.php rename to magento1/spec/CloudinaryExtension/CloudSpec.php diff --git a/spec/CloudinaryExtension/CloudinaryImageProviderSpec.php b/magento1/spec/CloudinaryExtension/CloudinaryImageProviderSpec.php similarity index 100% rename from spec/CloudinaryExtension/CloudinaryImageProviderSpec.php rename to magento1/spec/CloudinaryExtension/CloudinaryImageProviderSpec.php diff --git a/spec/CloudinaryExtension/ConfigurationSpec.php b/magento1/spec/CloudinaryExtension/ConfigurationSpec.php similarity index 100% rename from spec/CloudinaryExtension/ConfigurationSpec.php rename to magento1/spec/CloudinaryExtension/ConfigurationSpec.php diff --git a/spec/CloudinaryExtension/CredentialsSpec.php b/magento1/spec/CloudinaryExtension/CredentialsSpec.php similarity index 100% rename from spec/CloudinaryExtension/CredentialsSpec.php rename to magento1/spec/CloudinaryExtension/CredentialsSpec.php diff --git a/spec/CloudinaryExtension/Image/Transformation/DimensionsSpec.php b/magento1/spec/CloudinaryExtension/Image/Transformation/DimensionsSpec.php similarity index 100% rename from spec/CloudinaryExtension/Image/Transformation/DimensionsSpec.php rename to magento1/spec/CloudinaryExtension/Image/Transformation/DimensionsSpec.php diff --git a/spec/CloudinaryExtension/Image/TransformationSpec.php b/magento1/spec/CloudinaryExtension/Image/TransformationSpec.php similarity index 100% rename from spec/CloudinaryExtension/Image/TransformationSpec.php rename to magento1/spec/CloudinaryExtension/Image/TransformationSpec.php diff --git a/spec/CloudinaryExtension/ImageSpec.php b/magento1/spec/CloudinaryExtension/ImageSpec.php similarity index 100% rename from spec/CloudinaryExtension/ImageSpec.php rename to magento1/spec/CloudinaryExtension/ImageSpec.php diff --git a/spec/CloudinaryExtension/Migration/BatchUploaderSpec.php b/magento1/spec/CloudinaryExtension/Migration/BatchUploaderSpec.php similarity index 100% rename from spec/CloudinaryExtension/Migration/BatchUploaderSpec.php rename to magento1/spec/CloudinaryExtension/Migration/BatchUploaderSpec.php diff --git a/spec/CloudinaryExtension/Migration/QueueSpec.php b/magento1/spec/CloudinaryExtension/Migration/QueueSpec.php similarity index 100% rename from spec/CloudinaryExtension/Migration/QueueSpec.php rename to magento1/spec/CloudinaryExtension/Migration/QueueSpec.php diff --git a/spec/CloudinaryExtension/Security/CloudinaryEnvironmentVariableSpec.php b/magento1/spec/CloudinaryExtension/Security/CloudinaryEnvironmentVariableSpec.php similarity index 100% rename from spec/CloudinaryExtension/Security/CloudinaryEnvironmentVariableSpec.php rename to magento1/spec/CloudinaryExtension/Security/CloudinaryEnvironmentVariableSpec.php diff --git a/spec/CloudinaryExtension/Security/KeySpec.php b/magento1/spec/CloudinaryExtension/Security/KeySpec.php similarity index 100% rename from spec/CloudinaryExtension/Security/KeySpec.php rename to magento1/spec/CloudinaryExtension/Security/KeySpec.php diff --git a/spec/CloudinaryExtension/Security/SecretSpec.php b/magento1/spec/CloudinaryExtension/Security/SecretSpec.php similarity index 100% rename from spec/CloudinaryExtension/Security/SecretSpec.php rename to magento1/spec/CloudinaryExtension/Security/SecretSpec.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Manage.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Manage.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Manage.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Manage.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Manage/Grid.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Manage/Grid.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Manage/Grid.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Manage/Grid.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Page/Menu.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Page/Menu.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Page/Menu.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/Page/Menu.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/System/Config/Signup.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/System/Config/Signup.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/System/Config/Signup.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Block/Adminhtml/System/Config/Signup.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Helper/Autoloader.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Helper/Autoloader.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Helper/Autoloader.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Helper/Autoloader.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Helper/Console.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Helper/Console.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Helper/Console.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Helper/Console.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Helper/Data.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Helper/Data.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Helper/Data.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Helper/Data.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Helper/Image.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Helper/Image.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Helper/Image.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Helper/Image.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Image.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Image.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Image.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Image.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Media.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Media.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Media.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Media.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Media/Config.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Media/Config.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Media/Config.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Catalog/Product/Media/Config.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Adminhtml/Template/Filter.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Adminhtml/Template/Filter.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Adminhtml/Template/Filter.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Adminhtml/Template/Filter.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Synchronisation.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Synchronisation.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Synchronisation.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Synchronisation.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Template/Filter.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Template/Filter.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Template/Filter.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Template/Filter.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Uploader.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Uploader.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Uploader.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Uploader.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Wysiwyg/Images/Storage.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Wysiwyg/Images/Storage.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Wysiwyg/Images/Storage.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cms/Wysiwyg/Images/Storage.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/CollectionCounter.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/CollectionCounter.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/CollectionCounter.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/CollectionCounter.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Cron.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cron.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Cron.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cron.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Exception/BadFilePathException.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Exception/BadFilePathException.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Exception/BadFilePathException.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Exception/BadFilePathException.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Image.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Image.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Image.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Image.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Logger.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Logger.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Logger.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Logger.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/MagentoFolderTranslator.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/MagentoFolderTranslator.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/MagentoFolderTranslator.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/MagentoFolderTranslator.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Migration.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Migration.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Migration.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Migration.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/MigrationError.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/MigrationError.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/MigrationError.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/MigrationError.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Observer.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Observer.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Observer.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Observer.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Cms/Synchronisation/Collection.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Cms/Synchronisation/Collection.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Cms/Synchronisation/Collection.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Cms/Synchronisation/Collection.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Media/Collection.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Media/Collection.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Media/Collection.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Media/Collection.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Migration.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Migration.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Migration.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Migration.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/MigrationError.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/MigrationError.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Resource/MigrationError.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/MigrationError.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/MigrationError/Collection.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/MigrationError/Collection.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Resource/MigrationError/Collection.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/MigrationError/Collection.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Synchronisation.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Synchronisation.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Synchronisation.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Synchronisation.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Synchronisation/Collection.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Synchronisation/Collection.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Synchronisation/Collection.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Synchronisation/Collection.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/Synchronisation.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Synchronisation.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/Synchronisation.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Synchronisation.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/SynchronisedMediaUnifier.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/SynchronisedMediaUnifier.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/SynchronisedMediaUnifier.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/SynchronisedMediaUnifier.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Dpr.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Dpr.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Dpr.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Dpr.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Gravity.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Gravity.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Gravity.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Gravity.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Quality.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Quality.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Quality.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/Model/System/Config/Source/Dropdown/Quality.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/controllers/Adminhtml/CloudinaryController.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/controllers/Adminhtml/CloudinaryController.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/controllers/Adminhtml/CloudinaryController.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/controllers/Adminhtml/CloudinaryController.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/data/cloudinary_setup/data-upgrade-0.1.0-0.1.1.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/data/cloudinary_setup/data-upgrade-0.1.0-0.1.1.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/data/cloudinary_setup/data-upgrade-0.1.0-0.1.1.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/data/cloudinary_setup/data-upgrade-0.1.0-0.1.1.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/data/cloudinary_setup/data-upgrade-1.1.2-1.1.3.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/data/cloudinary_setup/data-upgrade-1.1.2-1.1.3.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/data/cloudinary_setup/data-upgrade-1.1.2-1.1.3.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/data/cloudinary_setup/data-upgrade-1.1.2-1.1.3.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/etc/adminhtml.xml b/magento1/src/app/code/community/Cloudinary/Cloudinary/etc/adminhtml.xml similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/etc/adminhtml.xml rename to magento1/src/app/code/community/Cloudinary/Cloudinary/etc/adminhtml.xml diff --git a/src/app/code/community/Cloudinary/Cloudinary/etc/config.xml b/magento1/src/app/code/community/Cloudinary/Cloudinary/etc/config.xml similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/etc/config.xml rename to magento1/src/app/code/community/Cloudinary/Cloudinary/etc/config.xml diff --git a/src/app/code/community/Cloudinary/Cloudinary/etc/system.xml b/magento1/src/app/code/community/Cloudinary/Cloudinary/etc/system.xml similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/etc/system.xml rename to magento1/src/app/code/community/Cloudinary/Cloudinary/etc/system.xml diff --git a/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/install-0.1.0.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/install-0.1.0.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/install-0.1.0.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/install-0.1.0.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-0.1.0-0.1.1.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-0.1.0-0.1.1.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-0.1.0-0.1.1.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-0.1.0-0.1.1.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.3-1.1.4.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.3-1.1.4.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.3-1.1.4.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.3-1.1.4.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.4-1.1.5.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.4-1.1.5.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.4-1.1.5.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.4-1.1.5.php diff --git a/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.5-1.1.6.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.5-1.1.6.php similarity index 100% rename from src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.5-1.1.6.php rename to magento1/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-1.1.5-1.1.6.php diff --git a/src/app/design/adminhtml/default/default/layout/cloudinary/cloudinary.xml b/magento1/src/app/design/adminhtml/default/default/layout/cloudinary/cloudinary.xml similarity index 100% rename from src/app/design/adminhtml/default/default/layout/cloudinary/cloudinary.xml rename to magento1/src/app/design/adminhtml/default/default/layout/cloudinary/cloudinary.xml diff --git a/src/app/design/adminhtml/default/default/template/cloudinary/manage.phtml b/magento1/src/app/design/adminhtml/default/default/template/cloudinary/manage.phtml similarity index 100% rename from src/app/design/adminhtml/default/default/template/cloudinary/manage.phtml rename to magento1/src/app/design/adminhtml/default/default/template/cloudinary/manage.phtml diff --git a/src/app/design/adminhtml/default/default/template/cloudinary/system/config/signup.phtml b/magento1/src/app/design/adminhtml/default/default/template/cloudinary/system/config/signup.phtml similarity index 100% rename from src/app/design/adminhtml/default/default/template/cloudinary/system/config/signup.phtml rename to magento1/src/app/design/adminhtml/default/default/template/cloudinary/system/config/signup.phtml diff --git a/src/app/etc/modules/Cloudinary_Cloudinary.xml b/magento1/src/app/etc/modules/Cloudinary_Cloudinary.xml similarity index 100% rename from src/app/etc/modules/Cloudinary_Cloudinary.xml rename to magento1/src/app/etc/modules/Cloudinary_Cloudinary.xml diff --git a/src/var/connect/Cloudinary_Cloudinary.xml b/magento1/src/var/connect/Cloudinary_Cloudinary.xml similarity index 100% rename from src/var/connect/Cloudinary_Cloudinary.xml rename to magento1/src/var/connect/Cloudinary_Cloudinary.xml From 57e65652a7d331f290938e7cdec5d0a578e8f021 Mon Sep 17 00:00:00 2001 From: Rick Peacock Date: Fri, 5 May 2017 17:03:16 +0100 Subject: [PATCH 2/4] Add release Magento1 module 2.2.0 in new repo structure --- magento1/composer.json | 6 +- magento1/composer.lock | 125 ++++----- .../bootstrap/EndToEnd/CloudinaryConfig.php | 61 +++++ .../features/bootstrap/EndToEnd/Context.php | 241 ++++++++++++++++++ .../features/bootstrap/EndToEnd/Fixture.php | 65 +++++ .../bootstrap/Facade/CloudinaryConsole.php | 33 +++ .../features/bootstrap/Facade/Magento.php | 94 +++++++ .../features/bootstrap/Fixtures/Apple.yaml | 17 ++ .../features/bootstrap/Fixtures/apple.jpg | Bin 0 -> 8571 bytes magento1/features/e2e/delete.feature | 14 + magento1/features/e2e/foldered.feature | 23 ++ magento1/features/e2e/upload.feature | 13 + .../Cloudinary/Cloudinary/Helper/CronSpec.php | 79 ++++++ .../Cloudinary/Cloudinary/Helper/Cron.php | 23 ++ .../Cloudinary/Model/Configuration.php | 209 +++++++++++++++ .../Cloudinary/Cloudinary/Model/Cron.php | 12 +- .../Cloudinary/Model/MigrationError.php | 18 ++ .../Cms/Synchronisation/Collection.php | 55 ++++ .../Resource/Synchronisation/Collection.php | 9 + .../Model/SynchronisedMediaUnifier.php | 48 +++- .../Model/SynchronizationChecker.php | 18 ++ .../Adminhtml/CloudinaryController.php | 21 ++ .../Cloudinary/Cloudinary/etc/config.xml | 2 +- .../cloudinary_setup/upgrade-2.0.0-2.1.0.php | 33 +++ .../src/lib/CloudinaryExtension/Cloud.php | 25 ++ .../CloudinaryImageProvider.php | 89 +++++++ .../lib/CloudinaryExtension/Configuration.php | 97 +++++++ .../lib/CloudinaryExtension/Credentials.php | 30 +++ .../Exception/InvalidCredentials.php | 10 + .../Exception/MigrationError.php | 50 ++++ .../CloudinaryExtension/FolderTranslator.php | 16 ++ .../src/lib/CloudinaryExtension/Image.php | 52 ++++ .../Image/Synchronizable.php | 10 + .../Image/Transformation.php | 106 ++++++++ .../Image/Transformation/Dimensions.php | 36 +++ .../Image/Transformation/Dpr.php | 23 ++ .../Image/Transformation/FetchFormat.php | 25 ++ .../Image/Transformation/Format.php | 25 ++ .../Image/Transformation/Gravity.php | 30 +++ .../Image/Transformation/Quality.php | 23 ++ .../lib/CloudinaryExtension/ImageProvider.php | 13 + .../Migration/BatchUploader.php | 91 +++++++ .../CloudinaryExtension/Migration/Logger.php | 14 + .../CloudinaryExtension/Migration/Queue.php | 47 ++++ .../Migration/SynchronizedMediaRepository.php | 8 + .../CloudinaryExtension/Migration/Task.php | 14 + .../Security/ApiSignature.php | 26 ++ .../CloudinaryEnvironmentVariable.php | 50 ++++ .../Security/ConsoleUrl.php | 26 ++ .../Security/EnvironmentVariable.php | 9 + .../lib/CloudinaryExtension/Security/Key.php | 25 ++ .../CloudinaryExtension/Security/Secret.php | 24 ++ .../Security/SignedConsoleUrl.php | 31 +++ .../ValidateRemoteUrlRequest.php | 55 ++++ .../src/var/connect/Cloudinary_Cloudinary.xml | 14 +- 55 files changed, 2230 insertions(+), 83 deletions(-) create mode 100644 magento1/features/bootstrap/EndToEnd/CloudinaryConfig.php create mode 100755 magento1/features/bootstrap/EndToEnd/Context.php create mode 100644 magento1/features/bootstrap/EndToEnd/Fixture.php create mode 100644 magento1/features/bootstrap/Facade/CloudinaryConsole.php create mode 100644 magento1/features/bootstrap/Facade/Magento.php create mode 100644 magento1/features/bootstrap/Fixtures/Apple.yaml create mode 100644 magento1/features/bootstrap/Fixtures/apple.jpg create mode 100644 magento1/features/e2e/delete.feature create mode 100644 magento1/features/e2e/foldered.feature create mode 100644 magento1/features/e2e/upload.feature create mode 100644 magento1/spec/Cloudinary/Cloudinary/Helper/CronSpec.php create mode 100644 magento1/src/app/code/community/Cloudinary/Cloudinary/Helper/Cron.php create mode 100644 magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Configuration.php create mode 100644 magento1/src/app/code/community/Cloudinary/Cloudinary/Model/SynchronizationChecker.php create mode 100644 magento1/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-2.0.0-2.1.0.php create mode 100755 magento1/src/lib/CloudinaryExtension/Cloud.php create mode 100755 magento1/src/lib/CloudinaryExtension/CloudinaryImageProvider.php create mode 100755 magento1/src/lib/CloudinaryExtension/Configuration.php create mode 100755 magento1/src/lib/CloudinaryExtension/Credentials.php create mode 100755 magento1/src/lib/CloudinaryExtension/Exception/InvalidCredentials.php create mode 100644 magento1/src/lib/CloudinaryExtension/Exception/MigrationError.php create mode 100644 magento1/src/lib/CloudinaryExtension/FolderTranslator.php create mode 100755 magento1/src/lib/CloudinaryExtension/Image.php create mode 100755 magento1/src/lib/CloudinaryExtension/Image/Synchronizable.php create mode 100755 magento1/src/lib/CloudinaryExtension/Image/Transformation.php create mode 100755 magento1/src/lib/CloudinaryExtension/Image/Transformation/Dimensions.php create mode 100755 magento1/src/lib/CloudinaryExtension/Image/Transformation/Dpr.php create mode 100755 magento1/src/lib/CloudinaryExtension/Image/Transformation/FetchFormat.php create mode 100755 magento1/src/lib/CloudinaryExtension/Image/Transformation/Format.php create mode 100755 magento1/src/lib/CloudinaryExtension/Image/Transformation/Gravity.php create mode 100755 magento1/src/lib/CloudinaryExtension/Image/Transformation/Quality.php create mode 100755 magento1/src/lib/CloudinaryExtension/ImageProvider.php create mode 100755 magento1/src/lib/CloudinaryExtension/Migration/BatchUploader.php create mode 100755 magento1/src/lib/CloudinaryExtension/Migration/Logger.php create mode 100755 magento1/src/lib/CloudinaryExtension/Migration/Queue.php create mode 100755 magento1/src/lib/CloudinaryExtension/Migration/SynchronizedMediaRepository.php create mode 100755 magento1/src/lib/CloudinaryExtension/Migration/Task.php create mode 100755 magento1/src/lib/CloudinaryExtension/Security/ApiSignature.php create mode 100755 magento1/src/lib/CloudinaryExtension/Security/CloudinaryEnvironmentVariable.php create mode 100755 magento1/src/lib/CloudinaryExtension/Security/ConsoleUrl.php create mode 100755 magento1/src/lib/CloudinaryExtension/Security/EnvironmentVariable.php create mode 100755 magento1/src/lib/CloudinaryExtension/Security/Key.php create mode 100755 magento1/src/lib/CloudinaryExtension/Security/Secret.php create mode 100755 magento1/src/lib/CloudinaryExtension/Security/SignedConsoleUrl.php create mode 100755 magento1/src/lib/CloudinaryExtension/ValidateRemoteUrlRequest.php diff --git a/magento1/composer.json b/magento1/composer.json index 6b488a3..3b8266d 100755 --- a/magento1/composer.json +++ b/magento1/composer.json @@ -5,9 +5,9 @@ "description": "Cloudinary Magento Integration.", "require": { "php": ">=5.4.0", - "cloudinary/cloudinary_php": "~1.6.0", - "inviqa/cloudinary-core": "~1.1.0", - "inviqa/cloudinary-m1-testcard": "~1.1.1" + "cloudinary/cloudinary_php": "1.6.2", + "inviqa/cloudinary-core": "1.3.1", + "inviqa/cloudinary-m1-testcard": "1.1.2" }, "require-dev": { "phpspec/phpspec": "2.1.0-RC1", diff --git a/magento1/composer.lock b/magento1/composer.lock index 4b3e582..96bed97 100755 --- a/magento1/composer.lock +++ b/magento1/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "859d806db18164a306d47e49657e3114", - "content-hash": "439eeda0b48d168d7b0aa8fa8d595174", + "hash": "23dc88dfbdefa71f1eee1733ddde3896", + "content-hash": "40bb986bab834c8310f9e5acdf6e99ca", "packages": [ { "name": "cloudinary/cloudinary_php", @@ -163,16 +163,16 @@ }, { "name": "inviqa/cloudinary-core", - "version": "1.1.0", + "version": "1.3.1", "source": { "type": "git", "url": "git@github.com:inviqa/cloudinary-core.git", - "reference": "46a05bb73c6d597ebb96a7609034c3d35300e00d" + "reference": "870367032dd84827f9ddde095caf4ef922f80fc7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/inviqa/cloudinary-core/zipball/46a05bb73c6d597ebb96a7609034c3d35300e00d", - "reference": "46a05bb73c6d597ebb96a7609034c3d35300e00d", + "url": "https://api.github.com/repos/inviqa/cloudinary-core/zipball/870367032dd84827f9ddde095caf4ef922f80fc7", + "reference": "870367032dd84827f9ddde095caf4ef922f80fc7", "shasum": "" }, "require": { @@ -203,10 +203,10 @@ ], "description": "Cloudinary Core.", "support": { - "source": "https://github.com/inviqa/cloudinary-core/tree/1.1.0", + "source": "https://github.com/inviqa/cloudinary-core/tree/1.3.1", "issues": "https://github.com/inviqa/cloudinary-core/issues" }, - "time": "2017-03-16 14:47:52" + "time": "2017-04-24 11:07:11" }, { "name": "inviqa/cloudinary-m1-testcard", @@ -461,16 +461,16 @@ }, { "name": "symfony/console", - "version": "v2.8.18", + "version": "v2.8.19", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "81508e6fac4476771275a3f4f53c3fee9b956bfa" + "reference": "86407ff20855a5eaa2a7219bd815e9c40a88633e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/81508e6fac4476771275a3f4f53c3fee9b956bfa", - "reference": "81508e6fac4476771275a3f4f53c3fee9b956bfa", + "url": "https://api.github.com/repos/symfony/console/zipball/86407ff20855a5eaa2a7219bd815e9c40a88633e", + "reference": "86407ff20855a5eaa2a7219bd815e9c40a88633e", "shasum": "" }, "require": { @@ -518,7 +518,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-03-04 11:00:12" + "time": "2017-04-03 20:37:06" }, { "name": "symfony/debug", @@ -1066,25 +1066,29 @@ }, { "name": "behat/transliterator", - "version": "v1.1.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/Behat/Transliterator.git", - "reference": "868e05be3a9f25ba6424c2dd4849567f50715003" + "reference": "826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Transliterator/zipball/868e05be3a9f25ba6424c2dd4849567f50715003", - "reference": "868e05be3a9f25ba6424c2dd4849567f50715003", + "url": "https://api.github.com/repos/Behat/Transliterator/zipball/826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c", + "reference": "826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "require-dev": { + "chuyskywalker/rolling-curl": "^3.1", + "php-yaoi/php-yaoi": "^1.0" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "1.2-dev" } }, "autoload": { @@ -1102,7 +1106,7 @@ "slug", "transliterator" ], - "time": "2015-09-28 16:26:35" + "time": "2017-04-04 11:38:05" }, { "name": "bossa/phpspec2-expect", @@ -2478,21 +2482,24 @@ }, { "name": "react/promise", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "2760f3898b7e931aa71153852dcd48a75c9b95db" + "reference": "62785ae604c8d69725d693eb370e1d67e94c4053" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/2760f3898b7e931aa71153852dcd48a75c9b95db", - "reference": "2760f3898b7e931aa71153852dcd48a75c9b95db", + "url": "https://api.github.com/repos/reactphp/promise/zipball/62785ae604c8d69725d693eb370e1d67e94c4053", + "reference": "62785ae604c8d69725d693eb370e1d67e94c4053", "shasum": "" }, "require": { "php": ">=5.4.0" }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, "type": "library", "autoload": { "psr-4": { @@ -2517,7 +2524,7 @@ "promise", "promises" ], - "time": "2016-12-22 14:09:01" + "time": "2017-03-25 12:08:31" }, { "name": "sebastian/exporter", @@ -2953,7 +2960,7 @@ }, { "name": "symfony/browser-kit", - "version": "v2.8.18", + "version": "v2.8.19", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", @@ -3010,7 +3017,7 @@ }, { "name": "symfony/class-loader", - "version": "v2.8.18", + "version": "v2.8.19", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", @@ -3063,16 +3070,16 @@ }, { "name": "symfony/config", - "version": "v2.8.18", + "version": "v2.8.19", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "06ce6bb46c24963ec09323da45d0f4f85d3cecd2" + "reference": "35b7dfa089d7605eb1fdd46281b3070fb9f38750" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/06ce6bb46c24963ec09323da45d0f4f85d3cecd2", - "reference": "06ce6bb46c24963ec09323da45d0f4f85d3cecd2", + "url": "https://api.github.com/repos/symfony/config/zipball/35b7dfa089d7605eb1fdd46281b3070fb9f38750", + "reference": "35b7dfa089d7605eb1fdd46281b3070fb9f38750", "shasum": "" }, "require": { @@ -3115,11 +3122,11 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2017-03-01 18:13:50" + "time": "2017-04-04 15:24:26" }, { "name": "symfony/css-selector", - "version": "v2.8.18", + "version": "v2.8.19", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -3232,7 +3239,7 @@ }, { "name": "symfony/dom-crawler", - "version": "v2.8.18", + "version": "v2.8.19", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", @@ -3288,16 +3295,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v2.8.18", + "version": "v2.8.19", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "bb4ec47e8e109c1c1172145732d0aa468d967cd0" + "reference": "88b65f0ac25355090e524aba4ceb066025df8bd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/bb4ec47e8e109c1c1172145732d0aa468d967cd0", - "reference": "bb4ec47e8e109c1c1172145732d0aa468d967cd0", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/88b65f0ac25355090e524aba4ceb066025df8bd2", + "reference": "88b65f0ac25355090e524aba4ceb066025df8bd2", "shasum": "" }, "require": { @@ -3344,7 +3351,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2017-02-21 08:33:48" + "time": "2017-04-03 20:37:06" }, { "name": "symfony/filesystem", @@ -3397,16 +3404,16 @@ }, { "name": "symfony/finder", - "version": "v2.8.18", + "version": "v2.8.19", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "5fc4b5cab38b9d28be318fcffd8066988e7d9451" + "reference": "7131327eb95d86d72039fd1216226c28f36fd02a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/5fc4b5cab38b9d28be318fcffd8066988e7d9451", - "reference": "5fc4b5cab38b9d28be318fcffd8066988e7d9451", + "url": "https://api.github.com/repos/symfony/finder/zipball/7131327eb95d86d72039fd1216226c28f36fd02a", + "reference": "7131327eb95d86d72039fd1216226c28f36fd02a", "shasum": "" }, "require": { @@ -3442,7 +3449,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-02-21 08:33:48" + "time": "2017-03-20 08:46:40" }, { "name": "symfony/polyfill-apcu", @@ -3499,7 +3506,7 @@ }, { "name": "symfony/process", - "version": "v2.8.18", + "version": "v2.8.19", "source": { "type": "git", "url": "https://github.com/symfony/process.git", @@ -3548,16 +3555,16 @@ }, { "name": "symfony/translation", - "version": "v2.8.18", + "version": "v2.8.19", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "b538355bc99db2ec7cc35284ec76d92ae7d1d256" + "reference": "047e97a64d609778cadfc76e3a09793696bb19f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/b538355bc99db2ec7cc35284ec76d92ae7d1d256", - "reference": "b538355bc99db2ec7cc35284ec76d92ae7d1d256", + "url": "https://api.github.com/repos/symfony/translation/zipball/047e97a64d609778cadfc76e3a09793696bb19f1", + "reference": "047e97a64d609778cadfc76e3a09793696bb19f1", "shasum": "" }, "require": { @@ -3608,20 +3615,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2017-03-04 12:20:59" + "time": "2017-03-21 21:39:01" }, { "name": "symfony/yaml", - "version": "v2.8.18", + "version": "v2.8.19", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "2a7bab3c16f6f452c47818fdd08f3b1e49ffcf7d" + "reference": "286d84891690b0e2515874717e49360d1c98a703" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/2a7bab3c16f6f452c47818fdd08f3b1e49ffcf7d", - "reference": "2a7bab3c16f6f452c47818fdd08f3b1e49ffcf7d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/286d84891690b0e2515874717e49360d1c98a703", + "reference": "286d84891690b0e2515874717e49360d1c98a703", "shasum": "" }, "require": { @@ -3657,7 +3664,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-03-01 18:13:50" + "time": "2017-03-20 09:41:44" }, { "name": "theseer/directoryscanner", @@ -3703,16 +3710,16 @@ }, { "name": "theseer/fdomdocument", - "version": "1.6.1", + "version": "1.6.5", "source": { "type": "git", "url": "https://github.com/theseer/fDOMDocument.git", - "reference": "d9ad139d6c2e8edf5e313ffbe37ff13344cf0684" + "reference": "8dcfd392135a5bd938c3c83ea71419501ad9855d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/d9ad139d6c2e8edf5e313ffbe37ff13344cf0684", - "reference": "d9ad139d6c2e8edf5e313ffbe37ff13344cf0684", + "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/8dcfd392135a5bd938c3c83ea71419501ad9855d", + "reference": "8dcfd392135a5bd938c3c83ea71419501ad9855d", "shasum": "" }, "require": { @@ -3739,7 +3746,7 @@ ], "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.", "homepage": "https://github.com/theseer/fDOMDocument", - "time": "2015-05-27 22:58:02" + "time": "2017-04-21 14:50:31" }, { "name": "theseer/fxsl", diff --git a/magento1/features/bootstrap/EndToEnd/CloudinaryConfig.php b/magento1/features/bootstrap/EndToEnd/CloudinaryConfig.php new file mode 100644 index 0000000..6061368 --- /dev/null +++ b/magento1/features/bootstrap/EndToEnd/CloudinaryConfig.php @@ -0,0 +1,61 @@ +getMagentoFacade()->setConfigEncrypted(Configuration::CONFIG_PATH_ENVIRONMENT_VARIABLE, $env); + } + + /** + * @Given the Cloudinary module integration is enabled + */ + public function theCloudinaryModuleIntegrationIsEnabled() + { + $this->getMagentoFacade()->setConfig( + Configuration::CONFIG_PATH_ENABLED, + Configuration::STATUS_ENABLED + ); + } + + /** + * @Given the Cloudinary module foldered mode is active + */ + public function theCloudinaryModuleFolderedModeIsActive() + { + $this->getMagentoFacade()->setConfig( + Configuration::CONFIG_FOLDERED_MIGRATION, + Configuration::STATUS_ENABLED + ); + } + + /** + * @Given the Cloudinary module foldered mode is inactive + */ + public function theCloudinaryModuleFolderedModeIsInactive() + { + $this->getMagentoFacade()->setConfig( + Configuration::CONFIG_FOLDERED_MIGRATION, + Configuration::STATUS_DISABLED + ); + } +} diff --git a/magento1/features/bootstrap/EndToEnd/Context.php b/magento1/features/bootstrap/EndToEnd/Context.php new file mode 100755 index 0000000..00416e8 --- /dev/null +++ b/magento1/features/bootstrap/EndToEnd/Context.php @@ -0,0 +1,241 @@ +parameters = $parameters; + $this->cloudinaryConsole = new CloudinaryConsole($parameters['cloudinary_env']); + $this->magentoFacade = new MagentoFacade(); + } + + /** + * @param FixtureManager $fixtureManager + */ + protected function setFixtureManager(FixtureManager $fixtureManager) + { + $this->fixtureManager = $fixtureManager; + } + + /** + * @return FixtureManager + */ + protected function getFixtureManager() + { + return $this->fixtureManager; + } + + /** + * @return array + */ + protected function getParameters() + { + return $this->parameters; + } + + /** + * @return CloudinaryConsole + */ + protected function getCloudinaryConsole() + { + return $this->cloudinaryConsole; + } + + /** + * @return MagentoFacade + */ + protected function getMagentoFacade() + { + return $this->magentoFacade; + } + + /** + * @When I wait for a keypress + */ + public function iWaitForAKeypress() + { + fread(STDIN, 1); + } + + /** + * @When image :arg1 is added to product :arg2 + */ + public function imageIsAddedToProduct($imageName, $productSku) + { + $this->getMagentoFacade()->addImageToProductWithSku( + $productSku, + $this->getFixtureFilePath($imageName) + ); + } + + /** + * @When I delete the images from product :arg1 + */ + public function iDeleteTheImagesFromProduct($productSku) + { + $this->getMagentoFacade()->deleteImagesFromProductWithSku($productSku); + } + + /** + * @Then the image for product :arg1 can be seen in the image provider root folder + */ + public function theImageForProductCanBeSeenInTheImageProviderRootFolder($sku) + { + $imagePath = $this->getMagentoFacade()->productWithSku($sku)->getImage(); + + $details = $this->getCloudinaryConsole()->detailsForImagePath($this->nameWithoutExtensionFromPath($imagePath)); + + expect($details['public_id'])->shouldBeEqualTo($this->nameWithoutExtensionFromPath($imagePath)); + } + + /** + * @Then the image can be seen on the image provider in the correct folder for product :arg1 + */ + public function theImageCanBeSeenOnTheImageProviderInTheCorrectFolderForProduct($sku) + { + $imagePath = $this->getMagentoFacade()->productWithSku($sku)->getImage(); + + $folderedPath = sprintf('media/catalog/product%s', $this->nameAndPathWithoutExtension($imagePath)); + + $details = $this->getCloudinaryConsole()->detailsForImagePath($folderedPath); + + expect($details['public_id'])->shouldBeEqualTo($folderedPath); + } + + /** + * @Given the product :arg1 has an image :arg2 on the image provider + */ + public function theProductHasAnImageOnTheImageProvider($productSku, $imageName) + { + $this->imageIsAddedToProduct($imageName, $productSku); + $this->productImagePathFromPreviousStep = $this->getMagentoFacade()->imagePathForProductWithSku($productSku); + $this->theImageForProductCanBeSeenInTheImageProviderRootFolder($productSku); + } + + /** + * @Given the image provider has an image :arg1 in the correct folder for product :arg2 + */ + public function theImageProviderHasAnImageInTheCorrectFolderForProduct($imageName, $productSku) + { + $this->imageIsAddedToProduct($imageName, $productSku); + $this->productImagePathFromPreviousStep = $this->getMagentoFacade()->imagePathForProductWithSku($productSku); + $this->theImageCanBeSeenOnTheImageProviderInTheCorrectFolderForProduct($productSku); + } + + /** + * @Then there are no images for the :arg1 product in the image provider root folder + */ + public function thereAreNoImagesForTheProductInTheImageProviderRootFolder($productSku) + { + try { + $details = $this->getCloudinaryConsole()->detailsForImagePath( + $this->nameWithoutExtensionFromPath($this->productImagePathFromPreviousStep) + ); + + throw new \Exception( + sprintf( + 'Expected nothing but found images for product image path: %s %s', + $this->productImagePathFromPreviousStep, + getenv('BEHAT_DEBUG') ? json_encode($details) : '' + ) + ); + } + + catch (\Cloudinary\Api\NotFound $e) {} + } + + /** + * @Then the image can not be seen on the image provider in the correct folder for product :arg1 + */ + public function theImageCanNotBeSeenOnTheImageProviderInTheCorrectFolderForProduct($productSku) + { + $folderedPath = sprintf( + 'media/catalog/product%s', + $this->nameAndPathWithoutExtension($this->productImagePathFromPreviousStep) + ); + + try { + $details = $this->getCloudinaryConsole()->detailsForImagePath($folderedPath); + + throw new \Exception( + sprintf( + 'Expected nothing but found images for product image path: %s %s', + $folderedPath, + getenv('BEHAT_DEBUG') ? json_encode($details) : '' + ) + ); + } + + catch (\Cloudinary\Api\NotFound $e) {} + } + + /** + * @Given the image provider has no images + */ + public function theImageProviderHasNoImages() + { + $this->getCloudinaryConsole()->deleteAll(); + } + + /** + * @param string $path + * @return string + */ + private function nameWithoutExtensionFromPath($path) + { + $info = pathinfo($path); + return $info['filename']; + } + + /** + * @param string $path + * @return string + */ + private function nameAndPathWithoutExtension($path) + { + $info = pathinfo($path); + return sprintf('%s%s%s', $info['dirname'], $info['dirname'] ? '/' : '', $info['filename']); + } +} diff --git a/magento1/features/bootstrap/EndToEnd/Fixture.php b/magento1/features/bootstrap/EndToEnd/Fixture.php new file mode 100644 index 0000000..cf7dccb --- /dev/null +++ b/magento1/features/bootstrap/EndToEnd/Fixture.php @@ -0,0 +1,65 @@ +setFixtureManager(new FixtureManager(new YamlProvider())); + } + + /** + * @AfterScenario + */ + public function afterScenario() + { + $this->getFixtureManager()->clear(); + } + + /** + * @Given the product :arg1 exists + */ + public function theProductExists($sku, $retry = true) + { + try { + $this->getFixtureManager()->loadFixture('catalog/product', __DIR__ . '/../Fixtures/' . $sku . '.yaml'); + } + + catch (\Zend_Db_Statement_Exception $e) { + if (!$retry) { + throw $e; + } + $product = Mage::getModel('catalog/product'); + $product->load($product->getIdBySku($sku)); + $product->delete(); + $this->theProductExists($sku, false); + } + + if (getenv('BEHAT_DEBUG')) { + echo 'Fixture product = ', $this->getMagentoFacade()->productWithSku($sku)->getId(); + } + } + + protected function getFixtureFilePath($filename) + { + $path = realpath(__DIR__ . '/../Fixtures/' . $filename); + + if (!file_exists($path)) { + throw new Exception('Fixture file not found: ' . $path); + } + + return $path; + } +} diff --git a/magento1/features/bootstrap/Facade/CloudinaryConsole.php b/magento1/features/bootstrap/Facade/CloudinaryConsole.php new file mode 100644 index 0000000..3992d20 --- /dev/null +++ b/magento1/features/bootstrap/Facade/CloudinaryConsole.php @@ -0,0 +1,33 @@ +cloudinaryEnvironmentVariable = $cloudinaryEnvironmentVariable; + } + + public function detailsForImagePath($imagePath) + { + if (getenv('BEHAT_DEBUG')) { + echo sprintf('Fetching details for image path: %s%s', $imagePath, PHP_EOL); + } + + $api = new Api(); + return $api->resource($imagePath); + } + + public function deleteAll() + { + CloudinaryEnvironmentVariable::fromString($this->cloudinaryEnvironmentVariable); + $api = new Api(); + $api->delete_all_resources(); + } +} diff --git a/magento1/features/bootstrap/Facade/Magento.php b/magento1/features/bootstrap/Facade/Magento.php new file mode 100644 index 0000000..02bde98 --- /dev/null +++ b/magento1/features/bootstrap/Facade/Magento.php @@ -0,0 +1,94 @@ +load($product->getIdBySku($sku)); + + if (!$product->getId()) { + throw new \Exception('Cannot find product with sku: ' . $sku); + } + + return $product; + } + + /** + * @param string $sku + * @return string + * @throws \Exception + */ + public function imagePathForProductWithSku($sku) + { + return $this->productWithSku($sku)->getImage(); + } + + /** + * @param string $sku + * @param string $imagePath + * @throws \Exception + */ + public function addImageToProductWithSku($sku, $imagePath) + { + Mage::app('default', 'store')->setCurrentStore(\Mage_Core_Model_App::ADMIN_STORE_ID); + $product = $this->productWithSku($sku); + $product->addImageToMediaGallery($imagePath, ['image', 'thumbnail', 'small_image'], false, false); + $product->save(); + } + + /** + * @param string $sku + */ + public function deleteImagesFromProductWithSku($sku) + { + Mage::app('default', 'store')->setCurrentStore(\Mage_Core_Model_App::ADMIN_STORE_ID); + $product = $this->productWithSku($sku); + + $galleryAttribute = \Mage::getModel('catalog/resource_eav_attribute') + ->loadByCode($product->getEntityTypeId(), 'media_gallery'); + + foreach ($product->getMediaGalleryImages() as $image) { + $galleryAttribute->getBackend()->removeImage($product, $image->getFile()); + } + + $product->save(); + } + + /** + * @param string $path + * @param string $value + */ + public function setConfig($path, $value) + { + Mage::app('default', 'store')->setCurrentStore(\Mage_Core_Model_App::ADMIN_STORE_ID); + Mage::getConfig()->saveConfig($path, $value)->reinit(); + Mage::app()->getStore()->setConfig($path, $value); + if (getenv('BEHAT_DEBUG')) { + echo sprintf('Set config path: %s with value: %s%s', $path, $value, PHP_EOL); + } + } + + /** + * @param string $path + * @param string $value + */ + public function setConfigEncrypted($path, $value) + { + Mage::app('default', 'store')->setCurrentStore(\Mage_Core_Model_App::ADMIN_STORE_ID); + Mage::getConfig()->saveConfig($path, Mage::helper('core')->encrypt($value))->reinit(); + Mage::app()->getStore()->setConfig($path, Mage::helper('core')->encrypt($value)); + if (getenv('BEHAT_DEBUG')) { + echo sprintf('Set config path: %s with encrypted value of: %s%s', $path, $value, PHP_EOL); + } + } +} diff --git a/magento1/features/bootstrap/Fixtures/Apple.yaml b/magento1/features/bootstrap/Fixtures/Apple.yaml new file mode 100644 index 0000000..70f10ab --- /dev/null +++ b/magento1/features/bootstrap/Fixtures/Apple.yaml @@ -0,0 +1,17 @@ +catalog/product: + sku: Apple + attribute_set_id: 4 + name: product name + weight: 2 + price: 10 + description: Granny Smith + short_description: An apple + tax_class_id: 1 + type_id: simple + visibility: 4 + status: 1 + stock_data: { is_in_stock: 1, qty: 99999 } + website_ids: [1] + media_gallery: + images: [] + diff --git a/magento1/features/bootstrap/Fixtures/apple.jpg b/magento1/features/bootstrap/Fixtures/apple.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dea4a873b122410fcacc65a6cf96ba8b58821609 GIT binary patch literal 8571 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'Cloudinary_Cloudinary_Helper_Cron'); + } + + function it_validates_true_when_migration_is_not_running( + \Cloudinary_Cloudinary_Model_Migration $migration + ) + { + $migration->hasStarted()->willReturn(false); + $this->validate($migration, 10)->shouldReturn(true); + } + + function it_validates_true_when_migration_is_running_and_time_elapsed_is_less_than_cron_interval( + \Cloudinary_Cloudinary_Model_Migration $migration + ) + { + $migration->hasStarted()->willReturn(true); + $migration->timeElapsed()->willReturn(1); + $migration->hasProgress()->willReturn(false); + $this->validate($migration, 10)->shouldReturn(true); + } + + function it_validates_false_when_migration_is_running_and_time_elapsed_is_more_than_cron_interval_and_no_batches_have_been_processed( + \Cloudinary_Cloudinary_Model_Migration $migration + ) + { + $migration->hasStarted()->willReturn(true); + $migration->timeElapsed()->willReturn(9999); + $migration->hasProgress()->willReturn(false); + $this->validate($migration, 10)->shouldReturn(false); + } + + function it_validates_true_when_migration_is_running_and_time_elapsed_is_more_than_cron_interval_and_batches_have_been_processed( + \Cloudinary_Cloudinary_Model_Migration $migration + ) + { + $migration->hasStarted()->willReturn(true); + $migration->timeElapsed()->willReturn(9999); + $migration->hasProgress()->willReturn(true); + $this->validate($migration, 10)->shouldReturn(true); + } + + function it_confirms_migration_is_not_initialising_when_migration_has_not_started( + \Cloudinary_Cloudinary_Model_Migration $migration + ) + { + $migration->hasStarted()->willReturn(false); + $migration->hasProgress()->willReturn(false); + $this->isInitialising($migration, 10)->shouldReturn(false); + } + + function it_confirms_migration_is_not_initialising_when_batches_have_been_processed( + \Cloudinary_Cloudinary_Model_Migration $migration + ) + { + $migration->hasStarted()->willReturn(true); + $migration->hasProgress()->willReturn(true); + $this->isInitialising($migration, 10)->shouldReturn(false); + } + + function it_confirms_migration_initialising_when_migration_started_and_there_is_no_progress( + \Cloudinary_Cloudinary_Model_Migration $migration + ) + { + $migration->hasStarted()->willReturn(true); + $migration->hasProgress()->willReturn(false); + $this->isInitialising($migration, 10)->shouldReturn(true); + } +} diff --git a/magento1/src/app/code/community/Cloudinary/Cloudinary/Helper/Cron.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Helper/Cron.php new file mode 100644 index 0000000..83e63af --- /dev/null +++ b/magento1/src/app/code/community/Cloudinary/Cloudinary/Helper/Cron.php @@ -0,0 +1,23 @@ +isInitialising($migration) && ($migration->timeElapsed() > $cronIntervalInSeconds)); + } + + /** + * @param Cloudinary_Cloudinary_Model_Migration $migration + * @return bool + */ + public function isInitialising(Cloudinary_Cloudinary_Model_Migration $migration) + { + return $migration->hasStarted() && !$migration->hasProgress(); + } +} diff --git a/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Configuration.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Configuration.php new file mode 100644 index 0000000..6326595 --- /dev/null +++ b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Configuration.php @@ -0,0 +1,209 @@ +folderTranslator = Mage::getModel('cloudinary_cloudinary/magentoFolderTranslator'); + } + + /** + * @return Cloud + */ + public function getCloud() + { + return $this->getEnvironmentVariable()->getCloud(); + } + + /** + * @return Credentials + */ + public function getCredentials() + { + return $this->getEnvironmentVariable()->getCredentials(); + } + + /** + * @return Transformation + */ + public function getDefaultTransformation() + { + $transformation = Transformation::builder() + ->withGravity(Gravity::fromString($this->getDefaultGravity())) + ->withFetchFormat(FetchFormat::fromString($this->getFetchFormat())) + ->withQuality(Quality::fromString($this->getImageQuality())) + ->withDpr(Dpr::fromString($this->getImageDpr())); + + if ($this->isSmartServing()){ + $transformation + ->addFlags(['lossy']) + ->withFetchFormat(FetchFormat::fromString(FetchFormat::FETCH_FORMAT_AUTO)) + ->withoutFormat(); + } + return $transformation; + } + + /** + * @return boolean + */ + public function getCdnSubdomainStatus() + { + return Mage::getStoreConfig(self::CONFIG_CDN_SUBDOMAIN); + } + + /** + * @return string + */ + public function getUserPlatform() + { + return sprintf( + self::USER_PLATFORM_TEMPLATE, + Mage::getConfig()->getModuleConfig('Cloudinary_Cloudinary')->version, + Mage::getVersion() + ); + } + + /** + * @return UploadConfig + */ + public function getUploadConfig() + { + return UploadConfig::fromBooleanValues(true, false, false); + } + + /** + * @return boolean + */ + public function isEnabled() + { + return Mage::getStoreConfigFlag(self::CONFIG_PATH_ENABLED); + } + + public function enable() + { + $this->setStoreConfig(self::CONFIG_PATH_ENABLED, self::STATUS_ENABLED); + } + + public function disable() + { + $this->setStoreConfig(self::CONFIG_PATH_ENABLED, self::STATUS_DISABLED); + } + + public function getFormatsToPreserve() { + return ['png', 'webp', 'gif', 'svg']; + } + + public function validateCredentials() + { + try { + $api = new \Cloudinary\Api(); + return $api->ping((new ConfigurationBuilder($this))->build()); + } catch (Exception $e) { + Mage::logException($e); + } + return false; + } + + public function getMigratedPath($file) + { + if ($this->isFolderedMigration()) { + $result = $this->folderTranslator->translate($file); + } else { + $result = basename($file); + } + return $result; + } + + public function reverseMigratedPathIfNeeded($migratedPath) + { + if ($this->isFolderedMigration()) { + return $this->folderTranslator->reverse($migratedPath); + } + return $migratedPath; + } + + public function isFolderedMigration() + { + return Mage::getStoreConfigFlag(self::CONFIG_FOLDERED_MIGRATION); + } + + private function setStoreConfig($configPath, $value) + { + Mage::getModel('core/config')->saveConfig($configPath, $value)->reinit(); + } + + /** + * @return CloudinaryEnvironmentVariable + */ + private function getEnvironmentVariable() + { + if (is_null($this->environmentVariable)) { + $value = Mage::helper('core')->decrypt(Mage::getStoreConfig(self::CONFIG_PATH_ENVIRONMENT_VARIABLE)); + $this->environmentVariable = CloudinaryEnvironmentVariable::fromString($value); + } + return $this->environmentVariable; + } + + /** + * Smart serving means lossy compression and automatic fetch format. + * @return bool + */ + private function isSmartServing() + { + return Mage::getStoreConfigFlag(self::CONFIG_SMART_SERVING); + } + + private function getDefaultGravity() + { + return Mage::getStoreConfig(self::CONFIG_DEFAULT_GRAVITY); + } + + /** + * @return null|string + */ + private function getFetchFormat() + { + if (Mage::getStoreConfigFlag(self::CONFIG_DEFAULT_FETCH_FORMAT)) { + return FetchFormat::FETCH_FORMAT_AUTO; + } + return ''; + } + + private function getImageQuality() + { + return Mage::getStoreConfig(self::CONFIG_DEFAULT_QUALITY); + } + + private function getImageDpr() + { + return Mage::getStoreConfig(self::CONFIG_DEFAULT_DPR); + } +} diff --git a/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cron.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cron.php index 6bd4682..51e4aac 100755 --- a/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cron.php +++ b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Cron.php @@ -1,6 +1,7 @@ process(); - foreach ($batchUploader->getMigrationErrors() as $error) { - Cloudinary_Cloudinary_Model_MigrationError::saveFromException($error); - } - return $this; } } diff --git a/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/MigrationError.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/MigrationError.php index 721533c..ae48b8a 100644 --- a/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/MigrationError.php +++ b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/MigrationError.php @@ -3,11 +3,16 @@ class Cloudinary_Cloudinary_Model_MigrationError extends Mage_Core_Model_Abstract { + const REMOVE_ORPHAN_MESSAGE = 'Image found in sync table that no longer exists. Removing reference: %s'; + public function __construct() { $this->_init('cloudinary_cloudinary/migrationError'); } + /** + * @param \CloudinaryExtension\Exception\MigrationError $e + */ public static function saveFromException(\CloudinaryExtension\Exception\MigrationError $e) { $image = $e->getImage(); @@ -23,4 +28,17 @@ public static function saveFromException(\CloudinaryExtension\Exception\Migratio $entry->save(); } + + /** + * @param Cloudinary_Cloudinary_Model_Synchronisation $orphanImage + * @return $this + */ + public function orphanRemoved(Cloudinary_Cloudinary_Model_Synchronisation $orphanImage) + { + $this->setFilePath($orphanImage->getImageName()); + $this->setRelativePath($orphanImage->getImageName()); + $this->setMessage(sprintf(self::REMOVE_ORPHAN_MESSAGE, $orphanImage->getImageName())); + $this->setTimestamp(time()); + return $this; + } } diff --git a/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Cms/Synchronisation/Collection.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Cms/Synchronisation/Collection.php index fed4da7..fcb3967 100755 --- a/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Cms/Synchronisation/Collection.php +++ b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Cms/Synchronisation/Collection.php @@ -1,5 +1,6 @@ addFieldToFilter('media_gallery_id', array('null' => true)) ->getData(); } + + private function _getSynchronisedRawImageNames() + { + $result = array_map( + function ($itemData) { + return $itemData['image_name']; + }, + $this->_getSynchronisedImageData() + ); + + return $result; + } + + /** + * @return [Cloudinary_Cloudinary_Model_Synchronisation] + */ + public function findOrphanedSynchronisedImages() + { + return $this->_synchronisationCollectionFromImageNames( + array_diff( + $this->_getSynchronisedRawImageNames(), + $this->_extractRelativePaths($this->getItems()) + ) + ); + } + + /** + * @param [string] $imageNames + * @return [Cloudinary_Cloudinary_Model_Synchronisation] + */ + private function _synchronisationCollectionFromImageNames(array $imageNames) + { + return Mage::getModel('cloudinary_cloudinary/synchronisation') + ->getCollection() + ->addFieldToFilter('image_name', ['in' => $imageNames]) + ->getItems(); + } + + /** + * @param [Synchronizable] $items + * @return [string] + */ + private function _extractRelativePaths(array $items) + { + return array_map( + function(Synchronizable $syncItem) { + return $syncItem->getRelativePath(); + }, + $items + ); + } } diff --git a/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Synchronisation/Collection.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Synchronisation/Collection.php index 5520440..496afdc 100755 --- a/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Synchronisation/Collection.php +++ b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/Resource/Synchronisation/Collection.php @@ -60,4 +60,13 @@ private function getQueryForSyncedImageNames() $select->where('media_gallery_value is not null'); return $select->columns('media_gallery_value'); } + + /** + * Only applicable for cms instance + * @return [Cloudinary_Cloudinary_Model_Synchronisation] + */ + public function findOrphanedSynchronisedImages() + { + return []; + } } diff --git a/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/SynchronisedMediaUnifier.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/SynchronisedMediaUnifier.php index 841ee47..5a105af 100755 --- a/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/SynchronisedMediaUnifier.php +++ b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/SynchronisedMediaUnifier.php @@ -4,24 +4,54 @@ class Cloudinary_Cloudinary_Model_SynchronisedMediaUnifier implements SynchronizedMediaRepository { - + /** + * @var [SynchronizedMediaRepository] + */ private $_synchronisedMediaRepositories; - private $_unsychronisedImages = array(); + /** + * Cloudinary_Cloudinary_Model_SynchronisedMediaUnifier constructor. + * @param [SynchronizedMediaRepository] + */ public function __construct(array $synchronisedMediaRepositories) { $this->_synchronisedMediaRepositories = $synchronisedMediaRepositories; } + /** + * @param int $limit + * @return [Cloudinary_Cloudinary_Model_Synchronisation] + */ public function findUnsynchronisedImages($limit = 200) { - foreach ($this->_synchronisedMediaRepositories as $synchronisedMediaRepository) { - $this->_unsychronisedImages = array_merge( - $this->_unsychronisedImages, - $synchronisedMediaRepository->findUnsynchronisedImages() - ); - } - return array_slice($this->_unsychronisedImages, 0, $limit); + return array_slice($this->findUnlimitedUnsynchronisedImages(), 0, $limit); + } + + /** + * @return [Cloudinary_Cloudinary_Model_Synchronisation] + */ + private function findUnlimitedUnsynchronisedImages() + { + return array_reduce( + $this->_synchronisedMediaRepositories, + function($carry, $synchronisedMediaRepository) { + return $carry + $synchronisedMediaRepository->findUnsynchronisedImages(); + }, + [] + ); } + /** + * @return [Cloudinary_Cloudinary_Model_Synchronisation] + */ + public function findOrphanedSynchronisedImages() + { + return array_reduce( + $this->_synchronisedMediaRepositories, + function($carry, $synchronisedMediaRepository) { + return $carry + $synchronisedMediaRepository->findOrphanedSynchronisedImages(); + }, + [] + ); + } } diff --git a/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/SynchronizationChecker.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/SynchronizationChecker.php new file mode 100644 index 0000000..c46eaf5 --- /dev/null +++ b/magento1/src/app/code/community/Cloudinary/Cloudinary/Model/SynchronizationChecker.php @@ -0,0 +1,18 @@ +getCollection(); + $table = $coll->getMainTable(); + // case sensitive check + $query = "select count(*) from $table where binary image_name = '$imageName' limit 1"; + return $coll->getConnection()->query($query)->fetchColumn() > 0; + } +} diff --git a/magento1/src/app/code/community/Cloudinary/Cloudinary/controllers/Adminhtml/CloudinaryController.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/controllers/Adminhtml/CloudinaryController.php index 1814f6c..f7ec55a 100755 --- a/magento1/src/app/code/community/Cloudinary/Cloudinary/controllers/Adminhtml/CloudinaryController.php +++ b/magento1/src/app/code/community/Cloudinary/Cloudinary/controllers/Adminhtml/CloudinaryController.php @@ -4,6 +4,9 @@ class Cloudinary_Cloudinary_Adminhtml_CloudinaryController extends Mage_Adminhtm { const CRON_INTERVAL = 300; + /** + * @var Cloudinary_Cloudinary_Model_Migration + */ private $_migrationTask; /** @@ -21,6 +24,8 @@ public function preDispatch() public function indexAction() { + $this->removeOrphanSyncEntries(); + $this->_displayMigrationMessages(); $layout = $this->loadLayout(); @@ -43,6 +48,22 @@ public function indexAction() $this->renderLayout(); } + public function removeOrphanSyncEntries() + { + $combinedMediaRepository = Mage::getModel( + 'cloudinary_cloudinary/synchronisedMediaUnifier', + [ + Mage::getResourceModel('cloudinary_cloudinary/synchronisation_collection'), + Mage::getResourceModel('cloudinary_cloudinary/cms_synchronisation_collection') + ] + ); + + foreach ($combinedMediaRepository->findOrphanedSynchronisedImages() as $orphanImage) { + Mage::getModel('cloudinary_cloudinary/migrationError')->orphanRemoved($orphanImage)->save(); + $orphanImage->delete(); + } + } + public function configAction() { $this->_redirect("*/system_config/edit/section/cloudinary/"); diff --git a/magento1/src/app/code/community/Cloudinary/Cloudinary/etc/config.xml b/magento1/src/app/code/community/Cloudinary/Cloudinary/etc/config.xml index b4929aa..abf73ca 100755 --- a/magento1/src/app/code/community/Cloudinary/Cloudinary/etc/config.xml +++ b/magento1/src/app/code/community/Cloudinary/Cloudinary/etc/config.xml @@ -2,7 +2,7 @@ - 2.1.0 + 2.2.0 diff --git a/magento1/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-2.0.0-2.1.0.php b/magento1/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-2.0.0-2.1.0.php new file mode 100644 index 0000000..18c2d6b --- /dev/null +++ b/magento1/src/app/code/community/Cloudinary/Cloudinary/sql/cloudinary_setup/upgrade-2.0.0-2.1.0.php @@ -0,0 +1,33 @@ +startSetup(); + +$conn = $installer->getConnection(); +$table = $installer->getTable('cloudinary_cloudinary/migration'); + +$conn->addColumn( + $table, + 'started_at', + [ + 'type' => Varien_Db_Ddl_Table::TYPE_DATETIME, + 'comment' => 'The time the migration started', + 'nullable' => true, + 'default' => '0000-00-00 00:00:00' + ] +); + +$conn->addColumn( + $table, + 'batch_count', + [ + 'type' => Varien_Db_Ddl_Table::TYPE_INTEGER, + 'comment' => 'Batches run for current migration', + 'nullable' => false, + 'default' => 0 + ] +); + +$installer->endSetup(); diff --git a/magento1/src/lib/CloudinaryExtension/Cloud.php b/magento1/src/lib/CloudinaryExtension/Cloud.php new file mode 100755 index 0000000..559a02c --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Cloud.php @@ -0,0 +1,25 @@ +cloudName = (string)$cloudName; + } + + public static function fromName($aCloudName) + { + return new Cloud($aCloudName); + } + + public function __toString() + { + return $this->cloudName; + } +} diff --git a/magento1/src/lib/CloudinaryExtension/CloudinaryImageProvider.php b/magento1/src/lib/CloudinaryExtension/CloudinaryImageProvider.php new file mode 100755 index 0000000..ecb9385 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/CloudinaryImageProvider.php @@ -0,0 +1,89 @@ + true, + "unique_filename" => false, + "overwrite" => false + ); + + private function __construct(Configuration $configuration) + { + $this->configuration = $configuration; + $this->authorise(); + } + + public static function fromConfiguration(Configuration $configuration) + { + return new CloudinaryImageProvider($configuration); + } + + public function upload(Image $image) + { + try{ + $imagePath = (string)$image; + $uploadOptionsAndFolder = $this->uploadConfig + ["folder" => $image->getRelativeFolder()]; + $uploadResult = Uploader::upload($imagePath, $uploadOptionsAndFolder); + + if ($uploadResult['existing'] == 1) { + MigrationError::throwWith($image, MigrationError::CODE_FILE_ALREADY_EXISTS); + } + return $uploadResult; + } catch (\Exception $e) { + MigrationError::throwWith($image, MigrationError::CODE_API_ERROR, $e->getMessage()); + } + } + + public function transformImage(Image $image, Transformation $transformation = null) + { + if ($transformation === null) { + $transformation = $this->configuration->getDefaultTransformation(); + } + return Image::fromPath(\cloudinary_url($image->getId(), $transformation->build()), $image->getRelativePath()); + } + + public function validateCredentials() + { + $signedValidationUrl = $this->getSignedValidationUrl(); + return $this->validationResult($signedValidationUrl); + } + + public function deleteImage(Image $image) + { + Uploader::destroy($image->getId()); + } + + private function authorise() + { + Cloudinary::config($this->configuration->build()); + Cloudinary::$USER_PLATFORM = $this->configuration->getUserPlatform(); + } + + private function getSignedValidationUrl() + { + $consoleUrl = Security\ConsoleUrl::fromPath("media_library/cms"); + return (string)Security\SignedConsoleUrl::fromConsoleUrlAndCredentials( + $consoleUrl, + $this->configuration->getCredentials() + ); + } + + private function validationResult($signedValidationUrl) + { + $request = new ValidateRemoteUrlRequest($signedValidationUrl); + return $request->validate(); + } +} diff --git a/magento1/src/lib/CloudinaryExtension/Configuration.php b/magento1/src/lib/CloudinaryExtension/Configuration.php new file mode 100755 index 0000000..8b57648 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Configuration.php @@ -0,0 +1,97 @@ +cdnSubdomain = false; + $this->credentials = $credentials; + $this->cloud = $cloud; + $this->defaultTransformation = Transformation::builder(); + } + + public static function fromCloudAndCredentials(Cloud $cloud, Credentials $credentials) + { + return new Configuration($cloud, $credentials); + } + + public static function fromEnvironmentVariable(EnvironmentVariable $environmentVariable) + { + return new Configuration($environmentVariable->getCloud(), $environmentVariable->getCredentials()); + } + + public function getCloud() + { + return $this->cloud; + } + + public function getCredentials() + { + return $this->credentials; + } + + public function getDefaultTransformation() + { + return $this->defaultTransformation; + } + + public function build() + { + $configuration = $this->getMandatoryConfiguration(); + if($this->cdnSubdomain) { + $configuration['cdn_subdomain'] = true; + } + + return $configuration; + } + + public function enableCdnSubdomain() + { + $this->cdnSubdomain = true; + } + + public function getCdnSubdomainStatus() + { + return $this->cdnSubdomain; + } + + private function getMandatoryConfiguration() + { + return array( + "cloud_name" => (string)$this->cloud, + "api_key" => (string)$this->credentials->getKey(), + "api_secret" => (string)$this->credentials->getSecret() + ); + } + + /** + * @return string + */ + public function getUserPlatform() + { + return $this->userPlatform; + } + + /** + * @param string $userPlatform + */ + public function setUserPlatform($userPlatform) + { + $this->userPlatform = $userPlatform; + } +} diff --git a/magento1/src/lib/CloudinaryExtension/Credentials.php b/magento1/src/lib/CloudinaryExtension/Credentials.php new file mode 100755 index 0000000..c49c5e2 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Credentials.php @@ -0,0 +1,30 @@ +key = $key; + $this->secret = $secret; + } + + public function getKey() + { + return $this->key; + } + + public function getSecret() + { + return $this->secret; + } +} diff --git a/magento1/src/lib/CloudinaryExtension/Exception/InvalidCredentials.php b/magento1/src/lib/CloudinaryExtension/Exception/InvalidCredentials.php new file mode 100755 index 0000000..5dd83b8 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Exception/InvalidCredentials.php @@ -0,0 +1,10 @@ + 'File already exists (cloudinary is case insensitive!!).', + self::CODE_API_ERROR => 'Internal API error' + ]; + + private $image; + + /** + * @return Image + */ + public function getImage() + { + return $this->image; + } + + /** + * @param Image $image + * @param $code + * @param $message overrides the default message attached to the code + * @return MigrationError + */ + private static function build(Image $image, $code, $message = '') + { + $result = new MigrationError($message ?: self::$messages[$code], $code); + $result->image = $image; + return $result; + } + + public static function throwWith(Image $image, $code, $message = '') + { + throw self::build($image, $code, $message); + } + +} diff --git a/magento1/src/lib/CloudinaryExtension/FolderTranslator.php b/magento1/src/lib/CloudinaryExtension/FolderTranslator.php new file mode 100644 index 0000000..2949dc6 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/FolderTranslator.php @@ -0,0 +1,16 @@ +imagePath = $imagePath; + $this->relativePath = $relativePath; + $this->pathInfo = pathinfo($this->imagePath); + } + + public static function fromPath($imagePath, $relativePath = '') + { + return new Image($imagePath, $relativePath); + } + + public function __toString() + { + return $this->imagePath; + } + + public function getRelativePath() + { + return $this->relativePath; + } + + public function getRelativeFolder() + { + $result = dirname($this->getRelativePath()); + return $result == '.' ? '' : $result; + } + + public function getId() + { + if ($this->relativePath) { + return $this->getRelativeFolder() . DS . $this->pathInfo['filename']; + } else { + return $this->pathInfo['filename']; + } + } + + public function getExtension() + { + return $this->pathInfo['extension']; + } +} diff --git a/magento1/src/lib/CloudinaryExtension/Image/Synchronizable.php b/magento1/src/lib/CloudinaryExtension/Image/Synchronizable.php new file mode 100755 index 0000000..c6a4aed --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Image/Synchronizable.php @@ -0,0 +1,10 @@ +fetchFormat = FetchFormat::fromString(Format::FETCH_FORMAT_AUTO); + $this->crop = 'pad'; + $this->format = Format::fromExtension('jpg'); + $this->validFormats = array('gif', 'jpg', 'png', 'svg'); + } + + public function withGravity(Gravity $gravity) + { + $this->gravity = $gravity; + $this->crop = ((string) $gravity) ? 'crop' : 'pad'; + + return $this; + } + + public function withDimensions(Dimensions $dimensions) + { + $this->dimensions = $dimensions; + + return $this; + } + + public function withFetchFormat(FetchFormat $fetchFormat) + { + $this->fetchFormat = $fetchFormat; + + return $this; + } + + public function withFormat(Format $format) + { + if (in_array((string) $format, $this->validFormats)) { + $this->format = $format; + } + + return $this; + } + + public function withQuality(Quality $quality) + { + $this->quality = $quality; + + return $this; + } + + public function withDpr(Dpr $dpr) + { + $this->dpr = $dpr; + + return $this; + } + + public function withOptimisationDisabled() + { + $this->withFetchFormat(FetchFormat::fromString('')); + return $this; + } + + public static function builder() + { + return new Transformation(); + } + + public function build() + { + return array( + 'fetch_format' => (string) $this->fetchFormat, + 'quality' => (string) $this->quality, + 'crop' => (string) $this->crop, + 'gravity' => (string) $this->gravity ?: null, + 'width' => $this->dimensions ? $this->dimensions->getWidth() : null, + 'height' => $this->dimensions ? $this->dimensions->getHeight() : null, + 'format' => (string) $this->format, + 'dpr' => (string) $this->dpr + ); + } +} + diff --git a/magento1/src/lib/CloudinaryExtension/Image/Transformation/Dimensions.php b/magento1/src/lib/CloudinaryExtension/Image/Transformation/Dimensions.php new file mode 100755 index 0000000..ec0fa8b --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Image/Transformation/Dimensions.php @@ -0,0 +1,36 @@ +width = is_null($width) ? null : (int) round($width); + $this->height = is_null($height) ? null : (int) round($height); + } + + public function getWidth() + { + return $this->width; + } + + public function getHeight() + { + return $this->height; + } + + public static function fromWidthAndHeight($width, $height) + { + return new Dimensions($width, $height); + } + + public static function null() + { + return new Dimensions(null, null); + } +} diff --git a/magento1/src/lib/CloudinaryExtension/Image/Transformation/Dpr.php b/magento1/src/lib/CloudinaryExtension/Image/Transformation/Dpr.php new file mode 100755 index 0000000..8c15aa5 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Image/Transformation/Dpr.php @@ -0,0 +1,23 @@ +value = $value; + } + + public static function fromString($value) + { + return new Dpr($value); + } + + public function __toString() + { + return $this->value; + } +} diff --git a/magento1/src/lib/CloudinaryExtension/Image/Transformation/FetchFormat.php b/magento1/src/lib/CloudinaryExtension/Image/Transformation/FetchFormat.php new file mode 100755 index 0000000..8b34308 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Image/Transformation/FetchFormat.php @@ -0,0 +1,25 @@ +value = $value; + } + + public static function fromString($value) + { + return new FetchFormat($value); + } + + public function __toString() + { + return $this->value; + } +} diff --git a/magento1/src/lib/CloudinaryExtension/Image/Transformation/Format.php b/magento1/src/lib/CloudinaryExtension/Image/Transformation/Format.php new file mode 100755 index 0000000..94da009 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Image/Transformation/Format.php @@ -0,0 +1,25 @@ +value = $value; + } + + public static function fromExtension($value) + { + return new Format($value); + } + + public function __toString() + { + return $this->value; + } +} diff --git a/magento1/src/lib/CloudinaryExtension/Image/Transformation/Gravity.php b/magento1/src/lib/CloudinaryExtension/Image/Transformation/Gravity.php new file mode 100755 index 0000000..9eb0d1a --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Image/Transformation/Gravity.php @@ -0,0 +1,30 @@ +value = $value; + } + + public function __toString() + { + return $this->value; + } + + public static function fromString($value) + { + return new Gravity($value); + } + + public static function null() + { + return new Gravity(null); + } +} + + diff --git a/magento1/src/lib/CloudinaryExtension/Image/Transformation/Quality.php b/magento1/src/lib/CloudinaryExtension/Image/Transformation/Quality.php new file mode 100755 index 0000000..6b7f4d3 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Image/Transformation/Quality.php @@ -0,0 +1,23 @@ +value = $value; + } + + public static function fromString($value) + { + return new Quality($value); + } + + public function __toString() + { + return $this->value; + } +} diff --git a/magento1/src/lib/CloudinaryExtension/ImageProvider.php b/magento1/src/lib/CloudinaryExtension/ImageProvider.php new file mode 100755 index 0000000..ea140aa --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/ImageProvider.php @@ -0,0 +1,13 @@ +imageProvider = $imageProvider; + $this->migrationTask = $migrationTask; + $this->baseMediaPath = $baseMediaPath; + $this->logger = $logger; + } + + public function uploadImages(array $images) + { + $this->countMigrated = 0; + foreach ($images as $image) { + + if ($this->migrationTask->hasBeenStopped()) { + break; + } + $this->uploadImage($image); + } + $this->logger->notice(sprintf(self::MESSAGE_STATUS, $this->countMigrated, $this->countFailed)); + } + + private function getAbsolutePath(Synchronizable $image) + { + return sprintf('%s%s', $this->baseMediaPath, $image->getFilename()); + } + + private function uploadImage(Synchronizable $image) + { + $absolutePath = $this->getAbsolutePath($image); + $relativePath = $image->getRelativePath(); + $apiImage = Image::fromPath($absolutePath, $relativePath); + + try { + $this->imageProvider->upload($apiImage); + $image->tagAsSynchronized(); + $this->countMigrated++; + $this->logger->notice(sprintf(self::MESSAGE_UPLOADED, $absolutePath . ' - ' . $relativePath)); + } catch (\Exception $e) { + $this->errors[] = $e; + $this->countFailed++; + $this->logger->error(sprintf(self::MESSAGE_UPLOAD_ERROR, $e->getMessage(), $absolutePath . ' - ' . $relativePath)); + } + } + + /** + * @return array + */ + public function getErrors() + { + return $this->errors; + } + + public function getMigrationErrors() + { + return array_filter($this->errors, function ($val) { + return $val instanceof MigrationError; + }); + } + +} diff --git a/magento1/src/lib/CloudinaryExtension/Migration/Logger.php b/magento1/src/lib/CloudinaryExtension/Migration/Logger.php new file mode 100755 index 0000000..74bba5a --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Migration/Logger.php @@ -0,0 +1,14 @@ +migrationTask = $migrationTask; + $this->synchronizedMediaRepository = $synchronizedMediaRepository; + $this->logger = $logger; + $this->batchUploader = $batchUploader; + } + + public function process() + { + if ($this->migrationTask->hasBeenStopped()) { + return; + } + + $images = $this->synchronizedMediaRepository->findUnsynchronisedImages(); + + if (!$images) { + $this->logger->notice(self::MESSAGE_COMPLETE); + $this->migrationTask->stop(); + } else { + $this->logger->notice(self::MESSAGE_PROCESSING); + $this->batchUploader->uploadImages($images); + } + } +} diff --git a/magento1/src/lib/CloudinaryExtension/Migration/SynchronizedMediaRepository.php b/magento1/src/lib/CloudinaryExtension/Migration/SynchronizedMediaRepository.php new file mode 100755 index 0000000..c01dc34 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Migration/SynchronizedMediaRepository.php @@ -0,0 +1,8 @@ +apiSignature = Cloudinary::api_sign_request($params, (string) $secret); + } + + public static function fromSecretAndParams(Secret $secret, array $params = array()) + { + return new ApiSignature($secret, $params); + } + + public function __toString() + { + return $this->apiSignature; + } +} diff --git a/magento1/src/lib/CloudinaryExtension/Security/CloudinaryEnvironmentVariable.php b/magento1/src/lib/CloudinaryExtension/Security/CloudinaryEnvironmentVariable.php new file mode 100755 index 0000000..1dcc5d0 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Security/CloudinaryEnvironmentVariable.php @@ -0,0 +1,50 @@ +environmentVariable = (string)$environmentVariable; + $cloudinaryUrl = str_replace('CLOUDINARY_URL=', '', $environmentVariable); + if ($this->isUrlValid($cloudinaryUrl)) { + Cloudinary::config_from_url($cloudinaryUrl); + } + + } + + public static function fromString($environmentVariable) + { + return new CloudinaryEnvironmentVariable($environmentVariable); + } + + public function getCloud() + { + return Cloud::fromName(Cloudinary::config_get('cloud_name')); + } + + public function getCredentials() + { + return new Credentials( + Key::fromString(Cloudinary::config_get('api_key')), + Secret::fromString(Cloudinary::config_get('api_secret')) + ); + } + + public function __toString() + { + return $this->environmentVariable; + } + + private function isUrlValid($cloudinaryUrl) + { + return parse_url($cloudinaryUrl, PHP_URL_SCHEME) == "cloudinary"; + } +} diff --git a/magento1/src/lib/CloudinaryExtension/Security/ConsoleUrl.php b/magento1/src/lib/CloudinaryExtension/Security/ConsoleUrl.php new file mode 100755 index 0000000..e34c900 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Security/ConsoleUrl.php @@ -0,0 +1,26 @@ +consoleUrl = self::CLOUDINARY_CONSOLE_BASE_URL . $path; + } + + public static function fromPath($path) + { + return new ConsoleUrl($path); + } + + public function __toString() + { + return $this->consoleUrl; + } +} diff --git a/magento1/src/lib/CloudinaryExtension/Security/EnvironmentVariable.php b/magento1/src/lib/CloudinaryExtension/Security/EnvironmentVariable.php new file mode 100755 index 0000000..ae51017 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Security/EnvironmentVariable.php @@ -0,0 +1,9 @@ +key = (string)$key; + } + + public static function fromString($aKey) + { + return new Key($aKey); + } + + public function __toString() + { + return $this->key; + } + +} diff --git a/magento1/src/lib/CloudinaryExtension/Security/Secret.php b/magento1/src/lib/CloudinaryExtension/Security/Secret.php new file mode 100755 index 0000000..5a57ef8 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Security/Secret.php @@ -0,0 +1,24 @@ +secret = (string)$secret; + } + + public static function fromString($aSecret) + { + return new Secret($aSecret); + } + + public function __toString() + { + return $this->secret; + } +} diff --git a/magento1/src/lib/CloudinaryExtension/Security/SignedConsoleUrl.php b/magento1/src/lib/CloudinaryExtension/Security/SignedConsoleUrl.php new file mode 100755 index 0000000..dce9b23 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/Security/SignedConsoleUrl.php @@ -0,0 +1,31 @@ + time(), "mode" => "check"); + $params["signature"] = (string)ApiSignature::fromSecretAndParams($credentials->getSecret(), $params); + $params["api_key"] = (string)$credentials->getKey(); + $query = http_build_query($params); + + $this->signedConsoleUrl = (string)$url . '?' . $query; + } + + public static function fromConsoleUrlAndCredentials(ConsoleUrl $url, Credentials $credentials) + { + return new SignedConsoleUrl($url, $credentials); + } + + public function __toString() + { + return $this->signedConsoleUrl; + } +} diff --git a/magento1/src/lib/CloudinaryExtension/ValidateRemoteUrlRequest.php b/magento1/src/lib/CloudinaryExtension/ValidateRemoteUrlRequest.php new file mode 100755 index 0000000..b2c5499 --- /dev/null +++ b/magento1/src/lib/CloudinaryExtension/ValidateRemoteUrlRequest.php @@ -0,0 +1,55 @@ +curlHandler = curl_init($url); + $this->setCurlOptions(); + } + + public function validate() + { + $result = $this->execute(); + + if ($result->responseCode == 200 && is_null($result->error)) { + return true; + } + return false; + } + + private function execute() + { + curl_exec($this->curlHandler); + + $result = new \stdClass(); + $result->responseCode = $this->getResponseCode(); + $result->error = $this->getErrorMessage(); + + curl_close($this->curlHandler); + + return $result; + } + + private function getResponseCode() + { + return curl_getinfo($this->curlHandler, CURLINFO_HTTP_CODE); + } + + private function getErrorMessage() + { + return curl_errno($this->curlHandler) ? curl_error($this->curlHandler) : null; + } + + private function setCurlOptions() + { + curl_setopt($this->curlHandler, CURLOPT_HEADER, 1); + curl_setopt($this->curlHandler, CURLOPT_FAILONERROR, 1); + curl_setopt($this->curlHandler, CURLOPT_RETURNTRANSFER, 1); + } +} diff --git a/magento1/src/var/connect/Cloudinary_Cloudinary.xml b/magento1/src/var/connect/Cloudinary_Cloudinary.xml index efdde5f..572683d 100644 --- a/magento1/src/var/connect/Cloudinary_Cloudinary.xml +++ b/magento1/src/var/connect/Cloudinary_Cloudinary.xml @@ -9,13 +9,15 @@ Cloudinary supercharges your images! Upload images to the cloud, deliver optimized via a fast CDN, perform smart resizing and apply effects. MIT License (MITL) - 2.1.0 + 2.2.0 stable - v1.2.0 - - Release Highlights: - - * Foldered migration + + Release 2.2.0 notes: + - Existing images are not remigrated during migration, and do not fail migration + - Fix bug where migration status shows value above 100% + - Preserve original file extensions in Cloudinary image urls + - Retry migration when network failures occur + Cloudinary From 5d88e16ab08adce4c7fc0fc66508a4362947934e Mon Sep 17 00:00:00 2001 From: Rick Peacock Date: Mon, 8 May 2017 10:42:12 +0100 Subject: [PATCH 3/4] Add core and Magento2 module to new repo structure --- core/.cp-remote-settings.yml | 0 core/.gitignore | 3 + core/INSTALL.md | 44 + core/LICENSE | 0 core/README.md | 1 + core/behat.yml | 27 + core/composer.json | 31 + core/composer.lock | 3361 ++++++++++++++ core/features/admin/delete_image.feature | 9 + .../admin/extension_disable_enable.feature | 22 + .../prompted_to_sign_up_to_cloudinary.feature | 16 + .../bootstrap/Domain/ConfigurationContext.php | 83 + .../Domain/DeleteImageDomainContext.php | 57 + .../bootstrap/Domain/DomainContext.php | 215 + core/features/bootstrap/Domain/Doubles.php | 63 + .../Domain/TransformationContext.php | 225 + core/features/bootstrap/Fixtures/Admin.yaml | 6 + .../ImageProviders/ConfigImageProvider.php | 51 + .../ImageProviders/ConfigurationProvider.php | 119 + .../ImageProviders/FakeImageProvider.php | 88 + .../TransformingImageProvider.php | 46 + core/features/bootstrap/Page/AdminLogin.php | 37 + .../CloudinaryAdminSystemConfiguration.php | 41 + .../bootstrap/Page/CloudinaryManagement.php | 35 + .../bootstrap/Ui/AdminCredentialsContext.php | 196 + .../bootstrap/Ui/ModuleEnableContext.php | 76 + core/features/configuration.feature | 9 + .../features/image_provider_transform.feature | 55 + core/features/image_provider_upload.feature | 27 + .../migration/admin_migrates_images.feature | 31 + .../cloudinary_enable_disable.feature | 16 + core/features/validate_credentials.feature | 16 + core/lib/CloudinaryExtension/Cloud.php | 25 + .../CloudinaryImageManager.php | 52 + .../CloudinaryImageProvider.php | 96 + .../ConfigurationBuilder.php | 30 + .../ConfigurationInterface.php | 67 + .../CredentialValidator.php | 18 + core/lib/CloudinaryExtension/Credentials.php | 35 + .../Exception/InvalidCredentials.php | 10 + .../Exception/MigrationError.php | 49 + .../CloudinaryExtension/FolderTranslator.php | 16 + core/lib/CloudinaryExtension/Image.php | 54 + .../Image/ImageFactory.php | 46 + .../CloudinaryExtension/Image/LocalImage.php | 25 + .../Image/Synchronizable.php | 21 + .../Image/SynchronizationChecker.php | 11 + .../Image/Transformation.php | 110 + .../Image/Transformation/Dimensions.php | 36 + .../Image/Transformation/Dpr.php | 23 + .../Image/Transformation/FetchFormat.php | 25 + .../Image/Transformation/Format.php | 25 + .../Image/Transformation/Gravity.php | 30 + .../Image/Transformation/Quality.php | 23 + .../CloudinaryExtension/ImageInterface.php | 11 + .../lib/CloudinaryExtension/ImageProvider.php | 14 + .../Migration/BatchUploader.php | 91 + .../CloudinaryExtension/Migration/Logger.php | 14 + .../CloudinaryExtension/Migration/Queue.php | 47 + .../Migration/SynchronizedMediaRepository.php | 8 + .../CloudinaryExtension/Migration/Task.php | 14 + .../Security/ApiSignature.php | 26 + .../CloudinaryEnvironmentVariable.php | 47 + .../Security/ConsoleUrl.php | 26 + .../Security/EnvironmentVariable.php | 9 + core/lib/CloudinaryExtension/Security/Key.php | 25 + .../CloudinaryExtension/Security/Secret.php | 24 + .../Security/SignedConsoleUrl.php | 31 + .../SynchroniseAssetsRepositoryInterface.php | 18 + core/lib/CloudinaryExtension/UploadConfig.php | 73 + .../UploadResponseValidator.php | 17 + core/lib/CloudinaryExtension/UrlGenerator.php | 65 + .../ValidateRemoteUrlRequest.php | 55 + core/phpspec.yml | 3 + core/phpunit.xml.dist | 28 + core/spec/CloudinaryExtension/CloudSpec.php | 20 + .../CloudinaryImageProviderSpec.php | 46 + .../ConfigurationBuilderSpec.php | 59 + .../CredentialValidatorSpec.php | 10 + .../CloudinaryExtension/CredentialsSpec.php | 31 + .../Image/ImageFactorySpec.php | 27 + .../Image/Transformation/DimensionsSpec.php | 40 + .../Image/TransformationSpec.php | 144 + core/spec/CloudinaryExtension/ImageSpec.php | 24 + .../Migration/BatchUploaderSpec.php | 145 + .../Migration/QueueSpec.php | 71 + .../CloudinaryEnvironmentVariableSpec.php | 31 + .../CloudinaryExtension/Security/KeySpec.php | 19 + .../Security/SecretSpec.php | 19 + .../UploadResponseValidatorSpec.php | 25 + .../CloudinaryExtension/UrlGeneratorSpec.php | 78 + magento2/.gitignore | 6 + .../SynchronisationRepositoryInterface.php | 22 + magento2/Command/UploadImages.php | 52 + magento2/Model/BatchUploader.php | 99 + magento2/Model/Config/Source/Dropdown/Dpr.php | 22 + .../Model/Config/Source/Dropdown/Gravity.php | 70 + .../Model/Config/Source/Dropdown/Quality.php | 50 + magento2/Model/Configuration.php | 202 + magento2/Model/ImageRepository.php | 82 + magento2/Model/MigrationTask.php | 47 + .../Model/Observer/DeleteProductImage.php | 45 + .../Model/Observer/UploadProductImage.php | 45 + magento2/Model/ProductImageFinder.php | 63 + .../ProductImageFinder/DeletedImageFilter.php | 15 + .../Model/ProductImageFinder/ImageCreator.php | 53 + .../Model/ProductImageFinder/ImageFilter.php | 16 + .../ProductImageFinder/NewImageFilter.php | 18 + .../Model/ResourceModel/Synchronisation.php | 13 + .../Synchronisation/Collection.php | 15 + magento2/Model/Synchronisation.php | 40 + magento2/Model/SynchronisationChecker.php | 35 + magento2/Model/SynchronisationRepository.php | 203 + magento2/Model/Template/Filter.php | 106 + magento2/Plugin/FileRemover.php | 50 + magento2/Plugin/FileUploader.php | 81 + magento2/Plugin/ImageHelper.php | 145 + magento2/Plugin/MediaConfig.php | 47 + magento2/README.md | 1 + magento2/Setup/InstallSchema.php | 43 + magento2/behat.yml | 41 + magento2/composer.json | 53 + magento2/composer.lock | 3899 +++++++++++++++++ magento2/etc/adminhtml/events.xml | 9 + magento2/etc/adminhtml/system.xml | 57 + magento2/etc/config.xml | 16 + magento2/etc/crontab.xml | 7 + magento2/etc/di.xml | 49 + magento2/etc/module.xml | 5 + .../features/bootstrap/FeatureContext.php | 219 + .../bootstrap/Fixtures/CloudinaryConfig.php | 30 + .../bootstrap/Fixtures/CloudinaryManager.php | 25 + .../bootstrap/Fixtures/ProductManager.php | 19 + .../Helpers/PageObjectHelperMethods.php | 146 + .../features/bootstrap/Page/Admin/Login.php | 27 + magento2/features/bootstrap/Page/Product.php | 29 + .../features/fixtures/images/pink_dress.gif | Bin 0 -> 167624 bytes magento2/registration.php | 7 + .../Cloudinary/Model/BatchUploaderSpec.php | 58 + .../DeletedImageFilterSpec.php | 26 + .../ProductImageFinder/ImageCreatorSpec.php | 35 + .../ProductImageFinder/NewImageFilterSpec.php | 20 + .../Model/SynchronisationCheckerSpec.php | 52 + .../Cloudinary/Plugin/FileUploaderSpec.php | 52 + 144 files changed, 13835 insertions(+) create mode 100644 core/.cp-remote-settings.yml create mode 100644 core/.gitignore create mode 100644 core/INSTALL.md create mode 100644 core/LICENSE create mode 100644 core/README.md create mode 100644 core/behat.yml create mode 100644 core/composer.json create mode 100644 core/composer.lock create mode 100644 core/features/admin/delete_image.feature create mode 100644 core/features/admin/extension_disable_enable.feature create mode 100644 core/features/admin/prompted_to_sign_up_to_cloudinary.feature create mode 100644 core/features/bootstrap/Domain/ConfigurationContext.php create mode 100644 core/features/bootstrap/Domain/DeleteImageDomainContext.php create mode 100644 core/features/bootstrap/Domain/DomainContext.php create mode 100644 core/features/bootstrap/Domain/Doubles.php create mode 100644 core/features/bootstrap/Domain/TransformationContext.php create mode 100644 core/features/bootstrap/Fixtures/Admin.yaml create mode 100644 core/features/bootstrap/ImageProviders/ConfigImageProvider.php create mode 100644 core/features/bootstrap/ImageProviders/ConfigurationProvider.php create mode 100644 core/features/bootstrap/ImageProviders/FakeImageProvider.php create mode 100644 core/features/bootstrap/ImageProviders/TransformingImageProvider.php create mode 100644 core/features/bootstrap/Page/AdminLogin.php create mode 100644 core/features/bootstrap/Page/CloudinaryAdminSystemConfiguration.php create mode 100644 core/features/bootstrap/Page/CloudinaryManagement.php create mode 100644 core/features/bootstrap/Ui/AdminCredentialsContext.php create mode 100644 core/features/bootstrap/Ui/ModuleEnableContext.php create mode 100644 core/features/configuration.feature create mode 100644 core/features/image_provider_transform.feature create mode 100644 core/features/image_provider_upload.feature create mode 100644 core/features/migration/admin_migrates_images.feature create mode 100644 core/features/migration/cloudinary_enable_disable.feature create mode 100644 core/features/validate_credentials.feature create mode 100644 core/lib/CloudinaryExtension/Cloud.php create mode 100644 core/lib/CloudinaryExtension/CloudinaryImageManager.php create mode 100644 core/lib/CloudinaryExtension/CloudinaryImageProvider.php create mode 100644 core/lib/CloudinaryExtension/ConfigurationBuilder.php create mode 100644 core/lib/CloudinaryExtension/ConfigurationInterface.php create mode 100644 core/lib/CloudinaryExtension/CredentialValidator.php create mode 100644 core/lib/CloudinaryExtension/Credentials.php create mode 100644 core/lib/CloudinaryExtension/Exception/InvalidCredentials.php create mode 100644 core/lib/CloudinaryExtension/Exception/MigrationError.php create mode 100644 core/lib/CloudinaryExtension/FolderTranslator.php create mode 100644 core/lib/CloudinaryExtension/Image.php create mode 100644 core/lib/CloudinaryExtension/Image/ImageFactory.php create mode 100644 core/lib/CloudinaryExtension/Image/LocalImage.php create mode 100644 core/lib/CloudinaryExtension/Image/Synchronizable.php create mode 100644 core/lib/CloudinaryExtension/Image/SynchronizationChecker.php create mode 100644 core/lib/CloudinaryExtension/Image/Transformation.php create mode 100644 core/lib/CloudinaryExtension/Image/Transformation/Dimensions.php create mode 100644 core/lib/CloudinaryExtension/Image/Transformation/Dpr.php create mode 100644 core/lib/CloudinaryExtension/Image/Transformation/FetchFormat.php create mode 100644 core/lib/CloudinaryExtension/Image/Transformation/Format.php create mode 100644 core/lib/CloudinaryExtension/Image/Transformation/Gravity.php create mode 100644 core/lib/CloudinaryExtension/Image/Transformation/Quality.php create mode 100644 core/lib/CloudinaryExtension/ImageInterface.php create mode 100644 core/lib/CloudinaryExtension/ImageProvider.php create mode 100644 core/lib/CloudinaryExtension/Migration/BatchUploader.php create mode 100644 core/lib/CloudinaryExtension/Migration/Logger.php create mode 100644 core/lib/CloudinaryExtension/Migration/Queue.php create mode 100644 core/lib/CloudinaryExtension/Migration/SynchronizedMediaRepository.php create mode 100644 core/lib/CloudinaryExtension/Migration/Task.php create mode 100644 core/lib/CloudinaryExtension/Security/ApiSignature.php create mode 100644 core/lib/CloudinaryExtension/Security/CloudinaryEnvironmentVariable.php create mode 100644 core/lib/CloudinaryExtension/Security/ConsoleUrl.php create mode 100644 core/lib/CloudinaryExtension/Security/EnvironmentVariable.php create mode 100644 core/lib/CloudinaryExtension/Security/Key.php create mode 100644 core/lib/CloudinaryExtension/Security/Secret.php create mode 100644 core/lib/CloudinaryExtension/Security/SignedConsoleUrl.php create mode 100644 core/lib/CloudinaryExtension/SynchroniseAssetsRepositoryInterface.php create mode 100644 core/lib/CloudinaryExtension/UploadConfig.php create mode 100644 core/lib/CloudinaryExtension/UploadResponseValidator.php create mode 100644 core/lib/CloudinaryExtension/UrlGenerator.php create mode 100644 core/lib/CloudinaryExtension/ValidateRemoteUrlRequest.php create mode 100644 core/phpspec.yml create mode 100644 core/phpunit.xml.dist create mode 100644 core/spec/CloudinaryExtension/CloudSpec.php create mode 100644 core/spec/CloudinaryExtension/CloudinaryImageProviderSpec.php create mode 100644 core/spec/CloudinaryExtension/ConfigurationBuilderSpec.php create mode 100644 core/spec/CloudinaryExtension/CredentialValidatorSpec.php create mode 100644 core/spec/CloudinaryExtension/CredentialsSpec.php create mode 100644 core/spec/CloudinaryExtension/Image/ImageFactorySpec.php create mode 100644 core/spec/CloudinaryExtension/Image/Transformation/DimensionsSpec.php create mode 100644 core/spec/CloudinaryExtension/Image/TransformationSpec.php create mode 100644 core/spec/CloudinaryExtension/ImageSpec.php create mode 100644 core/spec/CloudinaryExtension/Migration/BatchUploaderSpec.php create mode 100644 core/spec/CloudinaryExtension/Migration/QueueSpec.php create mode 100644 core/spec/CloudinaryExtension/Security/CloudinaryEnvironmentVariableSpec.php create mode 100644 core/spec/CloudinaryExtension/Security/KeySpec.php create mode 100644 core/spec/CloudinaryExtension/Security/SecretSpec.php create mode 100644 core/spec/CloudinaryExtension/UploadResponseValidatorSpec.php create mode 100644 core/spec/CloudinaryExtension/UrlGeneratorSpec.php create mode 100644 magento2/.gitignore create mode 100644 magento2/Api/SynchronisationRepositoryInterface.php create mode 100644 magento2/Command/UploadImages.php create mode 100644 magento2/Model/BatchUploader.php create mode 100644 magento2/Model/Config/Source/Dropdown/Dpr.php create mode 100644 magento2/Model/Config/Source/Dropdown/Gravity.php create mode 100644 magento2/Model/Config/Source/Dropdown/Quality.php create mode 100644 magento2/Model/Configuration.php create mode 100644 magento2/Model/ImageRepository.php create mode 100644 magento2/Model/MigrationTask.php create mode 100644 magento2/Model/Observer/DeleteProductImage.php create mode 100644 magento2/Model/Observer/UploadProductImage.php create mode 100644 magento2/Model/ProductImageFinder.php create mode 100644 magento2/Model/ProductImageFinder/DeletedImageFilter.php create mode 100644 magento2/Model/ProductImageFinder/ImageCreator.php create mode 100644 magento2/Model/ProductImageFinder/ImageFilter.php create mode 100644 magento2/Model/ProductImageFinder/NewImageFilter.php create mode 100644 magento2/Model/ResourceModel/Synchronisation.php create mode 100644 magento2/Model/ResourceModel/Synchronisation/Collection.php create mode 100644 magento2/Model/Synchronisation.php create mode 100644 magento2/Model/SynchronisationChecker.php create mode 100644 magento2/Model/SynchronisationRepository.php create mode 100644 magento2/Model/Template/Filter.php create mode 100644 magento2/Plugin/FileRemover.php create mode 100644 magento2/Plugin/FileUploader.php create mode 100644 magento2/Plugin/ImageHelper.php create mode 100644 magento2/Plugin/MediaConfig.php create mode 100644 magento2/README.md create mode 100644 magento2/Setup/InstallSchema.php create mode 100644 magento2/behat.yml create mode 100644 magento2/composer.json create mode 100644 magento2/composer.lock create mode 100644 magento2/etc/adminhtml/events.xml create mode 100644 magento2/etc/adminhtml/system.xml create mode 100644 magento2/etc/config.xml create mode 100644 magento2/etc/crontab.xml create mode 100644 magento2/etc/di.xml create mode 100644 magento2/etc/module.xml create mode 100644 magento2/features/bootstrap/FeatureContext.php create mode 100644 magento2/features/bootstrap/Fixtures/CloudinaryConfig.php create mode 100644 magento2/features/bootstrap/Fixtures/CloudinaryManager.php create mode 100644 magento2/features/bootstrap/Fixtures/ProductManager.php create mode 100644 magento2/features/bootstrap/Helpers/PageObjectHelperMethods.php create mode 100644 magento2/features/bootstrap/Page/Admin/Login.php create mode 100644 magento2/features/bootstrap/Page/Product.php create mode 100644 magento2/features/fixtures/images/pink_dress.gif create mode 100644 magento2/registration.php create mode 100644 magento2/spec/Cloudinary/Cloudinary/Model/BatchUploaderSpec.php create mode 100644 magento2/spec/Cloudinary/Cloudinary/Model/ProductImageFinder/DeletedImageFilterSpec.php create mode 100644 magento2/spec/Cloudinary/Cloudinary/Model/ProductImageFinder/ImageCreatorSpec.php create mode 100644 magento2/spec/Cloudinary/Cloudinary/Model/ProductImageFinder/NewImageFilterSpec.php create mode 100644 magento2/spec/Cloudinary/Cloudinary/Model/SynchronisationCheckerSpec.php create mode 100644 magento2/spec/Cloudinary/Cloudinary/Plugin/FileUploaderSpec.php diff --git a/core/.cp-remote-settings.yml b/core/.cp-remote-settings.yml new file mode 100644 index 0000000..e69de29 diff --git a/core/.gitignore b/core/.gitignore new file mode 100644 index 0000000..cc8b12c --- /dev/null +++ b/core/.gitignore @@ -0,0 +1,3 @@ +bin/ +vendor/ +composer.phar diff --git a/core/INSTALL.md b/core/INSTALL.md new file mode 100644 index 0000000..cffadb6 --- /dev/null +++ b/core/INSTALL.md @@ -0,0 +1,44 @@ +# INSTALLATION + +## Composer + +To install the Cloudinary extenstion via composer you'll need to specify the release you want to install, and the path for the extension repository. You'll also need to specify the path for the `magento-composer-installer` composer plugin. + +The following example of what to add to a `composer.json` file, in order to install via composer, assumes that Magento resides inside a folder named `public/` inside your codebase: + +```JSON +{ + "require": { + "inviqa/cloudinary": "dev-master" + }, + "repositories": [ + { + "type": "vcs", + "url": "git@github.com:inviqa/cloudinary.git" + }, + { + "type": "vcs", + "url": "https://github.com/magento-hackathon/magento-composer-installer" + } + ], + "extra":{ + "magento-root-dir": "./public", + "magento-deploystrategy": "copy" + }, + "autoload": { + "psr-0": { + "": [ + "public/app/code/local", + "public/app/code/community", + "public/app/code/core", + "public/app", + "public/lib" + ], + "Mage" : "public/app/code/core" + } + } +} +``` + +Although the `master` branch should always be stable, for compatibility reasons you probably want to change `dev-master` to a specific release, making sure the extension will only be upgraded to a newer version when explicitly changed in the `composer.json` file. +At the time of writing (December 2014) the current version is `0.1.1` and this should be used in place of `dev-master`. diff --git a/core/LICENSE b/core/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000..4dcdb3a --- /dev/null +++ b/core/README.md @@ -0,0 +1 @@ +# cloudinary-core diff --git a/core/behat.yml b/core/behat.yml new file mode 100644 index 0000000..286893d --- /dev/null +++ b/core/behat.yml @@ -0,0 +1,27 @@ +default: + suites: + domain: + filters: { tags: '~@not-automated&&~@ui' } + contexts: + - Domain\DomainContext + - Domain\TransformationContext + - Domain\DeleteImageDomainContext + - Domain\ConfigurationContext + + extensions: + SensioLabs\Behat\PageObjectExtension: ~ + Behat\MinkExtension\ServiceContainer\MinkExtension: + base_url: 'http://cloudinary-test-environment.dev/' + goutte: + guzzle_parameters: + curl.options: + CURLOPT_SSL_VERIFYPEER: false + CURLOPT_CERTINFO: false + CURLOPT_TIMEOUT: 120 + ssl.certificate_authority: false + selenium2: + wd_host: http://localhost:4444/wd/hub + browser: phantomjs +# command to open the failing html pages: + show_cmd: echo '%s' + show_tmp_dir: /vagrant diff --git a/core/composer.json b/core/composer.json new file mode 100644 index 0000000..2de6832 --- /dev/null +++ b/core/composer.json @@ -0,0 +1,31 @@ +{ + "name": "inviqa/cloudinary-core", + "type": "cloudinary-core", + "license": "proprietary", + "description": "Cloudinary Core.", + "require": { + "php": ">=5.4.0", + "cloudinary/cloudinary_php": "~1.6.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.4.0", + "sensiolabs/behat-page-object-extension": "*@dev", + "behat/mink-selenium2-driver": "*", + "behat/mink-goutte-driver": "^1.0", + "squizlabs/php_codesniffer": "1.*", + "phpunit/phpunit": "3.7.*", + "mayflower/php-codebrowser": "^1.1", + "bossa/phpspec2-expect": "1.0.3" + }, + "config": { + "bin-dir": "bin" + }, + "autoload": { + "psr-0": { + "": [ + "features/bootstrap", + "lib" + ] + } + } +} diff --git a/core/composer.lock b/core/composer.lock new file mode 100644 index 0000000..02f68fd --- /dev/null +++ b/core/composer.lock @@ -0,0 +1,3361 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "8bf037e0a1e683b613e9e9bf9a1e8530", + "content-hash": "3882e107d8575e5cf28c9cf036fb347a", + "packages": [ + { + "name": "cloudinary/cloudinary_php", + "version": "1.6.2", + "source": { + "type": "git", + "url": "https://github.com/cloudinary/cloudinary_php.git", + "reference": "8b89be228b39bcdb36d5e642e9796c756760737e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cloudinary/cloudinary_php/zipball/8b89be228b39bcdb36d5e642e9796c756760737e", + "reference": "8b89be228b39bcdb36d5e642e9796c756760737e", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "4.7.*" + }, + "type": "library", + "autoload": { + "classmap": [ + "src" + ], + "files": [ + "src/Helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Cloudinary", + "homepage": "https://github.com/cloudinary/cloudinary_php/graphs/contributors" + } + ], + "description": "Cloudinary PHP SDK", + "homepage": "https://github.com/cloudinary/cloudinary_php", + "keywords": [ + "cdn", + "cloud", + "cloudinary", + "image management", + "sdk" + ], + "time": "2017-02-23 01:10:18" + } + ], + "packages-dev": [ + { + "name": "behat/behat", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Behat.git", + "reference": "15a3a1857457eaa29cdf41564a5e421effb09526" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Behat/zipball/15a3a1857457eaa29cdf41564a5e421effb09526", + "reference": "15a3a1857457eaa29cdf41564a5e421effb09526", + "shasum": "" + }, + "require": { + "behat/gherkin": "^4.4.4", + "behat/transliterator": "~1.0", + "container-interop/container-interop": "^1.1", + "ext-mbstring": "*", + "php": ">=5.3.3", + "symfony/class-loader": "~2.1||~3.0", + "symfony/config": "~2.3||~3.0", + "symfony/console": "~2.5||~3.0", + "symfony/dependency-injection": "~2.1||~3.0", + "symfony/event-dispatcher": "~2.1||~3.0", + "symfony/translation": "~2.3||~3.0", + "symfony/yaml": "~2.1||~3.0" + }, + "require-dev": { + "herrera-io/box": "~1.6.1", + "phpunit/phpunit": "~4.5", + "symfony/process": "~2.5|~3.0" + }, + "suggest": { + "behat/mink-extension": "for integration with Mink testing framework", + "behat/symfony2-extension": "for integration with Symfony2 web framework", + "behat/yii-extension": "for integration with Yii web framework" + }, + "bin": [ + "bin/behat" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Behat": "src/", + "Behat\\Testwork": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Scenario-oriented BDD framework for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "Agile", + "BDD", + "ScenarioBDD", + "Scrum", + "StoryBDD", + "User story", + "business", + "development", + "documentation", + "examples", + "symfony", + "testing" + ], + "time": "2016-12-25 13:43:52" + }, + { + "name": "behat/gherkin", + "version": "v4.4.5", + "source": { + "type": "git", + "url": "https://github.com/Behat/Gherkin.git", + "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/5c14cff4f955b17d20d088dec1bde61c0539ec74", + "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "~4.5|~5", + "symfony/phpunit-bridge": "~2.7|~3", + "symfony/yaml": "~2.3|~3" + }, + "suggest": { + "symfony/yaml": "If you want to parse features, represented in YAML files" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Gherkin": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Gherkin DSL parser for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "BDD", + "Behat", + "Cucumber", + "DSL", + "gherkin", + "parser" + ], + "time": "2016-10-30 11:50:56" + }, + { + "name": "behat/mink", + "version": "v1.7.1", + "source": { + "type": "git", + "url": "https://github.com/minkphp/Mink.git", + "reference": "e6930b9c74693dff7f4e58577e1b1743399f3ff9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/Mink/zipball/e6930b9c74693dff7f4e58577e1b1743399f3ff9", + "reference": "e6930b9c74693dff7f4e58577e1b1743399f3ff9", + "shasum": "" + }, + "require": { + "php": ">=5.3.1", + "symfony/css-selector": "~2.1|~3.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7|~3.0" + }, + "suggest": { + "behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)", + "behat/mink-goutte-driver": "fast headless driver for any app without JS emulation", + "behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)", + "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Browser controller/emulator abstraction for PHP", + "homepage": "http://mink.behat.org/", + "keywords": [ + "browser", + "testing", + "web" + ], + "time": "2016-03-05 08:26:18" + }, + { + "name": "behat/mink-browserkit-driver", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkBrowserKitDriver.git", + "reference": "10e67fb4a295efcd62ea0bf16025a85ea19534fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/10e67fb4a295efcd62ea0bf16025a85ea19534fb", + "reference": "10e67fb4a295efcd62ea0bf16025a85ea19534fb", + "shasum": "" + }, + "require": { + "behat/mink": "^1.7.1@dev", + "php": ">=5.3.6", + "symfony/browser-kit": "~2.3|~3.0", + "symfony/dom-crawler": "~2.3|~3.0" + }, + "require-dev": { + "silex/silex": "~1.2", + "symfony/phpunit-bridge": "~2.7|~3.0" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Symfony2 BrowserKit driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "Mink", + "Symfony2", + "browser", + "testing" + ], + "time": "2016-03-05 08:59:47" + }, + { + "name": "behat/mink-extension", + "version": "v2.2", + "source": { + "type": "git", + "url": "https://github.com/Behat/MinkExtension.git", + "reference": "5b4bda64ff456104564317e212c823e45cad9d59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/5b4bda64ff456104564317e212c823e45cad9d59", + "reference": "5b4bda64ff456104564317e212c823e45cad9d59", + "shasum": "" + }, + "require": { + "behat/behat": "~3.0,>=3.0.5", + "behat/mink": "~1.5", + "php": ">=5.3.2", + "symfony/config": "~2.2|~3.0" + }, + "require-dev": { + "behat/mink-goutte-driver": "~1.1", + "phpspec/phpspec": "~2.0" + }, + "type": "behat-extension", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\MinkExtension": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + }, + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com" + } + ], + "description": "Mink extension for Behat", + "homepage": "http://extensions.behat.org/mink", + "keywords": [ + "browser", + "gui", + "test", + "web" + ], + "time": "2016-02-15 07:55:18" + }, + { + "name": "behat/mink-goutte-driver", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkGoutteDriver.git", + "reference": "8b9ad6d2d95bc70b840d15323365f52fcdaea6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkGoutteDriver/zipball/8b9ad6d2d95bc70b840d15323365f52fcdaea6ca", + "reference": "8b9ad6d2d95bc70b840d15323365f52fcdaea6ca", + "shasum": "" + }, + "require": { + "behat/mink": "~1.6@dev", + "behat/mink-browserkit-driver": "~1.2@dev", + "fabpot/goutte": "~1.0.4|~2.0|~3.1", + "php": ">=5.3.1" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7|~3.0" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Goutte driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "browser", + "goutte", + "headless", + "testing" + ], + "time": "2016-03-05 09:04:22" + }, + { + "name": "behat/mink-selenium2-driver", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkSelenium2Driver.git", + "reference": "473a9f3ebe0c134ee1e623ce8a9c852832020288" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/473a9f3ebe0c134ee1e623ce8a9c852832020288", + "reference": "473a9f3ebe0c134ee1e623ce8a9c852832020288", + "shasum": "" + }, + "require": { + "behat/mink": "~1.7@dev", + "instaclick/php-webdriver": "~1.1", + "php": ">=5.3.1" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Pete Otaqui", + "email": "pete@otaqui.com", + "homepage": "https://github.com/pete-otaqui" + } + ], + "description": "Selenium2 (WebDriver) driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "ajax", + "browser", + "javascript", + "selenium", + "testing", + "webdriver" + ], + "time": "2016-03-05 09:10:18" + }, + { + "name": "behat/transliterator", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Transliterator.git", + "reference": "868e05be3a9f25ba6424c2dd4849567f50715003" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Transliterator/zipball/868e05be3a9f25ba6424c2dd4849567f50715003", + "reference": "868e05be3a9f25ba6424c2dd4849567f50715003", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Transliterator": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Artistic-1.0" + ], + "description": "String transliterator", + "keywords": [ + "i18n", + "slug", + "transliterator" + ], + "time": "2015-09-28 16:26:35" + }, + { + "name": "bossa/phpspec2-expect", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/BossaConsulting/phpspec2-expect.git", + "reference": "f3a80b7fa743b8a1078a7e320bd3e8b8b6283780" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/BossaConsulting/phpspec2-expect/zipball/f3a80b7fa743b8a1078a7e320bd3e8b8b6283780", + "reference": "f3a80b7fa743b8a1078a7e320bd3e8b8b6283780", + "shasum": "" + }, + "require": { + "phpspec/phpspec": "~2.0" + }, + "type": "library", + "autoload": { + "files": [ + "expect.php" + ], + "psr-0": { + "Bossa\\PhpSpec\\Expect\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marcello Duarte", + "homepage": "http://marcelloduarte.net/" + } + ], + "description": "Helper that decorates any SUS with a phpspec lazy object wrapper", + "keywords": [ + "BDD", + "SpecBDD", + "TDD", + "spec", + "specification" + ], + "time": "2014-09-12 09:25:51" + }, + { + "name": "container-interop/container-interop", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/container-interop/container-interop.git", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "shasum": "" + }, + "require": { + "psr/container": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/Interop/Container/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", + "homepage": "https://github.com/container-interop/container-interop", + "time": "2017-02-14 19:40:03" + }, + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14 21:17:01" + }, + { + "name": "fabpot/goutte", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/Goutte.git", + "reference": "db5c28f4a010b4161d507d5304e28a7ebf211638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/db5c28f4a010b4161d507d5304e28a7ebf211638", + "reference": "db5c28f4a010b4161d507d5304e28a7ebf211638", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0", + "php": ">=5.5.0", + "symfony/browser-kit": "~2.1|~3.0", + "symfony/css-selector": "~2.1|~3.0", + "symfony/dom-crawler": "~2.1|~3.0" + }, + "type": "application", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Goutte\\": "Goutte" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "A simple PHP Web Scraper", + "homepage": "https://github.com/FriendsOfPHP/Goutte", + "keywords": [ + "scraper" + ], + "time": "2017-01-03 13:21:43" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.2.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "ebf29dee597f02f09f4d5bbecc68230ea9b08f60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ebf29dee597f02f09f4d5bbecc68230ea9b08f60", + "reference": "ebf29dee597f02f09f4d5bbecc68230ea9b08f60", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.3.1", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0", + "psr/log": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2016-10-08 15:01:37" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20 10:07:11" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "0d6c7ca039329247e4f0f8f8f6506810e8248855" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/0d6c7ca039329247e4f0f8f8f6506810e8248855", + "reference": "0d6c7ca039329247e4f0f8f8f6506810e8248855", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2017-02-27 10:51:17" + }, + { + "name": "instaclick/php-webdriver", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/instaclick/php-webdriver.git", + "reference": "0c20707dcf30a32728fd6bdeeab996c887fdb2fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/instaclick/php-webdriver/zipball/0c20707dcf30a32728fd6bdeeab996c887fdb2fb", + "reference": "0c20707dcf30a32728fd6bdeeab996c887fdb2fb", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.2" + }, + "require-dev": { + "satooshi/php-coveralls": "dev-master" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "WebDriver": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Justin Bishop", + "email": "jubishop@gmail.com", + "role": "Developer" + }, + { + "name": "Anthon Pang", + "email": "apang@softwaredevelopment.ca", + "role": "Fork Maintainer" + } + ], + "description": "PHP WebDriver for Selenium 2", + "homepage": "http://instaclick.com/", + "keywords": [ + "browser", + "selenium", + "webdriver", + "webtest" + ], + "time": "2015-06-15 20:19:33" + }, + { + "name": "mayflower/php-codebrowser", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/mayflower/PHP_CodeBrowser.git", + "reference": "b44cb1867211b3eb9efe8bb61a57fe782c84831f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mayflower/PHP_CodeBrowser/zipball/b44cb1867211b3eb9efe8bb61a57fe782c84831f", + "reference": "b44cb1867211b3eb9efe8bb61a57fe782c84831f", + "shasum": "" + }, + "require": { + "monolog/monolog": "~1.7", + "phpunit/php-file-iterator": "~1.3", + "symfony/console": "~2.1|~3.0" + }, + "require-dev": { + "phploc/phploc": "*", + "phpmd/phpmd": "1.5.*", + "phpunit/phpunit": "3.7.*", + "sebastian/phpcpd": "*", + "squizlabs/php_codesniffer": "1.*" + }, + "bin": [ + "bin/phpcb" + ], + "type": "library", + "autoload": { + "psr-0": { + "PHPCodeBrowser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Robin Gloster", + "email": "robin.gloster@mayflower.de", + "role": "developer" + } + ], + "description": "A code browser that augments the code with information from various QA tools.", + "homepage": "https://github.com/Mayflower/PHP_CodeBrowser", + "time": "2016-01-14 12:43:42" + }, + { + "name": "monolog/monolog", + "version": "1.22.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "bad29cb8d18ab0315e6c477751418a82c850d558" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bad29cb8d18ab0315e6c477751418a82c850d558", + "reference": "bad29cb8d18ab0315e6c477751418a82c850d558", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "jakub-onderka/php-parallel-lint": "0.9", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpunit/phpunit": "~4.5", + "phpunit/phpunit-mock-objects": "2.3.0", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "~5.3" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "time": "2016-11-26 00:15:39" + }, + { + "name": "ocramius/proxy-manager", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/ProxyManager.git", + "reference": "57e9272ec0e8deccf09421596e0e2252df440e11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/ProxyManager/zipball/57e9272ec0e8deccf09421596e0e2252df440e11", + "reference": "57e9272ec0e8deccf09421596e0e2252df440e11", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "zendframework/zend-code": ">2.2.5,<3.0" + }, + "require-dev": { + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "1.5.*" + }, + "suggest": { + "ocramius/generated-hydrator": "To have very fast object to array to object conversion for ghost objects", + "zendframework/zend-json": "To have the JsonRpc adapter (Remote Object feature)", + "zendframework/zend-soap": "To have the Soap adapter (Remote Object feature)", + "zendframework/zend-stdlib": "To use the hydrator proxy", + "zendframework/zend-xmlrpc": "To have the XmlRpc adapter (Remote Object feature)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "ProxyManager\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A library providing utilities to generate, instantiate and generally operate with Object Proxies", + "homepage": "https://github.com/Ocramius/ProxyManager", + "keywords": [ + "aop", + "lazy loading", + "proxy", + "proxy pattern", + "service proxies" + ], + "time": "2015-08-09 04:28:19" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2015-12-27 11:43:31" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0@dev", + "phpdocumentor/type-resolver": "^0.2.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2016-09-30 07:12:33" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.2.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", + "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2016-11-25 06:54:22" + }, + { + "name": "phpspec/php-diff", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/phpspec/php-diff.git", + "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/php-diff/zipball/30e103d19519fe678ae64a60d77884ef3d71b28a", + "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-0": { + "Diff": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Chris Boulton", + "homepage": "http://github.com/chrisboulton", + "role": "Original developer" + } + ], + "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).", + "time": "2013-11-01 13:02:21" + }, + { + "name": "phpspec/phpspec", + "version": "2.5.5", + "source": { + "type": "git", + "url": "https://github.com/phpspec/phpspec.git", + "reference": "db395f435eb8e820448e8690de1a8db86d5dd8af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/phpspec/zipball/db395f435eb8e820448e8690de1a8db86d5dd8af", + "reference": "db395f435eb8e820448e8690de1a8db86d5dd8af", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.1", + "ext-tokenizer": "*", + "php": ">=5.3.3", + "phpspec/php-diff": "~1.0.0", + "phpspec/prophecy": "~1.4", + "sebastian/exporter": "~1.0", + "symfony/console": "~2.3|~3.0", + "symfony/event-dispatcher": "~2.1|~3.0", + "symfony/finder": "~2.1|~3.0", + "symfony/process": "^2.6|~3.0", + "symfony/yaml": "~2.1|~3.0" + }, + "require-dev": { + "behat/behat": "^3.0.11", + "ciaranmcnulty/versionbasedtestskipper": "^0.2.1", + "phpunit/phpunit": "~4.4", + "symfony/filesystem": "~2.1|~3.0" + }, + "suggest": { + "phpspec/nyan-formatters": "~1.0 – Adds Nyan formatters" + }, + "bin": [ + "bin/phpspec" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "PhpSpec": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "homepage": "http://marcelloduarte.net/" + } + ], + "description": "Specification-oriented BDD framework for PHP 5.3+", + "homepage": "http://phpspec.net/", + "keywords": [ + "BDD", + "SpecBDD", + "TDD", + "spec", + "specification", + "testing", + "tests" + ], + "time": "2016-12-04 21:03:31" + }, + { + "name": "phpspec/prophecy", + "version": "v1.6.2", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "6c52c2722f8460122f96f86346600e1077ce22cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/6c52c2722f8460122f96f86346600e1077ce22cb", + "reference": "6c52c2722f8460122f96f86346600e1077ce22cb", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", + "sebastian/comparator": "^1.1", + "sebastian/recursion-context": "^1.0|^2.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.0", + "phpunit/phpunit": "^4.8 || ^5.6.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2016-11-21 14:58:47" + }, + { + "name": "phpunit/php-code-coverage", + "version": "1.2.18", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b", + "reference": "fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": ">=1.3.0@stable", + "phpunit/php-text-template": ">=1.2.0@stable", + "phpunit/php-token-stream": ">=1.1.3,<1.3.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*@dev" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.0.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2014-09-02 10:13:14" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2016-10-03 07:40:28" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21 13:50:34" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2017-02-26 11:10:40" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "ad4e1e23ae01b483c16f600ff1bebec184588e32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/ad4e1e23ae01b483c16f600ff1bebec184588e32", + "reference": "ad4e1e23ae01b483c16f600ff1bebec184588e32", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2014-03-03 05:10:30" + }, + { + "name": "phpunit/phpunit", + "version": "3.7.38", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "38709dc22d519a3d1be46849868aa2ddf822bcf6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/38709dc22d519a3d1be46849868aa2ddf822bcf6", + "reference": "38709dc22d519a3d1be46849868aa2ddf822bcf6", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.3.3", + "phpunit/php-code-coverage": "~1.2", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.1", + "phpunit/php-timer": "~1.0", + "phpunit/phpunit-mock-objects": "~1.2", + "symfony/yaml": "~2.0" + }, + "require-dev": { + "pear-pear.php.net/pear": "1.9.4" + }, + "suggest": { + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "composer/bin/phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPUnit/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "", + "../../symfony/yaml/" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "http://www.phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2014-10-17 09:04:17" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "5794e3c5c5ba0fb037b11d8151add2a07fa82875" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/5794e3c5c5ba0fb037b11d8151add2a07fa82875", + "reference": "5794e3c5c5ba0fb037b11d8151add2a07fa82875", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-text-template": ">=1.1.1@stable" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "autoload": { + "classmap": [ + "PHPUnit/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2013-01-13 10:24:48" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14 16:28:37" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06 14:39:51" + }, + { + "name": "psr/log", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-10-10 12:19:37" + }, + { + "name": "sebastian/comparator", + "version": "1.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2 || ~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2017-01-29 09:50:25" + }, + { + "name": "sebastian/diff", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-12-08 07:14:41" + }, + { + "name": "sebastian/exporter", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2016-06-17 09:04:28" + }, + { + "name": "sebastian/recursion-context", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2015-11-11 19:50:13" + }, + { + "name": "sensiolabs/behat-page-object-extension", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sensiolabs/BehatPageObjectExtension.git", + "reference": "3348a58ecc907597d854d7ed4bbfe8b0eb3af709" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sensiolabs/BehatPageObjectExtension/zipball/3348a58ecc907597d854d7ed4bbfe8b0eb3af709", + "reference": "3348a58ecc907597d854d7ed4bbfe8b0eb3af709", + "shasum": "" + }, + "require": { + "behat/behat": "^3.0.6", + "behat/mink": "^1.6", + "behat/mink-extension": "^2.0", + "ocramius/proxy-manager": "^1.0||^2.0", + "php": ">=5.3.0" + }, + "require-dev": { + "behat/mink-goutte-driver": "^1.0", + "bossa/phpspec2-expect": "^1.0.3||^2.0", + "fabpot/goutte": "^1.0.4||^2.0||^3.0", + "phpspec/phpspec": "^2.5||^3.0", + "symfony/filesystem": "^2.8||^3.0", + "symfony/process": "^2.8||^3.0", + "symfony/yaml": "^2.8||^3.0" + }, + "suggest": { + "bossa/phpspec2-expect": "Allows to use PHPSpec2 matchers in Behat context files" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-0": { + "SensioLabs\\Behat\\PageObjectExtension\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marcello Duarte", + "email": "mduarte@inviqa.com" + }, + { + "name": "Jakub Zalas", + "email": "jakub@zalas.pl" + } + ], + "description": "Page object extension for Behat", + "homepage": "https://github.com/sensiolabs/BehatPageObjectExtension", + "keywords": [ + "BDD", + "Behat", + "page" + ], + "time": "2017-02-24 09:42:13" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "1.5.6", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "6f3e42d311b882b25b4d409d23a289f4d3b803d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/6f3e42d311b882b25b4d409d23a289f4d3b803d5", + "reference": "6f3e42d311b882b25b4d409d23a289f4d3b803d5", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.1.2" + }, + "suggest": { + "phpunit/php-timer": "dev-master" + }, + "bin": [ + "scripts/phpcs" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-phpcs-fixer": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "CodeSniffer.php", + "CodeSniffer/CLI.php", + "CodeSniffer/Exception.php", + "CodeSniffer/File.php", + "CodeSniffer/Report.php", + "CodeSniffer/Reporting.php", + "CodeSniffer/Sniff.php", + "CodeSniffer/Tokens.php", + "CodeSniffer/Reports/", + "CodeSniffer/CommentParser/", + "CodeSniffer/Tokenizers/", + "CodeSniffer/DocGenerators/", + "CodeSniffer/Standards/AbstractPatternSniff.php", + "CodeSniffer/Standards/AbstractScopeSniff.php", + "CodeSniffer/Standards/AbstractVariableSniff.php", + "CodeSniffer/Standards/IncorrectPatternException.php", + "CodeSniffer/Standards/Generic/Sniffs/", + "CodeSniffer/Standards/MySource/Sniffs/", + "CodeSniffer/Standards/PEAR/Sniffs/", + "CodeSniffer/Standards/PSR1/Sniffs/", + "CodeSniffer/Standards/PSR2/Sniffs/", + "CodeSniffer/Standards/Squiz/Sniffs/", + "CodeSniffer/Standards/Zend/Sniffs/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenises PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "http://www.squizlabs.com/php-codesniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2014-12-04 22:32:15" + }, + { + "name": "symfony/browser-kit", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "394a2475a3a89089353fde5714a7f402fbb83880" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/394a2475a3a89089353fde5714a7f402fbb83880", + "reference": "394a2475a3a89089353fde5714a7f402fbb83880", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/dom-crawler": "~2.8|~3.0" + }, + "require-dev": { + "symfony/css-selector": "~2.8|~3.0", + "symfony/process": "~2.8|~3.0" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony BrowserKit Component", + "homepage": "https://symfony.com", + "time": "2017-01-31 21:49:23" + }, + { + "name": "symfony/class-loader", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/class-loader.git", + "reference": "2847d56f518ad5721bf85aa9174b3aa3fd12aa03" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/2847d56f518ad5721bf85aa9174b3aa3fd12aa03", + "reference": "2847d56f518ad5721bf85aa9174b3aa3fd12aa03", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "symfony/finder": "~2.8|~3.0", + "symfony/polyfill-apcu": "~1.1" + }, + "suggest": { + "symfony/polyfill-apcu": "For using ApcClassLoader on HHVM" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\ClassLoader\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony ClassLoader Component", + "homepage": "https://symfony.com", + "time": "2017-01-21 17:06:35" + }, + { + "name": "symfony/config", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "9f99453e77771e629af8a25eeb0a6c4ed1e19da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/9f99453e77771e629af8a25eeb0a6c4ed1e19da2", + "reference": "9f99453e77771e629af8a25eeb0a6c4ed1e19da2", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/filesystem": "~2.8|~3.0" + }, + "require-dev": { + "symfony/yaml": "~3.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Config Component", + "homepage": "https://symfony.com", + "time": "2017-02-14 16:27:43" + }, + { + "name": "symfony/console", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0e5e6899f82230fcb1153bcaf0e106ffaa44b870" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0e5e6899f82230fcb1153bcaf0e106ffaa44b870", + "reference": "0e5e6899f82230fcb1153bcaf0e106ffaa44b870", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/debug": "~2.8|~3.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.8|~3.0", + "symfony/filesystem": "~2.8|~3.0", + "symfony/process": "~2.8|~3.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/filesystem": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2017-02-16 14:07:22" + }, + { + "name": "symfony/css-selector", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "f0e628f04fc055c934b3211cfabdb1c59eefbfaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/f0e628f04fc055c934b3211cfabdb1c59eefbfaa", + "reference": "f0e628f04fc055c934b3211cfabdb1c59eefbfaa", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "time": "2017-01-02 20:32:22" + }, + { + "name": "symfony/debug", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "9b98854cb45bc59d100b7d4cc4cf9e05f21026b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/9b98854cb45bc59d100b7d4cc4cf9e05f21026b9", + "reference": "9b98854cb45bc59d100b7d4cc4cf9e05f21026b9", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/class-loader": "~2.8|~3.0", + "symfony/http-kernel": "~2.8|~3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2017-02-16 16:34:18" + }, + { + "name": "symfony/dependency-injection", + "version": "v3.1.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "f4a04433f82eb8ca58555d1b6375293fc7c90d18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f4a04433f82eb8ca58555d1b6375293fc7c90d18", + "reference": "f4a04433f82eb8ca58555d1b6375293fc7c90d18", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "symfony/config": "~2.8|~3.0", + "symfony/expression-language": "~2.8|~3.0", + "symfony/yaml": "~2.8.7|~3.0.7|~3.1.1|~3.2" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DependencyInjection Component", + "homepage": "https://symfony.com", + "time": "2017-01-28 00:04:57" + }, + { + "name": "symfony/dom-crawler", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "b814b41373fc4e535aff8c765abe39545216f391" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/b814b41373fc4e535aff8c765abe39545216f391", + "reference": "b814b41373fc4e535aff8c765abe39545216f391", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "~2.8|~3.0" + }, + "suggest": { + "symfony/css-selector": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DomCrawler Component", + "homepage": "https://symfony.com", + "time": "2017-01-21 17:14:11" + }, + { + "name": "symfony/event-dispatcher", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "9137eb3a3328e413212826d63eeeb0217836e2b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9137eb3a3328e413212826d63eeeb0217836e2b6", + "reference": "9137eb3a3328e413212826d63eeeb0217836e2b6", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0", + "symfony/dependency-injection": "~2.8|~3.0", + "symfony/expression-language": "~2.8|~3.0", + "symfony/stopwatch": "~2.8|~3.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2017-01-02 20:32:22" + }, + { + "name": "symfony/filesystem", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "a0c6ef2dc78d33b58d91d3a49f49797a184d06f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/a0c6ef2dc78d33b58d91d3a49f49797a184d06f4", + "reference": "a0c6ef2dc78d33b58d91d3a49f49797a184d06f4", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2017-01-08 20:47:33" + }, + { + "name": "symfony/finder", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "8c71141cae8e2957946b403cc71a67213c0380d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/8c71141cae8e2957946b403cc71a67213c0380d6", + "reference": "8c71141cae8e2957946b403cc71a67213c0380d6", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2017-01-02 20:32:22" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2016-11-14 01:06:16" + }, + { + "name": "symfony/process", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "0ab87c1e7570b3534a6e51eb4ca8e9f6d7327856" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/0ab87c1e7570b3534a6e51eb4ca8e9f6d7327856", + "reference": "0ab87c1e7570b3534a6e51eb4ca8e9f6d7327856", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2017-02-16 14:07:22" + }, + { + "name": "symfony/translation", + "version": "v3.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "d6825c6bb2f1da13f564678f9f236fe8242c0029" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/d6825c6bb2f1da13f564678f9f236fe8242c0029", + "reference": "d6825c6bb2f1da13f564678f9f236fe8242c0029", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/config": "<2.8" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0", + "symfony/intl": "~2.8|~3.0", + "symfony/yaml": "~2.8|~3.0" + }, + "suggest": { + "psr/log": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Translation Component", + "homepage": "https://symfony.com", + "time": "2017-02-16 22:46:52" + }, + { + "name": "symfony/yaml", + "version": "v2.8.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "322a8c2dfbca15ad6b1b27e182899f98ec0e0153" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/322a8c2dfbca15ad6b1b27e182899f98ec0e0153", + "reference": "322a8c2dfbca15ad6b1b27e182899f98ec0e0153", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2017-01-21 16:40:50" + }, + { + "name": "webmozart/assert", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2016-11-23 20:04:58" + }, + { + "name": "zendframework/zend-code", + "version": "2.6.3", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-code.git", + "reference": "95033f061b083e16cdee60530ec260d7d628b887" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-code/zipball/95033f061b083e16cdee60530ec260d7d628b887", + "reference": "95033f061b083e16cdee60530ec260d7d628b887", + "shasum": "" + }, + "require": { + "php": "^5.5 || 7.0.0 - 7.0.4 || ^7.0.6", + "zendframework/zend-eventmanager": "^2.6 || ^3.0" + }, + "require-dev": { + "doctrine/annotations": "~1.0", + "fabpot/php-cs-fixer": "1.7.*", + "phpunit/phpunit": "^4.8.21", + "zendframework/zend-stdlib": "^2.7 || ^3.0" + }, + "suggest": { + "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", + "zendframework/zend-stdlib": "Zend\\Stdlib component" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev", + "dev-develop": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Code\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "provides facilities to generate arbitrary code using an object oriented interface", + "homepage": "https://github.com/zendframework/zend-code", + "keywords": [ + "code", + "zf2" + ], + "time": "2016-04-20 17:26:42" + }, + { + "name": "zendframework/zend-eventmanager", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-eventmanager.git", + "reference": "c3bce7b7d47c54040b9ae51bc55491c72513b75d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/c3bce7b7d47c54040b9ae51bc55491c72513b75d", + "reference": "c3bce7b7d47c54040b9ae51bc55491c72513b75d", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "athletic/athletic": "^0.1", + "container-interop/container-interop": "^1.1.0", + "phpunit/phpunit": "^5.6", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-stdlib": "^2.7.3 || ^3.0" + }, + "suggest": { + "container-interop/container-interop": "^1.1.0, to use the lazy listeners feature", + "zendframework/zend-stdlib": "^2.7.3 || ^3.0, to use the FilterChain feature" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev", + "dev-develop": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\EventManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Trigger and listen to events within a PHP application", + "homepage": "https://github.com/zendframework/zend-eventmanager", + "keywords": [ + "event", + "eventmanager", + "events", + "zf2" + ], + "time": "2016-12-19 21:47:12" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "sensiolabs/behat-page-object-extension": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.4.0" + }, + "platform-dev": [] +} diff --git a/core/features/admin/delete_image.feature b/core/features/admin/delete_image.feature new file mode 100644 index 0000000..6146042 --- /dev/null +++ b/core/features/admin/delete_image.feature @@ -0,0 +1,9 @@ +Feature: Deleting images from the image provider + In order to keep the image gallery content relevant + As a store admin + I want to be able to delete images from the image provider + + Scenario: Administrator deletes image from image provider + Given the image provider has an image "blue-shirt.jpg" + When I delete the "blue-shirt.jpg" image + Then the image "blue-shirt.jpg" should no longer be available in the image provider \ No newline at end of file diff --git a/core/features/admin/extension_disable_enable.feature b/core/features/admin/extension_disable_enable.feature new file mode 100644 index 0000000..5e3f27d --- /dev/null +++ b/core/features/admin/extension_disable_enable.feature @@ -0,0 +1,22 @@ +@not-automated +Feature: Enabling and disabling the Cloudinary extension + + As an Integrator + + + Scenario: Integrator enables the extension but image has not been migrated + Given the cloudinary media gallery contains the image "lolcat.png" + And this image has not yet been migrated to cloudinary + When the integrator enables the module + Then the image should be provided locally + + Scenario: Integrator enables the extension and image has been migrated + Given the cloudinary media gallery contains the image "lolcat.png" + And this image has already been migrated to cloudinary + When the integrator enables the module + Then the image should be provided by cloudinary + + Scenario: Integrator disables the extension + Given the cloudinary media gallery contains the image "lolcat.png" + When the integrator disables the module + Then the image should be provided locally \ No newline at end of file diff --git a/core/features/admin/prompted_to_sign_up_to_cloudinary.feature b/core/features/admin/prompted_to_sign_up_to_cloudinary.feature new file mode 100644 index 0000000..6661f3e --- /dev/null +++ b/core/features/admin/prompted_to_sign_up_to_cloudinary.feature @@ -0,0 +1,16 @@ +Feature: Admin is prompted to sign up to Cloudinary + In order to register for a Cloudinary account after installing the extension + As a store admin + I should be prompted to sign up to Cloudinary + + @javascript @ui + Scenario: Being prompted to sign up to Cloudinary + Given I have not configured my environment variable + When I go to the Cloudinary configuration + Then I should be prompted to sign up to Cloudinary + + @javascript @ui + Scenario: Not being prompted to sign up to Cloudinary + Given I have configured my environment variable + When I go to the Cloudinary configuration + Then I should not be prompted to sign up to Cloudinary \ No newline at end of file diff --git a/core/features/bootstrap/Domain/ConfigurationContext.php b/core/features/bootstrap/Domain/ConfigurationContext.php new file mode 100644 index 0000000..5a5b24d --- /dev/null +++ b/core/features/bootstrap/Domain/ConfigurationContext.php @@ -0,0 +1,83 @@ +configuration = Doubles::getConfiguration(); + } + + /** + * @Given I have a configuration to use multiple sub-domains + */ + public function iHaveAConfigurationToUseMultipleSubDomains() + { + Doubles::getConfigurationProphecy()->getCdnSubdomainStatus()->willReturn(true); + } + + /** + * @When I apply the configuration to the image provider + */ + public function iApplyTheConfigurationToTheImageProvider() + { + $this->imageProvider = new ConfigImageProvider($this->configuration); + } + + /** + * @Then the image provider should use multiple sub-domains + */ + public function theImageProviderShouldUseMultipleSubDomains() + { + $request1 = $this->imageProvider->retrieveTransformed(Image::fromPath('somePath'), Transformation::builder()); + $request2 = $this->imageProvider->retrieveTransformed(Image::fromPath('someOtherPath'), Transformation::builder()); + + expect($this->requestPrefixIsTheSame($request1, $request2))->toBe(false); + } + + /** + * @Given the cloudinary module is disabled + */ + public function theCloudinaryModuleIsDisabled() + { + Doubles::getConfigurationProphecy()->isEnabled()->willReturn(false); + } + + /** + * @Given the cloudinary module is enabled + */ + public function theCloudinaryModuleIsEnabled() + { + Doubles::getConfigurationProphecy()->isEnabled()->willReturn(true); + } + + private function requestPrefixIsTheSame($request1, $request2) + { + return substr($request1, 0, 4) === substr($request2, 0, 4); + } +} \ No newline at end of file diff --git a/core/features/bootstrap/Domain/DeleteImageDomainContext.php b/core/features/bootstrap/Domain/DeleteImageDomainContext.php new file mode 100644 index 0000000..f79cda3 --- /dev/null +++ b/core/features/bootstrap/Domain/DeleteImageDomainContext.php @@ -0,0 +1,57 @@ +imageProvider = new FakeImageProvider($environmentVariable); + + $this->imageProvider->upload($anImage); + } + + /** + * @When I delete the :anImage image + */ + public function iDeleteTheImage($anImage) + { + $this->imageProvider->delete($anImage); + } + + /** + * @Then the image :anImage should no longer be available in the image provider + */ + public function theImageShouldNoLongerBeAvailableInTheImageProvider($anImage) + { + expect($this->imageProvider->retrieveTransformed($anImage, Transformation::builder()))->toBe(''); + } + +} diff --git a/core/features/bootstrap/Domain/DomainContext.php b/core/features/bootstrap/Domain/DomainContext.php new file mode 100644 index 0000000..5992a8f --- /dev/null +++ b/core/features/bootstrap/Domain/DomainContext.php @@ -0,0 +1,215 @@ +provider = new FakeImageProvider($environmentVariable); + + $cloud = Cloud::fromName('session-digital'); + $key = Key::fromString('ABC123'); + $secret = Secret::fromString('DEF456'); + $this->provider->setMockCloud($cloud); + $this->provider->setMockCredentials($key, $secret); + } + + /** + * @Transform :anImage + */ + public function transformStringToAnImage($string) + { + return Image::fromPath($string); + } + + /** + * @Given I have an image :anImage + */ + public function iHaveAnImage(Image $anImage) + { + $this->image = $anImage; + } + + /** + * @When I upload the image :anImage + */ + public function iUploadTheImage(Image $anImage) + { + try { + $this->provider->upload($anImage); + } catch (\Exception $e) { + $this->impageAlreadyUplaoded = true; + } + } + + /** + * @Given the image :anImage does not exist on the provider + */ + public function theImageDoesNotExistOnTheProvider(Image $anImage) + { + expect($this->provider->getImageUrlByName((string)$anImage))->toBe(''); + } + + /** + * @Given the image :anImage has already been uploaded + */ + public function theImageHasAlreadyBeenUploaded(Image $anImage) + { + try { + $this->provider->upload($anImage); + } catch (\Exception $e) { + $this->impageAlreadyUplaoded = true; + } + } + + /** + * @Then the image :anImage will be provided remotely + */ + public function theImageWillBeProvidedRemotely(Image $anImage) + { + $this->setupStubs($anImage); + + $imageFactory = new ImageFactory(Doubles::getConfiguration(), Doubles::getSynchronizationChecker()); + + $image = $imageFactory->build((string)$anImage, [$this, 'getLocalUrl']); + + $urlGenerator = new UrlGenerator(Doubles::getConfiguration(), $this->provider); + + expect($urlGenerator->generateFor($image))->toBe('uploaded image URL'); + } + + /** + * @Then I should see an error image already exists + */ + public function iShouldSeeAnErrorImageAlreadyExists() + { + expect($this->impageAlreadyUplaoded)->toBe(true); + } + + /** + * @Then the image should be available through the image provider + */ + public function theImageShouldBeAvailableThroughTheImageProvider() + { + expect($this->provider->getImageUrlByName($this->getImageName()))->notToBe(''); + } + + private function getImageName() + { + $imagePath = explode('/', $this->image); + return $imagePath[count($imagePath) - 1]; + } + + /** + * @Given I have used a valid environment variable in the configuration + */ + public function iHaveUsedAValidEnvironmentVariableInTheConfiguration() + { + $environmentVariable = CloudinaryEnvironmentVariable::fromString('CLOUDINARY_URL=cloudinary://ABC123:DEF456@session-digital'); + $this->provider = new FakeImageProvider($environmentVariable); + } + + /** + * @Given I have used an invalid environment variable in the configuration + */ + public function iHaveUsedAnInvalidEnvironmentVariableInTheConfiguration() + { + $environmentVariable = CloudinaryEnvironmentVariable::fromString('CLOUDINARY_URL=cloudinary://UVW789:XYZ123@session-digital'); + $this->provider = new FakeImageProvider($environmentVariable); + } + + /** + * @When I ask the provider to validate my credentials + */ + public function iAskTheProviderToValidateMyCredentials() + { + $cloud = Cloud::fromName('session-digital'); + $key = Key::fromString('ABC123'); + $secret = Secret::fromString('DEF456'); + $this->provider->setMockCloud($cloud); + $this->provider->setMockCredentials($key, $secret); + + $this->areCredentialsValid = $this->provider->validateCredentials(); + } + + /** + * @Then I should be informed my credentials are valid + */ + public function iShouldBeInformedMyCredentialsAreValid() + { + expect($this->areCredentialsValid)->toBe(true); + } + + /** + * @Then I should be informed that my credentials are not valid + */ + public function iShouldBeInformedThatMyCredentialsAreNotValid() + { + expect($this->areCredentialsValid)->toBe(false); + } + + /** + * @Given I am logged in as an administrator + */ + public function iAmLoggedInAsAnAdministrator() + { + // not required for domain suite + } + + /** + * @Then the image :anImage will be provided locally + */ + public function theImageWillBeProvidedLocally(Image $anImage) + { + $this->setupStubs($anImage); + + $imageFactory = new ImageFactory(Doubles::getConfiguration(), Doubles::getSynchronizationChecker()); + + $image = $imageFactory->build((string)$anImage, [$this, 'getLocalUrl']); + + $urlGenerator = new UrlGenerator(Doubles::getConfiguration(), $this->provider); + + expect($urlGenerator->generateFor($image))->toBe('local image path'); + } + + public function getLocalUrl() + { + return 'local image path'; + } + + /** + * @param Image $anImage + */ + private function setupStubs(Image $anImage) + { + Doubles::getConfigurationProphecy()->getMigratedPath((string)$anImage)->willReturn((string)$anImage); + Doubles::getSynchronizationCheckerProphecy()->isSynchronized((string)$anImage)->willReturn(true); + } +} diff --git a/core/features/bootstrap/Domain/Doubles.php b/core/features/bootstrap/Domain/Doubles.php new file mode 100644 index 0000000..ae312c3 --- /dev/null +++ b/core/features/bootstrap/Domain/Doubles.php @@ -0,0 +1,63 @@ +prophesize(ConfigurationInterface::class); + self::$synchronizationCheckerProphecy = (new Prophet())->prophesize(SynchronizationChecker::class); + self::$configurationProphecy->getFormatsToPreserve()->willReturn([]); + self::$setupDone = true; + } + } + + public static function getConfiguration() + { + self::setup(); + return self::$configurationProphecy->reveal(); + } + + public static function getSynchronizationChecker() + { + self::setup(); + return self::$synchronizationCheckerProphecy->reveal(); + } + + public static function getConfigurationProphecy() + { + self::setup(); + return self::$configurationProphecy; + } + + public static function getSynchronizationCheckerProphecy() + { + self::setup(); + return self::$synchronizationCheckerProphecy; + } + + +} \ No newline at end of file diff --git a/core/features/bootstrap/Domain/TransformationContext.php b/core/features/bootstrap/Domain/TransformationContext.php new file mode 100644 index 0000000..ee33742 --- /dev/null +++ b/core/features/bootstrap/Domain/TransformationContext.php @@ -0,0 +1,225 @@ +configuration = Doubles::getConfiguration(); + + $defaultTransformation = (new Transformation()) + ->withQuality(Quality::fromString('80')) + ->withDpr(Dpr::fromString('1.0')); + + Doubles::getConfigurationProphecy()->getDefaultTransformation()->willReturn($defaultTransformation); + + $this->imageProvider = new TransformingImageProvider($this->configuration); + } + + /** + * @Transform :aDpr + */ + public function transformStringToDpr($string) + { + return Dpr::fromString($string); + } + + /** + * @Transform :aQuality + */ + public function transformStringToQuality($string) + { + return Quality::fromString($string); + } + + /** + * @Transform :aDimension + */ + public function transformStringToDimensions($string) + { + $dimensions = explode('x', $string); + + return Dimensions::fromWidthAndHeight($dimensions[0], $dimensions[1]); + } + + /** + * @Given there's an image :anImage in the image provider + */ + public function thereIsAnImageInTheImageProvider(Image $anImage) + { + $this->image = $anImage; + $this->imageProvider->upload($this->image); + } + + /** + * @When I request the image from the image provider + */ + public function iRequestTheImageFromTheImageProvider() + { + $this->imageUrl = $this->imageProvider->retrieve($this->image); + } + + /** + * @Then I should get an optimised image from the image provider + */ + public function iShouldGetAnOptimisedImageFromTheImageProvider() + { + expect($this->urlIsOptimised())->toBe(true); + } + + /** + * @Given image optimisation is disabled + */ + public function imageOptimisationIsDisabled() + { + $this->configuration->getDefaultTransformation()->withOptimisationDisabled(); + } + + /** + * @Then I should get the original image from the image provider + */ + public function iShouldGetTheOriginalImageFromTheImageProvider() + { + expect($this->isOriginalImage())->toBe(true); + } + + /** + * @Then I should get an image with :aQuality percent quality from the image provider + */ + public function iShouldGetAnImageWithPercentQualityFromTheImageProvider(Quality $aQuality) + { + expect($this->isPercentageQuality((string)$aQuality))->toBe(true); + } + + /** + * @Given I set image quality to :aQuality percent + */ + public function iTransformTheImageToHavePercentQuality(Quality $aQuality) + { + $transformation = $this->configuration->getDefaultTransformation(); + + Doubles::getConfigurationProphecy() + ->getDefaultTransformation() + ->willReturn($transformation->withQuality($aQuality)); + } + + /** + * @When I ask the image provider for :imageName transformed to :aDimension + */ + public function iRequestTheImageProvideForTransformedTo($imageName, Dimensions $aDimension) + { + $this->imageUrl = $this->imageProvider->retrieveTransformed( + Image::fromPath($imageName), + Transformation::builder()->withDimensions($aDimension) + ); + } + + /** + * @Then I should receive that image with the dimensions :aDimension + */ + public function iShouldReceiveThatImageWithTheDimensions(Dimensions $aDimension) + { + expect($this->hasDimensions($aDimension))->toBe(true); + } + + /** + * @Then I should get the image :image with the default DPR + */ + public function iShouldGetAnImageWithTheDefaultDpr($image) + { + expect(basename($this->imageUrl))->toBe($image); + expect($this->hasDefaultDpr())->toBe(true); + } + + /** + * @Given my DPR is set to :aDpr in the configuration + */ + public function myDprIsSetToInTheConfiguration(Dpr $aDpr) + { + $transformation = $this->configuration->getDefaultTransformation(); + + Doubles::getConfigurationProphecy() + ->getDefaultTransformation() + ->willReturn($transformation->withDpr($aDpr)); + + } + + /** + * @Then I should get an image with DPR :aDpr + */ + public function iShouldGetAnImageWithDpr(Dpr $aDpr) + { + expect($this->hasDpr($aDpr))->toBe(true); + } + + private function urlIsOptimised() + { + return strpos($this->imageUrl, 'fetch_format=auto') !== false; + } + + private function isPercentageQuality($percentage) + { + return strpos($this->imageUrl, "quality=$percentage") !== false; + } + + private function hasDimensions(Dimensions $dimension) + { + $hasWidth = strpos($this->imageUrl, "width={$dimension->getWidth()}") !== false; + $hasHeight = strpos($this->imageUrl, "height={$dimension->getHeight()}") !== false; + return $hasWidth && $hasHeight; + } + + private function hasDefaultDpr() + { + return $this->hasDpr('1.0'); + } + + private function hasDpr($dpr) + { + return strpos($this->imageUrl, "dpr=$dpr") !== false; + } + + /** + * @return bool + */ + protected function isOriginalImage() + { + return strpos($this->imageUrl, $this->image->__toString()) !== false && + strpos($this->imageUrl, '&quality=80&') !== false && + strpos($this->imageUrl, '&dpr=1.0/') !== false; + + } +} \ No newline at end of file diff --git a/core/features/bootstrap/Fixtures/Admin.yaml b/core/features/bootstrap/Fixtures/Admin.yaml new file mode 100644 index 0000000..4ddfaf2 --- /dev/null +++ b/core/features/bootstrap/Fixtures/Admin.yaml @@ -0,0 +1,6 @@ +admin/user: + username: testadmin + firstname: admin + lastname: admin + email: testadmin@example.com + password: testadmin123 diff --git a/core/features/bootstrap/ImageProviders/ConfigImageProvider.php b/core/features/bootstrap/ImageProviders/ConfigImageProvider.php new file mode 100644 index 0000000..83e4a02 --- /dev/null +++ b/core/features/bootstrap/ImageProviders/ConfigImageProvider.php @@ -0,0 +1,51 @@ +configuration = $configuration; + } + + public function upload(Image $image) + { + } + + public function retrieveTransformed(Image $image, Transformation $transformation) + { + $prefix = $this->subdomains[$this->prefixCount % 2]; + + if($this->configuration->getCdnSubdomainStatus() === true) + { + $this->prefixCount += 1; + } + + return $prefix . "/" . $image; + } + + public function retrieve(Image $image) + { + + } + + public function delete(Image $image) + { + } + + public function validateCredentials() + { + } +} \ No newline at end of file diff --git a/core/features/bootstrap/ImageProviders/ConfigurationProvider.php b/core/features/bootstrap/ImageProviders/ConfigurationProvider.php new file mode 100644 index 0000000..237824e --- /dev/null +++ b/core/features/bootstrap/ImageProviders/ConfigurationProvider.php @@ -0,0 +1,119 @@ +defaultTransformation = Transformation::builder(); + $this->isEnabledCdnSubdomain = false; + } + /** + * @return Cloud + */ + public function getCloud() + { + return Cloud::fromName('aCloudName'); + } + + /** + * @return Credentials + */ + public function getCredentials() + { + return Credentials::fromKeyAndSecret( + Key::fromString('aKey'), + Secret::fromString('aSecret') + ); + } + + /** + * @return Transformation + */ + public function getDefaultTransformation() + { + return $this->defaultTransformation; + } + + /** + * @param Transformation $transformation + */ + public function setDefaultTransformation(Transformation $transformation) + { + $this->defaultTransformation = $transformation; + } + + /** + * @return boolean + */ + public function getCdnSubdomainStatus() + { + return $this->isEnabledCdnSubdomain; + } + + /** + * @return string + */ + public function getUserPlatform() + { + + } + + /** + * @return UploadConfig + */ + public function getUploadConfig() + { + + } + + /** + * @return boolean + */ + public function isEnabled() + { + + } + + public function enable() + { + + } + + public function disable() + { + + } + + /** + * @return array + */ + public function getFormatsToPreserve() + { + + } + + public function enableCdnSubdomain() + { + $this->isEnabledCdnSubdomain = true; + } +} \ No newline at end of file diff --git a/core/features/bootstrap/ImageProviders/FakeImageProvider.php b/core/features/bootstrap/ImageProviders/FakeImageProvider.php new file mode 100644 index 0000000..b7355cf --- /dev/null +++ b/core/features/bootstrap/ImageProviders/FakeImageProvider.php @@ -0,0 +1,88 @@ +credentials = $environmentVariable->getCredentials(); + $this->cloud = $environmentVariable->getCloud(); + } + + public function setMockCredentials(Key $aKey, Secret $aSecret) + { + $this->key = $aKey; + $this->secret = $aSecret; + } + + public function setMockCloud(Cloud $mockCloud) + { + $this->mockCloud = $mockCloud; + } + + public function upload(Image $image) + { + if (array_key_exists((string)$image, $this->uploadedImageUrl)) { + throw new \Exception('Image already exist at the provider'); + } + $this->uploadedImageUrl[(string)$image] = 'uploaded image URL'; + } + + public function getImageUrlByName($image, $options = array()) + { + $imageName = (string)$image; + if($this->areCredentialsCorrect() && $this->isCloudCorrect()) { + return array_key_exists($imageName, $this->uploadedImageUrl) ? $this->uploadedImageUrl[$imageName] : ''; + } + return ''; + } + + public function validateCredentials() + { + return $this->areCredentialsCorrect(); + } + + private function areCredentialsCorrect() + { + return (string)$this->credentials->getKey() === (string)$this->key && (string)$this->credentials->getSecret() === (string)$this->secret; + } + + private function isCloudCorrect() + { + return (string)$this->mockCloud == (string)$this->cloud; + } + + public function retrieveTransformed(Image $image, \CloudinaryExtension\Image\Transformation $transformation) + { + $imageName = (string)$image; + if($this->areCredentialsCorrect() && $this->isCloudCorrect()) { + return array_key_exists($imageName, $this->uploadedImageUrl) ? $this->uploadedImageUrl[$imageName] : ''; + } + return ''; + } + + public function retrieve(Image $image) + { + + } + + public function delete(Image $image) + { + unset($this->uploadedImageUrl[(string)$image]); + } +} \ No newline at end of file diff --git a/core/features/bootstrap/ImageProviders/TransformingImageProvider.php b/core/features/bootstrap/ImageProviders/TransformingImageProvider.php new file mode 100644 index 0000000..a0121ee --- /dev/null +++ b/core/features/bootstrap/ImageProviders/TransformingImageProvider.php @@ -0,0 +1,46 @@ +configuration = $configuration; + } + + public function upload(Image $image) + { + $this->images[(string)$image] = $image; + } + + public function retrieveTransformed(Image $image, Transformation $transformation) + { + return http_build_query($transformation->build()) .'/'. $this->images[(string)$image]; + } + + public function retrieve(Image $image) + { + return $this->retrieveTransformed($image, $this->configuration->getDefaultTransformation()); + } + + public function delete(Image $image) + { + } + + public function validateCredentials() + { + } + +} \ No newline at end of file diff --git a/core/features/bootstrap/Page/AdminLogin.php b/core/features/bootstrap/Page/AdminLogin.php new file mode 100644 index 0000000..1e55e3f --- /dev/null +++ b/core/features/bootstrap/Page/AdminLogin.php @@ -0,0 +1,37 @@ + array('xpath' => '//*[@id="username"]'), + 'Password' => array('xpath' => '//*[@id="login"]'), + 'Login Button' => array('xpath' => '//*[@title="Login"]'), + ); + + public function login($username, $password) + { + $this->getElement('User Name')->setValue($username); + $this->getElement('Password')->setValue($password); + $this->getElement('Login Button')->click(); + } + + public function sessionLogin($username, $password, $mageSession) + { + if (!$this->isOpen()) { + $this->open(); + } + + $this->getSession() + ->setCookie( + 'adminhtml', + $mageSession->adminLogin($username, $password) + ) + ; + } +} diff --git a/core/features/bootstrap/Page/CloudinaryAdminSystemConfiguration.php b/core/features/bootstrap/Page/CloudinaryAdminSystemConfiguration.php new file mode 100644 index 0000000..abd96a3 --- /dev/null +++ b/core/features/bootstrap/Page/CloudinaryAdminSystemConfiguration.php @@ -0,0 +1,41 @@ + array('xpath' => '//*[@id="cloudinary_setup-head"]'), + 'Environment Variable' => array('xpath' => '//*[@id="cloudinary_setup_cloudinary_environment_variable"]'), + 'Save Config' => array('xpath' => '//*[@title="Save Config"]'), + 'Image Transformations Header' => array('xpath' => '//*[@id="cloudinary_transformations-head"]'), + 'Default Gravity for Images' => array('css' => "#cloudinary_transformations_cloudinary_gravity option[selected='selected']"), + 'Sign Up Prompt' => array('xpath' => '//*[@id="config_edit_form"]//h3[@id="cloudinary-signup-prompt"]'), + ); + + public function enterEnvironmentVariable($anEnvironmentVariable) + { + $this->getElement('Setup Header')->click(); + $this->getElement('Environment Variable')->setValue($anEnvironmentVariable); + } + + public function saveCloudinaryConfiguration() + { + $this->getElement('Save Config')->click(); + } + + public function getSelectedGravity() + { + return $this->getElement('Default Gravity for Images')->getHtml(); + } + + public function containsSignUpPrompt() + { + return $this->hasElement('Sign Up Prompt'); + } + +} diff --git a/core/features/bootstrap/Page/CloudinaryManagement.php b/core/features/bootstrap/Page/CloudinaryManagement.php new file mode 100644 index 0000000..6510c19 --- /dev/null +++ b/core/features/bootstrap/Page/CloudinaryManagement.php @@ -0,0 +1,35 @@ + array('css' => 'button[title="Enable Cloudinary"]'), + 'Disable Button' => array('css' => 'button[title="Disable Cloudinary"]'), + ); + + public function enable() + { + $this->getElement('Enable Button')->click(); + } + + public function disable() + { + $this->getElement('Disable Button')->click(); + } + + public function hasDisableButton() + { + return $this->hasElement('Disable Button'); + } + + public function hasEnableButton() + { + return $this->hasElement('Enable Button'); + } +} diff --git a/core/features/bootstrap/Ui/AdminCredentialsContext.php b/core/features/bootstrap/Ui/AdminCredentialsContext.php new file mode 100644 index 0000000..772b49d --- /dev/null +++ b/core/features/bootstrap/Ui/AdminCredentialsContext.php @@ -0,0 +1,196 @@ +adminConfigPage = $adminSystemConfiguration; + $this->adminLoginPage = $adminLoginPage; + } + + /** + * @BeforeScenario + */ + public function beforeScenario() + { + $this->_fixtureManager = new FixtureManager(new YamlProvider()); + $this->_fixtureManager->loadFixture('admin/user', __DIR__ . DS . '../Fixtures/Admin.yaml'); + } + + /** + * @AfterScenario + */ + public function afterScenario() + { + $this->_fixtureManager->clear(); + } + + /** + * @Transform :anImage + */ + public function transformStringToAnImage($string) + { + return Image::fromPath($string); + } + + /** + * @Given I have an image :anImage + */ + public function iHaveAnImage($anImage) + { + $this->image = $anImage; + } + + /** + * @When I upload the image :anImage + */ + public function iUploadTheImage(Image $anImage) + { + $environmentVariable = CloudinaryEnvironmentVariable::fromString('CLOUDINARY_URL=cloudinary://ABC123:DEF456@session-digital'); + $this->saveEnvironmentVariableToMagentoConfiguration($environmentVariable); + + $this->imageProvider = new FakeImageProvider($environmentVariable); + + $this->imageProvider->setMockCloud(Cloud::fromName('session-digital')); + $this->imageProvider->setMockCredentials(Key::fromString('ABC123'), Secret::fromString('DEF456')); + + $this->imageProvider->upload($anImage); + } + + /** + * @Then the image should be available through the image provider + */ + public function theImageShouldBeAvailableThroughTheImageProvider() + { + expect($this->imageProvider->getImageUrlByName((string)$this->image))->notToBe(''); + } + + /** + * @Given I have used a valid environment variable in the configuration + */ + public function iHaveUsedAValidEnvironmentVariableInTheConfiguration() + { + $environmentVariable = CloudinaryEnvironmentVariable::fromString('CLOUDINARY_URL=cloudinary://ABC123:DEF456@session-digital'); + $this->imageProvider = new FakeImageProvider($environmentVariable); + } + + /** + * @Given I have used an invalid environment variable in the configuration + */ + public function iHaveUsedAnInvalidEnvironmentVariableInTheConfiguration() + { + $environmentVariable = CloudinaryEnvironmentVariable::fromString('CLOUDINARY_URL=cloudinary://UVW789:XYZ123@session-digital'); + $this->imageProvider = new FakeImageProvider($environmentVariable); + } + + /** + * @Given I have not configured my environment variable + */ + public function iHaveNotConfiguredMyEnvironmentVariable() + { + $this->saveEnvironmentVariableToMagentoConfiguration(''); + } + + /** + * @Given I have configured my environment variable + */ + public function iHaveConfiguredMyEnvironmentVariable() + { + $this->saveEnvironmentVariableToMagentoConfiguration('anEnvironmentVariable'); + } + + /** + * @When I ask the provider to validate my credentials + */ + public function iAskTheProviderToValidateMyCredentials() + { + $this->imageProvider->setMockCloud(Cloud::fromName('session-digital')); + $this->imageProvider->setMockCredentials(Key::fromString('ABC123'), Secret::fromString('DEF456')); + + $this->areCredentialsValid = $this->imageProvider->validateCredentials(); + } + + /** + * @Then I should be informed my credentials are valid + */ + public function iShouldBeInformedMyCredentialsAreValid() + { + expect($this->areCredentialsValid)->toBe(true); + } + + /** + * @Then I should be informed that my credentials are not valid + */ + public function iShouldBeInformedThatMyCredentialsAreNotValid() + { + expect($this->areCredentialsValid)->toBe(false); + } + + /** + * @Given I have not configured my cloud and credentials + */ + public function iHaveNotConfiguredMyCloudAndCredentials() + { + $this->saveCredentialsAndCloudToMagentoConfiguration('', '', ''); + } + + /** + * @When I go to the Cloudinary configuration + */ + public function iGoToTheCloudinaryConfiguration() + { + $this->adminConfigPage->open(); + } + + /** + * @Then I should be prompted to sign up to Cloudinary + */ + public function iShouldBePromptedToSignUpToCloudinary() + { + expect($this->adminConfigPage->containsSignUpPrompt())->toBe(true); + } + + /** + * @Then I should not be prompted to sign up to Cloudinary + */ + public function iShouldNotBePromptedToSignUpToCloudinary() + { + expect($this->adminConfigPage->containsSignUpPrompt())->toBe(false); + } + + private function saveEnvironmentVariableToMagentoConfiguration($environmentVariable) + { + $this->adminLoginPage->sessionLogin('testadmin', 'testadmin123', $this->getSessionService()); + + $this->adminConfigPage->open(); + + $this->adminConfigPage->enterEnvironmentVariable($environmentVariable); + $this->adminConfigPage->saveCloudinaryConfiguration(); + + } + +} diff --git a/core/features/bootstrap/Ui/ModuleEnableContext.php b/core/features/bootstrap/Ui/ModuleEnableContext.php new file mode 100644 index 0000000..9c7705f --- /dev/null +++ b/core/features/bootstrap/Ui/ModuleEnableContext.php @@ -0,0 +1,76 @@ +adminLogin = $adminLogin; + $this->cloudinaryManagement = $cloudinaryManagement; + } + + /** + * @Given I am logged in as an administrator + */ + public function iAmLoggedInAsAnAdministrator() + { + $this->adminLogin->sessionLogin('testadmin', 'testadmin123', $this->getSessionService()); + } + + /** + * @Given the Cloudinary module is disabled + */ + public function theCloudinaryModuleIsDisabled() + { + \Mage::helper('cloudinary_cloudinary/configuration')->disable(); + } + + /** + * @When I access the Cloudinary configuration + */ + public function iAccessTheCloudinaryConfiguration() + { + $this->cloudinaryManagement->open(); + } + + /** + * @Then I should be able to enable the module + */ + public function iShouldBeAbleToEnableTheModule() + { + $this->cloudinaryManagement->enable(); + + expect($this->cloudinaryManagement)->toHaveDisableButton(); + } + + /** + * @Given the Cloudinary module is enabled + */ + public function theCloudinaryModuleIsEnabled() + { + \Mage::helper('cloudinary_cloudinary/configuration')->enable(); + } + + /** + * @Then I should be able to disable the module + */ + public function iShouldBeAbleToDisableTheModule() + { + $this->cloudinaryManagement->disable(); + + expect($this->cloudinaryManagement)->toHaveEnableButton(); + } +} diff --git a/core/features/configuration.feature b/core/features/configuration.feature new file mode 100644 index 0000000..6dd37f3 --- /dev/null +++ b/core/features/configuration.feature @@ -0,0 +1,9 @@ +Feature: Configuring the image provider + In order to fit the image provider to my needs + As an image provider user + I want to be able to provide configuration to it + + Scenario: Configuring the image provider to use multiple sub-domains + Given I have a configuration to use multiple sub-domains + When I apply the configuration to the image provider + Then the image provider should use multiple sub-domains diff --git a/core/features/image_provider_transform.feature b/core/features/image_provider_transform.feature new file mode 100644 index 0000000..8ec2938 --- /dev/null +++ b/core/features/image_provider_transform.feature @@ -0,0 +1,55 @@ +Feature: Getting transformed images from the provider + In order to reduce load times + As a Store Admin + I want provide transformed versions of the images + + Scenario: Getting a cropped image from the provider + Given there's an image "pink_dress.gif" in the image provider + When I ask the image provider for "pink_dress.gif" transformed to "100x150" + Then I should receive that image with the dimensions "100x150" + + @not-automated + Scenario: Getting a image without gravity transformation from the provider + Given my image provider has an image "pink_dress.gif" + When I ask the image provider for "pink_dress.gif" + Then I should receive that image with no gravity set + + @not-automated + Scenario: Getting a gravity transformed image from the provider + Given my image provider has an image "pink_dress.gif" + And I have set the default image gravity to "Center" + When I ask the image provider for "pink_dress.gif" + Then I should receive that image with gravity "Center" + + Scenario: Getting an optimised image from the image provider + Given there's an image "white_and_gold_dress.jpg" in the image provider + When I request the image from the image provider + Then I should get an optimised image from the image provider + + Scenario: Getting the original image from the image provider + Given there's an image "blue_and_black_dress.jpg" in the image provider + And image optimisation is disabled + When I request the image from the image provider + Then I should get the original image from the image provider + + Scenario: Getting an image at the default quality of 80 percent + Given there's an image "red-shirt.jpg" in the image provider + When I request the image from the image provider + Then I should get an image with 80 percent quality from the image provider + + Scenario: Changing image quality to 60 percent + Given there's an image "red-shirt.jpg" in the image provider + And I set image quality to 60 percent + When I request the image from the image provider + Then I should get an image with 60 percent quality from the image provider + + Scenario: Getting an image with the default DPR setting + Given there's an image "red-shirt.jpg" in the image provider + When I request the image from the image provider + Then I should get the image "red-shirt.jpg" with the default DPR + + Scenario: Changing the DRP setting + Given there's an image "red-shirt.jpg" in the image provider + And my DPR is set to 2.0 in the configuration + When I request the image from the image provider + Then I should get an image with DPR 2.0 \ No newline at end of file diff --git a/core/features/image_provider_upload.feature b/core/features/image_provider_upload.feature new file mode 100644 index 0000000..11dd860 --- /dev/null +++ b/core/features/image_provider_upload.feature @@ -0,0 +1,27 @@ +@javascript @critical +Feature: Uploading images to an image provider + In order to optimise images for specific web clients + As a Store Admin + I want to be able to upload images to the image provider + + Background: + Given I am logged in as an administrator + And the cloudinary module is disabled + And the image "pink_dress.gif" does not exist on the provider + + Scenario: Image is provided locally when module is disabled + Given the cloudinary module is disabled + When I upload the image "pink_dress.gif" + Then the image "pink_dress.gif" will be provided locally + + Scenario: Image is provided remotely when module is enabled + Given the cloudinary module is enabled + And the image "pink_dress.gif" does not exist on the provider + When I upload the image "pink_dress.gif" + Then the image "pink_dress.gif" will be provided remotely + + Scenario: Image with same ID already exists in the provider + Given the cloudinary module is enabled + But the image "pink_dress.gif" has already been uploaded + When I upload the image "pink_dress.gif" + Then I should see an error image already exists diff --git a/core/features/migration/admin_migrates_images.feature b/core/features/migration/admin_migrates_images.feature new file mode 100644 index 0000000..4ca785a --- /dev/null +++ b/core/features/migration/admin_migrates_images.feature @@ -0,0 +1,31 @@ +@not-automated +Feature: Product image migration + In order to easily install and use the Cloudinary module + As an integrator + I need an easy mechanism to migrate all existing catalogue images to Cloudinary + + Scenario: Integrator triggers the migration + Given the media gallery contains the images "chair.png", "table.png" and "house.png" + And those images have not been migrated to cloudinary + When the integrator triggers the migration + Then the images should be migrated to cloudinary + + Scenario: Integrator is unable to start the migration when a process is already running + Given the cloudinary migration has been triggered + And the cloudinary migration is still in progress + When the integrator tries to trigger the migration + Then they should not be able to start the migration + And there should be feedback that triggering a migration is currently disabled + + Scenario: Integrator is unable to start the migration when there are no images to migrate + Given there are no images to migrate + When the integrator tries to trigger the migration + Then they should not be able to start the migration + And there should be feedback that triggering a migration is currently disabled + + Scenario: Integrator receives feedback of the migration progress + Given the media gallery contains the images "chair.png", "table.png" and "house.png" + And a migration has been started + When the images "chair.png" and "table.png" have been migrated + And the image "house.png" haven not been migrated yet + Then the integrator should receive feedback saying that the migration is at "66%" diff --git a/core/features/migration/cloudinary_enable_disable.feature b/core/features/migration/cloudinary_enable_disable.feature new file mode 100644 index 0000000..03d441c --- /dev/null +++ b/core/features/migration/cloudinary_enable_disable.feature @@ -0,0 +1,16 @@ +@javascript @ui +Feature: Cloudinary can be enabled or disabled + + Background: + Given I am logged in as an administrator + + Scenario: Being able to enable cloudinary when its disabled + Given the Cloudinary module is disabled + When I access the Cloudinary configuration + Then I should be able to enable the module + + Scenario: Being able to disable cloudinary when its enabled + Given the Cloudinary module is enabled + When I access the Cloudinary configuration + Then I should be able to disable the module + diff --git a/core/features/validate_credentials.feature b/core/features/validate_credentials.feature new file mode 100644 index 0000000..a5aab1a --- /dev/null +++ b/core/features/validate_credentials.feature @@ -0,0 +1,16 @@ +Feature: Validating environment variable used for image provider + In order to interact with the image provider + As a store admin + I want to know that the environment variable I have configured is valid + + @javascript @critical + Scenario: Validate correct environment variable is being used + Given I have used a valid environment variable in the configuration + When I ask the provider to validate my credentials + Then I should be informed my credentials are valid + + @javascript @critical + Scenario: Report an error if incorrect environment variable is used + Given I have used an invalid environment variable in the configuration + When I ask the provider to validate my credentials + Then I should be informed that my credentials are not valid \ No newline at end of file diff --git a/core/lib/CloudinaryExtension/Cloud.php b/core/lib/CloudinaryExtension/Cloud.php new file mode 100644 index 0000000..559a02c --- /dev/null +++ b/core/lib/CloudinaryExtension/Cloud.php @@ -0,0 +1,25 @@ +cloudName = (string)$cloudName; + } + + public static function fromName($aCloudName) + { + return new Cloud($aCloudName); + } + + public function __toString() + { + return $this->cloudName; + } +} diff --git a/core/lib/CloudinaryExtension/CloudinaryImageManager.php b/core/lib/CloudinaryExtension/CloudinaryImageManager.php new file mode 100644 index 0000000..62813bc --- /dev/null +++ b/core/lib/CloudinaryExtension/CloudinaryImageManager.php @@ -0,0 +1,52 @@ +cloudinaryImageProvider = $cloudinaryImageProvider; + $this->synchronisationRepository = $synchronisationRepository; + } + + /** + * @param Image $image + */ + public function uploadAndSynchronise(Image $image) + { + $this->cloudinaryImageProvider->upload($image); + $this->synchronisationRepository->saveAsSynchronized($image->getRelativePath()); + + } + + /** + * @param Image $image + */ + public function removeAndUnSynchronise(Image $image) + { + $this->cloudinaryImageProvider->delete($image); + $this->synchronisationRepository->removeSynchronised($image->getRelativePath()); + } +} \ No newline at end of file diff --git a/core/lib/CloudinaryExtension/CloudinaryImageProvider.php b/core/lib/CloudinaryExtension/CloudinaryImageProvider.php new file mode 100644 index 0000000..dd1852f --- /dev/null +++ b/core/lib/CloudinaryExtension/CloudinaryImageProvider.php @@ -0,0 +1,96 @@ +configuration = $configuration; + $this->uploadResponseValidator = $uploadResponseValidator; + $this->configurationBuilder = $configurationBuilder; + $this->credentialValidator = $credentialValidator; + $this->authorise(); + } + + public static function fromConfiguration(ConfigurationInterface $configuration){ + return new CloudinaryImageProvider( + $configuration, + new ConfigurationBuilder($configuration), + new UploadResponseValidator(), + new CredentialValidator() + ); + } + + public function upload(Image $image) + { + try { + $uploadResult = Uploader::upload( + (string)$image, + $this->configuration->getUploadConfig()->toArray() + [ "folder" => $image->getRelativeFolder()] + ); + + return $this->uploadResponseValidator->validateResponse($image, $uploadResult); + + } catch (\Exception $e) { + MigrationError::throwWith($image, MigrationError::CODE_API_ERROR, $e->getMessage()); + } + } + + public function retrieveTransformed(Image $image, Transformation $transformation) + { + return Image::fromPath( + \cloudinary_url($image->getId(), $transformation->build() + ["secure" => true]), + $image->getRelativePath() + ); + } + + public function retrieve(Image $image) + { + return $this->retrieveTransformed($image, $this->configuration->getDefaultTransformation()); + } + + public function delete(Image $image) + { + Uploader::destroy($image->getId()); + } + + public function validateCredentials() + { + return $this->credentialValidator->validate($this->configuration->getCredentials()); + } + + private function authorise() + { + Cloudinary::config($this->configurationBuilder->build()); + Cloudinary::$USER_PLATFORM = $this->configuration->getUserPlatform(); + } +} \ No newline at end of file diff --git a/core/lib/CloudinaryExtension/ConfigurationBuilder.php b/core/lib/CloudinaryExtension/ConfigurationBuilder.php new file mode 100644 index 0000000..7bb9c82 --- /dev/null +++ b/core/lib/CloudinaryExtension/ConfigurationBuilder.php @@ -0,0 +1,30 @@ +configuration = $configuration; + } + + public function build() + { + $config = [ + "cloud_name" => (string)$this->configuration->getCloud(), + "api_key" => (string)$this->configuration->getCredentials()->getKey(), + "api_secret" => (string)$this->configuration->getCredentials()->getSecret() + ]; + + if ($this->configuration->getCdnSubdomainStatus()) { + $config['cdn_subdomain'] = true; + } + return $config; + } +} diff --git a/core/lib/CloudinaryExtension/ConfigurationInterface.php b/core/lib/CloudinaryExtension/ConfigurationInterface.php new file mode 100644 index 0000000..0bb3bfb --- /dev/null +++ b/core/lib/CloudinaryExtension/ConfigurationInterface.php @@ -0,0 +1,67 @@ +validate(); + + } +} \ No newline at end of file diff --git a/core/lib/CloudinaryExtension/Credentials.php b/core/lib/CloudinaryExtension/Credentials.php new file mode 100644 index 0000000..56b5cd5 --- /dev/null +++ b/core/lib/CloudinaryExtension/Credentials.php @@ -0,0 +1,35 @@ +key = $key; + $this->secret = $secret; + } + + public static function fromKeyAndSecret(Key $key,Secret $secret) + { + return new Credentials($key, $secret); + } + + public function getKey() + { + return $this->key; + } + + public function getSecret() + { + return $this->secret; + } +} diff --git a/core/lib/CloudinaryExtension/Exception/InvalidCredentials.php b/core/lib/CloudinaryExtension/Exception/InvalidCredentials.php new file mode 100644 index 0000000..5dd83b8 --- /dev/null +++ b/core/lib/CloudinaryExtension/Exception/InvalidCredentials.php @@ -0,0 +1,10 @@ + 'File already exists (cloudinary is case insensitive!!).', + self::CODE_API_ERROR => 'Internal API error' + ]; + + private $image; + + /** + * @return Image + */ + public function getImage() + { + return $this->image; + } + + /** + * @param Image $image + * @param $code + * @param $message overrides the default message attached to the code + * @return MigrationError + */ + private static function build(Image $image, $code, $message = '') + { + $result = new MigrationError($message ?: self::$messages[$code], $code); + $result->image = $image; + return $result; + } + + public static function throwWith(Image $image, $code, $message = '') + { + throw MigrationError::build($image, $code, $message); + } +} diff --git a/core/lib/CloudinaryExtension/FolderTranslator.php b/core/lib/CloudinaryExtension/FolderTranslator.php new file mode 100644 index 0000000..2949dc6 --- /dev/null +++ b/core/lib/CloudinaryExtension/FolderTranslator.php @@ -0,0 +1,16 @@ +imagePath = $imagePath; + $this->relativePath = $relativePath; + $this->pathInfo = pathinfo($this->imagePath); + } + + public static function fromPath($imagePath, $relativePath = '') + { + return new Image($imagePath, $relativePath); + } + + public function __toString() + { + return $this->imagePath; + } + + public function getRelativePath() + { + return $this->relativePath; + } + + public function getRelativeFolder() + { + $result = dirname($this->getRelativePath()); + return $result == '.' ? '' : $result; + } + + public function getId() + { + if ($this->relativePath) { + return $this->getRelativeFolder() . DIRECTORY_SEPARATOR . $this->pathInfo['filename']; + } else { + return $this->pathInfo['filename']; + } + } + + public function getExtension() + { + return $this->pathInfo['extension']; + } +} diff --git a/core/lib/CloudinaryExtension/Image/ImageFactory.php b/core/lib/CloudinaryExtension/Image/ImageFactory.php new file mode 100644 index 0000000..8866785 --- /dev/null +++ b/core/lib/CloudinaryExtension/Image/ImageFactory.php @@ -0,0 +1,46 @@ +configuration = $configuration; + $this->synchronizationChecker = $synchronizationChecker; + } + + /** + * @param $imagePath + * @return Image + */ + public function build($imagePath, callable $localPathGenerator) + { + $migratedPath = $this->configuration->getMigratedPath($imagePath); + + if ($this->configuration->isEnabled() && $this->synchronizationChecker->isSynchronized($migratedPath)) { + return Image::fromPath($imagePath, $migratedPath); + } else { + return new LocalImage($localPathGenerator); + } + } +} diff --git a/core/lib/CloudinaryExtension/Image/LocalImage.php b/core/lib/CloudinaryExtension/Image/LocalImage.php new file mode 100644 index 0000000..556a555 --- /dev/null +++ b/core/lib/CloudinaryExtension/Image/LocalImage.php @@ -0,0 +1,25 @@ +localPathGenerator = $localPathGenerator; + } + + public function __toString() + { + return call_user_func($this->localPathGenerator); + } +} diff --git a/core/lib/CloudinaryExtension/Image/Synchronizable.php b/core/lib/CloudinaryExtension/Image/Synchronizable.php new file mode 100644 index 0000000..c9d22d4 --- /dev/null +++ b/core/lib/CloudinaryExtension/Image/Synchronizable.php @@ -0,0 +1,21 @@ +fetchFormat = FetchFormat::fromString(Format::FETCH_FORMAT_AUTO); + $this->crop = 'pad'; + $this->format = Format::fromExtension('jpg'); + $this->validFormats = array('gif', 'jpg', 'png', 'svg', 'webp'); + $this->flags = []; + } + + public function withGravity(Gravity $gravity) + { + $this->gravity = $gravity; + $this->crop = ((string)$gravity) ? 'crop' : 'pad'; + return $this; + } + + public function withDimensions(Dimensions $dimensions) + { + $this->dimensions = $dimensions; + return $this; + } + + public function withFetchFormat(FetchFormat $fetchFormat) + { + $this->fetchFormat = $fetchFormat; + return $this; + } + + public function withFormat(Format $format) + { + if (in_array((string)$format, $this->validFormats)) { + $this->format = $format; + } + + return $this; + } + + public function withoutFormat() + { + $this->format = null; + return $this; + } + + public function withQuality(Quality $quality) + { + $this->quality = $quality; + return $this; + } + + public function withDpr(Dpr $dpr) + { + $this->dpr = $dpr; + return $this; + } + + public function withOptimisationDisabled() + { + return $this->withFetchFormat(FetchFormat::fromString('')); + } + + public function addFlags(array $flags = []) + { + $this->flags += $flags; + return $this; + } + + public static function builder() + { + return new Transformation(); + } + + public function build() + { + return array( + 'fetch_format' => (string)$this->fetchFormat, + 'quality' => (string)$this->quality, + 'crop' => (string)$this->crop, + 'gravity' => (string)$this->gravity ?: null, + 'width' => $this->dimensions ? $this->dimensions->getWidth() : null, + 'height' => $this->dimensions ? $this->dimensions->getHeight() : null, + 'format' => (string)$this->format, + 'dpr' => (string)$this->dpr, + 'flags' => $this->flags + ); + } +} + diff --git a/core/lib/CloudinaryExtension/Image/Transformation/Dimensions.php b/core/lib/CloudinaryExtension/Image/Transformation/Dimensions.php new file mode 100644 index 0000000..ec0fa8b --- /dev/null +++ b/core/lib/CloudinaryExtension/Image/Transformation/Dimensions.php @@ -0,0 +1,36 @@ +width = is_null($width) ? null : (int) round($width); + $this->height = is_null($height) ? null : (int) round($height); + } + + public function getWidth() + { + return $this->width; + } + + public function getHeight() + { + return $this->height; + } + + public static function fromWidthAndHeight($width, $height) + { + return new Dimensions($width, $height); + } + + public static function null() + { + return new Dimensions(null, null); + } +} diff --git a/core/lib/CloudinaryExtension/Image/Transformation/Dpr.php b/core/lib/CloudinaryExtension/Image/Transformation/Dpr.php new file mode 100644 index 0000000..8c15aa5 --- /dev/null +++ b/core/lib/CloudinaryExtension/Image/Transformation/Dpr.php @@ -0,0 +1,23 @@ +value = $value; + } + + public static function fromString($value) + { + return new Dpr($value); + } + + public function __toString() + { + return $this->value; + } +} diff --git a/core/lib/CloudinaryExtension/Image/Transformation/FetchFormat.php b/core/lib/CloudinaryExtension/Image/Transformation/FetchFormat.php new file mode 100644 index 0000000..8b34308 --- /dev/null +++ b/core/lib/CloudinaryExtension/Image/Transformation/FetchFormat.php @@ -0,0 +1,25 @@ +value = $value; + } + + public static function fromString($value) + { + return new FetchFormat($value); + } + + public function __toString() + { + return $this->value; + } +} diff --git a/core/lib/CloudinaryExtension/Image/Transformation/Format.php b/core/lib/CloudinaryExtension/Image/Transformation/Format.php new file mode 100644 index 0000000..94da009 --- /dev/null +++ b/core/lib/CloudinaryExtension/Image/Transformation/Format.php @@ -0,0 +1,25 @@ +value = $value; + } + + public static function fromExtension($value) + { + return new Format($value); + } + + public function __toString() + { + return $this->value; + } +} diff --git a/core/lib/CloudinaryExtension/Image/Transformation/Gravity.php b/core/lib/CloudinaryExtension/Image/Transformation/Gravity.php new file mode 100644 index 0000000..9eb0d1a --- /dev/null +++ b/core/lib/CloudinaryExtension/Image/Transformation/Gravity.php @@ -0,0 +1,30 @@ +value = $value; + } + + public function __toString() + { + return $this->value; + } + + public static function fromString($value) + { + return new Gravity($value); + } + + public static function null() + { + return new Gravity(null); + } +} + + diff --git a/core/lib/CloudinaryExtension/Image/Transformation/Quality.php b/core/lib/CloudinaryExtension/Image/Transformation/Quality.php new file mode 100644 index 0000000..6b7f4d3 --- /dev/null +++ b/core/lib/CloudinaryExtension/Image/Transformation/Quality.php @@ -0,0 +1,23 @@ +value = $value; + } + + public static function fromString($value) + { + return new Quality($value); + } + + public function __toString() + { + return $this->value; + } +} diff --git a/core/lib/CloudinaryExtension/ImageInterface.php b/core/lib/CloudinaryExtension/ImageInterface.php new file mode 100644 index 0000000..ffc3293 --- /dev/null +++ b/core/lib/CloudinaryExtension/ImageInterface.php @@ -0,0 +1,11 @@ +imageProvider = $imageProvider; + $this->migrationTask = $migrationTask; + $this->baseMediaPath = $baseMediaPath; + $this->logger = $logger; + } + + public function uploadImages(array $images) + { + $this->countMigrated = 0; + foreach ($images as $image) { + + if ($this->migrationTask->hasBeenStopped()) { + break; + } + $this->uploadImage($image); + } + $this->logger->notice(sprintf(self::MESSAGE_STATUS, $this->countMigrated, $this->countFailed)); + } + + private function getAbsolutePath(Synchronizable $image) + { + return sprintf('%s%s', $this->baseMediaPath, $image->getFilename()); + } + + private function uploadImage(Synchronizable $image) + { + $absolutePath = $this->getAbsolutePath($image); + $relativePath = $image->getRelativePath(); + $apiImage = Image::fromPath($absolutePath, $relativePath); + + try { + $this->imageProvider->upload($apiImage); + $image->tagAsSynchronized(); + $this->countMigrated++; + $this->logger->notice(sprintf(self::MESSAGE_UPLOADED, $absolutePath . ' - ' . $relativePath)); + } catch (\Exception $e) { + $this->errors[] = $e; + $this->countFailed++; + $this->logger->error(sprintf(self::MESSAGE_UPLOAD_ERROR, $e->getMessage(), $absolutePath . ' - ' . $relativePath)); + } + } + + /** + * @return array + */ + public function getErrors() + { + return $this->errors; + } + + public function getMigrationErrors() + { + return array_filter($this->errors, function ($val) { + return $val instanceof MigrationError; + }); + } + +} diff --git a/core/lib/CloudinaryExtension/Migration/Logger.php b/core/lib/CloudinaryExtension/Migration/Logger.php new file mode 100644 index 0000000..74bba5a --- /dev/null +++ b/core/lib/CloudinaryExtension/Migration/Logger.php @@ -0,0 +1,14 @@ +migrationTask = $migrationTask; + $this->synchronizedMediaRepository = $synchronizedMediaRepository; + $this->logger = $logger; + $this->batchUploader = $batchUploader; + } + + public function process() + { + if ($this->migrationTask->hasBeenStopped()) { + return; + } + + $images = $this->synchronizedMediaRepository->findUnsynchronisedImages(); + + if (!$images) { + $this->logger->notice(self::MESSAGE_COMPLETE); + $this->migrationTask->stop(); + } else { + $this->logger->notice(self::MESSAGE_PROCESSING); + $this->batchUploader->uploadImages($images); + } + } +} diff --git a/core/lib/CloudinaryExtension/Migration/SynchronizedMediaRepository.php b/core/lib/CloudinaryExtension/Migration/SynchronizedMediaRepository.php new file mode 100644 index 0000000..c01dc34 --- /dev/null +++ b/core/lib/CloudinaryExtension/Migration/SynchronizedMediaRepository.php @@ -0,0 +1,8 @@ +apiSignature = Cloudinary::api_sign_request($params, (string) $secret); + } + + public static function fromSecretAndParams(Secret $secret, array $params = array()) + { + return new ApiSignature($secret, $params); + } + + public function __toString() + { + return $this->apiSignature; + } +} diff --git a/core/lib/CloudinaryExtension/Security/CloudinaryEnvironmentVariable.php b/core/lib/CloudinaryExtension/Security/CloudinaryEnvironmentVariable.php new file mode 100644 index 0000000..b18209b --- /dev/null +++ b/core/lib/CloudinaryExtension/Security/CloudinaryEnvironmentVariable.php @@ -0,0 +1,47 @@ +environmentVariable = (string)$environmentVariable; + try { + Cloudinary::config_from_url(str_replace('CLOUDINARY_URL=', '', $environmentVariable)); + } catch (\Exception $e){ + throw new \CloudinaryExtension\Exception\InvalidCredentials('Cloudinary config creation from environment variable failed'); + } + } + + public static function fromString($environmentVariable) + { + return new CloudinaryEnvironmentVariable($environmentVariable); + } + + public function getCloud() + { + return Cloud::fromName(Cloudinary::config_get('cloud_name')); + } + + public function getCredentials() + { + return Credentials::fromKeyAndSecret( + Key::fromString(Cloudinary::config_get('api_key')), + Secret::fromString(Cloudinary::config_get('api_secret')) + ); + } + + public function __toString() + { + return $this->environmentVariable; + } + +} \ No newline at end of file diff --git a/core/lib/CloudinaryExtension/Security/ConsoleUrl.php b/core/lib/CloudinaryExtension/Security/ConsoleUrl.php new file mode 100644 index 0000000..e34c900 --- /dev/null +++ b/core/lib/CloudinaryExtension/Security/ConsoleUrl.php @@ -0,0 +1,26 @@ +consoleUrl = self::CLOUDINARY_CONSOLE_BASE_URL . $path; + } + + public static function fromPath($path) + { + return new ConsoleUrl($path); + } + + public function __toString() + { + return $this->consoleUrl; + } +} diff --git a/core/lib/CloudinaryExtension/Security/EnvironmentVariable.php b/core/lib/CloudinaryExtension/Security/EnvironmentVariable.php new file mode 100644 index 0000000..ae51017 --- /dev/null +++ b/core/lib/CloudinaryExtension/Security/EnvironmentVariable.php @@ -0,0 +1,9 @@ +key = (string)$key; + } + + public static function fromString($aKey) + { + return new Key($aKey); + } + + public function __toString() + { + return $this->key; + } + +} diff --git a/core/lib/CloudinaryExtension/Security/Secret.php b/core/lib/CloudinaryExtension/Security/Secret.php new file mode 100644 index 0000000..5a57ef8 --- /dev/null +++ b/core/lib/CloudinaryExtension/Security/Secret.php @@ -0,0 +1,24 @@ +secret = (string)$secret; + } + + public static function fromString($aSecret) + { + return new Secret($aSecret); + } + + public function __toString() + { + return $this->secret; + } +} diff --git a/core/lib/CloudinaryExtension/Security/SignedConsoleUrl.php b/core/lib/CloudinaryExtension/Security/SignedConsoleUrl.php new file mode 100644 index 0000000..dce9b23 --- /dev/null +++ b/core/lib/CloudinaryExtension/Security/SignedConsoleUrl.php @@ -0,0 +1,31 @@ + time(), "mode" => "check"); + $params["signature"] = (string)ApiSignature::fromSecretAndParams($credentials->getSecret(), $params); + $params["api_key"] = (string)$credentials->getKey(); + $query = http_build_query($params); + + $this->signedConsoleUrl = (string)$url . '?' . $query; + } + + public static function fromConsoleUrlAndCredentials(ConsoleUrl $url, Credentials $credentials) + { + return new SignedConsoleUrl($url, $credentials); + } + + public function __toString() + { + return $this->signedConsoleUrl; + } +} diff --git a/core/lib/CloudinaryExtension/SynchroniseAssetsRepositoryInterface.php b/core/lib/CloudinaryExtension/SynchroniseAssetsRepositoryInterface.php new file mode 100644 index 0000000..3c9516d --- /dev/null +++ b/core/lib/CloudinaryExtension/SynchroniseAssetsRepositoryInterface.php @@ -0,0 +1,18 @@ +useFilename = $useFilename; + $this->uniqueFilename = $uniqueFilename; + $this->overwrite = $overwrite; + } + + public static function fromBooleanValues($useFilename, $uniqueFilename, $overwrite) + { + return new UploadConfig($useFilename, $uniqueFilename, $overwrite); + } + + /** + * @return boolean + */ + public function useFilename() + { + return $this->useFilename; + } + + /** + * @return boolean + */ + public function uniqueFilename() + { + return $this->uniqueFilename; + } + + /** + * @return boolean + */ + public function overwrite() + { + return $this->overwrite; + } + + public function toArray() + { + return [ + "use_filename" => $this->useFilename, + "unique_filename" => $this->uniqueFilename, + "overwrite" => $this->overwrite, + ] ; + } +} \ No newline at end of file diff --git a/core/lib/CloudinaryExtension/UploadResponseValidator.php b/core/lib/CloudinaryExtension/UploadResponseValidator.php new file mode 100644 index 0000000..8bdedca --- /dev/null +++ b/core/lib/CloudinaryExtension/UploadResponseValidator.php @@ -0,0 +1,17 @@ +configuration = $configuration; + $this->imageProvider = $imageProvider; + } + + /** + * @param ImageInterface $image + * @param Transformation $transformation + * + * @return string + */ + public function generateFor(ImageInterface $image, Transformation $transformation = null) + { + if ($image instanceof LocalImage) { + return (string)$image; + } + + $transformation = clone ($transformation ?: $this->configuration->getDefaultTransformation()); + + if (in_array($image->getExtension(), $this->configuration->getFormatsToPreserve())) { + $transformation->withoutFormat(); + } + + return (string)$this->imageProvider->retrieveTransformed($image, $transformation); + } + + /** + * @param Image $image + * @param Dimensions $dimensions + * + * @return string + */ + public function generateWithDimensions(ImageInterface $image, Dimensions $dimensions) + { + $transformation = clone $this->configuration->getDefaultTransformation(); + + return $this->generateFor($image, $transformation->withDimensions($dimensions)); + } +} diff --git a/core/lib/CloudinaryExtension/ValidateRemoteUrlRequest.php b/core/lib/CloudinaryExtension/ValidateRemoteUrlRequest.php new file mode 100644 index 0000000..b2c5499 --- /dev/null +++ b/core/lib/CloudinaryExtension/ValidateRemoteUrlRequest.php @@ -0,0 +1,55 @@ +curlHandler = curl_init($url); + $this->setCurlOptions(); + } + + public function validate() + { + $result = $this->execute(); + + if ($result->responseCode == 200 && is_null($result->error)) { + return true; + } + return false; + } + + private function execute() + { + curl_exec($this->curlHandler); + + $result = new \stdClass(); + $result->responseCode = $this->getResponseCode(); + $result->error = $this->getErrorMessage(); + + curl_close($this->curlHandler); + + return $result; + } + + private function getResponseCode() + { + return curl_getinfo($this->curlHandler, CURLINFO_HTTP_CODE); + } + + private function getErrorMessage() + { + return curl_errno($this->curlHandler) ? curl_error($this->curlHandler) : null; + } + + private function setCurlOptions() + { + curl_setopt($this->curlHandler, CURLOPT_HEADER, 1); + curl_setopt($this->curlHandler, CURLOPT_FAILONERROR, 1); + curl_setopt($this->curlHandler, CURLOPT_RETURNTRANSFER, 1); + } +} diff --git a/core/phpspec.yml b/core/phpspec.yml new file mode 100644 index 0000000..d02c98a --- /dev/null +++ b/core/phpspec.yml @@ -0,0 +1,3 @@ +suites: + cloudinary: + src_path: lib diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist new file mode 100644 index 0000000..3bc5730 --- /dev/null +++ b/core/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + + + + tests + + + + + + diff --git a/core/spec/CloudinaryExtension/CloudSpec.php b/core/spec/CloudinaryExtension/CloudSpec.php new file mode 100644 index 0000000..341eb65 --- /dev/null +++ b/core/spec/CloudinaryExtension/CloudSpec.php @@ -0,0 +1,20 @@ +beConstructedThrough('fromName', ['cloud_name']); + } + + function it_is_initializable() + { + $this->shouldHaveType('CloudinaryExtension\Cloud'); + } +} diff --git a/core/spec/CloudinaryExtension/CloudinaryImageProviderSpec.php b/core/spec/CloudinaryExtension/CloudinaryImageProviderSpec.php new file mode 100644 index 0000000..a88030c --- /dev/null +++ b/core/spec/CloudinaryExtension/CloudinaryImageProviderSpec.php @@ -0,0 +1,46 @@ +getKey()->willReturn('apiKey'); + $credentials->getSecret()->willReturn('apiSecret'); + + $configuration->getCloud()->willReturn('testCloud'); + $configuration->getCredentials()->willReturn($credentials); + $configuration->getCdnSubdomainStatus()->willReturn(true); + + $this->beConstructedThrough('fromConfiguration', [ + $configuration, + $configurationBuilder, + $uploadResponseValidator, + $credentialValidator + ]); + } + + function it_sets_user_agent_string(ConfigurationInterface $configuration) + { + $configuration->getUserPlatform()->willReturn('Test User Agent String'); + + $this->getWrappedObject(); + expect(Cloudinary::$USER_PLATFORM)->toBe('Test User Agent String'); + } +} diff --git a/core/spec/CloudinaryExtension/ConfigurationBuilderSpec.php b/core/spec/CloudinaryExtension/ConfigurationBuilderSpec.php new file mode 100644 index 0000000..e51572c --- /dev/null +++ b/core/spec/CloudinaryExtension/ConfigurationBuilderSpec.php @@ -0,0 +1,59 @@ +beConstructedWith($configuration); + + $configuration->getCloud()->willReturn(Cloud::fromName('testCloud')); + $configuration->getCredentials()->willReturn(Credentials::fromKeyAndSecret( + Key::fromString('apiKey'), + Secret::fromString('apiSecret') + )); + $configuration->getDefaultTransformation()->willReturn(Transformation::builder()); + $configuration->getUserPlatform()->willReturn(''); + $configuration->getUploadConfig()->willReturn(UploadConfig::fromBooleanValues(false, false, false)); + } + + function it_should_build_configuration_with_all_values(ConfigurationInterface $configuration) + { + $configuration->getCdnSubdomainStatus()->willReturn(true); + + $expected = [ + 'cloud_name' => 'testCloud', + 'api_key' => 'apiKey', + 'api_secret' => 'apiSecret', + 'cdn_subdomain' => true + ]; + + $this->build()->shouldReturn($expected); + } + + function it_should_build_configuration_with_out_cdn(ConfigurationInterface $configuration) + { + $configuration->getCdnSubdomainStatus()->willReturn(false); + + $expected = [ + 'cloud_name' => 'testCloud', + 'api_key' => 'apiKey', + 'api_secret' => 'apiSecret' + ]; + + $this->build()->shouldReturn($expected); + } +} diff --git a/core/spec/CloudinaryExtension/CredentialValidatorSpec.php b/core/spec/CloudinaryExtension/CredentialValidatorSpec.php new file mode 100644 index 0000000..500ea64 --- /dev/null +++ b/core/spec/CloudinaryExtension/CredentialValidatorSpec.php @@ -0,0 +1,10 @@ +beConstructedThrough('fromKeyAndSecret', [ + Key::fromString($this->key), Secret::fromString($this->secret) + ]); + } + + function it_returns_the_correct_key() + { + $this->getKey()->shouldBeLike($this->key); + } + + function it_returns_the_correct_secret() + { + $this->getSecret()->shouldBeLike($this->secret); + } +} diff --git a/core/spec/CloudinaryExtension/Image/ImageFactorySpec.php b/core/spec/CloudinaryExtension/Image/ImageFactorySpec.php new file mode 100644 index 0000000..bd05b54 --- /dev/null +++ b/core/spec/CloudinaryExtension/Image/ImageFactorySpec.php @@ -0,0 +1,27 @@ +beConstructedWith( + $configuration, + $synchronizable + ); + } + + function it_is_initializable() + { + $this->shouldHaveType('CloudinaryExtension\Image\ImageFactory'); + } +} diff --git a/core/spec/CloudinaryExtension/Image/Transformation/DimensionsSpec.php b/core/spec/CloudinaryExtension/Image/Transformation/DimensionsSpec.php new file mode 100644 index 0000000..db9c1f7 --- /dev/null +++ b/core/spec/CloudinaryExtension/Image/Transformation/DimensionsSpec.php @@ -0,0 +1,40 @@ +beConstructedThrough('fromWidthAndHeight', [100, 150]); + } + + function it_exposes_its_width_and_height() + { + $this->getWidth()->shouldBe(100); + $this->getHeight()->shouldBe(150); + } + + function it_rounds_float_values() + { + $this->beConstructedThrough('fromWidthAndHeight', [1.5, 2.9]); + + $this->getWidth()->shouldBe(2); + $this->getHeight()->shouldBe(3); + } + + function it_exposes_null_properties_when_created_null() + { + $this->beConstructedThrough('null', []); + + $this->getWidth()->shouldBe(null); + $this->getHeight()->shouldBe(null); + } +} diff --git a/core/spec/CloudinaryExtension/Image/TransformationSpec.php b/core/spec/CloudinaryExtension/Image/TransformationSpec.php new file mode 100644 index 0000000..73eabe7 --- /dev/null +++ b/core/spec/CloudinaryExtension/Image/TransformationSpec.php @@ -0,0 +1,144 @@ +shouldBeAnInstanceOf('CloudinaryExtension\Image\Transformation'); + } + + function it_overrides_fetch_format_if_provided() + { + $transformationArray = self::builder() + ->withFetchFormat(FetchFormat::fromString('')) + ->build(); + + $transformationArray->offsetGet('fetch_format')->shouldBe(''); + } + + function it_overrides_quality_if_provided() + { + $transformationArray = self::builder() + ->withQuality(Quality::fromString('100')) + ->build(); + + $transformationArray->offsetGet('quality')->shouldBe('100'); + } + + function it_builds_no_dimensions_by_default() + { + $transformationArray = self::builder()->build(); + + $transformationArray->offsetGet('width')->shouldBe(null); + $transformationArray->offsetGet('height')->shouldBe(null); + } + + function it_builds_with_dimensions_when_provided() + { + $transformationArray = self::builder() + ->withDimensions(Dimensions::fromWidthAndHeight('80', '90')) + ->build(); + + $transformationArray->offsetGet('width')->shouldBe(80); + $transformationArray->offsetGet('height')->shouldBe(90); + } + + function it_builds_with_no_gravity_by_default() + { + $transformationArray = self::builder()->build(); + + $transformationArray->offsetGet('gravity')->shouldBe(null); + } + + function it_builds_with_gravity() + { + $transformationArray = self::builder() + ->withGravity(Gravity::fromString('center')) + ->build(); + + $transformationArray->offsetGet('gravity')->shouldBe('center'); + } + + function it_builds_with_crop_set_to_crop_when_gravity_is_set() + { + $transformationArray = self::builder() + ->withGravity(Gravity::fromString('center')) + ->build(); + + $transformationArray->offsetGet('crop')->shouldBe('crop'); + } + + function it_builds_with_crop_set_to_pad_when_gravity_is_not_set() + { + $transformationArray = self::builder()->build(); + + $transformationArray->offsetGet('crop')->shouldBe('pad'); + } + + function it_builds_with_jpeg_format_by_default() + { + $transformationArray = self::builder()->build(); + + $transformationArray->offsetGet('format')->shouldBe('jpg'); + } + + function it_builds_with_format_from_original_extension_when_extension_is_valid() + { + $transformationArray = self::builder() + ->withFormat(Format::fromExtension('png')) + ->build(); + + $transformationArray->offsetGet('format')->shouldBe('png'); + } + + function it_builds_with_jpeg_format_when_original_extension_is_not_valid() + { + $transformationArray = self::builder() + ->withFormat(Format::fromExtension('xpm')) + ->build(); + + $transformationArray->offsetGet('format')->shouldBe('jpg'); + } + + function it_builds_with_image_optimisation_enabled() + { + $transformationArray = self::builder()->build(); + + $transformationArray->offsetGet('fetch_format')->shouldBe('auto'); + } + + function it_builds_with_image_optimisation_disabled() + { + $transformationArray = self::builder()->withOptimisationDisabled()->build(); + + $transformationArray->offsetGet('fetch_format')->shouldBe(''); + } + + function it_builds_with_fetch_format_auto_by_default() + { + $transformationArray = self::builder()->build(); + $transformationArray->offsetGet('fetch_format')->shouldBe('auto'); + } + + function it_handles_flags(){ + $flags = ['1', '2']; + + /** + * @var Transformation $transFormation + */ + $transFormation = self::builder()->addFlags($flags); + $array = $transFormation->build(); + $array->offsetGet('flags')->shouldBe($flags); + } +} diff --git a/core/spec/CloudinaryExtension/ImageSpec.php b/core/spec/CloudinaryExtension/ImageSpec.php new file mode 100644 index 0000000..35f0149 --- /dev/null +++ b/core/spec/CloudinaryExtension/ImageSpec.php @@ -0,0 +1,24 @@ +beConstructedThrough('fromPath', ['image_path.gif']); + } + + function it_provides_a_public_id_from_path() + { + $this->getId()->shouldBe('image_path'); + } + + function it_provides_the_extension_from_path() + { + $this->getExtension()->shouldBe('gif'); + } +} diff --git a/core/spec/CloudinaryExtension/Migration/BatchUploaderSpec.php b/core/spec/CloudinaryExtension/Migration/BatchUploaderSpec.php new file mode 100644 index 0000000..474fe1a --- /dev/null +++ b/core/spec/CloudinaryExtension/Migration/BatchUploaderSpec.php @@ -0,0 +1,145 @@ +beConstructedWith($imageProvider, $migrationTask, $logger, self::MEDIA_PATH); + + $image1->tagAsSynchronized()->willReturn(); + $image2->tagAsSynchronized()->willReturn(); + $migrationTask->hasBeenStopped()->willReturn(false, false); + } + + function it_uploads_and_synchronizes_a_collection_of_images( + ImageProvider $imageProvider, + Logger $logger, + Synchronizable $image1, + Synchronizable $image2 + ) { + $path1 = '/z/b/image1.jpg'; + $path2 = '/r/b/image2.jpg'; + $relativePath1 = basename($path1); + $relativePath2 = basename($path2); + $absolutePath1 = self::MEDIA_PATH . $path1; + $absolutePath2 = self::MEDIA_PATH . $path2; + + $image1->getFilename()->willReturn($path1); + $image2->getFilename()->willReturn($path2); + $image1->getRelativePath()->willReturn($relativePath1); + $image2->getRelativePath()->willReturn($relativePath2); + + $images = array($image1, $image2); + + $this->uploadImages($images); + + $imageProvider->upload(Image::fromPath($absolutePath1, $relativePath1))->shouldHaveBeenCalled(); + $imageProvider->upload(Image::fromPath($absolutePath2, $relativePath2))->shouldHaveBeenCalled(); + + $image1->tagAsSynchronized()->shouldHaveBeenCalled(); + $image2->tagAsSynchronized()->shouldHaveBeenCalled(); + + $logger->notice(sprintf(BatchUploader::MESSAGE_UPLOADED, "$absolutePath1 - $relativePath1"))->shouldHaveBeenCalled(); + $logger->notice(sprintf(BatchUploader::MESSAGE_UPLOADED, "$absolutePath2 - $relativePath2"))->shouldHaveBeenCalled(); + + $logger->notice(sprintf(BatchUploader::MESSAGE_STATUS, 2, 0))->shouldHaveBeenCalled(); + } + + function it_logs_an_error_if_any_of_the_image_uploads_fails( + ImageProvider $imageProvider, + Logger $logger, + Synchronizable $image1, + Synchronizable $image2 + ) { + $path1 = '/z/b/image1.jpg'; + $path2 = '/invalid'; + $relativePath1 = basename($path1); + $relativePath2 = basename($path2); + $absolutePath1 = self::MEDIA_PATH . $path1; + $absolutePath2 = self::MEDIA_PATH . $path2; + $apiImage1 = Image::fromPath($absolutePath1, $relativePath1); + $apiImage2 = Image::fromPath($absolutePath2, $relativePath2); + + $image1->getFilename()->willReturn($path1); + $image2->getFilename()->willReturn($path2); + $image1->getRelativePath()->willReturn($relativePath1); + $image2->getRelativePath()->willReturn($relativePath2); + + + $exception = new \Exception('Invalid file'); + + $images = array($image1, $image2); + + $imageProvider->upload($apiImage1)->shouldBeCalled(); + $imageProvider->upload($apiImage2)->willThrow($exception); + + $this->uploadImages($images); + + $image1->tagAsSynchronized()->shouldHaveBeenCalled(); + $image2->tagAsSynchronized()->shouldNotHaveBeenCalled(); + + $logger->error( + sprintf(BatchUploader::MESSAGE_UPLOAD_ERROR, $exception->getMessage(), "$absolutePath2 - $relativePath2") + )->shouldHaveBeenCalled(); + + $logger->notice(sprintf(BatchUploader::MESSAGE_STATUS, 1, 1))->shouldHaveBeenCalled(); + } + + + function it_stops_the_upload_process_if_task_is_stopped( + ImageProvider $imageProvider, + Task $migrationTask, + Logger $logger, + Synchronizable $image1, + Synchronizable $image2 + ) { + $path1 = '/z/b/image1.jpg'; + $path2 = '/invalid'; + $relativePath1 = basename($path1); + $relativePath2 = basename($path2); + $absolutePath1 = self::MEDIA_PATH . $path1; + $absolutePath2 = self::MEDIA_PATH . $path2; + $apiImage1 = Image::fromPath($absolutePath1, $relativePath1); + $apiImage2 = Image::fromPath($absolutePath2, $relativePath2); + + $image1->getFilename()->willReturn($path1); + $image2->getFilename()->willReturn($path2); + $image1->getRelativePath()->willReturn($relativePath1); + $image2->getRelativePath()->willReturn($relativePath2); + + $migrationTask->hasBeenStopped()->willReturn(false, true); + + $images = array($image1, $image2); + + $this->uploadImages($images); + + $imageProvider->upload($apiImage1)->shouldHaveBeenCalled(); + $image1->tagAsSynchronized()->shouldHaveBeenCalled(); + + $imageProvider->upload($apiImage2)->shouldNotHaveBeenCalled(); + $image2->tagAsSynchronized()->shouldNotHaveBeenCalled(); + + $logger->notice(sprintf(BatchUploader::MESSAGE_STATUS, 1, 0))->shouldHaveBeenCalled(); + } +} + diff --git a/core/spec/CloudinaryExtension/Migration/QueueSpec.php b/core/spec/CloudinaryExtension/Migration/QueueSpec.php new file mode 100644 index 0000000..0bbfbc8 --- /dev/null +++ b/core/spec/CloudinaryExtension/Migration/QueueSpec.php @@ -0,0 +1,71 @@ +beConstructedWith($migrationTask, $synchronizedMediaRepository, $batchUploader, $logger); + } + + function it_does_not_process_the_migration_queue_if_task_has_been_stopped( + Task $migrationTask, + SynchronizedMediaRepository $synchronizedMediaRepository, + Logger $logger + ) { + $migrationTask->hasBeenStopped()->willReturn(true); + + $synchronizedMediaRepository->findUnsynchronisedImages()->shouldNotBeCalled(); + $logger->notice(Argument::any())->shouldNotBeCalled(); + + $this->process(); + } + + + function it_processes_the_migration_queue_if_task_has_been_started( + Task $migrationTask, + SynchronizedMediaRepository $synchronizedMediaRepository, + Logger $logger, + BatchUploader $batchUploader + ) { + $migrationTask->hasBeenStopped()->willReturn(false); + $migrationTask->stop()->willReturn(); + + $logger->notice(Queue::MESSAGE_PROCESSING)->shouldBeCalled(); + $synchronizedMediaRepository->findUnsynchronisedImages()->willReturn(array('image1', 'image2')); + + $batchUploader->uploadImages(array('image1', 'image2'))->shouldBeCalled(); + + $this->process(); + } + + function it_stops_the_migration_task_if_there_is_nothing_left_to_process( + Task $migrationTask, + SynchronizedMediaRepository $synchronizedMediaRepository, + Logger $logger, + BatchUploader $batchUploader + ) { + $migrationTask->hasBeenStopped()->willReturn(false); + $synchronizedMediaRepository->findUnsynchronisedImages()->willReturn(array()); + + $logger->notice(Queue::MESSAGE_COMPLETE)->shouldBeCalled(); + $migrationTask->stop()->shouldBeCalled(); + + $batchUploader->uploadImages(Argument::any())->shouldNotBeCalled(); + + $this->process(); + } +} diff --git a/core/spec/CloudinaryExtension/Security/CloudinaryEnvironmentVariableSpec.php b/core/spec/CloudinaryExtension/Security/CloudinaryEnvironmentVariableSpec.php new file mode 100644 index 0000000..2c14797 --- /dev/null +++ b/core/spec/CloudinaryExtension/Security/CloudinaryEnvironmentVariableSpec.php @@ -0,0 +1,31 @@ +beConstructedThrough('fromString', array('CLOUDINARY_URL=cloudinary://aKey:aSecret@aCloud')); + } + + function it_should_extract_the_cloud_name_from_the_environment_variable() + { + $this->getCloud()->shouldBeLike(Cloud::fromName('aCloud')); + } + + function it_should_extract_the_credentials_from_the_environment_variable() + { + $credentials = Credentials::fromKeyAndSecret(Key::fromString('aKey'), Secret::fromString('aSecret')); + $this->getCredentials()->shouldBeLike($credentials); + } + +} diff --git a/core/spec/CloudinaryExtension/Security/KeySpec.php b/core/spec/CloudinaryExtension/Security/KeySpec.php new file mode 100644 index 0000000..ba14236 --- /dev/null +++ b/core/spec/CloudinaryExtension/Security/KeySpec.php @@ -0,0 +1,19 @@ +beConstructedThrough('fromString', ['secret_key']); + } + + function it_is_initializable() + { + $this->shouldHaveType('CloudinaryExtension\Security\Key'); + } +} diff --git a/core/spec/CloudinaryExtension/Security/SecretSpec.php b/core/spec/CloudinaryExtension/Security/SecretSpec.php new file mode 100644 index 0000000..02641f9 --- /dev/null +++ b/core/spec/CloudinaryExtension/Security/SecretSpec.php @@ -0,0 +1,19 @@ +beConstructedThrough('fromString', ['secret_secret']); + } + + function it_is_initializable() + { + $this->shouldHaveType('CloudinaryExtension\Security\Secret'); + } +} diff --git a/core/spec/CloudinaryExtension/UploadResponseValidatorSpec.php b/core/spec/CloudinaryExtension/UploadResponseValidatorSpec.php new file mode 100644 index 0000000..ea9aa26 --- /dev/null +++ b/core/spec/CloudinaryExtension/UploadResponseValidatorSpec.php @@ -0,0 +1,25 @@ + 0, 'test' => 'data']; + + $this->validateResponse($image, $response)->shouldReturn($response); + } + + function it_should_return_throw_exception_if_the_image_already_exists(Image $image) + { + $response = ['existing' => 1, 'test' => 'data']; + + $this->shouldThrow('CloudinaryExtension\Exception\MigrationError') + ->duringValidateResponse($image, $response); + } +} diff --git a/core/spec/CloudinaryExtension/UrlGeneratorSpec.php b/core/spec/CloudinaryExtension/UrlGeneratorSpec.php new file mode 100644 index 0000000..f41f0d0 --- /dev/null +++ b/core/spec/CloudinaryExtension/UrlGeneratorSpec.php @@ -0,0 +1,78 @@ +getFormatsToPreserve()->willReturn(['jpg', 'png']); + + $this->beConstructedWith($configuration, $imageProvider); + } + + function it_generates_url_from_given_path_and_transformation( + Image $image, + ImageProvider $imageProvider + ) + { + $transformation = Transformation::builder(); + $image->getExtension()->willReturn('gif'); + + $this->generateFor($image, $transformation); + + $imageProvider->retrieveTransformed($image, $transformation)->shouldHaveBeenCalled(); + } + + function it_removes_image_format_if_its_in_list_of_formats_to_preserve( + Image $image, + ImageProvider $imageProvider + ) + { + $transformation = Transformation::builder(); + $image->getExtension()->willReturn('jpg'); + + $this->generateFor($image, $transformation); + + $imageProvider->retrieveTransformed($image, $transformation->withoutFormat())->shouldHaveBeenCalled(); + } + + function it_generates_url_from_given_path_when_no_transformation_given( + Image $image, + ImageProvider $imageProvider, + ConfigurationInterface $configuration + ) + { + $transformation = Transformation::builder(); + $configuration->getDefaultTransformation()->willReturn($transformation); + + $image->getExtension()->willReturn('gif'); + + $this->generateFor($image); + + $imageProvider->retrieveTransformed($image, $transformation)->shouldHaveBeenCalled(); + } + + function it_does_not_modify_the_transformation_passed(Image $image) + { + $transformation = Transformation::builder(); + $image->getExtension()->willReturn('jpg'); + + $this->generateFor($image, $transformation); + + expect($transformation)->toBeLike(Transformation::builder()); + } +} diff --git a/magento2/.gitignore b/magento2/.gitignore new file mode 100644 index 0000000..5849395 --- /dev/null +++ b/magento2/.gitignore @@ -0,0 +1,6 @@ +bin/ +vendor/ +composer.phar +src/lib/Cloudinary/.gitignore +.cp-remote-env-settings.yml +cp-remote-logs/ diff --git a/magento2/Api/SynchronisationRepositoryInterface.php b/magento2/Api/SynchronisationRepositoryInterface.php new file mode 100644 index 0000000..8446a1b --- /dev/null +++ b/magento2/Api/SynchronisationRepositoryInterface.php @@ -0,0 +1,22 @@ +batchUploader = $batchUploader; + } + + /** + * Configure the command + * + * @return void + */ + protected function configure() + { + $this->setName('cloudinary:upload:all'); + $this->setDescription('Upload unsynchronised images'); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return void + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + try { + $this->batchUploader->uploadUnsynchronisedImages($output); + } catch (\Exception $e) { + $output->writeln($e->getMessage()); + } + } +} diff --git a/magento2/Model/BatchUploader.php b/magento2/Model/BatchUploader.php new file mode 100644 index 0000000..1fda783 --- /dev/null +++ b/magento2/Model/BatchUploader.php @@ -0,0 +1,99 @@ +imageRepository = $imageRepository; + $this->configuration = $configuration; + $this->migrationTask = $migrationTask; + $this->cloudinaryImageManager = $cloudinaryImageManager; + } + + /** + * Find unsynchronised images and upload them to cloudinary + * + * @param OutputInterface|null $output + * @return bool + * @throws \Exception + */ + public function uploadUnsynchronisedImages(OutputInterface $output = null) + { + if ($this->migrationTask->hasStarted()) { + $this->displayMessage($output, self::ERROR_MIGRATION_ALREADY_RUNNING); + return false; + } + + try { + $this->migrationTask->start(); + + $images = $this->imageRepository->findUnsynchronisedImages(); + foreach ($images as $image) { + $this->displayMessage($output, sprintf(self::MESSAGE_UPLOAD_IMAGE, $image)); + $this->cloudinaryImageManager->uploadAndSynchronise($image); + } + + $this->migrationTask->stop(); + $this->displayMessage($output, sprintf(self::MESSAGE_UPLOAD_COMPLETE, count($images))); + + return true; + + } catch (\Exception $e) { + $this->migrationTask->stop(); + throw $e; + } + } + + /** + * @param OutputInterface $output + * @param string $message + */ + private function displayMessage(OutputInterface $output, $message) + { + if ($output) { + $output->writeln($message); + } + } +} diff --git a/magento2/Model/Config/Source/Dropdown/Dpr.php b/magento2/Model/Config/Source/Dropdown/Dpr.php new file mode 100644 index 0000000..1726467 --- /dev/null +++ b/magento2/Model/Config/Source/Dropdown/Dpr.php @@ -0,0 +1,22 @@ + '1.0', + 'label' => '1.0', + ), + array( + 'value' => '2.0', + 'label' => '2.0', + ), + ); + } +} diff --git a/magento2/Model/Config/Source/Dropdown/Gravity.php b/magento2/Model/Config/Source/Dropdown/Gravity.php new file mode 100644 index 0000000..b1be8a9 --- /dev/null +++ b/magento2/Model/Config/Source/Dropdown/Gravity.php @@ -0,0 +1,70 @@ + '', + 'label' => 'Default', + ), + array( + 'value' => 'face', + 'label' => 'Face', + ), + array( + 'value' => 'faces', + 'label' => 'Faces', + ), + array( + 'value' => 'north_west', + 'label' => 'North West', + ), + array( + 'value' => 'north', + 'label' => 'North', + ), + array( + 'value' => 'north_east', + 'label' => 'North East', + ), + array( + 'value' => 'east', + 'label' => 'East', + ), + array( + 'value' => 'center', + 'label' => 'Center', + ), + array( + 'value' => 'west', + 'label' => 'West', + ), + array( + 'value' => 'south_west', + 'label' => 'South West', + ), + array( + 'value' => 'south', + 'label' => 'South', + ), + array( + 'value' => 'south_east', + 'label' => 'South East', + ), + array( + 'value' => 'face:center', + 'label' => 'Face (Center)', + ), + array( + 'value' => 'faces:center', + 'label' => 'Faces (Center)', + ), + ); + } +} diff --git a/magento2/Model/Config/Source/Dropdown/Quality.php b/magento2/Model/Config/Source/Dropdown/Quality.php new file mode 100644 index 0000000..1f3fd38 --- /dev/null +++ b/magento2/Model/Config/Source/Dropdown/Quality.php @@ -0,0 +1,50 @@ + '20', + 'label' => '20%', + ), + array( + 'value' => '30', + 'label' => '30%', + ), + array( + 'value' => '40', + 'label' => '40%', + ), + array( + 'value' => '50', + 'label' => '50%', + ), + array( + 'value' => '60', + 'label' => '60%', + ), + array( + 'value' => '70', + 'label' => '70%', + ), + array( + 'value' => '80', + 'label' => '80%', + ), + array( + 'value' => '90', + 'label' => '90%', + ), + array( + 'value' => '100', + 'label' => '100%', + ), + ); + } +} diff --git a/magento2/Model/Configuration.php b/magento2/Model/Configuration.php new file mode 100644 index 0000000..8189d0f --- /dev/null +++ b/magento2/Model/Configuration.php @@ -0,0 +1,202 @@ +configReader = $configReader; + $this->configWriter = $configWriter; + $this->decryptor = $decryptor; + } + + /** + * @return Cloud + */ + public function getCloud() + { + return $this->getEnvironmentVariable()->getCloud(); + } + + /** + * @return Credentials + */ + public function getCredentials() + { + return $this->getEnvironmentVariable()->getCredentials(); + } + + /** + * @return Transformation + */ + public function getDefaultTransformation() + { + return Transformation::builder() + ->withGravity(Gravity::fromString($this->getDefaultGravity())) + ->withQuality(Quality::fromString($this->getImageQuality())) + ->withDpr(Dpr::fromString($this->getImageDpr())); + } + + /** + * @return boolean + */ + public function getCdnSubdomainStatus() + { + return $this->configReader->isSetFlag(self::CONFIG_CDN_SUBDOMAIN); + } + + /** + * @return string + */ + public function getUserPlatform() + { + return sprintf(self::USER_PLATFORM_TEMPLATE, '1.0.0', '2.0.0'); + } + + /** + * @return UploadConfig + */ + public function getUploadConfig() + { + return UploadConfig::fromBooleanValues(self::USE_FILENAME, self::UNIQUE_FILENAME, self::OVERWRITE); + } + + /** + * @return boolean + */ + public function isEnabled() + { + return $this->configReader->isSetFlag(self::CONFIG_PATH_ENABLED); + } + + public function enable() + { + $this->configWriter->save(self::CONFIG_PATH_ENABLED, self::SCOPE_ID_ONE); + } + + public function disable() + { + $this->configWriter->save(self::CONFIG_PATH_ENABLED, self::SCOPE_ID_ZERO); + } + + /** + * @return array + */ + public function getFormatsToPreserve() + { + return ['png', 'webp', 'gif', 'svg']; + } + + public function getMigratedPath($file) + { + return $file; + } + + /** + * @return string + */ + public function getDefaultGravity() + { + return (string) $this->configReader->getValue(self::CONFIG_DEFAULT_GRAVITY); + } + + /** + * @return string + */ + public function getFetchFormat() + { + if ($this->configReader->isSetFlag(self::CONFIG_DEFAULT_FETCH_FORMAT)) { + return FetchFormat::FETCH_FORMAT_AUTO; + } + return ''; + } + + /** + * @return string + */ + public function getImageQuality() + { + return $this->configReader->getValue(self::CONFIG_DEFAULT_QUALITY); + } + + /** + * @return string + */ + public function getImageDpr() + { + return $this->configReader->getValue(self::CONFIG_DEFAULT_DPR); + } + + /** + * @return CloudinaryEnvironmentVariable + */ + private function getEnvironmentVariable() + { + if (is_null($this->environmentVariable)) { + $this->environmentVariable = CloudinaryEnvironmentVariable::fromString( + $this->decryptor->decrypt( + $this->configReader->getValue(self::CONFIG_PATH_ENVIRONMENT_VARIABLE) + ) + ); + } + return $this->environmentVariable; + } +} diff --git a/magento2/Model/ImageRepository.php b/magento2/Model/ImageRepository.php new file mode 100644 index 0000000..9fc2e60 --- /dev/null +++ b/magento2/Model/ImageRepository.php @@ -0,0 +1,82 @@ +mediaDirectory = $filesystem->getDirectoryRead(DirectoryList::MEDIA); + $this->synchronizationChecker = $synchronizationChecker; + } + + /** + * @return array + */ + public function findUnsynchronisedImages() + { + $images = []; + + foreach ($this->getRecursiveIterator($this->mediaDirectory->getAbsolutePath()) as $item) { + $absolutePath = $item->getRealPath(); + $relativePath = $this->mediaDirectory->getRelativePath($item->getRealPath()); + if ($this->isValidImageFile($item) && !$this->synchronizationChecker->isSynchronized($relativePath)) { + $images[] = Image::fromPath($absolutePath, $relativePath); + } + } + + return $images; + } + + /** + * @param $directory + * @return \RecursiveIteratorIterator + */ + private function getRecursiveIterator($directory) + { + return new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory), + \RecursiveIteratorIterator::SELF_FIRST + ); + } + + /** + * @param $item + * @return bool + */ + private function isValidImageFile($item) + { + return $item->isFile() && + strpos($item->getRealPath(), 'cache') === false && + strpos($item->getRealPath(), 'tmp') === false && + preg_match( + sprintf('#^[a-z0-9\.\-\_]+\.(?:%s)$#i', implode('|', $this->allowedImgExtensions)), + $item->getFilename() + ); + } +} diff --git a/magento2/Model/MigrationTask.php b/magento2/Model/MigrationTask.php new file mode 100644 index 0000000..0d8908d --- /dev/null +++ b/magento2/Model/MigrationTask.php @@ -0,0 +1,47 @@ +flagDir = $filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + } + + public function hasStarted() + { + return $this->flagDir->isExist(self::MIGRATION_RUNNING_FLAG_FILENAME); + } + + public function hasBeenStopped() + { + return !$this->hasStarted(); + } + + public function stop() + { + $this->flagDir->delete(self::MIGRATION_RUNNING_FLAG_FILENAME); + } + + public function start() + { + $this->flagDir->touch(self::MIGRATION_RUNNING_FLAG_FILENAME); + } +} diff --git a/magento2/Model/Observer/DeleteProductImage.php b/magento2/Model/Observer/DeleteProductImage.php new file mode 100644 index 0000000..d964047 --- /dev/null +++ b/magento2/Model/Observer/DeleteProductImage.php @@ -0,0 +1,45 @@ +productImageFinder = $productImageFinder; + $this->cloudinaryImageManager = $cloudinaryImageManager; + } + + /** + * @param Observer $observer + */ + public function execute(Observer $observer) + { + $product = $observer->getEvent()->getProduct(); + + foreach ($this->productImageFinder->findDeletedImages($product) as $image) { + $this->cloudinaryImageManager->removeAndUnSynchronise($image); + } + } +} diff --git a/magento2/Model/Observer/UploadProductImage.php b/magento2/Model/Observer/UploadProductImage.php new file mode 100644 index 0000000..3f699be --- /dev/null +++ b/magento2/Model/Observer/UploadProductImage.php @@ -0,0 +1,45 @@ +productImageFinder = $productImageFinder; + $this->cloudinaryImageManager = $cloudinaryImageManager; + } + + /** + * @param Observer $observer + */ + public function execute(Observer $observer) + { + $product = $observer->getEvent()->getProduct(); + + foreach ($this->productImageFinder->findNewImages($product) as $image) { + $this->cloudinaryImageManager->uploadAndSynchronise($image); + } + } +} diff --git a/magento2/Model/ProductImageFinder.php b/magento2/Model/ProductImageFinder.php new file mode 100644 index 0000000..bc4483f --- /dev/null +++ b/magento2/Model/ProductImageFinder.php @@ -0,0 +1,63 @@ +imageCreator = $imageCreator; + } + + /** + * @param Product $product + * + * @return \CloudinaryExtension\Image[] + */ + public function findNewImages(Product $product) + { + return $this->find($product, new NewImageFilter()); + } + + /** + * @param Product $product + * + * @return \CloudinaryExtension\Image[] + */ + public function findDeletedImages(Product $product) + { + return $this->find($product, new DeletedImageFilter()); + } + + /** + * @param Product $product + * @param ImageFilter $filter + * + * @return \CloudinaryExtension\Image[] + */ + private function find(Product $product, ImageFilter $filter) + { + return array_map($this->imageCreator, array_filter( + $product->getMediaGallery('images'), + $filter + )); + } +} diff --git a/magento2/Model/ProductImageFinder/DeletedImageFilter.php b/magento2/Model/ProductImageFinder/DeletedImageFilter.php new file mode 100644 index 0000000..32e8f0c --- /dev/null +++ b/magento2/Model/ProductImageFinder/DeletedImageFilter.php @@ -0,0 +1,15 @@ +mediaDirectory = $filesystem->getDirectoryRead(DirectoryList::MEDIA); + $this->baseMediaPath = $mediaConfig->getBaseMediaPath(); + } + + /** + * @param array $imageData + * + * @return Image + */ + public function __invoke(array $imageData) + { + $fullPath = $this->baseMediaPath . $imageData['file']; + + return Image::fromPath( + $this->mediaDirectory->getAbsolutePath($fullPath), + $fullPath + ); + } +} \ No newline at end of file diff --git a/magento2/Model/ProductImageFinder/ImageFilter.php b/magento2/Model/ProductImageFinder/ImageFilter.php new file mode 100644 index 0000000..48670a6 --- /dev/null +++ b/magento2/Model/ProductImageFinder/ImageFilter.php @@ -0,0 +1,16 @@ +_init('cloudinary_synchronisation', 'cloudinary_synchronisation_id'); + } +} diff --git a/magento2/Model/ResourceModel/Synchronisation/Collection.php b/magento2/Model/ResourceModel/Synchronisation/Collection.php new file mode 100644 index 0000000..6a4be56 --- /dev/null +++ b/magento2/Model/ResourceModel/Synchronisation/Collection.php @@ -0,0 +1,15 @@ +_init(SynchronisationModel::class, SynchronisationResourceModel::class); + } +} diff --git a/magento2/Model/Synchronisation.php b/magento2/Model/Synchronisation.php new file mode 100644 index 0000000..f1c16fd --- /dev/null +++ b/magento2/Model/Synchronisation.php @@ -0,0 +1,40 @@ +_init(SynchronisationResourceModel::class); + } + + public function setImagePath($imagePath) + { + return $this->setData('image_path', $imagePath); + } + + public function getImagePath() + { + return $this->getData('image_path'); + } + + public function getFilename() + { + return basename($this->getImagePath()); + } + + public function getRelativePath() + { + return $this->getImagePath(); + } + + public function tagAsSynchronized() + { + $this->save(); + } +} diff --git a/magento2/Model/SynchronisationChecker.php b/magento2/Model/SynchronisationChecker.php new file mode 100644 index 0000000..bf37a30 --- /dev/null +++ b/magento2/Model/SynchronisationChecker.php @@ -0,0 +1,35 @@ +synchronisationRepository = $synchronisationRepository; + } + + /** + * @param $imageName + * @return bool + */ + public function isSynchronized($imageName) + { + if (!$imageName) { + return false; + } + + return $this->synchronisationRepository->getListByImagePath($imageName)->getTotalCount() > 0; + } +} diff --git a/magento2/Model/SynchronisationRepository.php b/magento2/Model/SynchronisationRepository.php new file mode 100644 index 0000000..22a080e --- /dev/null +++ b/magento2/Model/SynchronisationRepository.php @@ -0,0 +1,203 @@ +filterBuilder = $filterBuilder; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->collectionFactory = $collectionFactory; + $this->searchResult = $searchResult; + $this->searchResultsFactory = $searchResultsFactory; + $this->synchronisationFactory = $synchronisationFactory; + } + + /** + * Retrieve data which match a specified criteria. + * + * @api + * + * @param SearchCriteriaInterface $searchCriteria + * @return SearchResultsInterface + */ + public function getList(SearchCriteriaInterface $searchCriteria) + { + $collection = $this->collectionFactory->create(); + + $this->setFilters($searchCriteria, $collection); + $this->setSortOrder($searchCriteria, $collection); + $this->setPageSize($searchCriteria, $collection); + $this->setCurrentPage($searchCriteria, $collection); + + $searchResult = $this->searchResultsFactory->create(); + $searchResult->setSearchCriteria($searchCriteria); + $searchResult->setItems($collection->getItems()); + $searchResult->setTotalCount($collection->getSize()); + + return $searchResult; + } + + /** + * @param string $imagePath + * + * @return SearchResultsInterface + */ + public function getListByImagePath($imagePath) + { + $this->searchCriteriaBuilder->addFilters([$this->createImagePathFilter($imagePath)]); + + return $this->getList($this->searchCriteriaBuilder->create()); + } + + /** + * @param string $imagePath + */ + public function saveAsSynchronized($imagePath) + { + $this->synchronisationFactory->create() + ->setImagePath($imagePath) + ->tagAsSynchronized(); + } + + /** + * @param string $imagePath + */ + public function removeSynchronised($imagePath) + { + $result = $this->getListByImagePath($imagePath); + + foreach ($result->getItems() as $item) { + $item->delete(); + } + } + + /** + * Create image name filter + * + * @param string $imagePath + * @return \Magento\Framework\Api\Filter + */ + private function createImagePathFilter($imagePath) + { + $this->filterBuilder->setField('image_path'); + $this->filterBuilder->setConditionType('eq'); + $this->filterBuilder->setValue($imagePath); + + return $this->filterBuilder->create(); + } + + /** + * @param SearchCriteriaInterface $searchCriteria + * @param SynchronisationCollection $collection + */ + private function setFilters(SearchCriteriaInterface $searchCriteria, $collection) + { + foreach ($searchCriteria->getFilterGroups() as $filterGroup) { + foreach ($filterGroup->getFilters() as $filter) { + $collection->addFieldToFilter( + $filter->getField(), + [$filter->getConditionType() => $filter->getValue()] + ); + } + } + } + + /** + * @param SearchCriteriaInterface $searchCriteria + * @param SynchronisationCollection $collection + */ + private function setSortOrder(SearchCriteriaInterface $searchCriteria, $collection) + { + if ($searchCriteria->getSortOrders()) { + foreach ($searchCriteria->getSortOrders() as $sortOrder) { + $collection->addOrder( + $sortOrder->getField(), + $sortOrder->getDirection() === SearchCriteriaInterface::SORT_ASC ? 'ASC' : 'DESC' + ); + } + } + } + + /** + * @param SearchCriteriaInterface $searchCriteria + * @param SynchronisationCollection $collection + */ + private function setPageSize(SearchCriteriaInterface $searchCriteria, $collection) + { + if ($searchCriteria->getPageSize()) { + $collection->setPageSize($searchCriteria->getPageSize()); + } + } + + /** + * @param SearchCriteriaInterface $searchCriteria + * @param SynchronisationCollection $collection + */ + private function setCurrentPage(SearchCriteriaInterface $searchCriteria, $collection) + { + if ($searchCriteria->getCurrentPage()) { + $collection->setCurPage($searchCriteria->getCurrentPage()); + } + } +} diff --git a/magento2/Model/Template/Filter.php b/magento2/Model/Template/Filter.php new file mode 100644 index 0000000..0f41e69 --- /dev/null +++ b/magento2/Model/Template/Filter.php @@ -0,0 +1,106 @@ +imageFactory = $imageFactory; + $this->urlGenerator = $urlGenerator; + + parent::__construct( + $string, + $logger, + $escaper, + $assetRepo, + $scopeConfig, + $coreVariableFactory, + $storeManager, + $layout, + $layoutFactory, + $appState, + $urlModel, + $emogrifier, + $configVariables, + $widgetResource, + $widget + ); + } + + /** + * Retrieve media file URL directive + * + * @param string[] $construction + * @return string + */ + public function mediaDirective($construction) + { + $params = $this->getParameters($construction[2]); + $storeManager = $this->_storeManager; + + $image = $this->imageFactory->build( + $params['url'], + function() use ($storeManager, $params) { + return sprintf( + '%s%s', + $storeManager->getStore()->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA), + $params['url'] + ); + } + ); + + return $this->urlGenerator->generateFor($image); + } +} diff --git a/magento2/Plugin/FileRemover.php b/magento2/Plugin/FileRemover.php new file mode 100644 index 0000000..8b31453 --- /dev/null +++ b/magento2/Plugin/FileRemover.php @@ -0,0 +1,50 @@ +cloudinaryImageManager = $cloudinaryImageManager; + $this->mediaDirectory = $filesystem->getDirectoryRead(DirectoryList::MEDIA); + } + + /** + * Delete file (and its thumbnail if exists) from storage + * + * @param string $target File path to be deleted + * @return $this + */ + public function beforeDeleteFile(Storage $storage, $target) + { + $this->cloudinaryImageManager->removeAndUnSynchronise( + Image::fromPath($target, $this->mediaDirectory->getRelativePath($target)) + ); + + return [$target]; + } +} diff --git a/magento2/Plugin/FileUploader.php b/magento2/Plugin/FileUploader.php new file mode 100644 index 0000000..8992fb6 --- /dev/null +++ b/magento2/Plugin/FileUploader.php @@ -0,0 +1,81 @@ +cloudinaryImageManager = $cloudinaryImageManager; + $this->directoryList = $directoryList; + } + + /** + * @param Uploader $uploader + * @param array $result + * @return array + */ + public function afterSave(Uploader $uploader, $result) + { + $filepath = $this->absolutePath($result); + + if (!$this->isTemporaryPath($filepath)) { + + $this->cloudinaryImageManager->uploadAndSynchronise( + Image::fromPath($filepath, $this->mediaRelativePath($filepath)) + ); + + } + + return $result; + } + + /** + * @param string $filepath + * @return bool + */ + protected function isTemporaryPath($filepath) + { + return strpos($filepath, sprintf('%s/tmp', $this->directoryList->getPath('media'))) === 0; + } + + /** + * @param array $result + * @return string + */ + protected function absolutePath(array $result) + { + return sprintf('%s%s%s', $result['path'], DIRECTORY_SEPARATOR, $result['file']); + } + + /** + * @param string $filepath + * @return string + */ + protected function mediaRelativePath($filepath) + { + $mediaPath = $this->directoryList->getPath('media') . DIRECTORY_SEPARATOR; + return (strpos($filepath, $mediaPath) === 0) ? str_replace($mediaPath, '', $filepath) : $filepath; + } +} diff --git a/magento2/Plugin/ImageHelper.php b/magento2/Plugin/ImageHelper.php new file mode 100644 index 0000000..7858d7f --- /dev/null +++ b/magento2/Plugin/ImageHelper.php @@ -0,0 +1,145 @@ +imageFactory = $imageFactory; + $this->urlGenerator = $urlGenerator; + $this->configuration = $configuration; + $this->dimensions = null; + $this->imageFile = null; + } + + /** + * @param CatalogImageHelper $helper + * @param ProductInterface $product + * @param string $imageId + * @param array $attributes + * + * @return array + */ + public function beforeInit(CatalogImageHelper $helper, $product, $imageId, $attributes = []) + { + $this->product = $product; + $this->dimensions = null; + $this->imageFile = null; + $this->keepFrame = true; + return [$product, $imageId, $attributes]; + } + + /** + * @param CatalogImageHelper $helper + * @param string $file + * + * @return string[] + */ + public function beforeSetImageFile(CatalogImageHelper $helper, $file) + { + $this->imageFile = $file; + return [$file]; + } + + /** + * @param CatalogImageHelper $helper + * @param int $width + * @param int $height + * + * @return array + */ + public function beforeResize(CatalogImageHelper $helper, $width, $height = null) + { + $this->dimensions = Dimensions::fromWidthAndHeight($width, $height); + + return [$width, $height]; + } + + /** + * @param CatalogImageHelper $helper + * @param bool $flag + */ + public function beforeKeepFrame(CatalogImageHelper $helper, $flag) + { + $this->keepFrame = (bool)$flag; + } + + /** + * @param CatalogImageHelper $helper + * @param \Closure $originalMethod + * + * @return string + */ + public function aroundGetUrl(CatalogImageHelper $helper, \Closure $originalMethod) + { + $image = $this->imageFactory->build( + sprintf('catalog/product%s', $this->imageFile ?: $this->product->getData($helper->getType())), + $originalMethod + ); + + $dimensions = $this->dimensions ?: Dimensions::fromWidthAndHeight($helper->getWidth(), $helper->getHeight()); + + $transform = $this->configuration->getDefaultTransformation()->withDimensions($dimensions); + + if ($this->keepFrame) { + $transform->withCrop(Crop::fromString('pad')) + ->withDimensions(Dimensions::squareMissingDimension($dimensions)); + } else { + $transform->withCrop(Crop::fromString('fit')); + } + + return $this->urlGenerator->generateFor($image, $transform); + } +} diff --git a/magento2/Plugin/MediaConfig.php b/magento2/Plugin/MediaConfig.php new file mode 100644 index 0000000..5dd30b2 --- /dev/null +++ b/magento2/Plugin/MediaConfig.php @@ -0,0 +1,47 @@ +imageFactory = $imageFactory; + $this->urlGenerator = $urlGenerator; + } + + /** + * @param CatalogMediaConfig $mediaConfig + * @param \Closure $originalMethod + * @param string $file + * + * @return string + */ + public function aroundGetMediaUrl(CatalogMediaConfig $mediaConfig, \Closure $originalMethod, $file) + { + $image = $this->imageFactory->build( + $mediaConfig->getBaseMediaPath() . $file, + function() use ($originalMethod, $file) { return $originalMethod($file); } + ); + + return $this->urlGenerator->generateFor($image); + } +} diff --git a/magento2/README.md b/magento2/README.md new file mode 100644 index 0000000..69a39e9 --- /dev/null +++ b/magento2/README.md @@ -0,0 +1 @@ +# cloudinary-magento2 \ No newline at end of file diff --git a/magento2/Setup/InstallSchema.php b/magento2/Setup/InstallSchema.php new file mode 100644 index 0000000..500da40 --- /dev/null +++ b/magento2/Setup/InstallSchema.php @@ -0,0 +1,43 @@ +startSetup(); + + /** + * Create table 'cloudinary_synchronisation' + */ + $table = $setup->getConnection()->newTable( + $setup->getTable('cloudinary_synchronisation') + )->addColumn( + 'cloudinary_synchronisation_id', + Table::TYPE_INTEGER, + null, + ['identity' => true, 'nullable' => false, 'primary' => true, 'unsigned' => true], + 'Cloudinary Synchronisation ID' + )->addColumn( + 'image_path', + Table::TYPE_TEXT, + 255, + ['nullable' => false], + 'Image Path' + ); + + $setup->getConnection()->createTable($table); + + $setup->endSetup(); + } +} diff --git a/magento2/behat.yml b/magento2/behat.yml new file mode 100644 index 0000000..4101e21 --- /dev/null +++ b/magento2/behat.yml @@ -0,0 +1,41 @@ +default: + suites: + ui: + paths: + features: ../cloudinary-core/features + bootstrap: features/bootstrap + filters: { tags: '@critical,@ui' } + contexts: + - FeatureContext + + extensions: + Bex\Behat\Magento2InitExtension: + magento_bootstrap_path: /vagrant/app/bootstrap.php + Bex\Behat\BrowserInitialiserExtension: + close_browser_after_scenario: true + browser_window_size: max + Bex\Behat\Magento2InitExtension: + magento_configs: + - + path: 'admin/security/use_form_key' + value: 0 + Bex\Behat\ScreenshotExtension: + image_drivers: + local: + screenshot_directory: /vagrant + SensioLabs\Behat\PageObjectExtension: ~ + Behat\MinkExtension\ServiceContainer\MinkExtension: + base_url: 'http://magento2.dev/' + goutte: + guzzle_parameters: + curl.options: + CURLOPT_SSL_VERIFYPEER: false + CURLOPT_CERTINFO: false + CURLOPT_TIMEOUT: 120 + ssl.certificate_authority: false + selenium2: + wd_host: http://localhost:4444/wd/hub + browser: phantomjs +# command to open the failing html pages: + show_cmd: echo '%s' + show_tmp_dir: /vagrant diff --git a/magento2/composer.json b/magento2/composer.json new file mode 100644 index 0000000..a49a4a2 --- /dev/null +++ b/magento2/composer.json @@ -0,0 +1,53 @@ +{ + "name": "inviqa/cloudinary-magento2", + "description": "Cloudinary Magento 2 Integration.", + "type": "magento2-module", + "version": "1.0.0", + "minimum-stability": "dev", + "require": { + "php": ">=5.5", + "inviqa/cloudinary-core": "1.1.0" + }, + "require-dev": { + "phpspec/phpspec": "^3.0", + "behat/behat": "~3.0.15", + "sensiolabs/behat-page-object-extension": "2.0.*@dev", + "behat/mink-selenium2-driver": "*", + "behat/mink-goutte-driver": "^1.0", + "squizlabs/php_codesniffer": "1.*", + "phpmd/phpmd": "1.*", + "sebastian/phpcpd": "2.*", + "pdepend/pdepend": "1.*", + "phploc/phploc": "2.*", + "theseer/phpdox": "0.6.*", + "theseer/fxsl": "1.0.*@dev", + "covex-nn/phpcb": "1.0.*@dev", + "bossa/phpspec2-expect": "^2.0", + "bex/behat-magento2-init": "dev-master", + "bex/behat-browser-initialiser": "^1.0", + "bex/behat-screenshot": "^1.0" + }, + "config": { + "bin-dir": "bin", + "use-include-path": true + }, + "repositories": [ + { + "type": "git", + "url": "git@github.com:inviqa/cloudinary-core.git" + } + ], + "autoload-dev": { + "psr-0": { + "": [ + "features/bootstrap", + "features/fixtures", + "/app/app/code/" + ] + }, + "psr-4": { + "Magento\\Framework\\": "/app/vendor/magento/framework/", + "Magento\\Catalog\\": "/app/vendor/magento/module-catalog" + } + } +} diff --git a/magento2/composer.lock b/magento2/composer.lock new file mode 100644 index 0000000..61e4a38 --- /dev/null +++ b/magento2/composer.lock @@ -0,0 +1,3899 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "1e3dc60115a4782ab9fb942f3364f13c", + "content-hash": "104b0bac1900d9c84ff5e0f9476c8f92", + "packages": [ + { + "name": "cloudinary/cloudinary_php", + "version": "1.6.2", + "source": { + "type": "git", + "url": "https://github.com/cloudinary/cloudinary_php.git", + "reference": "8b89be228b39bcdb36d5e642e9796c756760737e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cloudinary/cloudinary_php/zipball/8b89be228b39bcdb36d5e642e9796c756760737e", + "reference": "8b89be228b39bcdb36d5e642e9796c756760737e", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "4.7.*" + }, + "type": "library", + "autoload": { + "classmap": [ + "src" + ], + "files": [ + "src/Helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Cloudinary", + "homepage": "https://github.com/cloudinary/cloudinary_php/graphs/contributors" + } + ], + "description": "Cloudinary PHP SDK", + "homepage": "https://github.com/cloudinary/cloudinary_php", + "keywords": [ + "cdn", + "cloud", + "cloudinary", + "image management", + "sdk" + ], + "time": "2017-02-23 01:10:18" + }, + { + "name": "inviqa/cloudinary-core", + "version": "1.2.1", + "source": { + "type": "git", + "url": "git@github.com:inviqa/cloudinary-core.git", + "reference": "315636abf5d9fe2c10dd06eae5b2684c4d980603" + }, + "require": { + "cloudinary/cloudinary_php": "~1.6.0", + "php": ">=5.4.0" + }, + "require-dev": { + "behat/mink-goutte-driver": "^1.0", + "behat/mink-selenium2-driver": "*", + "bossa/phpspec2-expect": "1.0.3", + "mayflower/php-codebrowser": "^1.1", + "phpspec/phpspec": "^2.4.0", + "phpunit/phpunit": "3.7.*", + "sensiolabs/behat-page-object-extension": "*@dev", + "squizlabs/php_codesniffer": "1.*" + }, + "type": "cloudinary-core", + "autoload": { + "psr-0": { + "": [ + "features/bootstrap", + "lib" + ] + } + }, + "license": [ + "proprietary" + ], + "description": "Cloudinary Core.", + "time": "2017-03-23 15:22:25" + } + ], + "packages-dev": [ + { + "name": "behat/behat", + "version": "v3.0.15", + "source": { + "type": "git", + "url": "https://github.com/Behat/Behat.git", + "reference": "b35ae3d45332d80c532af69cc36f780a9397a996" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Behat/zipball/b35ae3d45332d80c532af69cc36f780a9397a996", + "reference": "b35ae3d45332d80c532af69cc36f780a9397a996", + "shasum": "" + }, + "require": { + "behat/gherkin": "~4.3", + "behat/transliterator": "~1.0", + "ext-mbstring": "*", + "php": ">=5.3.3", + "symfony/class-loader": "~2.1", + "symfony/config": "~2.3", + "symfony/console": "~2.1", + "symfony/dependency-injection": "~2.1", + "symfony/event-dispatcher": "~2.1", + "symfony/translation": "~2.3", + "symfony/yaml": "~2.1" + }, + "require-dev": { + "phpspec/prophecy-phpunit": "~1.0", + "phpunit/phpunit": "~4.0", + "symfony/process": "~2.1" + }, + "suggest": { + "behat/mink-extension": "for integration with Mink testing framework", + "behat/symfony2-extension": "for integration with Symfony2 web framework", + "behat/yii-extension": "for integration with Yii web framework" + }, + "bin": [ + "bin/behat" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Behat": "src/", + "Behat\\Testwork": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Scenario-oriented BDD framework for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "Agile", + "BDD", + "ScenarioBDD", + "Scrum", + "StoryBDD", + "User story", + "business", + "development", + "documentation", + "examples", + "symfony", + "testing" + ], + "time": "2015-02-22 14:10:33" + }, + { + "name": "behat/gherkin", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Behat/Gherkin.git", + "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/5c14cff4f955b17d20d088dec1bde61c0539ec74", + "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "~4.5|~5", + "symfony/phpunit-bridge": "~2.7|~3", + "symfony/yaml": "~2.3|~3" + }, + "suggest": { + "symfony/yaml": "If you want to parse features, represented in YAML files" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Gherkin": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Gherkin DSL parser for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "BDD", + "Behat", + "Cucumber", + "DSL", + "gherkin", + "parser" + ], + "time": "2016-10-30 11:50:56" + }, + { + "name": "behat/mink", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/minkphp/Mink.git", + "reference": "9ea1cebe3dc529ba3861d87c818f045362c40484" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/Mink/zipball/9ea1cebe3dc529ba3861d87c818f045362c40484", + "reference": "9ea1cebe3dc529ba3861d87c818f045362c40484", + "shasum": "" + }, + "require": { + "php": ">=5.3.1", + "symfony/css-selector": "~2.1|~3.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7|~3.0" + }, + "suggest": { + "behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)", + "behat/mink-goutte-driver": "fast headless driver for any app without JS emulation", + "behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)", + "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Browser controller/emulator abstraction for PHP", + "homepage": "http://mink.behat.org/", + "keywords": [ + "browser", + "testing", + "web" + ], + "time": "2017-02-06 09:59:54" + }, + { + "name": "behat/mink-browserkit-driver", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkBrowserKitDriver.git", + "reference": "1c9c8ad8838af33448d10baa57658b4cb55f23d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/1c9c8ad8838af33448d10baa57658b4cb55f23d6", + "reference": "1c9c8ad8838af33448d10baa57658b4cb55f23d6", + "shasum": "" + }, + "require": { + "behat/mink": "^1.7.1@dev", + "php": ">=5.3.6", + "symfony/browser-kit": "~2.3|~3.0", + "symfony/dom-crawler": "~2.3|~3.0" + }, + "require-dev": { + "mink/driver-testsuite": "dev-master", + "symfony/http-kernel": "~2.3|~3.0" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Symfony2 BrowserKit driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "Mink", + "Symfony2", + "browser", + "testing" + ], + "time": "2016-10-03 08:27:03" + }, + { + "name": "behat/mink-extension", + "version": "v2.2", + "source": { + "type": "git", + "url": "https://github.com/Behat/MinkExtension.git", + "reference": "5b4bda64ff456104564317e212c823e45cad9d59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/5b4bda64ff456104564317e212c823e45cad9d59", + "reference": "5b4bda64ff456104564317e212c823e45cad9d59", + "shasum": "" + }, + "require": { + "behat/behat": "~3.0,>=3.0.5", + "behat/mink": "~1.5", + "php": ">=5.3.2", + "symfony/config": "~2.2|~3.0" + }, + "require-dev": { + "behat/mink-goutte-driver": "~1.1", + "phpspec/phpspec": "~2.0" + }, + "type": "behat-extension", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\MinkExtension": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + }, + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com" + } + ], + "description": "Mink extension for Behat", + "homepage": "http://extensions.behat.org/mink", + "keywords": [ + "browser", + "gui", + "test", + "web" + ], + "time": "2016-02-15 07:55:18" + }, + { + "name": "behat/mink-goutte-driver", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkGoutteDriver.git", + "reference": "7a4b2d49511865e23d61463514fa2754d42ec658" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkGoutteDriver/zipball/7a4b2d49511865e23d61463514fa2754d42ec658", + "reference": "7a4b2d49511865e23d61463514fa2754d42ec658", + "shasum": "" + }, + "require": { + "behat/mink-browserkit-driver": "~1.2@dev", + "fabpot/goutte": "~1.0.4|~2.0|~3.1", + "php": ">=5.3.1" + }, + "require-dev": { + "mink/driver-testsuite": "dev-master" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Goutte driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "browser", + "goutte", + "headless", + "testing" + ], + "time": "2016-10-14 20:19:06" + }, + { + "name": "behat/mink-selenium2-driver", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkSelenium2Driver.git", + "reference": "739b7570f0536bad9b07b511a62c885ee1ec029a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/739b7570f0536bad9b07b511a62c885ee1ec029a", + "reference": "739b7570f0536bad9b07b511a62c885ee1ec029a", + "shasum": "" + }, + "require": { + "behat/mink": "~1.7@dev", + "instaclick/php-webdriver": "~1.1", + "php": ">=5.3.1" + }, + "require-dev": { + "mink/driver-testsuite": "dev-master" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Pete Otaqui", + "email": "pete@otaqui.com", + "homepage": "https://github.com/pete-otaqui" + } + ], + "description": "Selenium2 (WebDriver) driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "ajax", + "browser", + "javascript", + "selenium", + "testing", + "webdriver" + ], + "time": "2017-02-06 08:22:23" + }, + { + "name": "behat/transliterator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Behat/Transliterator.git", + "reference": "02fa32d26934f99cb8a3eff2fe065a1fd53f847b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Transliterator/zipball/02fa32d26934f99cb8a3eff2fe065a1fd53f847b", + "reference": "02fa32d26934f99cb8a3eff2fe065a1fd53f847b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "chuyskywalker/rolling-curl": "^3.1", + "php-yaoi/php-yaoi": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Transliterator": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Artistic-1.0" + ], + "description": "String transliterator", + "keywords": [ + "i18n", + "slug", + "transliterator" + ], + "time": "2016-10-21 08:46:05" + }, + { + "name": "bex/behat-browser-initialiser", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/tkotosz/behat-browser-initialiser.git", + "reference": "8fe7d7771159673d13300b4c05102746a8dbf07c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tkotosz/behat-browser-initialiser/zipball/8fe7d7771159673d13300b4c05102746a8dbf07c", + "reference": "8fe7d7771159673d13300b4c05102746a8dbf07c", + "shasum": "" + }, + "require": { + "behat/behat": "^3.0.0", + "behat/mink-extension": "^2.0.0", + "php": ">=5.4" + }, + "require-dev": { + "behat/mink-selenium2-driver": "^1.3.0", + "bex/behat-test-runner": "^1.2.1", + "jakoch/phantomjs-installer": "^2.1.1", + "phpspec/phpspec": "^2.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tibor Kotosz", + "email": "kotosy@gmail.com", + "homepage": "https://github.com/tkotosz", + "role": "Developer" + } + ], + "description": "Extension for behat to help configure the browser", + "homepage": "https://github.com/tkotosz/behat-browser-initialiser", + "keywords": [ + "BDD", + "Behat", + "TDD" + ], + "time": "2016-06-13 11:09:22" + }, + { + "name": "bex/behat-extension-driver-locator", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/tkotosz/behat-extension-driver-locator.git", + "reference": "af9fb11f5f3cc220ee2c08071ee9d50f11048b86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tkotosz/behat-extension-driver-locator/zipball/af9fb11f5f3cc220ee2c08071ee9d50f11048b86", + "reference": "af9fb11f5f3cc220ee2c08071ee9d50f11048b86", + "shasum": "" + }, + "require": { + "behat/behat": "^3.0.0", + "php": ">=5.4" + }, + "require-dev": { + "phpspec/phpspec": "2.4.0-alpha2" + }, + "type": "library", + "autoload": { + "psr-0": { + "": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tibor Kotosz", + "email": "kotosy@gmail.com", + "homepage": "https://github.com/tkotosz", + "role": "Developer" + } + ], + "description": "Driver locator tool for behat extensions", + "homepage": "https://github.com/tkotosz/behat-extension-driver-locator", + "keywords": [ + "BDD", + "Behat", + "TDD" + ], + "time": "2015-12-17 13:26:09" + }, + { + "name": "bex/behat-magento2-init", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/tkotosz/behat-magento2-init.git", + "reference": "7ed7fe215d3cf64c6827c604dae83bb1288633f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tkotosz/behat-magento2-init/zipball/7ed7fe215d3cf64c6827c604dae83bb1288633f7", + "reference": "7ed7fe215d3cf64c6827c604dae83bb1288633f7", + "shasum": "" + }, + "require": { + "behat/behat": "^3.0.0", + "php": ">=5.4" + }, + "type": "library", + "autoload": { + "psr-0": { + "": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tibor Kotosz", + "email": "kotosy@gmail.com", + "homepage": "https://github.com/tkotosz", + "role": "Developer" + } + ], + "description": "Provides access to magento2 object manager from behat and allows to change magento config settings temporarly", + "homepage": "https://github.com/tkotosz/behat-magento2-init", + "keywords": [ + "BDD", + "Behat", + "TDD", + "magento2" + ], + "time": "2017-01-06 11:09:19" + }, + { + "name": "bex/behat-screenshot", + "version": "1.2.6", + "source": { + "type": "git", + "url": "https://github.com/elvetemedve/behat-screenshot.git", + "reference": "1e2f704a5dd26b679953d6aff9a1add7ec978f81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/elvetemedve/behat-screenshot/zipball/1e2f704a5dd26b679953d6aff9a1add7ec978f81", + "reference": "1e2f704a5dd26b679953d6aff9a1add7ec978f81", + "shasum": "" + }, + "require": { + "behat/behat": "^3.0.0", + "behat/mink-extension": "^2.0.0", + "bex/behat-extension-driver-locator": "^1.0.2", + "php": ">=5.4", + "symfony/filesystem": "^2.7|^3.0", + "symfony/finder": "^2.7|^3.0" + }, + "require-dev": { + "behat/mink-selenium2-driver": "^1.3.0", + "bex/behat-screenshot-image-driver-dummy": "^1.0", + "bex/behat-test-runner": "^1.2.1", + "jakoch/phantomjs-installer": "^2.1.1-p07", + "phpspec/phpspec": "^2.5" + }, + "suggest": { + "bex/behat-screenshot-image-driver-img42": "Allows to upload the screenshot to img42.com", + "bex/behat-screenshot-image-driver-unsee": "Allows to upload the screenshot to unsee.cc", + "bex/behat-screenshot-image-driver-uploadpie": "Allows to upload the screenshot to uploadpie.com" + }, + "type": "library", + "autoload": { + "psr-0": { + "Bex\\Behat\\ScreenshotExtension\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Geza Buza", + "email": "bghome@gmail.com", + "homepage": "https://twitter.com/medve540", + "role": "Developer" + }, + { + "name": "Tibor Kotosz", + "email": "kotosy@gmail.com", + "homepage": "https://github.com/tkotosz", + "role": "Developer" + } + ], + "description": "Extension for behat to help debug failing scenarios", + "homepage": "https://github.com/elvetemedve/behat-screenshot", + "keywords": [ + "BDD", + "Behat", + "TDD", + "behat-screenshot" + ], + "time": "2016-11-26 16:45:39" + }, + { + "name": "bossa/phpspec2-expect", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/BossaConsulting/phpspec2-expect.git", + "reference": "2a753ad2160a02725711c1cb4f4bf55d43512d60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/BossaConsulting/phpspec2-expect/zipball/2a753ad2160a02725711c1cb4f4bf55d43512d60", + "reference": "2a753ad2160a02725711c1cb4f4bf55d43512d60", + "shasum": "" + }, + "require": { + "phpspec/phpspec": "~3.0 <3.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.4" + }, + "type": "library", + "autoload": { + "files": [ + "expect.php" + ], + "psr-0": { + "Bossa\\PhpSpec\\Expect\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marcello Duarte", + "homepage": "http://marcelloduarte.net/" + } + ], + "description": "Helper that decorates any SUS with a phpspec lazy object wrapper", + "keywords": [ + "BDD", + "SpecBDD", + "TDD", + "spec", + "specification" + ], + "time": "2016-11-23 16:57:12" + }, + { + "name": "covex-nn/phpcb", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/covex-nn/PHP_CodeBrowser.git", + "reference": "f03291ba267baaad3ec1a3cde87ccedaa0b9fa35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/covex-nn/PHP_CodeBrowser/zipball/f03291ba267baaad3ec1a3cde87ccedaa0b9fa35", + "reference": "f03291ba267baaad3ec1a3cde87ccedaa0b9fa35", + "shasum": "" + }, + "require": { + "phpunit/php-file-iterator": "~1.3", + "symfony/console": ">=2.2.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "bin": [ + "bin/phpcb" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev", + "dev-fork": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "src/" + ], + "license": [ + "BSD-2-Clause" + ], + "description": "A code browser that augments the code with information from various QA tools.", + "abandoned": "mayflower/php-codebrowser", + "time": "2013-11-12 21:58:07" + }, + { + "name": "doctrine/instantiator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "5acd2bd8c2b600ad5cc4c9180ebf0a930604d6a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/5acd2bd8c2b600ad5cc4c9180ebf0a930604d6a5", + "reference": "5acd2bd8c2b600ad5cc4c9180ebf0a930604d6a5", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2017-02-16 16:15:51" + }, + { + "name": "fabpot/goutte", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/Goutte.git", + "reference": "db5c28f4a010b4161d507d5304e28a7ebf211638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/db5c28f4a010b4161d507d5304e28a7ebf211638", + "reference": "db5c28f4a010b4161d507d5304e28a7ebf211638", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0", + "php": ">=5.5.0", + "symfony/browser-kit": "~2.1|~3.0", + "symfony/css-selector": "~2.1|~3.0", + "symfony/dom-crawler": "~2.1|~3.0" + }, + "type": "application", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Goutte\\": "Goutte" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "A simple PHP Web Scraper", + "homepage": "https://github.com/FriendsOfPHP/Goutte", + "keywords": [ + "scraper" + ], + "time": "2017-01-03 13:21:43" + }, + { + "name": "guzzlehttp/guzzle", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "a1c4a74bf31d4e41d783fafb635c806cc19c2e9b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a1c4a74bf31d4e41d783fafb635c806cc19c2e9b", + "reference": "a1c4a74bf31d4e41d783fafb635c806cc19c2e9b", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2017-03-22 11:33:29" + }, + { + "name": "guzzlehttp/promises", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "73815f322dc2d2aa711f8af41f87d3e9d9246bae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/73815f322dc2d2aa711f8af41f87d3e9d9246bae", + "reference": "73815f322dc2d2aa711f8af41f87d3e9d9246bae", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2017-03-28 16:54:57" + }, + { + "name": "guzzlehttp/psr7", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2017-03-20 17:10:46" + }, + { + "name": "instaclick/php-webdriver", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/instaclick/php-webdriver.git", + "reference": "9c9836dc5f2dcb1db9a94dd2599aa1049fc64b13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/instaclick/php-webdriver/zipball/9c9836dc5f2dcb1db9a94dd2599aa1049fc64b13", + "reference": "9c9836dc5f2dcb1db9a94dd2599aa1049fc64b13", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.2" + }, + "require-dev": { + "satooshi/php-coveralls": "^1.0||^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "WebDriver": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Justin Bishop", + "email": "jubishop@gmail.com", + "role": "Developer" + }, + { + "name": "Anthon Pang", + "email": "apang@softwaredevelopment.ca", + "role": "Fork Maintainer" + } + ], + "description": "PHP WebDriver for Selenium 2", + "homepage": "http://instaclick.com/", + "keywords": [ + "browser", + "selenium", + "webdriver", + "webtest" + ], + "time": "2017-03-22 17:46:51" + }, + { + "name": "nikic/php-parser", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "b5935a4aff9550c26c913a78079bdc34834bbfc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/b5935a4aff9550c26c913a78079bdc34834bbfc4", + "reference": "b5935a4aff9550c26c913a78079bdc34834bbfc4", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "~5.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "time": "2017-03-17 10:35:48" + }, + { + "name": "ocramius/proxy-manager", + "version": "1.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/ProxyManager.git", + "reference": "57e9272ec0e8deccf09421596e0e2252df440e11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/ProxyManager/zipball/57e9272ec0e8deccf09421596e0e2252df440e11", + "reference": "57e9272ec0e8deccf09421596e0e2252df440e11", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "zendframework/zend-code": ">2.2.5,<3.0" + }, + "require-dev": { + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "1.5.*" + }, + "suggest": { + "ocramius/generated-hydrator": "To have very fast object to array to object conversion for ghost objects", + "zendframework/zend-json": "To have the JsonRpc adapter (Remote Object feature)", + "zendframework/zend-soap": "To have the Soap adapter (Remote Object feature)", + "zendframework/zend-stdlib": "To use the hydrator proxy", + "zendframework/zend-xmlrpc": "To have the XmlRpc adapter (Remote Object feature)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "ProxyManager\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A library providing utilities to generate, instantiate and generally operate with Object Proxies", + "homepage": "https://github.com/Ocramius/ProxyManager", + "keywords": [ + "aop", + "lazy loading", + "proxy", + "proxy pattern", + "service proxies" + ], + "time": "2015-08-09 04:28:19" + }, + { + "name": "pdepend/pdepend", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/pdepend/pdepend.git", + "reference": "1537f19d62d7b30c13ac173270106df7c6b9c459" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/1537f19d62d7b30c13ac173270106df7c6b9c459", + "reference": "1537f19d62d7b30c13ac173270106df7c6b9c459", + "shasum": "" + }, + "require": { + "php": ">=5.2.3" + }, + "bin": [ + "src/bin/pdepend" + ], + "type": "library", + "autoload": { + "psr-0": { + "PHP_": "src/main/php/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Official version of pdepend to be handled with Composer", + "time": "2013-12-04 17:46:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2015-12-27 11:43:31" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0@dev", + "phpdocumentor/type-resolver": "^0.2.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2016-09-30 07:12:33" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.2.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", + "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2016-11-25 06:54:22" + }, + { + "name": "phploc/phploc", + "version": "2.1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phploc.git", + "reference": "50e063abd41833b3a5d29a2e8fbef5859ac28bdc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phploc/zipball/50e063abd41833b3a5d29a2e8fbef5859ac28bdc", + "reference": "50e063abd41833b3a5d29a2e8fbef5859ac28bdc", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/finder-facade": "~1.1", + "sebastian/git": "~2.0", + "sebastian/version": "~1.0.3", + "symfony/console": "~2.5" + }, + "require-dev": { + "phpunit/phpunit": "~4" + }, + "bin": [ + "phploc" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "A tool for quickly measuring the size of a PHP project.", + "homepage": "https://github.com/sebastianbergmann/phploc", + "time": "2015-10-22 13:44:19" + }, + { + "name": "phpmd/phpmd", + "version": "1.5.x-dev", + "source": { + "type": "git", + "url": "https://github.com/phpmd/phpmd.git", + "reference": "f2d47500f4c5f80ee442d95829c62c2ece2bbeb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/f2d47500f4c5f80ee442d95829c62c2ece2bbeb6", + "reference": "f2d47500f4c5f80ee442d95829c62c2ece2bbeb6", + "shasum": "" + }, + "require": { + "pdepend/pdepend": "1.1.*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "3.*@stable", + "squizlabs/php_codesniffer": "@stable" + }, + "bin": [ + "src/bin/phpmd" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "../../pdepend/pdepend/src/main/php", + "src/main/php" + ], + "license": [ + "BSD-3-Clause" + ], + "description": "Official version of PHPMD handled with Composer.", + "time": "2014-09-16 14:26:49" + }, + { + "name": "phpspec/php-diff", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/php-diff.git", + "reference": "0464787bfa7cd13576c5a1e318709768798bec6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/php-diff/zipball/0464787bfa7cd13576c5a1e318709768798bec6a", + "reference": "0464787bfa7cd13576c5a1e318709768798bec6a", + "shasum": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Diff": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Chris Boulton", + "homepage": "http://github.com/chrisboulton" + } + ], + "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).", + "time": "2016-04-07 12:29:16" + }, + { + "name": "phpspec/phpspec", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/phpspec/phpspec.git", + "reference": "53d89ff6d328032c0e434a75af6b0e80ff2d669d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/phpspec/zipball/53d89ff6d328032c0e434a75af6b0e80ff2d669d", + "reference": "53d89ff6d328032c0e434a75af6b0e80ff2d669d", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.1", + "ext-tokenizer": "*", + "php": "^5.6 || ^7.0", + "phpspec/php-diff": "^1.0.0", + "phpspec/prophecy": "^1.5", + "sebastian/exporter": "^1.0", + "symfony/console": "^2.7 || ^3.0", + "symfony/event-dispatcher": "^2.7 || ^3.0", + "symfony/finder": "^2.7 || ^3.0", + "symfony/process": "^2.7 || ^3.0", + "symfony/yaml": "^2.7 || ^3.0" + }, + "require-dev": { + "behat/behat": "^3.1", + "ciaranmcnulty/versionbasedtestskipper": "^0.2.1", + "phpunit/phpunit": "^5.4", + "symfony/filesystem": "^3.0" + }, + "suggest": { + "phpspec/nyan-formatters": "~1.0 – Adds Nyan formatters" + }, + "bin": [ + "bin/phpspec" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "PhpSpec": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "homepage": "http://marcelloduarte.net/" + }, + { + "name": "Ciaran McNulty", + "homepage": "https://ciaranmcnulty.com/" + } + ], + "description": "Specification-oriented BDD framework for PHP 5.6+", + "homepage": "http://phpspec.net/", + "keywords": [ + "BDD", + "SpecBDD", + "TDD", + "spec", + "specification", + "testing", + "tests" + ], + "time": "2016-09-26 21:11:31" + }, + { + "name": "phpspec/prophecy", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "abe41cb27f4e4207c6f54a09272969fe55e0bbff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/abe41cb27f4e4207c6f54a09272969fe55e0bbff", + "reference": "abe41cb27f4e4207c6f54a09272969fe55e0bbff", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", + "sebastian/comparator": "^1.1|^2.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5|^3.2", + "phpunit/phpunit": "^4.8 || ^5.6.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2017-03-03 17:09:02" + }, + { + "name": "phpunit/php-file-iterator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2016-10-03 07:40:28" + }, + { + "name": "phpunit/php-timer", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "d107f347d368dd8a384601398280c7c608390ab7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/d107f347d368dd8a384601398280c7c608390ab7", + "reference": "d107f347d368dd8a384601398280c7c608390ab7", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2017-03-07 15:42:04" + }, + { + "name": "psr/http-message", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06 14:39:51" + }, + { + "name": "psr/log", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-10-10 12:19:37" + }, + { + "name": "sebastian/comparator", + "version": "1.2.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "18a5d97c25f408f48acaf6d1b9f4079314c5996a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/18a5d97c25f408f48acaf6d1b9f4079314c5996a", + "reference": "18a5d97c25f408f48acaf6d1b9f4079314c5996a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2 || ~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2017-03-07 10:34:43" + }, + { + "name": "sebastian/diff", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "763d7adeb8c35d2af2b04c0f6cafeee059074dfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/763d7adeb8c35d2af2b04c0f6cafeee059074dfb", + "reference": "763d7adeb8c35d2af2b04c0f6cafeee059074dfb", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2017-03-07 07:26:53" + }, + { + "name": "sebastian/exporter", + "version": "1.2.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "dcd43bcc0fd3551bd2ede0081882d549bb78225d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/dcd43bcc0fd3551bd2ede0081882d549bb78225d", + "reference": "dcd43bcc0fd3551bd2ede0081882d549bb78225d", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0", + "sebastian/recursion-context": "^1.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2017-02-26 13:09:30" + }, + { + "name": "sebastian/finder-facade", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/finder-facade.git", + "reference": "2a6f7f57efc0aa2d23297d9fd9e2a03111a8c0b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/2a6f7f57efc0aa2d23297d9fd9e2a03111a8c0b9", + "reference": "2a6f7f57efc0aa2d23297d9fd9e2a03111a8c0b9", + "shasum": "" + }, + "require": { + "symfony/finder": "~2.3|~3.0", + "theseer/fdomdocument": "~1.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.", + "homepage": "https://github.com/sebastianbergmann/finder-facade", + "time": "2016-02-17 07:02:23" + }, + { + "name": "sebastian/git", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/git.git", + "reference": "815bbbc963cf35e5413df195aa29df58243ecd24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/git/zipball/815bbbc963cf35e5413df195aa29df58243ecd24", + "reference": "815bbbc963cf35e5413df195aa29df58243ecd24", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Simple wrapper for Git", + "homepage": "http://www.github.com/sebastianbergmann/git", + "keywords": [ + "git" + ], + "time": "2017-01-23 20:57:12" + }, + { + "name": "sebastian/phpcpd", + "version": "2.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpcpd.git", + "reference": "24d9a880deadb0b8c9680e9cfe78e30b704225db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpcpd/zipball/24d9a880deadb0b8c9680e9cfe78e30b704225db", + "reference": "24d9a880deadb0b8c9680e9cfe78e30b704225db", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-timer": ">=1.0.6", + "sebastian/finder-facade": "~1.1", + "sebastian/version": "~1.0|~2.0", + "symfony/console": "~2.7|^3.0", + "theseer/fdomdocument": "~1.4" + }, + "bin": [ + "phpcpd" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Copy/Paste Detector (CPD) for PHP code.", + "homepage": "https://github.com/sebastianbergmann/phpcpd", + "time": "2016-04-17 19:32:49" + }, + { + "name": "sebastian/recursion-context", + "version": "1.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b19cc3298482a335a95f3016d2f8a6950f0fbcd7", + "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2016-10-03 07:41:43" + }, + { + "name": "sebastian/version", + "version": "1.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2015-06-21 13:59:46" + }, + { + "name": "sensiolabs/behat-page-object-extension", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sensiolabs/BehatPageObjectExtension.git", + "reference": "3348a58ecc907597d854d7ed4bbfe8b0eb3af709" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sensiolabs/BehatPageObjectExtension/zipball/3348a58ecc907597d854d7ed4bbfe8b0eb3af709", + "reference": "3348a58ecc907597d854d7ed4bbfe8b0eb3af709", + "shasum": "" + }, + "require": { + "behat/behat": "^3.0.6", + "behat/mink": "^1.6", + "behat/mink-extension": "^2.0", + "ocramius/proxy-manager": "^1.0||^2.0", + "php": ">=5.3.0" + }, + "require-dev": { + "behat/mink-goutte-driver": "^1.0", + "bossa/phpspec2-expect": "^1.0.3||^2.0", + "fabpot/goutte": "^1.0.4||^2.0||^3.0", + "phpspec/phpspec": "^2.5||^3.0", + "symfony/filesystem": "^2.8||^3.0", + "symfony/process": "^2.8||^3.0", + "symfony/yaml": "^2.8||^3.0" + }, + "suggest": { + "bossa/phpspec2-expect": "Allows to use PHPSpec2 matchers in Behat context files" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-0": { + "SensioLabs\\Behat\\PageObjectExtension\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marcello Duarte", + "email": "mduarte@inviqa.com" + }, + { + "name": "Jakub Zalas", + "email": "jakub@zalas.pl" + } + ], + "description": "Page object extension for Behat", + "homepage": "https://github.com/sensiolabs/BehatPageObjectExtension", + "keywords": [ + "BDD", + "Behat", + "page" + ], + "time": "2017-02-24 09:42:13" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "1.5.x-dev", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "6f3e42d311b882b25b4d409d23a289f4d3b803d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/6f3e42d311b882b25b4d409d23a289f4d3b803d5", + "reference": "6f3e42d311b882b25b4d409d23a289f4d3b803d5", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.1.2" + }, + "suggest": { + "phpunit/php-timer": "dev-master" + }, + "bin": [ + "scripts/phpcs" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-phpcs-fixer": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "CodeSniffer.php", + "CodeSniffer/CLI.php", + "CodeSniffer/Exception.php", + "CodeSniffer/File.php", + "CodeSniffer/Report.php", + "CodeSniffer/Reporting.php", + "CodeSniffer/Sniff.php", + "CodeSniffer/Tokens.php", + "CodeSniffer/Reports/", + "CodeSniffer/CommentParser/", + "CodeSniffer/Tokenizers/", + "CodeSniffer/DocGenerators/", + "CodeSniffer/Standards/AbstractPatternSniff.php", + "CodeSniffer/Standards/AbstractScopeSniff.php", + "CodeSniffer/Standards/AbstractVariableSniff.php", + "CodeSniffer/Standards/IncorrectPatternException.php", + "CodeSniffer/Standards/Generic/Sniffs/", + "CodeSniffer/Standards/MySource/Sniffs/", + "CodeSniffer/Standards/PEAR/Sniffs/", + "CodeSniffer/Standards/PSR1/Sniffs/", + "CodeSniffer/Standards/PSR2/Sniffs/", + "CodeSniffer/Standards/Squiz/Sniffs/", + "CodeSniffer/Standards/Zend/Sniffs/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenises PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "http://www.squizlabs.com/php-codesniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2014-12-04 22:32:15" + }, + { + "name": "symfony/browser-kit", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "6cc9a89c6b31c4a71bd0f2e2bc608c26b22de5f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/6cc9a89c6b31c4a71bd0f2e2bc608c26b22de5f1", + "reference": "6cc9a89c6b31c4a71bd0f2e2bc608c26b22de5f1", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/dom-crawler": "~2.8|~3.0" + }, + "require-dev": { + "symfony/css-selector": "~2.8|~3.0", + "symfony/process": "~2.8|~3.0" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony BrowserKit Component", + "homepage": "https://symfony.com", + "time": "2017-03-14 20:25:39" + }, + { + "name": "symfony/class-loader", + "version": "2.8.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/class-loader.git", + "reference": "2c8de07a8a4cc4da9c018ab7a81888b80e762f93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/2c8de07a8a4cc4da9c018ab7a81888b80e762f93", + "reference": "2c8de07a8a4cc4da9c018ab7a81888b80e762f93", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/polyfill-apcu": "~1.1" + }, + "require-dev": { + "symfony/finder": "^2.0.5|~3.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\ClassLoader\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony ClassLoader Component", + "homepage": "https://symfony.com", + "time": "2017-02-18 19:13:35" + }, + { + "name": "symfony/config", + "version": "2.8.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "06ce6bb46c24963ec09323da45d0f4f85d3cecd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/06ce6bb46c24963ec09323da45d0f4f85d3cecd2", + "reference": "06ce6bb46c24963ec09323da45d0f4f85d3cecd2", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/filesystem": "~2.3|~3.0.0" + }, + "require-dev": { + "symfony/yaml": "~2.7|~3.0.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Config Component", + "homepage": "https://symfony.com", + "time": "2017-03-01 18:13:50" + }, + { + "name": "symfony/console", + "version": "2.8.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "3e0b0cb5d669a7d39dd85050910af6ee77e40e7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/3e0b0cb5d669a7d39dd85050910af6ee77e40e7e", + "reference": "3e0b0cb5d669a7d39dd85050910af6ee77e40e7e", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/debug": "^2.7.2|~3.0.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.1|~3.0.0", + "symfony/process": "~2.1|~3.0.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2017-03-27 14:49:15" + }, + { + "name": "symfony/css-selector", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "54f65a2160bb98376455f29f2ddf1de676d4b8ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/54f65a2160bb98376455f29f2ddf1de676d4b8ed", + "reference": "54f65a2160bb98376455f29f2ddf1de676d4b8ed", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "time": "2017-02-21 10:07:34" + }, + { + "name": "symfony/debug", + "version": "3.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/697c527acd9ea1b2d3efac34d9806bf255278b0a", + "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/class-loader": "~2.8|~3.0", + "symfony/http-kernel": "~2.8|~3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2016-07-30 07:22:48" + }, + { + "name": "symfony/dependency-injection", + "version": "2.8.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "a5f3f1265731c33906a725c6f321cb93c2b49f67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/a5f3f1265731c33906a725c6f321cb93c2b49f67", + "reference": "a5f3f1265731c33906a725c6f321cb93c2b49f67", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "conflict": { + "symfony/expression-language": "<2.6" + }, + "require-dev": { + "symfony/config": "~2.2|~3.0.0", + "symfony/expression-language": "~2.6|~3.0.0", + "symfony/yaml": "~2.3.42|~2.7.14|~2.8.7|~3.0.7" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DependencyInjection Component", + "homepage": "https://symfony.com", + "time": "2017-03-21 21:39:01" + }, + { + "name": "symfony/dom-crawler", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "8e1daf43665f93b0dfc2d47d975e964bf44f018a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/8e1daf43665f93b0dfc2d47d975e964bf44f018a", + "reference": "8e1daf43665f93b0dfc2d47d975e964bf44f018a", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "~2.8|~3.0" + }, + "suggest": { + "symfony/css-selector": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DomCrawler Component", + "homepage": "https://symfony.com", + "time": "2017-02-21 10:07:34" + }, + { + "name": "symfony/event-dispatcher", + "version": "2.8.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "cd88acb5563c690bb02c21b5d7aa801af2520095" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/cd88acb5563c690bb02c21b5d7aa801af2520095", + "reference": "cd88acb5563c690bb02c21b5d7aa801af2520095", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^2.0.5|~3.0.0", + "symfony/dependency-injection": "~2.6|~3.0.0", + "symfony/expression-language": "~2.6|~3.0.0", + "symfony/stopwatch": "~2.3|~3.0.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2017-03-21 21:39:01" + }, + { + "name": "symfony/filesystem", + "version": "3.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "b2da5009d9bacbd91d83486aa1f44c793a8c380d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b2da5009d9bacbd91d83486aa1f44c793a8c380d", + "reference": "b2da5009d9bacbd91d83486aa1f44c793a8c380d", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2016-07-20 05:43:46" + }, + { + "name": "symfony/finder", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "851a8764b99bd4173b9992d09ab91050803f0385" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/851a8764b99bd4173b9992d09ab91050803f0385", + "reference": "851a8764b99bd4173b9992d09ab91050803f0385", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2017-03-20 10:06:58" + }, + { + "name": "symfony/polyfill-apcu", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-apcu.git", + "reference": "5d4474f447403c3348e37b70acc2b95475b7befa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-apcu/zipball/5d4474f447403c3348e37b70acc2b95475b7befa", + "reference": "5d4474f447403c3348e37b70acc2b95475b7befa", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting apcu_* functions to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "apcu", + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2016-11-14 01:06:16" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2016-11-14 01:06:16" + }, + { + "name": "symfony/process", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "3b6c34340b6440f8d981ad1741ac5404369a419e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/3b6c34340b6440f8d981ad1741ac5404369a419e", + "reference": "3b6c34340b6440f8d981ad1741ac5404369a419e", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2017-03-27 18:09:43" + }, + { + "name": "symfony/translation", + "version": "2.8.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "047e97a64d609778cadfc76e3a09793696bb19f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/047e97a64d609778cadfc76e3a09793696bb19f1", + "reference": "047e97a64d609778cadfc76e3a09793696bb19f1", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/config": "<2.7" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8", + "symfony/intl": "~2.7.25|^2.8.18|~3.2.5", + "symfony/yaml": "~2.2|~3.0.0" + }, + "suggest": { + "psr/log": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Translation Component", + "homepage": "https://symfony.com", + "time": "2017-03-21 21:39:01" + }, + { + "name": "symfony/yaml", + "version": "2.8.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "286d84891690b0e2515874717e49360d1c98a703" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/286d84891690b0e2515874717e49360d1c98a703", + "reference": "286d84891690b0e2515874717e49360d1c98a703", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2017-03-20 09:41:44" + }, + { + "name": "theseer/directoryscanner", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/theseer/DirectoryScanner.git", + "reference": "549aa9fdbc47d50365db42d9ade35fdef65f854c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/DirectoryScanner/zipball/549aa9fdbc47d50365db42d9ade35fdef65f854c", + "reference": "549aa9fdbc47d50365db42d9ade35fdef65f854c", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A recursive directory scanner and filter", + "time": "2015-03-24 21:28:20" + }, + { + "name": "theseer/fdomdocument", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/fDOMDocument.git", + "reference": "d9ad139d6c2e8edf5e313ffbe37ff13344cf0684" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/d9ad139d6c2e8edf5e313ffbe37ff13344cf0684", + "reference": "d9ad139d6c2e8edf5e313ffbe37ff13344cf0684", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "lib-libxml": "*", + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "lead" + } + ], + "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.", + "homepage": "https://github.com/theseer/fDOMDocument", + "time": "2015-05-27 22:58:02" + }, + { + "name": "theseer/fxsl", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/theseer/fXSL.git", + "reference": "a9246376c713156e55c080782d4104bb07d4b899" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/fXSL/zipball/a9246376c713156e55c080782d4104bb07d4b899", + "reference": "a9246376c713156e55c080782d4104bb07d4b899", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xsl": "*", + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "An XSL wrapper / extension to the PHP 5.x XSLTProcessor with Exception and extended Callback support", + "time": "2014-11-27 20:08:52" + }, + { + "name": "theseer/phpdox", + "version": "0.6.6.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/phpdox.git", + "reference": "6afa676788d16f2703906082b4a3c9f69772abcc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/phpdox/zipball/6afa676788d16f2703906082b4a3c9f69772abcc", + "reference": "6afa676788d16f2703906082b4a3c9f69772abcc", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-iconv": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xsl": "*", + "nikic/php-parser": ">=1.0.0", + "php": ">=5.3.3", + "phpunit/php-timer": ">=1.0.4", + "theseer/directoryscanner": ">=1.3.0", + "theseer/fdomdocument": ">=1.5.0", + "theseer/fxsl": ">=1.1", + "zetacomponents/base": ">=1.8", + "zetacomponents/console-tools": ">=1.6.0" + }, + "bin": [ + "composer/bin/phpdox" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A fast Documentation generator for PHP Code using standard technology (SRC, DOCBLOCK, XML and XSLT) with event based processing", + "time": "2014-05-04 14:51:11" + }, + { + "name": "webmozart/assert", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "4a8bf11547e139e77b651365113fc12850c43d9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/4a8bf11547e139e77b651365113fc12850c43d9a", + "reference": "4a8bf11547e139e77b651365113fc12850c43d9a", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2016-11-23 20:04:41" + }, + { + "name": "zendframework/zend-code", + "version": "2.6.3", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-code.git", + "reference": "95033f061b083e16cdee60530ec260d7d628b887" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-code/zipball/95033f061b083e16cdee60530ec260d7d628b887", + "reference": "95033f061b083e16cdee60530ec260d7d628b887", + "shasum": "" + }, + "require": { + "php": "^5.5 || 7.0.0 - 7.0.4 || ^7.0.6", + "zendframework/zend-eventmanager": "^2.6 || ^3.0" + }, + "require-dev": { + "doctrine/annotations": "~1.0", + "fabpot/php-cs-fixer": "1.7.*", + "phpunit/phpunit": "^4.8.21", + "zendframework/zend-stdlib": "^2.7 || ^3.0" + }, + "suggest": { + "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", + "zendframework/zend-stdlib": "Zend\\Stdlib component" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev", + "dev-develop": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Code\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "provides facilities to generate arbitrary code using an object oriented interface", + "homepage": "https://github.com/zendframework/zend-code", + "keywords": [ + "code", + "zf2" + ], + "time": "2016-04-20 17:26:42" + }, + { + "name": "zendframework/zend-eventmanager", + "version": "dev-develop", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-eventmanager.git", + "reference": "42d2abcdfc56ee2005e9271ac7666d88db7b91b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/42d2abcdfc56ee2005e9271ac7666d88db7b91b9", + "reference": "42d2abcdfc56ee2005e9271ac7666d88db7b91b9", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "athletic/athletic": "^0.1", + "container-interop/container-interop": "^1.1.0", + "phpunit/phpunit": "^5.6", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-stdlib": "^2.7.3 || ^3.0" + }, + "suggest": { + "container-interop/container-interop": "^1.1.0, to use the lazy listeners feature", + "zendframework/zend-stdlib": "^2.7.3 || ^3.0, to use the FilterChain feature" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev", + "dev-develop": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\EventManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Trigger and listen to events within a PHP application", + "homepage": "https://github.com/zendframework/zend-eventmanager", + "keywords": [ + "event", + "eventmanager", + "events", + "zf2" + ], + "time": "2017-03-05 07:56:37" + }, + { + "name": "zetacomponents/base", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/zetacomponents/Base.git", + "reference": "d816bd6d7902d949fae43df135c0ca36bfcf742d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zetacomponents/Base/zipball/d816bd6d7902d949fae43df135c0ca36bfcf742d", + "reference": "d816bd6d7902d949fae43df135c0ca36bfcf742d", + "shasum": "" + }, + "require-dev": { + "zetacomponents/unit-test": "*" + }, + "type": "library", + "autoload": { + "classmap": [ + "src" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Sergey Alexeev" + }, + { + "name": "Sebastian Bergmann" + }, + { + "name": "Jan Borsodi" + }, + { + "name": "Raymond Bosman" + }, + { + "name": "Frederik Holljen" + }, + { + "name": "Kore Nordmann" + }, + { + "name": "Derick Rethans" + }, + { + "name": "Vadym Savchuk" + }, + { + "name": "Tobias Schlitt" + }, + { + "name": "Alexandru Stanoi" + } + ], + "description": "The Base package provides the basic infrastructure that all packages rely on. Therefore every component relies on this package.", + "homepage": "https://github.com/zetacomponents", + "time": "2015-06-03 14:25:37" + }, + { + "name": "zetacomponents/console-tools", + "version": "1.6", + "source": { + "type": "git", + "url": "https://github.com/zetacomponents/ConsoleTools.git", + "reference": "e0a0def574009f7cfdf79bf0838a810bcf643775" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zetacomponents/ConsoleTools/zipball/e0a0def574009f7cfdf79bf0838a810bcf643775", + "reference": "e0a0def574009f7cfdf79bf0838a810bcf643775", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "src" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "apache2" + ], + "authors": [ + { + "name": "Sergey Alexeev" + }, + { + "name": "Sebastian Bergmann" + }, + { + "name": "Jan Borsodi" + }, + { + "name": "Raymond Bosman" + }, + { + "name": "Frederik Holljen" + }, + { + "name": "Kore Nordmann" + }, + { + "name": "Derick Rethans" + }, + { + "name": "Vadym Savchuk" + }, + { + "name": "Tobias Schlitt" + }, + { + "name": "Alexandru Stanoi" + } + ], + "description": "A set of classes to do different actions with the console (also called shell). It can render a progress bar, tables and a status bar and contains a class for parsing command line options.", + "homepage": "https://github.com/zetacomponents", + "time": "2009-12-21 12:19:33" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": { + "sensiolabs/behat-page-object-extension": 20, + "theseer/fxsl": 20, + "covex-nn/phpcb": 20, + "bex/behat-magento2-init": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "~5.5.0|~5.6.0|~7.0.0" + }, + "platform-dev": [] +} diff --git a/magento2/etc/adminhtml/events.xml b/magento2/etc/adminhtml/events.xml new file mode 100644 index 0000000..246bcb2 --- /dev/null +++ b/magento2/etc/adminhtml/events.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/magento2/etc/adminhtml/system.xml b/magento2/etc/adminhtml/system.xml new file mode 100644 index 0000000..1e36630 --- /dev/null +++ b/magento2/etc/adminhtml/system.xml @@ -0,0 +1,57 @@ + + + + +
+ + service + Cloudinary_Cloudinary::config_cloudinary + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + + + Set the credentials of your Cloudinary account. Copy the credentials string from the dashboard of Cloudinary's Management Console. It is available as the Environment variable of CLOUDINARY_URL. + Magento\Config\Model\Config\Backend\Encrypted + + + + + + + Enable multiple sub-domains of image delivery URLs for faster page load speed. + Magento\Config\Model\Config\Source\Yesno + + + + + + + Automatically deliver images converted to modern image formats based on viewing device and browser. For example, deliver WebP on Chrome and JPEG-XR on Internet Explorer for better performance and user experience. + Magento\Config\Model\Config\Source\Yesno + + + + Adjust quality of generated images to balance between visual quality and file size minimization. The quality is relevant for JPEG and WebP compression levels for example. + Cloudinary\Cloudinary\Model\Config\Source\Dropdown\Quality + + + + Define the part of the image to focus on when cropping images in order to better match your graphic design. + Cloudinary\Cloudinary\Model\Config\Source\Dropdown\Gravity + + + + Use DPR value higher than 1.0 to generate and deliver hi-res images for better visual result on HiDPI devices, such as Retina Display devices (e.g., 2.0). + Cloudinary\Cloudinary\Model\Config\Source\Dropdown\Dpr + + +
+
+
diff --git a/magento2/etc/config.xml b/magento2/etc/config.xml new file mode 100644 index 0000000..50461e5 --- /dev/null +++ b/magento2/etc/config.xml @@ -0,0 +1,16 @@ + + + + + + + 1 + 80 + 1.0 + + + 1 + + + + diff --git a/magento2/etc/crontab.xml b/magento2/etc/crontab.xml new file mode 100644 index 0000000..c2bf912 --- /dev/null +++ b/magento2/etc/crontab.xml @@ -0,0 +1,7 @@ + + + + */3 * * * * + + + diff --git a/magento2/etc/di.xml b/magento2/etc/di.xml new file mode 100644 index 0000000..230cb82 --- /dev/null +++ b/magento2/etc/di.xml @@ -0,0 +1,49 @@ + + + + + + + Cloudinary\Cloudinary\Command\UploadImages + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/magento2/etc/module.xml b/magento2/etc/module.xml new file mode 100644 index 0000000..a129768 --- /dev/null +++ b/magento2/etc/module.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/magento2/features/bootstrap/FeatureContext.php b/magento2/features/bootstrap/FeatureContext.php new file mode 100644 index 0000000..b0c7b50 --- /dev/null +++ b/magento2/features/bootstrap/FeatureContext.php @@ -0,0 +1,219 @@ +adminLogin = $adminLogin; + $this->productPage = $productPage; + $this->cloudinaryConfig = new CloudinaryConfig(); + $this->cloudinaryManager = new CloudinaryManager(); + $this->productManager = new ProductManager(); + } + + /** + * @Transform :anImage + */ + public function transformStringToAnImage($string) + { + return Image::fromPath($string, self::IMAGE_RELATIVE_PATH); + } + + /** + * @BeforeScenario + */ + public function beforeScenario() + { + $this->removeImageFromMediaFolder(); + $this->product = $this->productManager->createProduct(); + } + + /** + * @AfterScenario + */ + public function afterScenario() + { + $mediaGalleryData = $this->productManager + ->createProduct() + ->getMediaGallery(); + + if (isset($mediaGalleryData['images']) && is_array($mediaGalleryData['images'])) { + foreach ($mediaGalleryData['images'] as &$imageData) { + $imageData['removed'] = 1; + } + $this->product->setData('media_gallery', $mediaGalleryData); + $this->product->save(); + } + + $this->product = null; + $this->uploadException = false; + } + + /** + * @Given I am logged in as an administrator + */ + public function iAmLoggedInAsAnAdministrator() + { + try { + $this->adminLogin->openPage(); + $this->adminLogin->login('admin', 'admin123'); + } catch (\Exception $e) { + + } + } + + /** + * @Given the cloudinary module is disabled + */ + public function theCloudinaryModuleIsDisabled() + { + $this->cloudinaryConfig->disableCloudinary(); + exec('../../../bin/magento ca:cl config'); + } + + /** + * @Given the image :anImage does not exist on the provider + */ + public function theImageDoesNotExistOnTheProvider($anImage) + { + $this->cloudinaryManager->deleteImageFromCloudinary($anImage); + } + + /** + * @When I upload the image :anImage + */ + public function iUploadTheImage(Image $anImage) + { + $imageFilename = (string)$anImage; + + try { + $this->saveProductWithImage($imageFilename); + } catch (MigrationError $e) { + $this->uploadException = $e; + } + + expect(file_exists('../../../pub/media/catalog/product/p/i/'.$imageFilename))->toBe(true); + } + + /** + * @Then the image :anImage will be provided locally + */ + public function theImageWillBeProvidedLocally($anImage) + { + $this->productPage->openPage(['url_key' => $this->product->getUrlKey()]); + + expect($this->productPage)->toNotHaveCloudinaryImageUrl($anImage); + } + + /** + * @Given the cloudinary module is enabled + */ + public function theCloudinaryModuleIsEnabled() + { + $this->cloudinaryConfig->enableCloudinary(); + exec('../../../bin/magento ca:cl config'); + } + + /** + * @Then the image :anImage will be provided remotely + */ + public function theImageWillBeProvidedRemotely($anImage) + { + $this->productPage->openPage(['url_key' => $this->product->getUrlKey()]); + + expect($this->uploadException instanceof MigrationError)->toBe(false); + expect($this->productPage)->toHaveCloudinaryImageUrl($anImage); + } + + /** + * @Given the image :anImage has already been uploaded + */ + public function theImageHasAlreadyBeenUploaded(Image $anImage) + { + Uploader::upload( + __DIR__ . '/../fixtures/images/' . (string)$anImage, + [ + "use_filename" => true, + "unique_filename" => false, + "overwrite" => false, + "folder" => 'catalog/product/p/i/' + ] + ); + } + + /** + * @Then I should see an error image already exists + */ + public function iShouldSeeAnErrorImageAlreadyExists() + { + expect($this->uploadException instanceof MigrationError)->toBe(true); + } + + private function removeImageFromMediaFolder() + { + exec("rm -rf ../../../pub/media/catalog/product/p/i"); + } + + /** + * @param string $imageFilename + */ + private function saveProductWithImage($imageFilename) + { + exec('cp features/fixtures/images/' . $imageFilename . ' /vagrant/pub/media/'); + $this->product->addImageToMediaGallery( + '/vagrant/pub/media/' . $imageFilename, + null, + false, + false + )->save(); + } +} diff --git a/magento2/features/bootstrap/Fixtures/CloudinaryConfig.php b/magento2/features/bootstrap/Fixtures/CloudinaryConfig.php new file mode 100644 index 0000000..4ba304f --- /dev/null +++ b/magento2/features/bootstrap/Fixtures/CloudinaryConfig.php @@ -0,0 +1,30 @@ +configuration = $this->getMagentoObject(Configuration::class); + } + + public function enableCloudinary() + { + $this->configuration->enable(); + } + + public function disableCloudinary() + { + $this->configuration->disable(); + } +} diff --git a/magento2/features/bootstrap/Fixtures/CloudinaryManager.php b/magento2/features/bootstrap/Fixtures/CloudinaryManager.php new file mode 100644 index 0000000..cd06681 --- /dev/null +++ b/magento2/features/bootstrap/Fixtures/CloudinaryManager.php @@ -0,0 +1,25 @@ +cloudinaryImageProvider = CloudinaryImageProvider::fromConfiguration( + $this->getMagentoObject(Configuration::class) + ); + } + + public function deleteImageFromCloudinary($image) + { + $this->cloudinaryImageProvider->delete($image); + } +} diff --git a/magento2/features/bootstrap/Fixtures/ProductManager.php b/magento2/features/bootstrap/Fixtures/ProductManager.php new file mode 100644 index 0000000..b9d2051 --- /dev/null +++ b/magento2/features/bootstrap/Fixtures/ProductManager.php @@ -0,0 +1,19 @@ +createMagentoObject(ProductInterface::class); + return $testproduct->load(self::TEST_PRODUCT_ID); + } +} diff --git a/magento2/features/bootstrap/Helpers/PageObjectHelperMethods.php b/magento2/features/bootstrap/Helpers/PageObjectHelperMethods.php new file mode 100644 index 0000000..6011e8a --- /dev/null +++ b/magento2/features/bootstrap/Helpers/PageObjectHelperMethods.php @@ -0,0 +1,146 @@ +open($params); + $this->waitForPageLoad(); + } + + function acceptAlert() + { + $this->getDriver()->getWebDriverSession()->accept_alert(); + } + + /** + * @param mixed $condition + * @param int $maxWait + */ + function waitForCondition($condition, $maxWait = 120000) + { + $this->getSession()->wait($maxWait, $condition); + } + + /** + * @param int $maxWait + */ + function waitForPageLoad($maxWait = 120000) + { + $this->waitForCondition('(document.readyState == "complete") && (typeof window.jQuery == "function") && (jQuery.active == 0)', $maxWait); + } + + /** + * @param string $elementName + * @param int $maxWait + */ + function waitForElement($elementName, $maxWait = 120000) + { + $visibilityCheck = $this->getElementVisibilyCheck($elementName); + $this->waitForCondition("(typeof window.jQuery == 'function') && $visibilityCheck", $maxWait); + } + + /** + * @param string $elementName + * @param int $maxWait + */ + function waitUntilElementDisappear($elementName, $maxWait = 120000) + { + $visibilityCheck = $this->getElementVisibilyCheck($elementName); + $this->waitForCondition("(typeof window.jQuery == 'function') && !$visibilityCheck", $maxWait); + } + + /** + * @param int $waitTime + */ + function waitTime($waitTime) + { + $this->getSession()->wait($waitTime); + } + + function scrollToBottom() + { + $this->getSession()->executeScript('window.scrollTo(0,document.body.scrollHeight);'); + } + + /** + * @param string $elementName + */ + function clickElement($elementName) + { + $this->getElementWithWait($elementName)->click(); + } + + /** + * @param string $elementName + * @return mixed + */ + function getElementValue($elementName) + { + return $this->getElementWithWait($elementName)->getValue(); + } + + /** + * @param string $elementName + * @param string $value + */ + function setElementValue($elementName, $value) + { + $this->getElementWithWait($elementName)->setValue($value); + } + + /** + * @param string $elementName + */ + function getElementText($elementName) + { + return $this->getElementWithWait($elementName)->getText(); + } + + /** + * @param string $elementName + * @param int $waitTime + * @return mixed + */ + public function getElementWithWait($elementName, $waitTime = 2500) + { + $this->waitForElement($elementName, $waitTime); + return $this->getElement($elementName); + } + + /** + * @param $elementName + * @return string + */ + public function getElementVisibilyCheck($elementName) + { + $visibilityCheck = 'true'; + + if (isset($this->elements[$elementName]['css'])) { + $elementFinder = $this->elements[$elementName]['css']; + $visibilityCheck = "jQuery('$elementFinder').is(':visible')"; + } + + if (isset($this->elements[$elementName]['xpath'])) { + $elementFinder = $this->elements[$elementName]['xpath']; + $visibilityCheck = "jQuery(document.evaluate('$elementFinder', document, null, XPathResult.ANY_TYPE, null).iterateNext()).is(':visible')"; + } + + return $visibilityCheck; + } + + /** + * @param string $elementName + * @return mixed + */ + public function isElementVisible($elementName) + { + $xpath = $this->getElement($elementName)->getXpath(); + return $this->getDriver()->isVisible($xpath); + } +} diff --git a/magento2/features/bootstrap/Page/Admin/Login.php b/magento2/features/bootstrap/Page/Admin/Login.php new file mode 100644 index 0000000..a4d2053 --- /dev/null +++ b/magento2/features/bootstrap/Page/Admin/Login.php @@ -0,0 +1,27 @@ + ['css' => '#username'], + 'Password' => ['css' => '#login'], + 'Login button' => ['css' => '.action-login'] + ]; + + public function login($username, $password) + { + $this->setElementValue('Username', $username); + $this->setElementValue('Password', $password); + $this->clickElement('Login button'); + $this->waitForPageLoad(); + } +} diff --git a/magento2/features/bootstrap/Page/Product.php b/magento2/features/bootstrap/Page/Product.php new file mode 100644 index 0000000..2c6fbd1 --- /dev/null +++ b/magento2/features/bootstrap/Page/Product.php @@ -0,0 +1,29 @@ + ['css' => '.fotorama__img'] + ]; + + function getMainImageUrl() + { + return $this->getElementWithWait('Main Image')->getAttribute('src'); + } + + function hasCloudinaryImageUrl(Image $image) + { + return (strpos($this->getMainImageUrl(), 'cloudinary.com') !== false) + && (strpos($this->getMainImageUrl(), $image->getId()) !== false); + } +} diff --git a/magento2/features/fixtures/images/pink_dress.gif b/magento2/features/fixtures/images/pink_dress.gif new file mode 100644 index 0000000000000000000000000000000000000000..04dc5794247f4ec467e623e017d59b1c830367fc GIT binary patch literal 167624 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ðqgd_G-h*C9bj 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 JJ$v)v{{RZumj?g< literal 0 HcmV?d00001 diff --git a/magento2/registration.php b/magento2/registration.php new file mode 100644 index 0000000..422034d --- /dev/null +++ b/magento2/registration.php @@ -0,0 +1,7 @@ +beConstructedWith( + $imageRepository, + $configuration, + $migrationTask, + $cloudinaryImageManager + ); + } + + function it_should_not_do_batch_upload_when_migration_task_started( + OutputInterface $outputInterface, + MigrationTask $migrationTask + ) { + $migrationTask->hasStarted()->willReturn(true); + + $this->uploadUnsynchronisedImages($outputInterface)->shouldBe(false); + } + + function it_should_upload_and_synchronised_images( + OutputInterface $outputInterface, + MigrationTask $migrationTask, + ImageRepository $imageRepository, + Image $image + ) + { + $image->__toString()->willReturn('pink_image.gif'); + $image->getRelativePath()->willReturn('/p/i/pink_dress.gif'); + + $migrationTask->hasStarted()->willReturn(false); + + $migrationTask->start()->shouldBeCalled(); + $migrationTask->stop()->shouldBeCalled(); + + $imageRepository->findUnsynchronisedImages()->willReturn([$image]); + + $this->uploadUnsynchronisedImages($outputInterface)->shouldBe(true); + } +} diff --git a/magento2/spec/Cloudinary/Cloudinary/Model/ProductImageFinder/DeletedImageFilterSpec.php b/magento2/spec/Cloudinary/Cloudinary/Model/ProductImageFinder/DeletedImageFilterSpec.php new file mode 100644 index 0000000..dca0f9f --- /dev/null +++ b/magento2/spec/Cloudinary/Cloudinary/Model/ProductImageFinder/DeletedImageFilterSpec.php @@ -0,0 +1,26 @@ +__invoke($imageData)->shouldReturn(false); + } + + function it_should_return_empty_when_no_removed_images() + { + $imageData = ['removed' => 0]; + $this->__invoke($imageData)->shouldReturn(false); + } + + function it_should_return_images_marked_as_removed() + { + $imageData = ['removed' => 1]; + $this->__invoke($imageData)->shouldReturn(true); + } +} \ No newline at end of file diff --git a/magento2/spec/Cloudinary/Cloudinary/Model/ProductImageFinder/ImageCreatorSpec.php b/magento2/spec/Cloudinary/Cloudinary/Model/ProductImageFinder/ImageCreatorSpec.php new file mode 100644 index 0000000..01c3f74 --- /dev/null +++ b/magento2/spec/Cloudinary/Cloudinary/Model/ProductImageFinder/ImageCreatorSpec.php @@ -0,0 +1,35 @@ +beConstructedWith($filesystem, $mediaConfig); + } + + function it_should_return_instance_of_an_image( + Filesystem $filesystem, + ReadInterface $mediaDirectory + ) { + + $filesystem->getDirectoryRead(DirectoryList::MEDIA) + ->shouldBeCalled() + ->willReturn($mediaDirectory); + + $imageData = ['file' => '/p/i/pink_dress.gif']; + + $this->__invoke($imageData)->shouldBeAnInstanceOf('CloudinaryExtension\Image'); + } +} \ No newline at end of file diff --git a/magento2/spec/Cloudinary/Cloudinary/Model/ProductImageFinder/NewImageFilterSpec.php b/magento2/spec/Cloudinary/Cloudinary/Model/ProductImageFinder/NewImageFilterSpec.php new file mode 100644 index 0000000..e6c98fa --- /dev/null +++ b/magento2/spec/Cloudinary/Cloudinary/Model/ProductImageFinder/NewImageFilterSpec.php @@ -0,0 +1,20 @@ +__invoke($imageData)->shouldReturn(false); + } + + function it_should_return_images_marked_as_new() + { + $imageData = ['new_file' => 1]; + $this->__invoke($imageData)->shouldReturn(true); + } +} \ No newline at end of file diff --git a/magento2/spec/Cloudinary/Cloudinary/Model/SynchronisationCheckerSpec.php b/magento2/spec/Cloudinary/Cloudinary/Model/SynchronisationCheckerSpec.php new file mode 100644 index 0000000..be9945e --- /dev/null +++ b/magento2/spec/Cloudinary/Cloudinary/Model/SynchronisationCheckerSpec.php @@ -0,0 +1,52 @@ +beConstructedWith($synchronisationRepository); + } + + function it_validates_not_synchronized_for_null_image_name() + { + $imageName = ''; + $this->isSynchronized($imageName)->shouldBe(false); + } + + function it_validates_not_synchronized_if_collection_for_given_image_name_is_empty( + SynchronisationRepository $synchronisationRepository, + SearchResults $searchResults + ) + { + $imageName = 'pink_dress.gif'; + $searchResults->getTotalCount()->willReturn(0); + + $synchronisationRepository->getListByImagePath($imageName) + ->shouldBeCalled() + ->willReturn($searchResults); + + $this->isSynchronized($imageName)->shouldBe(false); + } + + function it_validates_synchronized_for_a_valid_image_name( + SynchronisationRepository $synchronisationRepository, + SearchResults $searchResults + ) + { + $imageName = 'pink_dress.gif'; + $searchResults->getTotalCount()->willReturn(1); + + $synchronisationRepository->getListByImagePath($imageName) + ->shouldBeCalled() + ->willReturn($searchResults); + + $this->isSynchronized($imageName)->shouldBe(true); + } +} \ No newline at end of file diff --git a/magento2/spec/Cloudinary/Cloudinary/Plugin/FileUploaderSpec.php b/magento2/spec/Cloudinary/Cloudinary/Plugin/FileUploaderSpec.php new file mode 100644 index 0000000..6b7baa7 --- /dev/null +++ b/magento2/spec/Cloudinary/Cloudinary/Plugin/FileUploaderSpec.php @@ -0,0 +1,52 @@ +getPath('media')->willReturn('/var/app/media'); + + $this->beConstructedWith( + $cloudinaryImageManager, + $directoryList + ); + } + + function it_is_initializable() + { + $this->shouldHaveType(FileUploader::class); + } + + function it_uploads_wysiwyg_file( + Uploader $uploader, + CloudinaryImageManager $cloudinaryImageManager + ) { + $image = Image::fromPath('/var/app/media/wysiwyg/foo.jpg', 'wysiwyg/foo.jpg'); + + $cloudinaryImageManager->uploadAndSynchronise($image)->shouldBeCalled(); + + $this->afterSave($uploader, ['path' => '/var/app/media/wysiwyg', 'file' => 'foo.jpg']); + } + + function it_does_not_upload_tmp_file( + Uploader $uploader, + CloudinaryImageManager $cloudinaryImageManager + ) { + $image = Image::fromPath('/var/app/media/tmp/foo.jpg', 'tmp/foo.jpg'); + + $cloudinaryImageManager->uploadAndSynchronise($image)->shouldNotBeCalled(); + + $this->afterSave($uploader, ['path' => '/var/app/media/tmp', 'file' => 'foo.jpg']); + } +} From 8b4ff9bfe86beb0d8dcef6494295160a705962b5 Mon Sep 17 00:00:00 2001 From: Rick Peacock Date: Mon, 8 May 2017 10:59:41 +0100 Subject: [PATCH 4/4] Add readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100755 README.md diff --git a/README.md b/README.md new file mode 100755 index 0000000..b2ac497 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Cloudinary Magento Extensions + +This repository contains packages to support Cloudinary on Magento platforms. + +- Magento 1 Cloudinary module +- Magento 2 Cloudinary module +- Shared core library used by both Magento modules.